Skip to content
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

Compromise Interface Redesign #25

Merged
merged 20 commits into from
Feb 29, 2024
Merged
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
133 changes: 91 additions & 42 deletions src/TermInterface.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
module TermInterface

"""
istree(x)
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved
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

Base.@deprecate_binding istree iscall
"""
istree(x)

Returns `true` if `x` is a term. If true, `operation`, `arguments`
must also be defined for `x` appropriately.
Alias of `iscall`
"""
istree(x) = false
export istree
istree
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved

"""
symtype(x)
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

Returns the symbolic type of `x`. By default this is just `typeof(x)`.
"""
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 All @@ -23,7 +37,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 +46,40 @@ 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

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

"""
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
operation(x) = iscall(x) ? first(children(x)) : error("operation called on a non-function call expression")
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved
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
arguments(x) = iscall(x) ? Iterators.drop(children(x), 1) : error("arguments called on a non-function call expression")
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved
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 +91,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,38 +101,76 @@ 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)
similarterm(x, op, args, symtype=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.
"""
function similarterm(x, head, args, symtype = nothing; metadata = nothing, exprhead = nothing)
head(args...)
function similarterm(x, op, args, symtype = nothing; metadata = nothing)
Base.depwarn("""`similarterm` is deprecated, use `maketerm` instead.
See https://github.com/JuliaSymbolics/TermInterface.jl for details.
The present call can be replaced by
`maketerm(typeof(x), $(callhead(x)), [op, args...], symtype, metadata)`""", :similarterm)

maketerm(typeof(x), callhead(x), [op, args...], symtype, metadata)
end

# Old fallback
function similarterm(T::Type, op, args, symtype = nothing; metadata = nothing)
Base.depwarn("`similarterm` is deprecated, use `maketerm` instead." *
"See https://github.com/JuliaSymbolics/TermInterface.jl for details.", :similarterm)
op(args...)
end

export similarterm


"""
callhead(x)
Used in this deprecation cycle of `similarterm` to find the `head` argument to
`makterm`. Do not implement this, or use `similarterm` if you're using this package.
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved
"""
callhead(x) = typeof(x)

"""
maketerm(T, head, children, type, metadata)

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 maketerm(T::Type, head, children, type, metadata)
error("maketerm for $T is not implemented")
end

include("utils.jl")

include("expr.jl")
Expand Down
25 changes: 6 additions & 19 deletions src/expr.jl
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
# 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)}())
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
# ^ this will implicitly define operation and arguments
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved

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, metadata)
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...)
11 changes: 4 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ using 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])
@test operation(ex) == :call
0x0f0f0f marked this conversation as resolved.
Show resolved Hide resolved
@test arguments(ex) == [:f, :a, :b]
@test ex == similarterm(ex, :call, [:f, :a, :b])

ex = :(arr[i, j])
@test operation(ex) == getindex
@test operation(ex) == :ref
@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
Loading