Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "TermInterface"
uuid = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c"
authors = ["Shashi Gowda <gowda@mit.edu>", "Alessandro Cheli <sudo-woodo3@protonmail.com>"]
version = "0.3.3"
version = "0.4"

[compat]
julia = "1"
Expand Down
76 changes: 38 additions & 38 deletions src/TermInterface.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
"""
This module defines a contains definitions for common functions that are useful
for symbolic expression manipulation. Its purpose is to provide a shared
interface between various symbolic programming Julia packages.

This is currently borrowed from TermInterface.jl. If you want to use
Metatheory.jl, please use this internal interface, as we are waiting that a
redesign proposal of the interface package will reach consensus. When this
happens, this module will be moved back into a separate package.

See https://github.com/JuliaSymbolics/TermInterface.jl/pull/22
"""
module TermInterface

"""
istree(x)

Returns `true` if `x` is a term. If true, `operation`, `arguments`
must also be defined for `x` appropriately.
Returns `true` if `x` is a term. If true, `operation`, `arguments` and
`is_function_call` must also be defined for `x` appropriately.
"""
istree(x) = false
export istree


"""
symtype(x)

Expand All @@ -22,6 +35,7 @@ function symtype(x)
end
export symtype


"""
issym(x)

Expand All @@ -31,44 +45,30 @@ on `x` and must return a Symbol.
issym(x) = false
export issym

"""
exprhead(x)

If `x` is a term as defined by `istree(x)`, `exprhead(x)` must return a symbol,
corresponding to the head of the `Expr` most similar to the term `x`.
If `x` represents a function call, for example, the `exprhead` is `:call`.
If `x` represents an indexing operation, such as `arr[i]`, then `exprhead` is `:ref`.
Note that `exprhead` is different from `operation` and both functions should
be defined correctly in order to let other packages provide code generation
and pattern matching features.
"""
function exprhead end
export exprhead


"""
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
is the function being called.
If `x` is a term as defined by `istree(x)`, `operation(x)` returns the operation of the
term. If `x` represents a function call term like `f(a,b)`, the operation
is the function being called, `f`.
Copy link
Contributor

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.

Copy link
Member Author

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.

"""
function operation end
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

@0x0f0f0f 0x0f0f0f Dec 9, 2023

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.

Copy link
Member Author

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

Copy link
Contributor

@willow-ahrens willow-ahrens Jan 13, 2024

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?

export operation


"""
arguments(x)

Get the arguments of `x`, must be defined if `istree(x)` is `true`.
Get the arguments of a term `x`, must be defined if `istree(x)` is `true`.
"""
function arguments end
export arguments


"""
unsorted_arguments(x::T)

If x is a term satisfying `istree(x)` and your term type `T` orovides
If x is a term satisfying `istree(x)` and your term type `T` provides
and optimized implementation for storing the arguments, this function can
be used to retrieve the arguments when the order of arguments does not matter
but the speed of the operation does.
Expand All @@ -83,7 +83,7 @@ export unsorted_arguments
Returns the number of arguments of `x`. Implicitly defined
if `arguments(x)` is defined.
"""
arity(x) = length(arguments(x))
arity(x)::Int = length(arguments(x))
export arity


Expand All @@ -102,29 +102,29 @@ export metadata
Returns a new term which has the structure of `x` but also has
the metadata `md` attached to it.
"""
function metadata(x, data)
error("Setting metadata on $x is not possible")
end
function metadata(x, data) end


"""
similarterm(x, head, args, symtype=nothing; metadata=nothing, exprhead=:call)
maketerm(T::Type, operation, arguments; type=Any, metadata=nothing)

Returns a term that is in the same closure of types as `typeof(x)`,
with `head` as the head and `args` as the arguments, `type` as the symtype
and `metadata` as the metadata. By default this will execute `head(args...)`.
`x` parameter can also be a `Type`. The `exprhead` keyword argument is useful
when manipulating `Expr`s.
Has to be implemented by the provider of the expression type T.
Returns a term that is in the same closure of types as `T`,
with `operation` as the operation and `arguments` as the arguments, `type` as the symtype
and `metadata` as the metadata.
"""
function similarterm(x, head, args, symtype = nothing; metadata = nothing, exprhead = nothing)
head(args...)
end
function maketerm end
export maketerm


export similarterm

include("utils.jl")
"""
node_count(t)
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 in 0) + 1 : 1
export node_count

Copy link
Contributor

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.

include("expr.jl")

Copy link
Contributor

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.

end # module

26 changes: 0 additions & 26 deletions src/expr.jl

This file was deleted.

16 changes: 0 additions & 16 deletions src/utils.jl

This file was deleted.

18 changes: 2 additions & 16 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
using TermInterface
using Test
using TermInterface, Test

@testset "Expr" begin
ex = :(f(a, b))
@test operation(ex) == :f
@test arguments(ex) == [:a, :b]
@test exprhead(ex) == :call
@test ex == similarterm(ex, :f, [:a, :b])

ex = :(arr[i, j])
@test operation(ex) == getindex
@test arguments(ex) == [:arr, :i, :j]
@test exprhead(ex) == :ref
@test ex == similarterm(ex, :ref, [:arr, :i, :j]; exprhead = :ref)
@test ex == similarterm(ex, :ref, [:arr, :i, :j])
end
@test true