diff --git a/Project.toml b/Project.toml index d45ea48..2513f7b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,37 +1,44 @@ name = "SimpleExpressions" uuid = "deba94f7-f32a-40ad-b45e-be020a5ded2f" authors = ["jverzani and contributors"] -version = "1.0.21" +version = "1.0.22" [deps] +Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c" [weakdeps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +Metatheory = "e9d8d322-4543-424a-9be4-0cc815abe26c" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" -TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c" [extensions] SimpleExpressionsAbstractTreesExt = "AbstractTrees" +SimpleExpressionsLatexifyExt = "Latexify" +SimpleExpressionsMetatheoryExt = "Metatheory" SimpleExpressionsRecipesBaseExt = "RecipesBase" SimpleExpressionsRootsExt = "Roots" SimpleExpressionsSpecialFunctionsExt = "SpecialFunctions" -SimpleExpressionsTermInterfaceExt = "TermInterface" [compat] AbstractTrees = "0.4" +Combinatorics = "1" +Latexify = "0.16, 1" +Metatheory = "3" RecipesBase = "1" Roots = "2" SpecialFunctions = "1,2" -TermInterface = "2" +TermInterface = "2,3" julia = "1.9" [extras] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +Metatheory = "e9d8d322-4543-424a-9be4-0cc815abe26c" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" -TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test","Metatheory"] diff --git a/docs/src/index.md b/docs/src/index.md index 1e5e419..303140c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -128,8 +128,7 @@ u = D(exp(x) * (sin(3x) + sin(101x))) No simplification is done so the expressions can quickly become unwieldy. There is an extension for `TermInterface` so rewriting of expressions, as is possible with the `Metatheory.jl` package is possible. For example, this pattern can factor out `exp(x)`: -``` -# @example expressions waiting on new Metatheory release +```@example expressions using Metatheory r = @rule (~x * ~a + ~x * ~b --> ~x * (~a + ~b)) r(u) diff --git a/ext/SimpleExpressionsLatexifyExt.jl b/ext/SimpleExpressionsLatexifyExt.jl new file mode 100644 index 0000000..b72b001 --- /dev/null +++ b/ext/SimpleExpressionsLatexifyExt.jl @@ -0,0 +1,10 @@ +module SimpleExpressionsLatexifyExt + +import SimpleExpressions +import Latexify + +Latexify.@latexrecipe function f(x::SimpleExpressions.AbstractSymbolic) + return string(x) +end + +end diff --git a/ext/SimpleExpressionsMetatheoryExt.jl b/ext/SimpleExpressionsMetatheoryExt.jl new file mode 100644 index 0000000..6a0b5e7 --- /dev/null +++ b/ext/SimpleExpressionsMetatheoryExt.jl @@ -0,0 +1,216 @@ +module SimpleExpressionsMetatheoryExt + +import SimpleExpressions +import SimpleExpressions: SymbolicNumber +import SimpleExpressions: permutations, combinations + +using Metatheory +using Metatheory.Library + +# Modified from MIT licensed SymbolicUtils.jl +# https://github.com/JuliaSymbolics/SymbolicUtils.jl/blob/master/src/rule.jl +struct ACRule{F,R} + sets::F + rule::R + arity::Int +end + +macro acrule(expr) + arity = length(expr.args[2].args[2:end]) + quote + ACRule(permutations, $(esc(:(@rule($(expr))))), $arity) + end +end + +macro ordered_acrule(expr) + arity = length(expr.args[2].args[2:end]) + quote + ACRule(combinations, $(esc(:(@rule($(expr))))), $arity) + end +end + +function (acr::ACRule)(term) + r = acr.rule + if !iscall(term) + r(term) + else + f = operation(term) + # # Assume that the matcher was formed by closing over a term + # if f != operation(r.lhs) # Maybe offer a fallback if m.term errors. + # return nothing + # end + + args = arguments(term) + + itr = acr.sets(eachindex(args), acr.arity) + + for inds in itr + result = r(f(args[inds]...)) #Term{T}(f, @views args[inds])) + if result !== nothing + # Assumption: inds are unique + length(args) == length(inds) && return result + return maketerm(typeof(term), f, [result, (args[i] for i in eachindex(args) if i ∉ inds)...], nothing) # metadata(term)) + end + end + end +end + +function SimpleExpressions.expand(ex::SimpleExpressions.SymbolicExpression) + + _expand_minus = @theory a b xs begin + -(a + b) => -a + -b + -1*(+(xs...)) => +(-xs...) + a - a => 0 + -a + a => 0 + end + + + + _expand_distributive = @theory x y z xs ys begin + z*(x + y) => z*x + z*y + (x + y) * z => z*x + z*y + # z * (+(xs...)) => sum(z*x for x in xs) + # +(xs...) * z --> + + z*(x - y) => z*x - z*y + (x - y) * z => z*x - z*y + + end + + _expand_binom = @theory x y n begin + (x + y)^1 => x + y + (x + y)^2 => x^2 + 2*x*y + y^2 + (x + y)^n::isinteger => sum(binomial(Int(n), k) * x^k * y^(n-k) for k in 0:Int(n)) + end + + _expand_trig = @theory a b begin + sin(2a) => 2sin(a)*cos(a) + sin(a + b) => sin(a)*cos(b) + cos(a)*sin(b) + cos(2a) => cos(a)^2 - sin(a)^2 + cos(a + b) => cos(a)*cos(b) - sin(a)*sin(b) + sec(a) => 1 / cos(a) + csc(a) => 1 / sin(a) + tan(a) => sin(a)/cos(a) + cot(a) => cos(a)/sin(a) + end + + + _expand_power = @theory x y a b begin + x^(a+b) => x^a*x^b + (x*y)^a => x^a * y^a + end + _expand_log = @theory x y n begin + log(x*y) => log(x) + log(y) + log(x^n) => n * log(x) + end + + _expand_misc = @theory a b begin + -a => (-1)*a + (1/a) * a => 1 + a * (1/a) => 1 + /(a,b) => *(a, b^(-1)) + end + + t = reduce(∪, ( + _expand_minus, + _expand_distributive, _expand_binom, _expand_trig, + _expand_power, _expand_log, + _expand_misc)) + + Metatheory.rewrite(ex, t) +end + + +function SimpleExpressions.simplify(ex::SimpleExpressions.SymbolicExpression) + + PLUS_DISTRIBUTE = [ + @acrule(*(~α, ~~x) + *(~β, ~~x) => *(~α + ~β, (~~x)...)) + @acrule(*(~~x, ~α) + *(~~x, ~β) => *(~α + ~β, (~~x)...)) + ] + + CANONICALIZE_TIMES = [ + #@rule(~x::isnotflat(*) => flatten_term(*, ~x)) + #@rule(~x::needs_sorting(*) => sort_args(*, ~x)) + + # @ordered_acrule(~a::is_literal_number * ~b::is_literal_number => ~a * ~b) + # @rule(*(~~x::hasrepeats) => *(merge_repeats(^, ~~x)...)) + + @acrule((~y)^(~n) * ~y => (~y)^(~n+1)) + + @ordered_acrule((~z::isone * ~x) => ~x) + @ordered_acrule((~z::iszero * ~x) => ~z) + @rule(*(~x) => ~x) + ] + + MUL_DISTRIBUTE = @ordered_acrule((~x)^(~n) * (~x)^(~m) => (~x)^(~n + ~m)) + + CANONICALIZE_POW = [ + @rule(^(*(~~x), ~y::isinteger) => *(map(a->pow(a, ~y), ~~x)...)) + @rule((((~x)^(~p::isinteger))^(~q::isinteger)) => (~x)^((~p)*(~q))) + @rule(^(~x, ~z::iszero) => 1) + @rule(^(~x, ~z::isone) => ~x) + @rule(inv(~x) => 1/(~x)) + ] + + POW_RULES = [ + @rule(^(~x::isone, ~z) => 1) + ] + + ASSORTED_RULES = [ + @rule(identity(~x) => ~x) + @rule(-(~x) => -1*~x) + @rule(-(~x, ~y) => ~x + -1(~y)) + @rule(~x::isone \ ~y => ~y) + @rule(~x \ ~y => ~y / (~x)) + @rule(one(~x) => 1) #one(symtype(~x))) + @rule(zero(~x) => 0) #zero(symtype(~x))) + @rule(conj(~x::isreal) => ~x) + @rule(real(~x::isreal) => ~x) + @rule(imag(~x::isreal) => 0)#zero(symtype(~x))) + # @rule(ifelse(~x::is_literal_number, ~y, ~z) => ~x ? ~y : ~z) + @rule(ifelse(~x, ~y, ~y) => ~y) + ] + + TRIG_EXP_RULES = [ + # @acrule(~r*~x::has_trig_exp + ~r*~y => ~r*(~x + ~y)) + # @acrule(~r*~x::has_trig_exp + -1*~r*~y => ~r*(~x - ~y)) + @acrule(sin(~x)^2 + cos(~x)^2 => one(~x)) + @acrule(sin(~x)^2 + -1 => -1*cos(~x)^2) + @acrule(cos(~x)^2 + -1 => -1*sin(~x)^2) + + @acrule(cos(~x)^2 + -1*sin(~x)^2 => cos(2 * ~x)) + @acrule(sin(~x)^2 + -1*cos(~x)^2 => -cos(2 * ~x)) + @acrule(cos(~x) * sin(~x) => sin(2 * ~x)/2) + + @acrule(tan(~x)^2 + -1*sec(~x)^2 => one(~x)) + @acrule(-1*tan(~x)^2 + sec(~x)^2 => one(~x)) + @acrule(tan(~x)^2 + 1 => sec(~x)^2) + @acrule(sec(~x)^2 + -1 => tan(~x)^2) + + @acrule(cot(~x)^2 + -1*csc(~x)^2 => one(~x)) + @acrule(cot(~x)^2 + 1 => csc(~x)^2) + @acrule(csc(~x)^2 + -1 => cot(~x)^2) + + @acrule(cosh(~x)^2 + -1*sinh(~x)^2 => one(~x)) + @acrule(cosh(~x)^2 + -1 => sinh(~x)^2) + @acrule(sinh(~x)^2 + 1 => cosh(~x)^2) + + @acrule(cosh(~x)^2 + sinh(~x)^2 => cosh(2 * ~x)) + @acrule(cosh(~x) * sinh(~x) => sinh(2 * ~x)/2) + + @acrule(exp(~x) * exp(~y) => _iszero(~x + ~y) ? 1 : exp(~x + ~y)) + @rule(exp(~x)^(~y) => exp(~x * ~y)) + ] + + t = vcat(PLUS_DISTRIBUTE, + MUL_DISTRIBUTE, + CANONICALIZE_POW, + POW_RULES, + ASSORTED_RULES, + TRIG_EXP_RULES) + + rewrite(ex, t) + +end + +end diff --git a/ext/SimpleExpressionsTermInterfaceExt.jl b/ext/SimpleExpressionsTermInterfaceExt.jl deleted file mode 100644 index c08e6d3..0000000 --- a/ext/SimpleExpressionsTermInterfaceExt.jl +++ /dev/null @@ -1,57 +0,0 @@ -module SimpleExpressionsTermInterfaceExt - -using SimpleExpressions - -import SimpleExpressions: AbstractSymbolic, Symbolic, SymbolicParameter, SymbolicExpression, SymbolicEquation - -using TermInterface - -#In other symbolic expression languages, such as SymbolicUtils.jl, the head of a node can correspond to operation and children can correspond to arguments. - -TermInterface.head(ex::SymbolicExpression) = operation(ex) -TermInterface.children(ex::SymbolicExpression) = arguments(ex) - -TermInterface.operation(X::SymbolicExpression) = X.op -TermInterface.arguments(X::SymbolicExpression) = collect(X.arguments) - - -TermInterface.iscall(ex::SymbolicExpression) = true -TermInterface.iscall(ex::AbstractSymbolic) = false - - -TermInterface.isexpr(::Symbolic) = false -TermInterface.isexpr(::SymbolicParameter) = false -TermInterface.isexpr(::AbstractSymbolic) = true - - - -function TermInterface.maketerm(T::Type{<:AbstractSymbolic}, head, children, metadata) - head(children...) -end - - -TermInterface.arity(::AbstractSymbolic) = 0 -TermInterface.arity(ex::SymbolicExpression) = length(ex.arguments) - -TermInterface.metadata(::AbstractSymbolic) = nothing - - -# convert from Expression to SimpleExpression -# all variables become `𝑥` except `p` becomes `𝑝`, a parameter -function SimpleExpressions.assymbolic(x::Expr) - body = _assymbolic(x) - eval(body) -end - -function _assymbolic(x) - if !TermInterface.istree(x) - isa(x, Symbol) && return x == :p ? :(SymbolicParameter(:𝑝)) : :(Symbolic(:𝑥)) - return x - end - - op = TermInterface.operation(x) - arguments = TermInterface.arguments(x) - Expr(:call, op, _assymbolic.(arguments)...) -end - -end diff --git a/src/SimpleExpressions.jl b/src/SimpleExpressions.jl index dfba4b1..a86b5c4 100644 --- a/src/SimpleExpressions.jl +++ b/src/SimpleExpressions.jl @@ -7,6 +7,9 @@ $(joinpath(@__DIR__, "..", "README.md") |> """ module SimpleExpressions +using Combinatorics +using TermInterface + export @symbolic @@ -194,9 +197,14 @@ abstract type AbstractSymbolic <: Function end struct Symbolic <: AbstractSymbolic x::Symbol end +Base.convert(::Type{<:AbstractSymbolic}, x::Symbol) = Symbolic(x) +Base.convert(::Type{<:AbstractSymbolic}, x::AbstractString) = Symbolic(Symbol(x)) + (X::Symbolic)(y, p=nothing) = subs(X,y,p) (X::Symbolic)() = X(nothing) + + # optional parameter struct SymbolicParameter <: AbstractSymbolic p::Symbol @@ -221,7 +229,7 @@ struct SymbolicExpression <: AbstractSymbolic end function (X::SymbolicExpression)(x, p=nothing) - ops = operation.(X.arguments) + ops = operation.(filter(x -> isa(x, SymbolicExpression), X.arguments)) X = subs(X, x, p) for op ∈ ops Base.Generator != op && continue # generators need to repeat... @@ -258,8 +266,44 @@ end Base.length(X::SymbolicEquation) = 2 ## ---- +TermInterface.operation(x::AbstractSymbolic) = nothing +TermInterface.operation(x::SymbolicExpression) = x.op +TermInterface.arguments(x::AbstractSymbolic) = nothing +TermInterface.arguments(x::SymbolicExpression) = collect(x.arguments) + +TermInterface.head(ex::SymbolicExpression) = operation(ex) +TermInterface.children(ex::SymbolicExpression) = arguments(ex) + +TermInterface.iscall(ex::SymbolicExpression) = true +TermInterface.iscall(ex::AbstractSymbolic) = false + + +TermInterface.isexpr(::Symbolic) = false +TermInterface.isexpr(::SymbolicParameter) = false +TermInterface.isexpr(::SymbolicNumber) = false +TermInterface.isexpr(::AbstractSymbolic) = true + +function TermInterface.maketerm(T::Type{<:AbstractSymbolic}, head, children, metadata) + head(children...) +end + + assymbolic(x::AbstractSymbolic) = x -assymbolic(x::Any) = SymbolicNumber(x) +assymbolic(x::Symbol) = Symbolic(x) +assymbolic(x::Number) = SymbolicNumber(x) +# convert from Expression to SimpleExpression +# all variables become `𝑥` except `p` becomes `𝑝`, a parameter +assymbolic(x::Expr) = eval(_assymbolic(x)) +function _assymbolic(x) + if !isexpr(x) + isa(x, Symbol) && return x == :p ? :(SymbolicParameter(:𝑝)) : :(Symbolic(:𝑥)) + return x + end + + op = operation(x) + arguments = arguments(x) + Expr(:call, op, _assymbolic.(arguments)...) +end issymbolic(x::AbstractSymbolic) = true issymbolic(::Any) = false @@ -281,15 +325,30 @@ function free_symbol(u::SymbolicExpression) a′ = free_symbol(a) isa(a′, Symbolic) && return a′ if isa(a′, SymbolicEquation) - u = free_symbol(a′) + a′′ = free_symbol(a′) isa(a′′, Symbolic) && return a′′ end end return nothing end -operation(x::SymbolicExpression) = x.op -operation(::Any) = nothing + +## ---- +""" + simplify(ex) + +Simplify expression using `Metatheory.jl` when that package is loaded +""" +simplify(x::AbstractSymbolic) = x # Metatheory.jl extension adds here +simplify(ex::SymbolicEquation) = SymbolicEquation(simplify.(ex)...) + +""" + expand(ex) + +Expand expression using `Metatheory.jl` when that package is loaded +""" +expand(x::AbstractSymbolic) = x # Metatheory.jl extension adds here +expand(ex::SymbolicEquation) = SymbolicEquation(expand.(ex)...) ## ---- @@ -313,14 +372,23 @@ function Base.show(io::IO, x::SymbolicExpression) show(io, only(arguments)) print(io, ")") else - a, b = arguments + n = length(arguments) + for (i, a) ∈ enumerate(arguments) + isa(a, SymbolicExpression) && a.op ∈ infix_ops && print(io, "(") + show(io, a) + isa(a, SymbolicExpression) && a.op ∈ infix_ops && print(io, ")") + i != n && print(io, " ", broadcast, string(op), " ") + end + #= + a, bs..., c = arguments isa(a, SymbolicExpression) && a.op ∈ infix_ops && print(io, "(") - show(io, first(arguments)) + show(io, a) isa(a, SymbolicExpression) && a.op ∈ infix_ops && print(io, ")") print(io, " ", broadcast, string(op), " ") isa(b, SymbolicExpression) && b.op ∈ infix_ops && print(io, "(") show(io, b) isa(b, SymbolicExpression) && b.op ∈ infix_ops && print(io, ")") + =# end elseif op == ifelse p,a,b = arguments @@ -368,20 +436,31 @@ subs(x::Symbolic, y::SymbolicNumber, p) = y # unary Base.:-(x::AbstractSymbolic) = SymbolicExpression(-, (x, )) # -function _commutative_op(op::typeof(+), x, y) - iszero(x) && return y - iszero(y) && return x - SymbolicExpression(+, _left_right(x,y)) -end +_isidentity(op::typeof(+), x) = iszero(x) +_isidentity(op::typeof(*), x) = isone(x) +_isidentity(op::Any, x) = false +_iszero(op::typeof(*), x) = iszero(x) +_iszero(op::Any, x) = false + +function _commutative_op(op::O, x, y) where {O <: Union{typeof(+), typeof(*)}} + _isidentity(op, x) && return y + _isidentity(op, y) && return x + _iszero(op,x) && return x + _iszero(op,y) && return y + + if (is_operation(op)(x) && is_operation(op)(y)) + args = tuplejoin(arguments(x), arguments(y)) + elseif (is_operation(op)(x) && !is_operation(op)(y)) + args = tuplejoin(arguments(x), (y,)) + elseif (!is_operation(op)(x) && is_operation(op)(y)) + args = tuplejoin((x,), arguments(y)) + else + args = (x,y) + end + return SymbolicExpression(op, args) -function _commutative_op(op::typeof(*), x, y) - isone(x) && return y - isone(y) && return x - (iszero(x) || iszero(y)) && return 0 - SymbolicExpression(*, _left_right(x,y)) end -# commutative binary; slight canonicalization # plans to incorporate simplify are WIP/DOA for op ∈ (:+, :*) @eval begin @@ -479,7 +558,7 @@ function _subs(::typeof(Base.broadcasted), args, y, p=nothing) end # only used for domain restrictions -Base.ifelse(p::AbstractSymbolic, a::Real, b::Real) = SymbolicExpression(ifelse, (p,a,b)) +Base.ifelse(p::AbstractSymbolic, a, b) = SymbolicExpression(ifelse, (p,a,b)) ## utils? Base.isequal(x::AbstractSymbolic, y::AbstractSymbolic) = hash(x) == hash(y) @@ -515,6 +594,44 @@ Base.convert(::Type{Expr}, x::SymbolicNumber) = x.x Base.convert(::Type{Expr}, x::SymbolicExpression) = Expr(:call, x.op, convert.(Expr, assymbolic.(x.arguments))...) +# isless +Base.isless(x::Symbolic, y::Symbolic) = isless(x.x, y.x) +Base.isless(x::Symbolic, y::SymbolicParameter) = isless(x.x, y.p) +Base.isless(x::SymbolicParameter, y::Symbolic) = isless(x.p, y.x) +Base.isless(x::SymbolicParameter, y::SymbolicParameter) = isless(x.p, y.p) + +Base.isless(x::SymbolicNumber, y::AbstractSymbolic) = true +Base.isless(x::AbstractSymbolic, y::SymbolicNumber) = false +Base.isless(x::SymbolicNumber, y::SymbolicNumber) = isless(x.x, y.x) + +Base.isless(x::SymbolicExpression, y::Symbolic) = false +Base.isless(x::Symbolic, y::SymbolicExpression) = !isless(y,x) +Base.isless(x::SymbolicExpression, y::SymbolicParameter) = false +Base.isless(x::SymbolicParameter, y::SymbolicExpression) = !isless(y, x) +Base.isless(x::SymbolicExpression, y::SymbolicNumber) = false +Base.isless(x::SymbolicNumber, y::SymbolicExpression) = !isless(y,x) + +Base.isless(x::AbstractSymbolic, y::Number) = false +Base.isless(x::Number, y::AbstractSymbolic) = true +op_val(f) = Base.operator_precedence(Symbol(f)) +function Base.isless(x::SymbolicExpression, y::SymbolicExpression) + xo, yo = op_val(operation(x)), op_val(operation(y)) + isless(xo,yo) && return true + isless(yo, xo) && return false + xc, yc = x.arguments, y.arguments + isless(length(xc), length(yc)) && return true + isless(length(yc), length(xc)) && return false + for (cx, cy) ∈ zip(xc, yc) + isless(cx, cy) && return true + isless(cy, cx) && return false + end + false +end +# tuplejoin (Discourse) +@inline tuplejoin(x) = x +@inline tuplejoin(x, y) = (x..., y...) +@inline tuplejoin(x, y, z...) = (x..., tuplejoin(y, z...)...) + ## includes include("scalar-derivative.jl") diff --git a/test/basic_tests.jl b/test/basic_tests.jl index e48bc8c..acabbc5 100644 --- a/test/basic_tests.jl +++ b/test/basic_tests.jl @@ -62,11 +62,11 @@ @symbolic x p @test repr(2x) == "2 * x" - @test repr(x*2) == "2 * x" + @test repr(x*2) == "x * 2" @test repr(x / 2) == "x / 2" - @test repr((x+2) / 2) == "(2 + x) / 2" - @test repr(x / (x+2)) == "x / (2 + x)" + @test repr((x+2) / 2) == "(x + 2) / 2" + @test repr(x / (x+2)) == "x / (x + 2)" @test repr(x .- sum(x)/length(x)) == "x .- (sum(x) / length(x))" # parens around expressions, like `sum(x)`. @test repr((1+x)^2) == "(1 + x) ^ 2" diff --git a/test/extension_tests.jl b/test/extension_tests.jl index 33dd699..e83d019 100644 --- a/test/extension_tests.jl +++ b/test/extension_tests.jl @@ -3,19 +3,9 @@ using SimpleExpressions @symbolic x p using Metatheory -#= -need the following -[compat] -Metatheory = "3" - -[extras] -Metatheory = "e9d8d322-4543-424a-9be4-0cc815abe26c" - -[targets] -test = ["Test","Metatheory"] -=# +@testset "Metatheory" begin r = @rule sin(2(~x)) --> 2sin(~x)*cos(~x) @test r(sin(2x)) === 2*sin(x) * cos(x) @@ -27,3 +17,5 @@ end @test isequal(rewrite(x + x, t), 2x) @test isequal(rewrite(x/x, t), 1) @test isequal(rewrite(1*x, t), x) + +end