Skip to content

Commit

Permalink
Merge pull request #25 from willow-ahrens/compromise-interface
Browse files Browse the repository at this point in the history
Compromise Interface Redesign
  • Loading branch information
0x0f0f0f authored Feb 29, 2024
2 parents d4d270b + 736fe48 commit 4263649
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 109 deletions.
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 <[email protected]>", "Alessandro Cheli <[email protected]>"]
version = "0.3.3"
version = "0.4.0"

[compat]
julia = "1"
Expand Down
79 changes: 43 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,69 @@ You should define the following methods for an expression tree type `T` with sym
with TermInterface.jl, and therefore with [SymbolicUtils.jl](https://github.com/JuliaSymbolics/SymbolicUtils.jl)
and [Metatheory.jl](https://github.com/0x0f0f0f/Metatheory.jl).

#### `istree(x::T)` or `istree(x::Type{T})`
#### `isexpr(x::T)`

Check if `x` represents an expression tree. If returns true,
it will be assumed that `operation(::T)` and `arguments(::T)`
methods are defined. Definining these three should allow use
of `SymbolicUtils.simplify` on custom types. Optionally `symtype(x)` can be
defined to return the expected type of the symbolic expression.
Returns `true` if `x` is an expression tree (an S-expression). If true, `head`
and `children` methods must be defined for `x`.

#### `iscall(x::T)`

#### `exprhead(x)`
Returns `true` if `x` is a function call expression. If true, `operation`, `arguments` must also be defined for `x::T`.

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.

#### `operation(x::T)`
#### `head(x)`

Returns the head (a function object) performed by an expression
tree. Called only if `istree(::T)` is true. Part of the API required
for `simplify` to work. Other required methods are `arguments` and `istree`
Returns the head of the S-expression.

#### `arguments(x::T)`
#### `children(x)`

Returns the arguments (a `Vector`) for an expression tree.
Called only if `istree(x)` is `true`. Part of the API required
for `simplify` to work. Other required methods are `operation` and `istree`
Returns the children (aka tail) of the S-expression.

In addition, the methods for `Base.hash` and `Base.isequal` should also be implemented by the types for the purposes of substitution and equality matching respectively.

#### `similarterm(t::MyType, f, args, symtype=T; metadata=nothing, exprhead=exprhead(t))`
#### `operation(x)`

Or `similarterm(t::Type{MyType}, f, args, symtype=T; metadata=nothing, exprhead=:call)`.
Returns the function a function call expression is calling. `iscall(x)` must be
true as a precondition.

Construct a new term with the operation `f` and arguments `args`, the term should be similar to `t` in type. if `t` is a `SymbolicUtils.Term` object a new Term is created with the same symtype as `t`. If not, the result is computed as `f(args...)`. Defining this method for your term type will reduce any performance loss in performing `f(args...)` (esp. the splatting, and redundant type computation). T is the symtype of the output term. You can use `SymbolicUtils.promote_symtype` to infer this type. The `exprhead` keyword argument is useful when creating `Expr`s.
#### `arguments(x)`

Returns the arguments to the function call in a function call expression.
`iscall(x)` must be true as a precondition.

#### `maketerm(T, head, children, type=nothing, metadata=nothing)`

Constructs an expression. `T` is a constructor type, `head` and `children` are
the head and tail of the S-expression, `type` is the `type` of the S-expression.
`metadata` is any metadata attached to this expression.

Note that `maketerm` may not necessarily return an object of type `T`. For example,
it may return a representation which is more efficient.

This function is used by term-manipulation routines to construct terms generically.
In these routines, `T` is usually the type of the input expression which is being manipulated.
For example, when a subexpression is substituted, the outer expression is re-constructed with
the sub-expression. `T` will be the type of the outer expression.

Packages providing expression types _must_ implement this method for each expression type.

If your types do not support type information or metadata, you still need to accept
these arguments and may choose to not use them.

### Optional

#### `unsorted_arguments(x)`
#### `arity(x)`

When `x` satisfies `iscall`, returns the number of arguments of `x`.
Implicitly defined if `arguments(x)` is defined.

If x is a term satisfying `istree(x)` and your term type `T` provides
an 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. Defaults to `arguments(x)`.

#### `symtype(x)`
#### `metadata(x)`

The supposed type of values in the domain of x. Tracing tools can use this type to
pick the right method to run or analyse code.
Returns the metadata attached to `x`.

This defaults to `typeof(x)` if `x` is numeric, or `Any` otherwise.
For the types defined in this SymbolicUtils.jl, namely `T<:Symbolic{S}` it is `S`.
#### `symtype(expr)`

Returns the symbolic type of `expr`. By default this is just `typeof(expr)`.
Define this for your symbolic types if you want `SymbolicUtils.simplify` to apply rules
specific to numbers (such as commutativity of multiplication). Or such
rules that may be implemented in the future.
Expand Down
98 changes: 56 additions & 42 deletions src/TermInterface.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
module TermInterface

"""
istree(x)
iscall(x)
Returns `true` if `x` is a function call expression. If true, `operation`, `arguments`
must also be defined for `x`.
"""
iscall(x) = false
export iscall

Returns `true` if `x` is a term. If true, `operation`, `arguments`
must also be defined for `x` appropriately.
"""
istree(x) = false
export istree
isexpr(x)
Returns `true` if `x` is an expression tree (an S-expression). If true, `head` and `children` methods must be defined for `x`.
"""
isexpr(x) = false
export isexpr

"""
symtype(x)
symtype(expr)
Returns the symbolic type of `x`. By default this is just `typeof(x)`.
Returns the symbolic type of `expr`. By default this is just `typeof(expr)`.
Define this for your symbolic types if you want `SymbolicUtils.simplify` to apply rules
specific to numbers (such as commutativity of multiplication). Or such
rules that may be implemented in the future.
Expand All @@ -23,7 +29,7 @@ end
export symtype

"""
issym(x)
issym(x)
Returns `true` if `x` is a symbol. If true, `nameof` must be defined
on `x` and must return a Symbol.
Expand All @@ -32,43 +38,41 @@ 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.
head(x)
Returns the head of the S-expression.
"""
function exprhead end
export exprhead
function head end
export head

"""
children(x)
Returns the children (aka tail) of the S-expression.
"""
function children end
export children

"""
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.
Returns the function a function call expression is calling.
`iscall(x)` must be true as a precondition.
"""
function operation end
export operation

"""
arguments(x)
Get the arguments of `x`, must be defined if `istree(x)` is `true`.
Returns the arguments to the function call in a function call expression.
`iscall(x)` must be true as a precondition.
"""
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 expression satisfying `iscall(x)` and your expression 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 @@ -80,8 +84,8 @@ export unsorted_arguments
"""
arity(x)
Returns the number of arguments of `x`. Implicitly defined
if `arguments(x)` is defined.
When `x` satisfies `iscall`, returns the number of arguments of `x`.
Implicitly defined if `arguments(x)` is defined.
"""
arity(x) = length(arguments(x))
export arity
Expand All @@ -90,37 +94,47 @@ export arity
"""
metadata(x)
Return the metadata attached to `x`.
Returns the metadata attached to `x`.
"""
metadata(x) = nothing
export metadata


"""
metadata(x, md)
metadata(expr, md)
Returns a new term which has the structure of `x` but also has
the metadata `md` attached to it.
Returns a `expr` with metadata `md` attached to it.
"""
function metadata(x, data)
error("Setting metadata on $x is not possible")
error("Setting metadata on $x is not implemented")
end


"""
similarterm(x, head, args, symtype=nothing; metadata=nothing, exprhead=:call)
maketerm(T, head, children, type=nothing, 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.
Constructs an expression. `T` is a constructor type, `head` and `children` are
the head and tail of the S-expression, `type` is the `type` of the S-expression.
`metadata` is any metadata attached to this expression.
Note that `maketerm` may not necessarily return an object of type `T`. For example,
it may return a representation which is more efficient.
This function is used by term-manipulation routines to construct terms generically.
In these routines, `T` is usually the type of the input expression which is being manipulated.
For example, when a subexpression is substituted, the outer expression is re-constructed with
the sub-expression. `T` will be the type of the outer expression.
Packages providing expression types _must_ implement this method for each expression type.
If your types do not support type information or metadata, you still need to accept
these arguments and may choose to not use them.
"""
function similarterm(x, head, args, symtype = nothing; metadata = nothing, exprhead = nothing)
head(args...)
end

export similarterm
function maketerm(T::Type, head, children, type=nothing, metadata=nothing)
error("maketerm for $T is not implemented")
end
export maketerm

include("utils.jl")

Expand Down
27 changes: 8 additions & 19 deletions src/expr.jl
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
# This file contains default definitions for TermInterface methods on Julia
# Builtin Expr type.

istree(x::Expr) = true
exprhead(e::Expr) = e.head
iscall(x::Expr) = x.head == :call

operation(e::Expr) = expr_operation(e, Val{exprhead(e)}())
arguments(e::Expr) = expr_arguments(e, Val{exprhead(e)}())
callhead(e::Expr) = e.head
head(e::Expr) = e.head
children(e::Expr) = e.args

# See https://docs.julialang.org/en/v1/devdocs/ast/
expr_operation(e::Expr, ::Union{Val{:call},Val{:macrocall}}) = e.args[1]
expr_operation(e::Expr, ::Union{Val{:ref}}) = getindex
expr_operation(e::Expr, ::Val{T}) where {T} = T
operation(e::Expr) = iscall(e) ? first(children(e)) : error("operation called on a non-function call expression")
arguments(e::Expr) = iscall(e) ? @view(e.args[2:end]) : error("arguments called on a non-function call expression")

expr_arguments(e::Expr, ::Union{Val{:call},Val{:macrocall}}) = e.args[2:end]
expr_arguments(e::Expr, _) = e.args


function similarterm(x::Expr, head, args, symtype = nothing; metadata = nothing, exprhead = exprhead(x))
expr_similarterm(head, args, Val{exprhead}())
function maketerm(::Type{Expr}, head, args, symtype=nothing, metadata=nothing)
Expr(head, args...)
end


expr_similarterm(head, args, ::Val{:call}) = Expr(:call, head, args...)
expr_similarterm(head, args, ::Val{:macrocall}) = Expr(:macrocall, head, args...) # discard linenumbernodes?
expr_similarterm(head, args, ::Val{eh}) where {eh} = Expr(eh, args...)
8 changes: 4 additions & 4 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
is_operation(f)
Returns a single argument anonymous function predicate, that returns `true` if and only if
the argument to the predicate satisfies `istree` and `operation(x) == f`
the argument to the predicate satisfies `iscall` and `operation(x) == f`
"""
is_operation(f) = @nospecialize(x) -> istree(x) && (operation(x) == f)
is_operation(f) = @nospecialize(x) -> iscall(x) && (operation(x) == f)
export is_operation


"""
node_count(t)
Count the nodes in a symbolic expression tree satisfying `istree` and `arguments`.
Count the nodes in a symbolic expression tree satisfying `isexpr` and `arguments`.
"""
node_count(t) = istree(t) ? reduce(+, node_count(x) for x in arguments(t), init = 0) + 1 : 1
node_count(t) = isexpr(t) ? reduce(+, node_count(x) for x in children(t); init=0) + 1 : 1
export node_count
17 changes: 10 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ using Test

@testset "Expr" begin
ex = :(f(a, b))
@test head(ex) == :call
@test children(ex) == [:f, :a, :b]
@test operation(ex) == :f
@test arguments(ex) == [:a, :b]
@test exprhead(ex) == :call
@test ex == similarterm(ex, :f, [:a, :b])
@test iscall(ex)
@test ex == maketerm(Expr, :call, [: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])
@test head(ex) == :ref
@test_throws ErrorException operation(ex)
@test_throws ErrorException arguments(ex)
@test !iscall(ex)
@test ex == maketerm(Expr, :ref, [:arr, :i, :j])
end

0 comments on commit 4263649

Please sign in to comment.