From 5f0b5d988e75f7b10aa389eb6d79270c4bfc20c8 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 4 Dec 2023 18:38:05 +0100 Subject: [PATCH 01/13] change interface --- src/TermInterface.jl | 50 ++++++++++++++++++++++++++++++++++---------- src/expr.jl | 45 ++++++++++++++++++++++++++------------- src/utils.jl | 16 -------------- 3 files changed, 69 insertions(+), 42 deletions(-) delete mode 100644 src/utils.jl diff --git a/src/TermInterface.jl b/src/TermInterface.jl index 78b57ca..a2f7ee6 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -45,12 +45,29 @@ and pattern matching features. function exprhead end export exprhead +""" + head(x) + +If `x` is a term as defined by `istree(x)`, `head(x)` returns the +head of the term if `x`. The `head` type has to be provided by the package. +""" +function head end +export head + +""" + tail(x) + +Get the arguments of `x`, must be defined if `istree(x)` is `true`. +""" +function tail end +export tail + """ 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 @@ -108,21 +125,32 @@ end """ - similarterm(x, head, args, symtype=nothing; metadata=nothing, exprhead=:call) + maketerm(head::H, tail; type=Any, metadata=nothing) +Has to be implemented by the provider of H. 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. +with `head` as the head and `tail` 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(head, tail; type=Any, metadata=nothing) end +export maketerm + +""" + 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` +""" +is_operation(f) = @nospecialize(x) -> istree(x) && (operation(x) == f) +export is_operation -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 = 0) + 1 : 1 +export node_count include("expr.jl") diff --git a/src/expr.jl b/src/expr.jl index 4bc6788..39f8fd3 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -1,26 +1,41 @@ # This file contains default definitions for TermInterface methods on Julia # Builtin Expr type. -istree(x::Expr) = true -exprhead(e::Expr) = e.head +struct ExprHead + head +end +export ExprHead -operation(e::Expr) = expr_operation(e, Val{exprhead(e)}()) -arguments(e::Expr) = expr_arguments(e, Val{exprhead(e)}()) +istree(x::Expr) = true +head(e::Expr) = ExprHead(head) +tail(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 - -expr_arguments(e::Expr, ::Union{Val{:call},Val{:macrocall}}) = e.args[2:end] -expr_arguments(e::Expr, _) = e.args +function operation(e::Expr) + h = head(e) + hh = h.head + if hh in (:call, :macrocall) + e.args[1] + elseif hh == :ref + getindex + else + hh + end +end +function arguments(e::Expr) + h = head(e) + hh = h.head + if hh in (:call, :macrocall) + e.args[2:end] + else + e.args + end + expr_arguments(e, Val{exprhead(e)}()) +end -function similarterm(x::Expr, head, args, symtype = nothing; metadata = nothing, exprhead = exprhead(x)) +function similarterm(x::Expr, head, args, symtype=nothing; metadata=nothing, exprhead=exprhead(x)) expr_similarterm(head, args, Val{exprhead}()) 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...) +maketerm(head::ExprHead, tail; type=Any, metadata=nothing) = Expr(head.head, tail...) \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index b9c732f..0000000 --- a/src/utils.jl +++ /dev/null @@ -1,16 +0,0 @@ -""" - 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` -""" -is_operation(f) = @nospecialize(x) -> istree(x) && (operation(x) == f) -export is_operation - - -""" - 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 = 0) + 1 : 1 -export node_count \ No newline at end of file From 2052ebdfb0f91ce7aef0db24c5510c8c14695346 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 4 Dec 2023 18:47:40 +0100 Subject: [PATCH 02/13] add tests --- src/expr.jl | 7 +------ test/runtests.jl | 16 +++++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/expr.jl b/src/expr.jl index 39f8fd3..1a4b6f8 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -7,7 +7,7 @@ end export ExprHead istree(x::Expr) = true -head(e::Expr) = ExprHead(head) +head(e::Expr) = ExprHead(e.head) tail(e::Expr) = e.args # See https://docs.julialang.org/en/v1/devdocs/ast/ @@ -31,11 +31,6 @@ function arguments(e::Expr) else e.args end - expr_arguments(e, Val{exprhead(e)}()) -end - -function similarterm(x::Expr, head, args, symtype=nothing; metadata=nothing, exprhead=exprhead(x)) - expr_similarterm(head, args, Val{exprhead}()) end maketerm(head::ExprHead, tail; type=Any, metadata=nothing) = Expr(head.head, tail...) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c344173..56b202b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,15 +3,21 @@ using Test @testset "Expr" begin ex = :(f(a, b)) + @test head(ex) == ExprHead(:call) + @test tail(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 ex == maketerm(ExprHead(:call), [:f, :a, :b]) ex = :(arr[i, j]) + @test head(ex) == ExprHead(:ref) @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 ex == maketerm(ExprHead(:ref), [:arr, :i, :j]) + + ex = Expr(:block, :a, :b, :c) + @test head(ex) == ExprHead(:block) + @test operation(ex) == :block + @test tail(ex) == arguments(ex) == [:a, :b, :c] + @test ex == maketerm(ExprHead(:block), [:a, :b, :c]) end From 3cb75688cbc13f8ab8c8b8bf3e4c5f1dae605e2e Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 12:17:45 +0100 Subject: [PATCH 03/13] head_symbol --- src/TermInterface.jl | 16 ++++++++++++++-- src/expr.jl | 10 +++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index a2f7ee6..580ef95 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -48,12 +48,24 @@ export exprhead """ head(x) -If `x` is a term as defined by `istree(x)`, `head(x)` returns the -head of the term if `x`. The `head` type has to be provided by the package. +If `x` is a term as defined by `istree(x)`, `head(x)` returns the head of the +term if `x`. The `head` type has to be provided by the package. """ function head end export head +""" + head_symbol(x::HeadType) + +If `x` is a head object, `head_symbol(T, x)` returns a `Symbol` object that +corresponds to `y.head` if `y` was the representation of the corresponding term +as a Julia Expression. This is useful to define interoperability between +symbolic term types defined in different packages and should be used when +calling `maketerm`. +""" +function head_symbol end +export head_symbol + """ tail(x) diff --git a/src/expr.jl b/src/expr.jl index 1a4b6f8..8d69ba1 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -6,6 +6,8 @@ struct ExprHead end export ExprHead +head_symbol(eh::ExprHead) = eh.head + istree(x::Expr) = true head(e::Expr) = ExprHead(e.head) tail(e::Expr) = e.args @@ -33,4 +35,10 @@ function arguments(e::Expr) end end -maketerm(head::ExprHead, tail; type=Any, metadata=nothing) = Expr(head.head, tail...) \ No newline at end of file +function maketerm(head::ExprHead, tail; type=Any, metadata=nothing) + if !isempty(tail) && first(tail) isa Union{Function,DataType} + Expr(head.head, nameof(first(tail)), @view(tail[2:end])...) + else + Expr(head.head, tail...) + end +end From f2356866761f3e3ee921e88b5141531424802926 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 12:24:56 +0100 Subject: [PATCH 04/13] add tests --- src/TermInterface.jl | 36 ++++++++++++++++++++++++++++++++++++ test/runtests.jl | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index 580ef95..006c3d1 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -166,5 +166,41 @@ export node_count include("expr.jl") +""" +Take a struct definition and automatically define `TermInterface` methods. +This will automatically define a head type. If the struct is called `Foo`, then +the head type will be called `FooHead`. The `head_symbol` of such head types +will default to `:call`. +""" +macro matchable(expr) + @assert expr.head == :struct + name = expr.args[2] + if name isa Expr + name.head === :(<:) && (name = name.args[1]) + name isa Expr && name.head === :curly && (name = name.args[1]) + end + fields = filter(x -> x isa Symbol || (x isa Expr && x.head == :(==)), expr.args[3].args) + get_name(s::Symbol) = s + get_name(e::Expr) = (@assert(e.head == :(::)); e.args[1]) + fields = map(get_name, fields) + head_name = Symbol(name, :Head) + quote + $expr + struct $head_name + head + end + TermInterface.head_symbol(x::$head_name) = x.head + # TODO default to call? + TermInterface.head(::$name) = $head_name(:call) + TermInterface.istree(::$name) = true + TermInterface.operation(::$name) = $name + TermInterface.arguments(x::$name) = getfield.((x,), ($(QuoteNode.(fields)...),)) + TermInterface.tail(x::$name) = [operation(x); arguments(x)...] + TermInterface.arity(x::$name) = $(length(fields)) + Base.length(x::$name) = $(length(fields) + 1) + end |> esc +end +export @matchable + end # module diff --git a/test/runtests.jl b/test/runtests.jl index 56b202b..fb5e799 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,4 @@ -using TermInterface -using Test +using TermInterface, Test @testset "Expr" begin ex = :(f(a, b)) @@ -21,3 +20,42 @@ using Test @test tail(ex) == arguments(ex) == [:a, :b, :c] @test ex == maketerm(ExprHead(:block), [:a, :b, :c]) end + +@testset "Custom Struct" begin + struct Foo + args + Foo(args...) = new(args) + end + struct FooHead + head + end + TermInterface.head(::Foo) = FooHead(:call) + TermInterface.head_symbol(q::FooHead) = q.head + TermInterface.operation(::Foo) = Foo + TermInterface.istree(::Foo) = true + TermInterface.arguments(x::Foo) = [x.args...] + TermInterface.tail(x::Foo) = [operation(x); x.args...] + + t = Foo(1, 2) + @test head(t) == FooHead(:call) + @test head_symbol(head(t)) == :call + @test operation(t) == Foo + @test istree(t) == true + @test arguments(t) == [1, 2] + @test tail(t) == [Foo, 1, 2] +end + +@testset "Automatically Generated Methods" begin + @matchable struct Bar + a + b + end + + t = Bar(1, 2) + @test head(t) == BarHead(:call) + @test head_symbol(head(t)) == :call + @test operation(t) == Bar + @test istree(t) == true + @test arguments(t) == (1, 2) + @test tail(t) == [Bar, 1, 2] +end \ No newline at end of file From bc1ec14fd9160caa2c3a0ad1909b30ea9695b448 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 14:33:15 +0100 Subject: [PATCH 05/13] adjust matchable --- src/TermInterface.jl | 14 +++++++++----- test/runtests.jl | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index 006c3d1..6f7582e 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -167,23 +167,27 @@ export node_count include("expr.jl") """ -Take a struct definition and automatically define `TermInterface` methods. -This will automatically define a head type. If the struct is called `Foo`, then + @matchable struct Foo fields... end [HeadType] + +Take a struct definition and automatically define `TermInterface` methods. This +will automatically define a head type. If `HeadType` is given then it will be +used as `head(::Foo)`. If it is omitted, and the struct is called `Foo`, then the head type will be called `FooHead`. The `head_symbol` of such head types will default to `:call`. """ -macro matchable(expr) +macro matchable(expr, head_name=nothing) @assert expr.head == :struct name = expr.args[2] if name isa Expr name.head === :(<:) && (name = name.args[1]) name isa Expr && name.head === :curly && (name = name.args[1]) end - fields = filter(x -> x isa Symbol || (x isa Expr && x.head == :(==)), expr.args[3].args) + fields = filter(x -> x isa Symbol || (x isa Expr && x.head == :(::)), expr.args[3].args) get_name(s::Symbol) = s get_name(e::Expr) = (@assert(e.head == :(::)); e.args[1]) fields = map(get_name, fields) - head_name = Symbol(name, :Head) + head_name = isnothing(head_name) ? Symbol(name, :Head) : head_name + quote $expr struct $head_name diff --git a/test/runtests.jl b/test/runtests.jl index fb5e799..8190dde 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -48,7 +48,7 @@ end @testset "Automatically Generated Methods" begin @matchable struct Bar a - b + b::Int end t = Bar(1, 2) From bcc4574060c376e67baef9466126002e9926c39d Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 15:19:15 +0100 Subject: [PATCH 06/13] adjust some of the tutorial --- src/expr.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/expr.jl b/src/expr.jl index 8d69ba1..81610dc 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -18,8 +18,6 @@ function operation(e::Expr) hh = h.head if hh in (:call, :macrocall) e.args[1] - elseif hh == :ref - getindex else hh end From e2aef1a3bd76328936090fa4ec75a27584428ebf Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 15:43:06 +0100 Subject: [PATCH 07/13] adjust tests --- test/runtests.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 8190dde..173e8ae 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,10 +10,19 @@ using TermInterface, Test ex = :(arr[i, j]) @test head(ex) == ExprHead(:ref) - @test operation(ex) == getindex + @test operation(ex) == :ref @test arguments(ex) == [:arr, :i, :j] @test ex == maketerm(ExprHead(:ref), [:arr, :i, :j]) + + ex = :(i, j) + @test head(ex) == ExprHead(:tuple) + @test operation(ex) == :tuple + @test arguments(ex) == [:i, :j] + @test tail(ex) == [:i, :j] + @test ex == maketerm(ExprHead(:tuple), [:i, :j]) + + ex = Expr(:block, :a, :b, :c) @test head(ex) == ExprHead(:block) @test operation(ex) == :block From 47610e739890fa035d37b5afc125970d4bcbfabb Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 15:43:52 +0100 Subject: [PATCH 08/13] version bump --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2de42ff..a414a10 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TermInterface" uuid = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c" authors = ["Shashi Gowda ", "Alessandro Cheli "] -version = "0.3.3" +version = "0.4" [compat] julia = "1" From 85d0931ff36cec7a5dd4c02a56eef7f34941b8c8 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 5 Dec 2023 16:57:51 +0100 Subject: [PATCH 09/13] apply suggestions --- src/TermInterface.jl | 14 +++++++------- src/expr.jl | 10 +++++----- test/runtests.jl | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index 6f7582e..f22ca5c 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -67,12 +67,12 @@ function head_symbol end export head_symbol """ - tail(x) + children(x) Get the arguments of `x`, must be defined if `istree(x)` is `true`. """ -function tail end -export tail +function children end +export children """ @@ -137,14 +137,14 @@ end """ - maketerm(head::H, tail; type=Any, metadata=nothing) + maketerm(head::H, children; type=Any, metadata=nothing) Has to be implemented by the provider of H. Returns a term that is in the same closure of types as `typeof(x)`, -with `head` as the head and `tail` as the arguments, `type` as the symtype +with `head` as the head and `children` as the arguments, `type` as the symtype and `metadata` as the metadata. """ -function maketerm(head, tail; type=Any, metadata=nothing) end +function maketerm end export maketerm """ @@ -199,7 +199,7 @@ macro matchable(expr, head_name=nothing) TermInterface.istree(::$name) = true TermInterface.operation(::$name) = $name TermInterface.arguments(x::$name) = getfield.((x,), ($(QuoteNode.(fields)...),)) - TermInterface.tail(x::$name) = [operation(x); arguments(x)...] + TermInterface.children(x::$name) = [operation(x); arguments(x)...] TermInterface.arity(x::$name) = $(length(fields)) Base.length(x::$name) = $(length(fields) + 1) end |> esc diff --git a/src/expr.jl b/src/expr.jl index 81610dc..9a0fdd2 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -10,7 +10,7 @@ head_symbol(eh::ExprHead) = eh.head istree(x::Expr) = true head(e::Expr) = ExprHead(e.head) -tail(e::Expr) = e.args +children(e::Expr) = e.args # See https://docs.julialang.org/en/v1/devdocs/ast/ function operation(e::Expr) @@ -33,10 +33,10 @@ function arguments(e::Expr) end end -function maketerm(head::ExprHead, tail; type=Any, metadata=nothing) - if !isempty(tail) && first(tail) isa Union{Function,DataType} - Expr(head.head, nameof(first(tail)), @view(tail[2:end])...) +function maketerm(head::ExprHead, children; type=Any, metadata=nothing) + if !isempty(children) && first(children) isa Union{Function,DataType} + Expr(head.head, nameof(first(children)), @view(children[2:end])...) else - Expr(head.head, tail...) + Expr(head.head, children...) end end diff --git a/test/runtests.jl b/test/runtests.jl index 173e8ae..17c791c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,7 @@ using TermInterface, Test @testset "Expr" begin ex = :(f(a, b)) @test head(ex) == ExprHead(:call) - @test tail(ex) == [:f, :a, :b] + @test children(ex) == [:f, :a, :b] @test operation(ex) == :f @test arguments(ex) == [:a, :b] @test ex == maketerm(ExprHead(:call), [:f, :a, :b]) @@ -19,14 +19,14 @@ using TermInterface, Test @test head(ex) == ExprHead(:tuple) @test operation(ex) == :tuple @test arguments(ex) == [:i, :j] - @test tail(ex) == [:i, :j] + @test children(ex) == [:i, :j] @test ex == maketerm(ExprHead(:tuple), [:i, :j]) ex = Expr(:block, :a, :b, :c) @test head(ex) == ExprHead(:block) @test operation(ex) == :block - @test tail(ex) == arguments(ex) == [:a, :b, :c] + @test children(ex) == arguments(ex) == [:a, :b, :c] @test ex == maketerm(ExprHead(:block), [:a, :b, :c]) end @@ -43,7 +43,7 @@ end TermInterface.operation(::Foo) = Foo TermInterface.istree(::Foo) = true TermInterface.arguments(x::Foo) = [x.args...] - TermInterface.tail(x::Foo) = [operation(x); x.args...] + TermInterface.children(x::Foo) = [operation(x); x.args...] t = Foo(1, 2) @test head(t) == FooHead(:call) @@ -51,7 +51,7 @@ end @test operation(t) == Foo @test istree(t) == true @test arguments(t) == [1, 2] - @test tail(t) == [Foo, 1, 2] + @test children(t) == [Foo, 1, 2] end @testset "Automatically Generated Methods" begin @@ -66,5 +66,5 @@ end @test operation(t) == Bar @test istree(t) == true @test arguments(t) == (1, 2) - @test tail(t) == [Bar, 1, 2] + @test children(t) == [Bar, 1, 2] end \ No newline at end of file From 62ff72aaab2c1e4f62eb7974a7962151e228562f Mon Sep 17 00:00:00 2001 From: a Date: Sun, 14 Jan 2024 17:38:17 +0100 Subject: [PATCH 10/13] adjusted proposal --- src/TermInterface.jl | 140 ++++++++++++++++--------------------------- src/expr.jl | 45 +++++--------- 2 files changed, 68 insertions(+), 117 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index f22ca5c..a192215 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -1,14 +1,36 @@ +""" +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, `head`, `children` and +`is_function_call` must also be defined for `x` appropriately. """ istree(x) = false export istree +""" + is_function_call(x) + +Returns true if a term abstractly represents a function call or function application. +Must be defined if `istree(x)` is defined. +Can be true only if `istree(x)` is true. +""" +function is_function_call end +export is_function_call + """ symtype(x) @@ -22,6 +44,7 @@ function symtype(x) end export symtype + """ issym(x) @@ -31,75 +54,32 @@ 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 """ head(x) If `x` is a term as defined by `istree(x)`, `head(x)` returns the head of the -term if `x`. The `head` type has to be provided by the package. +term. If `x` represents a function call term like `f(a,b)`, the head +is the function being called, `f`. """ function head end export head -""" - head_symbol(x::HeadType) - -If `x` is a head object, `head_symbol(T, x)` returns a `Symbol` object that -corresponds to `y.head` if `y` was the representation of the corresponding term -as a Julia Expression. This is useful to define interoperability between -symbolic term types defined in different packages and should be used when -calling `maketerm`. -""" -function head_symbol end -export head_symbol """ children(x) -Get the arguments of `x`, must be defined if `istree(x)` is `true`. +Get the children of a term `x`, must be defined if `istree(x)` is `true`. """ function children end export children - -""" - operation(x) - -If `x` is a term as defined by `istree(x)`, `operation(x)` returns the -operation of the term if `x` represents a function call, for example, the head -is the function being called. -""" -function operation end -export operation - -""" - arguments(x) - -Get the arguments of `x`, must be defined if `istree(x)` is `true`. """ -function arguments end -export arguments + unsorted_children(x::T) - -""" - unsorted_arguments(x::T) - -If x is a term satisfying `istree(x)` and your term type `T` orovides -and optimized implementation for storing the arguments, this function can -be used to retrieve the arguments when the order of arguments does not matter +If x is a term satisfying `istree(x)` and your term type `T` provides +and optimized implementation for storing the children, this function can +be used to retrieve the children when the order of arguments does not matter but the speed of the operation does. """ unsorted_arguments(x) = arguments(x) @@ -109,10 +89,10 @@ export unsorted_arguments """ arity(x) -Returns the number of arguments of `x`. Implicitly defined -if `arguments(x)` is defined. +Returns the number of children of `x`. Implicitly defined +if `children(x)` is defined. """ -arity(x) = length(arguments(x)) +arity(x)::Int = length(children(x)) export arity @@ -131,51 +111,40 @@ 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 """ - maketerm(head::H, children; type=Any, metadata=nothing) + maketerm(T::Type, head, children; is_call = true, type=Any, metadata=nothing) -Has to be implemented by the provider of H. -Returns a term that is in the same closure of types as `typeof(x)`, +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 `head` as the head and `children` as the arguments, `type` as the symtype and `metadata` as the metadata. + +`is_call` is used to determine if the constructed term represents a function +call. If `is_call = true`, then it must construct a term `x` such that +`is_function_call(x) = true`, and vice-versa for `is_call = false`. """ function maketerm end export maketerm -""" - 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` -""" -is_operation(f) = @nospecialize(x) -> istree(x) && (operation(x) == f) -export is_operation """ 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 = 0) + 1 : 1 +node_count(t) = istree(t) ? reduce(+, node_count(x) for x in children(t), init in 0) + 1 : 1 export node_count -include("expr.jl") - """ @matchable struct Foo fields... end [HeadType] -Take a struct definition and automatically define `TermInterface` methods. This -will automatically define a head type. If `HeadType` is given then it will be -used as `head(::Foo)`. If it is omitted, and the struct is called `Foo`, then -the head type will be called `FooHead`. The `head_symbol` of such head types -will default to `:call`. +Take a struct definition and automatically define `TermInterface` methods. +`is_function_call` of such type will default to `true`. """ -macro matchable(expr, head_name=nothing) +macro matchable(expr) @assert expr.head == :struct name = expr.args[2] if name isa Expr @@ -186,25 +155,20 @@ macro matchable(expr, head_name=nothing) get_name(s::Symbol) = s get_name(e::Expr) = (@assert(e.head == :(::)); e.args[1]) fields = map(get_name, fields) - head_name = isnothing(head_name) ? Symbol(name, :Head) : head_name quote $expr - struct $head_name - head - end - TermInterface.head_symbol(x::$head_name) = x.head - # TODO default to call? - TermInterface.head(::$name) = $head_name(:call) TermInterface.istree(::$name) = true - TermInterface.operation(::$name) = $name - TermInterface.arguments(x::$name) = getfield.((x,), ($(QuoteNode.(fields)...),)) - TermInterface.children(x::$name) = [operation(x); arguments(x)...] + TermInterface.is_function_call(::$name) = true + TermInterface.head(::$name) = $name + TermInterface.children(x::$name) = getfield.((x,), ($(QuoteNode.(fields)...),)) TermInterface.arity(x::$name) = $(length(fields)) Base.length(x::$name) = $(length(fields) + 1) end |> esc end export @matchable +include("expr.jl") + end # module diff --git a/src/expr.jl b/src/expr.jl index 9a0fdd2..3ec4434 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -1,42 +1,29 @@ + # This file contains default definitions for TermInterface methods on Julia # Builtin Expr type. -struct ExprHead - head -end -export ExprHead - -head_symbol(eh::ExprHead) = eh.head +is_function_call(e::Expr) = _is_function_call_expr_head(e.head) +_is_function_call_expr_head(x::Symbol) = x in (:call, :macrocall) istree(x::Expr) = true -head(e::Expr) = ExprHead(e.head) -children(e::Expr) = e.args # See https://docs.julialang.org/en/v1/devdocs/ast/ -function operation(e::Expr) - h = head(e) - hh = h.head - if hh in (:call, :macrocall) - e.args[1] - else - hh - end -end +head(e::Expr) = is_function_call(e) ? e.args[1] : e.head +children(e::Expr) = is_function_call(e) ? e.args[2:end] : e.args -function arguments(e::Expr) - h = head(e) - hh = h.head - if hh in (:call, :macrocall) - e.args[2:end] - else - e.args - end +function arity(e::Expr)::Int + l = length(e.args) + is_function_call(e) ? l - 1 : l end -function maketerm(head::ExprHead, children; type=Any, metadata=nothing) - if !isempty(children) && first(children) isa Union{Function,DataType} - Expr(head.head, nameof(first(children)), @view(children[2:end])...) +function maketerm(T::Type{Expr}, head, children; is_call=true, type=Any, metadata=nothing) + if is_call + Expr(:call, head, children...) else - Expr(head.head, children...) + Expr(head, children...) end end + +maketerm(T::Type{Expr}, head::Union{Function,DataType}, children; is_call=true, type=Any, metadata=nothing) = + maketerm(T, nameof(head), children; is_call, type, metadata) + From 59da1f7f85d71125987ec6534d820ff163ce2962 Mon Sep 17 00:00:00 2001 From: a Date: Sun, 14 Jan 2024 17:55:58 +0100 Subject: [PATCH 11/13] adjust tests --- test/runtests.jl | 68 +++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 17c791c..498e53a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,32 +2,33 @@ using TermInterface, Test @testset "Expr" begin ex = :(f(a, b)) - @test head(ex) == ExprHead(:call) - @test children(ex) == [:f, :a, :b] - @test operation(ex) == :f - @test arguments(ex) == [:a, :b] - @test ex == maketerm(ExprHead(:call), [:f, :a, :b]) + @test istree(ex) + @test is_function_call(ex) + @test head(ex) == :f + @test children(ex) == [:a, :b] + @test ex == maketerm(Expr, :f, [:a, :b]) ex = :(arr[i, j]) - @test head(ex) == ExprHead(:ref) - @test operation(ex) == :ref - @test arguments(ex) == [:arr, :i, :j] - @test ex == maketerm(ExprHead(:ref), [:arr, :i, :j]) + @test istree(ex) + @test !is_function_call(ex) + @test head(ex) == :ref + @test children(ex) == [:arr, :i, :j] + @test ex == maketerm(Expr, :ref, [:arr, :i, :j]; is_call=false) ex = :(i, j) - @test head(ex) == ExprHead(:tuple) - @test operation(ex) == :tuple - @test arguments(ex) == [:i, :j] + @test istree(ex) + @test !is_function_call(ex) + @test head(ex) == :tuple @test children(ex) == [:i, :j] - @test ex == maketerm(ExprHead(:tuple), [:i, :j]) - + @test ex == maketerm(Expr, :tuple, [:i, :j]; is_call=false) ex = Expr(:block, :a, :b, :c) - @test head(ex) == ExprHead(:block) - @test operation(ex) == :block - @test children(ex) == arguments(ex) == [:a, :b, :c] - @test ex == maketerm(ExprHead(:block), [:a, :b, :c]) + @test istree(ex) + @test !is_function_call(ex) + @test head(ex) == :block + @test children(ex) == [:a, :b, :c] + @test ex == maketerm(Expr, :block, [:a, :b, :c]; is_call=false) end @testset "Custom Struct" begin @@ -35,23 +36,16 @@ end args Foo(args...) = new(args) end - struct FooHead - head - end - TermInterface.head(::Foo) = FooHead(:call) - TermInterface.head_symbol(q::FooHead) = q.head - TermInterface.operation(::Foo) = Foo TermInterface.istree(::Foo) = true - TermInterface.arguments(x::Foo) = [x.args...] - TermInterface.children(x::Foo) = [operation(x); x.args...] + TermInterface.is_function_call(::Foo) = true + TermInterface.head(::Foo) = Foo + TermInterface.children(x::Foo) = collect(x.args) t = Foo(1, 2) - @test head(t) == FooHead(:call) - @test head_symbol(head(t)) == :call - @test operation(t) == Foo - @test istree(t) == true - @test arguments(t) == [1, 2] - @test children(t) == [Foo, 1, 2] + @test istree(t) + @test is_function_call(t) + @test head(t) == Foo + @test children(t) == [1, 2] end @testset "Automatically Generated Methods" begin @@ -61,10 +55,8 @@ end end t = Bar(1, 2) - @test head(t) == BarHead(:call) - @test head_symbol(head(t)) == :call - @test operation(t) == Bar - @test istree(t) == true - @test arguments(t) == (1, 2) - @test children(t) == [Bar, 1, 2] + @test istree(t) + @test is_function_call(t) + @test head(t) == Bar + @test children(t) == (1, 2) end \ No newline at end of file From eef82732cf4fec153424a9689be67c396a7ef2c6 Mon Sep 17 00:00:00 2001 From: a Date: Wed, 24 Jan 2024 16:49:52 +0100 Subject: [PATCH 12/13] minimal example --- src/TermInterface.jl | 78 +++++++++++--------------------------------- src/expr.jl | 29 ---------------- test/runtests.jl | 61 +--------------------------------- 3 files changed, 20 insertions(+), 148 deletions(-) delete mode 100644 src/expr.jl diff --git a/src/TermInterface.jl b/src/TermInterface.jl index a192215..4b3b0ff 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -15,21 +15,12 @@ module TermInterface """ istree(x) -Returns `true` if `x` is a term. If true, `head`, `children` and +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 -""" - is_function_call(x) - -Returns true if a term abstractly represents a function call or function application. -Must be defined if `istree(x)` is defined. -Can be true only if `istree(x)` is true. -""" -function is_function_call end -export is_function_call """ symtype(x) @@ -56,30 +47,30 @@ export issym """ - head(x) + operation(x) -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 +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`. """ -function head end -export head +function operation end +export operation """ - children(x) + arguments(x) -Get the children of a term `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 children end -export children +function arguments end +export arguments """ - unsorted_children(x::T) + unsorted_arguments(x::T) If x is a term satisfying `istree(x)` and your term type `T` provides -and optimized implementation for storing the children, this function can -be used to retrieve the children when the order of arguments does not matter +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. """ unsorted_arguments(x) = arguments(x) @@ -89,10 +80,10 @@ export unsorted_arguments """ arity(x) -Returns the number of children of `x`. Implicitly defined -if `children(x)` is defined. +Returns the number of arguments of `x`. Implicitly defined +if `arguments(x)` is defined. """ -arity(x)::Int = length(children(x)) +arity(x)::Int = length(arguments(x)) export arity @@ -115,11 +106,11 @@ function metadata(x, data) end """ - maketerm(T::Type, head, children; is_call = true, type=Any, metadata=nothing) + maketerm(T::Type, operation, arguments; is_call = true, type=Any, metadata=nothing) 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 `head` as the head and `children` as the arguments, `type` as the symtype +with `operation` as the operation and `arguments` as the arguments, `type` as the symtype and `metadata` as the metadata. `is_call` is used to determine if the constructed term represents a function @@ -135,40 +126,9 @@ export maketerm 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 children(t), init in 0) + 1 : 1 +node_count(t) = istree(t) ? reduce(+, node_count(x) for x in arguments(t), init in 0) + 1 : 1 export node_count -""" - @matchable struct Foo fields... end [HeadType] - -Take a struct definition and automatically define `TermInterface` methods. -`is_function_call` of such type will default to `true`. -""" -macro matchable(expr) - @assert expr.head == :struct - name = expr.args[2] - if name isa Expr - name.head === :(<:) && (name = name.args[1]) - name isa Expr && name.head === :curly && (name = name.args[1]) - end - fields = filter(x -> x isa Symbol || (x isa Expr && x.head == :(::)), expr.args[3].args) - get_name(s::Symbol) = s - get_name(e::Expr) = (@assert(e.head == :(::)); e.args[1]) - fields = map(get_name, fields) - - quote - $expr - TermInterface.istree(::$name) = true - TermInterface.is_function_call(::$name) = true - TermInterface.head(::$name) = $name - TermInterface.children(x::$name) = getfield.((x,), ($(QuoteNode.(fields)...),)) - TermInterface.arity(x::$name) = $(length(fields)) - Base.length(x::$name) = $(length(fields) + 1) - end |> esc -end -export @matchable - -include("expr.jl") end # module diff --git a/src/expr.jl b/src/expr.jl deleted file mode 100644 index 3ec4434..0000000 --- a/src/expr.jl +++ /dev/null @@ -1,29 +0,0 @@ - -# This file contains default definitions for TermInterface methods on Julia -# Builtin Expr type. - -is_function_call(e::Expr) = _is_function_call_expr_head(e.head) -_is_function_call_expr_head(x::Symbol) = x in (:call, :macrocall) - -istree(x::Expr) = true - -# See https://docs.julialang.org/en/v1/devdocs/ast/ -head(e::Expr) = is_function_call(e) ? e.args[1] : e.head -children(e::Expr) = is_function_call(e) ? e.args[2:end] : e.args - -function arity(e::Expr)::Int - l = length(e.args) - is_function_call(e) ? l - 1 : l -end - -function maketerm(T::Type{Expr}, head, children; is_call=true, type=Any, metadata=nothing) - if is_call - Expr(:call, head, children...) - else - Expr(head, children...) - end -end - -maketerm(T::Type{Expr}, head::Union{Function,DataType}, children; is_call=true, type=Any, metadata=nothing) = - maketerm(T, nameof(head), children; is_call, type, metadata) - diff --git a/test/runtests.jl b/test/runtests.jl index 498e53a..c1fc239 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,62 +1,3 @@ using TermInterface, Test -@testset "Expr" begin - ex = :(f(a, b)) - @test istree(ex) - @test is_function_call(ex) - @test head(ex) == :f - @test children(ex) == [:a, :b] - @test ex == maketerm(Expr, :f, [:a, :b]) - - ex = :(arr[i, j]) - @test istree(ex) - @test !is_function_call(ex) - @test head(ex) == :ref - @test children(ex) == [:arr, :i, :j] - @test ex == maketerm(Expr, :ref, [:arr, :i, :j]; is_call=false) - - - ex = :(i, j) - @test istree(ex) - @test !is_function_call(ex) - @test head(ex) == :tuple - @test children(ex) == [:i, :j] - @test ex == maketerm(Expr, :tuple, [:i, :j]; is_call=false) - - ex = Expr(:block, :a, :b, :c) - @test istree(ex) - @test !is_function_call(ex) - @test head(ex) == :block - @test children(ex) == [:a, :b, :c] - @test ex == maketerm(Expr, :block, [:a, :b, :c]; is_call=false) -end - -@testset "Custom Struct" begin - struct Foo - args - Foo(args...) = new(args) - end - TermInterface.istree(::Foo) = true - TermInterface.is_function_call(::Foo) = true - TermInterface.head(::Foo) = Foo - TermInterface.children(x::Foo) = collect(x.args) - - t = Foo(1, 2) - @test istree(t) - @test is_function_call(t) - @test head(t) == Foo - @test children(t) == [1, 2] -end - -@testset "Automatically Generated Methods" begin - @matchable struct Bar - a - b::Int - end - - t = Bar(1, 2) - @test istree(t) - @test is_function_call(t) - @test head(t) == Bar - @test children(t) == (1, 2) -end \ No newline at end of file +@test true \ No newline at end of file From a3eacead14ec88aaccdf018f3cfbe0f24aa6ab41 Mon Sep 17 00:00:00 2001 From: a Date: Wed, 24 Jan 2024 16:51:27 +0100 Subject: [PATCH 13/13] adjust docstring --- src/TermInterface.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/TermInterface.jl b/src/TermInterface.jl index 4b3b0ff..8c2d0ab 100644 --- a/src/TermInterface.jl +++ b/src/TermInterface.jl @@ -106,16 +106,12 @@ function metadata(x, data) end """ - maketerm(T::Type, operation, arguments; is_call = true, type=Any, metadata=nothing) + maketerm(T::Type, operation, arguments; type=Any, metadata=nothing) 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. - -`is_call` is used to determine if the constructed term represents a function -call. If `is_call = true`, then it must construct a term `x` such that -`is_function_call(x) = true`, and vice-versa for `is_call = false`. """ function maketerm end export maketerm