From 66026d6f2e7b50250d05625b111836d9d9dd1842 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Tue, 3 Jun 2025 17:30:42 +0200 Subject: [PATCH 1/8] first prototype of default value rules implemented --- src/matchers.jl | 82 +++++++++++++++++++++++++++++++++++++++++---- src/rule.jl | 89 ++++++++++++++++++++++++++++++++++++++++--------- test/rewrite.jl | 14 ++++++++ 3 files changed, 162 insertions(+), 23 deletions(-) diff --git a/src/matchers.jl b/src/matchers.jl index 7f4dea537..7911cd030 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -5,25 +5,51 @@ # 2. Dictionary # 3. Callback: takes arguments Dictionary × Number of elements matched # + function matcher(val::Any) - iscall(val) && return term_matcher(val) + # if val is a call (like an operation) creates a term matcher or term matcher with defslot + if iscall(val) + # if has two arguments and one of them is a DefSlot, create a term matcher with defslot + if length(arguments(val)) == 2 && any(x -> isa(x, DefSlot), arguments(val)) + return term_matcher_defslot(val) + # else return a normal term matcher + else + return term_matcher(val) + end + end + function literal_matcher(next, data, bindings) + # car data is the first element of data islist(data) && isequal(car(data), val) ? next(bindings, 1) : nothing end end function matcher(slot::Slot) function slot_matcher(next, data, bindings) - !islist(data) && return + !islist(data) && return nothing val = get(bindings, slot.name, nothing) + # if slot name already is in bindings, check if it matches if val !== nothing if isequal(val, car(data)) return next(bindings, 1) end - else - if slot.predicate(car(data)) - next(assoc(bindings, slot.name, car(data)), 1) + # elseif the first element of data matches the slot predicate, add it to bindings and call next + elseif slot.predicate(car(data)) + next(assoc(bindings, slot.name, car(data)), 1) + end + end +end + +function matcher(defslot::DefSlot) + function defslot_matcher(next, data, bindings) + !islist(data) && return + val = get(bindings, defslot.name, nothing) + if val !== nothing + if isequal(val, car(data)) + return next(bindings, 1) end + elseif defslot.predicate(car(data)) + next(assoc(bindings, defslot.name, car(data)), 1) end end end @@ -86,10 +112,52 @@ end function term_matcher(term) matchers = (matcher(operation(term)), map(matcher, arguments(term))...,) + function term_matcher(success, data, bindings) + !islist(data) && return nothing # if data is not a list, return nothing + !iscall(car(data)) && return nothing # if first element is not a call, return nothing - !islist(data) && return nothing - !iscall(car(data)) && return nothing + function loop(term, bindings′, matchers′) # Get it to compile faster + if !islist(matchers′) + if !islist(term) + return success(bindings′, 1) + end + return nothing + end + car(matchers′)(term, bindings′) do b, n + loop(drop_n(term, n), b, cdr(matchers′)) + end + # explenation of above 3 lines: + # car(matchers′)(b,n -> loop(drop_n(term, n), b, cdr(matchers′)), term, bindings′) + # ------- next(b,n) ----------------------------- + # car = first element of list, cdr = rest of the list, drop_n = drop first n elements of list + # Calls the first matcher, with the "next" function being loop again but with n terms dropepd from term + # Term is a linked list (a list and a index). drop n advances the index. when the index sorpasses + # the length of the list, is considered empty + end + + loop(car(data), bindings, matchers) # Try to eat exactly one term + end +end + + +# ~x + ~!y +function term_matcher_defslot(term) + matchers = (matcher(operation(term)), map(matcher, arguments(term))...) # create matchers for the operation and arguments of the term + + function term_matcher(success, data, bindings) + + !islist(data) && return nothing # if data is not a list, return nothing + if !iscall(car(data)) + a = arguments(term) + slot = a[findfirst(x -> isa(x, Slot), a)] # find the first slot in the term + defslot = a[findfirst(x -> isa(x, DefSlot), a)] # find the first defslot in the term + + bindings = assoc(bindings, slot.name, car(data)) + bindings = assoc(bindings, defslot.name, defslot.default) + + return success(bindings, 1) # if first element is not a call, return success with bindings and 1 + end function loop(term, bindings′, matchers′) # Get it to compile faster if !islist(matchers′) diff --git a/src/rule.jl b/src/rule.jl index 5de0aa79c..59066ee6c 100644 --- a/src/rule.jl +++ b/src/rule.jl @@ -16,6 +16,71 @@ Base.isequal(s1::Slot, s2::Slot) = s1.name == s2.name Base.show(io::IO, s::Slot) = (print(io, "~"); print(io, s.name)) +# for when the slot is a symbol, like `~x` +makeslot(s::Symbol, keys) = (push!(keys, s); Slot(s)) + +# for when the slot is an expression, like `~x::predicate` +function makeslot(s::Expr, keys) + if !(s.head == :(::)) + error("Syntax for specifying a slot is ~x::\$predicate, where predicate is a boolean function") + end + + name = s.args[1] + + push!(keys, name) + :(Slot($(QuoteNode(name)), $(esc(s.args[2])))) +end + +# matches one term with built in default value. +# syntax: ~!x +# Example usage: +# (~!x + ~y) can match (a + b) but also just "a" and x takes default value of zero. +# (~!x)*(~y) can match a*b but also just "a", and x takes default value of one. +# (~x + ~y)^(~!z) can match (a + b)^c but also just "a + b", and z takes default value of one. +# only these three operations are supported for default values. +# Note that the default value is not used in the consequent, it is only used to match the pattern. + +struct DefSlot{P, D} + name::Symbol + predicate::P + default::D +end + +DefSlot(s) = DefSlot(s, alwaystrue, nothing) +Base.isequal(s1::DefSlot, s2::DefSlot) = s1.name == s2.name +Base.show(io::IO, s::DefSlot) = (print(io, "~!"); print(io, s.name)) + +makeDefSlot(s::Symbol, keys, default) = (push!(keys, s); DefSlot(s, alwaystrue, default)) + +function makeDefSlot(s::Expr, keys, default) + if !(s.head == :(::)) + error("Syntax for specifying a default slot is ~!x::\$predicate, where predicate is a boolean function") + end + + name = s.args[1] + + push!(keys, name) + :(DefSlot($(QuoteNode(name)), $(esc(s.args[2])), $(esc(default)))) +end + +# parent | default +# + | 0 +# * | 1 +# ^ | 1 +function defaultValOfCall(call) + if call == :+ + return 0 + elseif call == :* + return 1 + elseif call == :^ + return 1 + end + + return nothing # no default value for this call +end + + + # matches zero or more terms # syntax: ~~x struct Segment{F} @@ -37,37 +102,29 @@ function makesegment(s::Expr, keys) end name = s.args[1] - + push!(keys, name) :(Segment($(QuoteNode(name)), $(esc(s.args[2])))) end -makeslot(s::Symbol, keys) = (push!(keys, s); Slot(s)) - -function makeslot(s::Expr, keys) - if !(s.head == :(::)) - error("Syntax for specifying a slot is ~x::\$predicate, where predicate is a boolean function") - end - - name = s.args[1] - - push!(keys, name) - :(Slot($(QuoteNode(name)), $(esc(s.args[2])))) -end - -function makepattern(expr, keys) +# parent call is needed to know which default value to give if any default slots are present +function makepattern(expr, keys, parentCall=nothing) if expr isa Expr if expr.head === :call if expr.args[1] === :(~) if expr.args[2] isa Expr && expr.args[2].args[1] == :(~) # matches ~~x::predicate makesegment(expr.args[2].args[2], keys) + elseif expr.args[2] isa Expr && expr.args[2].args[1] == :(!) + # matches ~!x::predicate + makeDefSlot(expr.args[2].args[2], keys, defaultValOfCall(parentCall)) else # matches ~x::predicate makeslot(expr.args[2], keys) end else - :(term($(map(x->makepattern(x, keys), expr.args)...); type=Any)) + # make a pattern for every argument of the expr. + :(term($(map(x->makepattern(x, keys, operation(expr)), expr.args)...); type=Any)) end elseif expr.head === :ref :(term(getindex, $(map(x->makepattern(x, keys), expr.args)...); type=Any)) diff --git a/test/rewrite.jl b/test/rewrite.jl index c2e920f9b..1d5b8b212 100644 --- a/test/rewrite.jl +++ b/test/rewrite.jl @@ -47,6 +47,20 @@ end @eqtest @rule(+(~~x,~y,~~x) => (~~x, ~y, ~~x))(term(+,6,type=Any)) == ([], 6, []) end +@testset "Slot matcher with default value" begin + r_sum = @rule (~x + ~!y)^2 => ~y + @test r_sum((a + b)^2) === b + @test r_sum(b^2) === 0 + + r_mult = @rule (~x * ~!y + ~z) => ~y + @test r_mult(c + a*b) === b + @test r_mult(c + b) === 1 + + r_pow = @rule (~x + ~y)^(~!m) => ~m + @test r_pow((a + b)^2) === 2 + @test r_pow(a + b) === 1 +end + using SymbolicUtils: @capture @testset "Capture form" begin From b9da16193d03bb3bcfaad450a8a20e94ce92a4a6 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Wed, 4 Jun 2025 12:03:21 +0200 Subject: [PATCH 2/8] added matching capapbility for optional operation with a tree of expressions --- src/matchers.jl | 75 ++++++++++++++++++++++++++++--------------------- src/rule.jl | 54 ++++++++++++++++++++--------------- test/rewrite.jl | 35 ++++++++++++++++++----- 3 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/matchers.jl b/src/matchers.jl index 7911cd030..427c36c49 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -11,10 +11,10 @@ function matcher(val::Any) if iscall(val) # if has two arguments and one of them is a DefSlot, create a term matcher with defslot if length(arguments(val)) == 2 && any(x -> isa(x, DefSlot), arguments(val)) - return term_matcher_defslot(val) + return defslot_term_matcher_constructor(val) # else return a normal term matcher else - return term_matcher(val) + return term_matcher_constructor(val) end end @@ -40,18 +40,11 @@ function matcher(slot::Slot) end end +# this is called only when defslot_term_matcher finds the operation and tries +# to match it, so no default value used. So the same function as slot_matcher +# can be used function matcher(defslot::DefSlot) - function defslot_matcher(next, data, bindings) - !islist(data) && return - val = get(bindings, defslot.name, nothing) - if val !== nothing - if isequal(val, car(data)) - return next(bindings, 1) - end - elseif defslot.predicate(car(data)) - next(assoc(bindings, defslot.name, car(data)), 1) - end - end + matcher(Slot(defslot.name, defslot.predicate)) end # returns n == offset, 0 if failed @@ -110,7 +103,7 @@ function matcher(segment::Segment) end end -function term_matcher(term) +function term_matcher_constructor(term) matchers = (matcher(operation(term)), map(matcher, arguments(term))...,) function term_matcher(success, data, bindings) @@ -129,7 +122,7 @@ function term_matcher(term) end # explenation of above 3 lines: # car(matchers′)(b,n -> loop(drop_n(term, n), b, cdr(matchers′)), term, bindings′) - # ------- next(b,n) ----------------------------- + # <------ next(b,n) ----------------------------> # car = first element of list, cdr = rest of the list, drop_n = drop first n elements of list # Calls the first matcher, with the "next" function being loop again but with n terms dropepd from term # Term is a linked list (a list and a index). drop n advances the index. when the index sorpasses @@ -140,25 +133,43 @@ function term_matcher(term) end end - -# ~x + ~!y -function term_matcher_defslot(term) - matchers = (matcher(operation(term)), map(matcher, arguments(term))...) # create matchers for the operation and arguments of the term - - function term_matcher(success, data, bindings) - - !islist(data) && return nothing # if data is not a list, return nothing - if !iscall(car(data)) - a = arguments(term) - slot = a[findfirst(x -> isa(x, Slot), a)] # find the first slot in the term - defslot = a[findfirst(x -> isa(x, DefSlot), a)] # find the first defslot in the term - - bindings = assoc(bindings, slot.name, car(data)) - bindings = assoc(bindings, defslot.name, defslot.default) - - return success(bindings, 1) # if first element is not a call, return success with bindings and 1 +# creates a matcher for a term containing a defslot, such as: +# (~x + ...complicated pattern...) * ~!y +# normal part (can bee a tree) operation defslot part + +# defslot_term_matcher works like this: +# checks wether data starts with the default operation. +# if yes (1): continues like term_matcher +# if no checks wether data matches the normal part +# if no returns nothing, rule is not applied +# if yes (2): adds the pair (default value name, default value) to the found bindings and +# calls the success function like term_matcher would do + +function defslot_term_matcher_constructor(term) + a = arguments(term) # lenght two bc defslot term matcher is allowed only with +,* and ^, that accept two arguments + matchers = (matcher(operation(term)), map(matcher, a)...) # create matchers for the operation and the two arguments of the term + + defslot_index = findfirst(x -> isa(x, DefSlot), a) # find the defslot in the term + defslot = a[defslot_index] + + function defslot_term_matcher(success, data, bindings) + # if data is not a list, return nothing + !islist(data) && return nothing + # if data (is not a tree and is just a symbol) or (is a tree not starting with the default operation) + if !iscall(car(data)) || (istree(car(data)) && string(defslot.operation) != string(operation(car(data)))) + other_part_matcher = matchers[defslot_index==2 ? 2 : 3] # find the matcher of the normal part + + # checks wether it matches the normal part + # <-----------------(2)-------------------------------> + bindings = other_part_matcher((b,n) -> assoc(b, defslot.name, defslot.defaultValue), data, bindings) + + if bindings === nothing + return nothing + end + return success(bindings, 1) end + # (1) function loop(term, bindings′, matchers′) # Get it to compile faster if !islist(matchers′) if !islist(term) diff --git a/src/rule.jl b/src/rule.jl index 59066ee6c..194fd2576 100644 --- a/src/rule.jl +++ b/src/rule.jl @@ -1,7 +1,7 @@ @inline alwaystrue(x) = true -# Matcher patterns with Slot and Segment +# Matcher patterns with Slot, DefSlot and Segment # matches one term # syntax: ~x @@ -31,6 +31,11 @@ function makeslot(s::Expr, keys) :(Slot($(QuoteNode(name)), $(esc(s.args[2])))) end + + + + + # matches one term with built in default value. # syntax: ~!x # Example usage: @@ -38,21 +43,37 @@ end # (~!x)*(~y) can match a*b but also just "a", and x takes default value of one. # (~x + ~y)^(~!z) can match (a + b)^c but also just "a + b", and z takes default value of one. # only these three operations are supported for default values. -# Note that the default value is not used in the consequent, it is only used to match the pattern. -struct DefSlot{P, D} +struct DefSlot{P, O} name::Symbol predicate::P - default::D + operation::O + defaultValue::Real +end + +# operation | default +# + | 0 +# * | 1 +# ^ | 1 +function defaultValOfCall(call) + if call == :+ + return 0 + elseif call == :* + return 1 + elseif call == :^ + return 1 + end + # else no default value for this call + return nothing end -DefSlot(s) = DefSlot(s, alwaystrue, nothing) +DefSlot(s) = DefSlot(s, alwaystrue, nothing, 0) Base.isequal(s1::DefSlot, s2::DefSlot) = s1.name == s2.name Base.show(io::IO, s::DefSlot) = (print(io, "~!"); print(io, s.name)) -makeDefSlot(s::Symbol, keys, default) = (push!(keys, s); DefSlot(s, alwaystrue, default)) +makeDefSlot(s::Symbol, keys, op) = (push!(keys, s); DefSlot(s, alwaystrue, op, defaultValOfCall(op))) -function makeDefSlot(s::Expr, keys, default) +function makeDefSlot(s::Expr, keys, op) if !(s.head == :(::)) error("Syntax for specifying a default slot is ~!x::\$predicate, where predicate is a boolean function") end @@ -60,24 +81,11 @@ function makeDefSlot(s::Expr, keys, default) name = s.args[1] push!(keys, name) - :(DefSlot($(QuoteNode(name)), $(esc(s.args[2])), $(esc(default)))) + tmp = defaultValOfCall(op) + :(DefSlot($(QuoteNode(name)), $(esc(s.args[2])), $(esc(op))), $(esc(tmp))) end -# parent | default -# + | 0 -# * | 1 -# ^ | 1 -function defaultValOfCall(call) - if call == :+ - return 0 - elseif call == :* - return 1 - elseif call == :^ - return 1 - end - return nothing # no default value for this call -end @@ -117,7 +125,7 @@ function makepattern(expr, keys, parentCall=nothing) makesegment(expr.args[2].args[2], keys) elseif expr.args[2] isa Expr && expr.args[2].args[1] == :(!) # matches ~!x::predicate - makeDefSlot(expr.args[2].args[2], keys, defaultValOfCall(parentCall)) + makeDefSlot(expr.args[2].args[2], keys, parentCall) else # matches ~x::predicate makeslot(expr.args[2], keys) diff --git a/test/rewrite.jl b/test/rewrite.jl index 1d5b8b212..c7073db66 100644 --- a/test/rewrite.jl +++ b/test/rewrite.jl @@ -52,13 +52,34 @@ end @test r_sum((a + b)^2) === b @test r_sum(b^2) === 0 - r_mult = @rule (~x * ~!y + ~z) => ~y - @test r_mult(c + a*b) === b - @test r_mult(c + b) === 1 - - r_pow = @rule (~x + ~y)^(~!m) => ~m - @test r_pow((a + b)^2) === 2 - @test r_pow(a + b) === 1 + r_mult = @rule ~x * ~!y => ~y + @test r_mult(a * b) === b + @test r_mult(a) === 1 + + r_mult2 = @rule (~x * ~!y + ~z) => ~y + @test r_mult2(c + a*b) === b + @test r_mult2(c + b) === 1 + + # here the "normal part" in the defslot_term_matcher is not a symbol but a tree + r_mult3 = @rule (~!x)*(~y + ~z) => ~x + @test r_mult3(a*(c+2)) === a + @test r_mult3(2*(c+2)) === 2 + @test r_mult3(c+2) === 1 + + r_pow = @rule (~x)^(~!m) => ~m + @test r_pow(a^(b+1)) === b+1 + @test r_pow(a) === 1 + @test r_pow(a+1) === 1 + + # here the "normal part" in the defslot_term_matcher is not a symbol but a tree + r_pow2 = @rule (~x + ~y)^(~!m) => ~m + @test r_pow2((a+b)^c) === c + @test r_pow2(a+b) === 1 + + r_mix = @rule (~x + (~y)*(~!c))^(~!m) => ~m + ~c + @test r_mix((a + b*c)^d) === c + d + @test r_mix((a + b*c)) === 1 + c + @test r_mix((a + b)) === 2 #1+1 end using SymbolicUtils: @capture From 1f1e52f02677fa2ff4161462902331d558f96bcd Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Thu, 5 Jun 2025 22:02:26 +0200 Subject: [PATCH 3/8] check for same operation made with nameof() and not with strings --- src/matchers.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matchers.jl b/src/matchers.jl index 427c36c49..6fcbdb593 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -156,7 +156,7 @@ function defslot_term_matcher_constructor(term) # if data is not a list, return nothing !islist(data) && return nothing # if data (is not a tree and is just a symbol) or (is a tree not starting with the default operation) - if !iscall(car(data)) || (istree(car(data)) && string(defslot.operation) != string(operation(car(data)))) + if !iscall(car(data)) || (istree(car(data)) && nameof(operation(car(data))) != defslot.operation) other_part_matcher = matchers[defslot_index==2 ? 2 : 3] # find the matcher of the normal part # checks wether it matches the normal part From 4e04a9b55d8f0d55fdbdd8aa086fa6fa080cf467 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Thu, 5 Jun 2025 22:03:04 +0200 Subject: [PATCH 4/8] symbol d was not definied in this testeset resulting in a error --- test/rewrite.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rewrite.jl b/test/rewrite.jl index c7073db66..b8996f79e 100644 --- a/test/rewrite.jl +++ b/test/rewrite.jl @@ -77,7 +77,7 @@ end @test r_pow2(a+b) === 1 r_mix = @rule (~x + (~y)*(~!c))^(~!m) => ~m + ~c - @test r_mix((a + b*c)^d) === c + d + @test r_mix((a + b*c)^2) === 2 + c @test r_mix((a + b*c)) === 1 + c @test r_mix((a + b)) === 2 #1+1 end From 4226dca2b31d2ae970b656cff7f6cb4d2089c703 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Thu, 5 Jun 2025 22:16:56 +0200 Subject: [PATCH 5/8] added error when using default slow with not supported operation --- src/rule.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rule.jl b/src/rule.jl index 194fd2576..d20598b2c 100644 --- a/src/rule.jl +++ b/src/rule.jl @@ -64,7 +64,7 @@ function defaultValOfCall(call) return 1 end # else no default value for this call - return nothing + error("You can use default slots only with +, * and ^, but you tried with: $call") end DefSlot(s) = DefSlot(s, alwaystrue, nothing, 0) From 2f5ae405d829d9ee163241d878c16b9659c72063 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Thu, 12 Jun 2025 16:03:30 +0200 Subject: [PATCH 6/8] istree` is deprecated, I use `iscall` instead --- src/matchers.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matchers.jl b/src/matchers.jl index 6fcbdb593..99b76ea18 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -156,7 +156,7 @@ function defslot_term_matcher_constructor(term) # if data is not a list, return nothing !islist(data) && return nothing # if data (is not a tree and is just a symbol) or (is a tree not starting with the default operation) - if !iscall(car(data)) || (istree(car(data)) && nameof(operation(car(data))) != defslot.operation) + if !iscall(car(data)) || (iscall(car(data)) && nameof(operation(car(data))) != defslot.operation) other_part_matcher = matchers[defslot_index==2 ? 2 : 3] # find the matcher of the normal part # checks wether it matches the normal part From 84e600a5ff60f659fb3e5c55c1ed9765cc74bc37 Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Sat, 14 Jun 2025 18:52:35 +0200 Subject: [PATCH 7/8] first version, really caothic, and doesn't work with defslot powers --- src/matchers.jl | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ test/rewrite.jl | 17 ++++++++++--- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/matchers.jl b/src/matchers.jl index 99b76ea18..1369d0f07 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -10,8 +10,12 @@ function matcher(val::Any) # if val is a call (like an operation) creates a term matcher or term matcher with defslot if iscall(val) # if has two arguments and one of them is a DefSlot, create a term matcher with defslot + # just two arguments bc defslot is only supported with operations with two args: *, ^, + if length(arguments(val)) == 2 && any(x -> isa(x, DefSlot), arguments(val)) return defslot_term_matcher_constructor(val) + # else (a)^(b) can also match 1/( (a)^(b) ) , just with b of oppsite sign + elseif operation(val) == ^ + return neg_pow_term_matcher_constructor(val) # else return a normal term matcher else return term_matcher_constructor(val) @@ -40,6 +44,20 @@ function matcher(slot::Slot) end end +function opposite_sign_matcher(slot::Slot) + function slot_matcher(next, data, bindings) + !islist(data) && return nothing + val = get(bindings, slot.name, nothing) + if val !== nothing + if isequal(val, car(data)) + return next(bindings, 1) + end + elseif slot.predicate(car(data)) + next(assoc(bindings, slot.name, -car(data)), 1) # this - is the only differenct wrt matcher(slot::Slot) + end + end +end + # this is called only when defslot_term_matcher finds the operation and tries # to match it, so no default value used. So the same function as slot_matcher # can be used @@ -133,6 +151,51 @@ function term_matcher_constructor(term) end end +# (a)^(b) can also match 1/( (a)^(b) ) , just with b of oppsite sign +function neg_pow_term_matcher_constructor(term) + matchers = (matcher(operation(term)), map(matcher, arguments(term))...,) + + function neg_pow_term_matcher(success, data, bindings) + !islist(data) && return nothing # if data is not a list, return nothing + !iscall(car(data)) && return nothing # if first element is not a call, return nothing + + function loop(term, bindings′, matchers′) + if !islist(matchers′) + if !islist(term) + return success(bindings′, 1) + end + return nothing + end + car(matchers′)(term, bindings′) do b, n + loop(drop_n(term, n), b, cdr(matchers′)) + end + end + + result = loop(car(data), bindings, matchers) + # if data is of the form 1/(...)^(...), it might match with negative exponent + if result === nothing && (operation(car(data))==/) && arguments(car(data))[1]==1 && iscall(arguments(car(data))[2]) && (operation(arguments(car(data))[2])==^) + denominator = arguments(car(data))[2] + # let's say data = a^b with a and b can be whatever + # if b is not a number then call the loop function with a^-b + if !isa(arguments(denominator)[2], Number) + frankestein = arguments(denominator)[1] ^ -(arguments(denominator)[2]) + result = loop(frankestein, bindings, matchers) + else + # if b is a number, like 3, we cant call loop with a^-3 bc it + # will automatically transform into 1/a^3. Therfore we need to + # create a matcher that flips the sign of the exponent. I created + # this matecher just for `Slot`s and not for terms, because if b + # is a number and not a call, certainly doesn't match a term (I hope). + if isa(arguments(term)[2], Slot) + matchers2 = (matcher(operation(term)), matcher(arguments(term)[1]), opposite_sign_matcher(arguments(term)[2])) # is this ok to be here or should it be outside neg_pow_term_matcher? + result = loop(denominator, bindings, matchers2) + end + end + end + result + end +end + # creates a matcher for a term containing a defslot, such as: # (~x + ...complicated pattern...) * ~!y # normal part (can bee a tree) operation defslot part diff --git a/test/rewrite.jl b/test/rewrite.jl index b8996f79e..e0eadd2fa 100644 --- a/test/rewrite.jl +++ b/test/rewrite.jl @@ -77,9 +77,20 @@ end @test r_pow2(a+b) === 1 r_mix = @rule (~x + (~y)*(~!c))^(~!m) => ~m + ~c - @test r_mix((a + b*c)^2) === 2 + c - @test r_mix((a + b*c)) === 1 + c - @test r_mix((a + b)) === 2 #1+1 + @test r_mix((a + b*c)^2) === (2, c) + @test r_mix((a + b*c)) === (1, c) + @test r_mix((a + b)) === (1, 1) +end + +@testset "1/power matches power with exponent of opposite sign" begin + r1 = @rule (~x)^(~y) => (~x, ~y) # rule with slot as exponent + @test r1(1/a^b) === (a, -b) # uses frankestein + @test r1(1/a^(b+2c)) === (a, -b-2c) # uses frankestein + @test r1(1/a^2) === (a, -2) # uses opposite_sign_matcher + + r2 = @rule (~x)^(~y + ~z) => (~x, ~y, ~z) # rule with term as exponent + @test r2(1/a^(b+2c)) === (a, -b, -2c) # uses frankestein + @test r2(1/a^3) === nothing # should use a term_matcher that flips the sign, but is not implemented end using SymbolicUtils: @capture From dc942329fe8709b13ddef7c6875d7c382645bbae Mon Sep 17 00:00:00 2001 From: Bumblebee00 Date: Sat, 14 Jun 2025 19:34:29 +0200 Subject: [PATCH 8/8] second version, really caothic, but works with defslotpowers --- src/matchers.jl | 143 ++++++++++++++++++++++++++---------------------- test/rewrite.jl | 6 ++ 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/src/matchers.jl b/src/matchers.jl index 1369d0f07..e5ace3a10 100644 --- a/src/matchers.jl +++ b/src/matchers.jl @@ -13,9 +13,6 @@ function matcher(val::Any) # just two arguments bc defslot is only supported with operations with two args: *, ^, + if length(arguments(val)) == 2 && any(x -> isa(x, DefSlot), arguments(val)) return defslot_term_matcher_constructor(val) - # else (a)^(b) can also match 1/( (a)^(b) ) , just with b of oppsite sign - elseif operation(val) == ^ - return neg_pow_term_matcher_constructor(val) # else return a normal term matcher else return term_matcher_constructor(val) @@ -58,6 +55,10 @@ function opposite_sign_matcher(slot::Slot) end end +function opposite_sign_matcher(defslot::DefSlot) + opposite_sign_matcher(Slot(defslot.name, defslot.predicate)) +end + # this is called only when defslot_term_matcher finds the operation and tries # to match it, so no default value used. So the same function as slot_matcher # can be used @@ -147,48 +148,27 @@ function term_matcher_constructor(term) # the length of the list, is considered empty end - loop(car(data), bindings, matchers) # Try to eat exactly one term - end -end - -# (a)^(b) can also match 1/( (a)^(b) ) , just with b of oppsite sign -function neg_pow_term_matcher_constructor(term) - matchers = (matcher(operation(term)), map(matcher, arguments(term))...,) - - function neg_pow_term_matcher(success, data, bindings) - !islist(data) && return nothing # if data is not a list, return nothing - !iscall(car(data)) && return nothing # if first element is not a call, return nothing - - function loop(term, bindings′, matchers′) - if !islist(matchers′) - if !islist(term) - return success(bindings′, 1) - end - return nothing - end - car(matchers′)(term, bindings′) do b, n - loop(drop_n(term, n), b, cdr(matchers′)) - end - end - result = loop(car(data), bindings, matchers) - # if data is of the form 1/(...)^(...), it might match with negative exponent - if result === nothing && (operation(car(data))==/) && arguments(car(data))[1]==1 && iscall(arguments(car(data))[2]) && (operation(arguments(car(data))[2])==^) - denominator = arguments(car(data))[2] - # let's say data = a^b with a and b can be whatever - # if b is not a number then call the loop function with a^-b - if !isa(arguments(denominator)[2], Number) - frankestein = arguments(denominator)[1] ^ -(arguments(denominator)[2]) - result = loop(frankestein, bindings, matchers) - else - # if b is a number, like 3, we cant call loop with a^-3 bc it - # will automatically transform into 1/a^3. Therfore we need to - # create a matcher that flips the sign of the exponent. I created - # this matecher just for `Slot`s and not for terms, because if b - # is a number and not a call, certainly doesn't match a term (I hope). - if isa(arguments(term)[2], Slot) - matchers2 = (matcher(operation(term)), matcher(arguments(term)[1]), opposite_sign_matcher(arguments(term)[2])) # is this ok to be here or should it be outside neg_pow_term_matcher? - result = loop(denominator, bindings, matchers2) + # if data is of the alternative form 1/(...)^(...), it might match with negative exponent + if operation(term)==^ + alternative_form = (operation(car(data))==/) && arguments(car(data))[1]==1 && iscall(arguments(car(data))[2]) && (operation(arguments(car(data))[2])==^) + if result === nothing && alternative_form + denominator = arguments(car(data))[2] + # let's say data = a^b with a and b can be whatever + # if b is not a number then call the loop function with a^-b + if !isa(arguments(denominator)[2], Number) + frankestein = arguments(denominator)[1] ^ -(arguments(denominator)[2]) + result = loop(frankestein, bindings, matchers) + else + # if b is a number, like 3, we cant call loop with a^-3 bc it + # will automatically transform into 1/a^3. Therfore we need to + # create a matcher that flips the sign of the exponent. I created + # this matecher just for `Slot`s and not for terms, because if b + # is a number and not a call, certainly doesn't match a term (I hope). + if isa(arguments(term)[2], Slot) + matchers2 = (matcher(operation(term)), matcher(arguments(term)[1]), opposite_sign_matcher(arguments(term)[2])) # is this ok to be here or should it be outside neg_pow_term_matcher? + result = loop(denominator, bindings, matchers2) + end end end end @@ -201,11 +181,11 @@ end # normal part (can bee a tree) operation defslot part # defslot_term_matcher works like this: -# checks wether data starts with the default operation. -# if yes (1): continues like term_matcher -# if no checks wether data matches the normal part +# checks wether data is a call. +# if yes (1): continues like term_matcher (if it finds a match returns (2)) +# if still no match found checks wether data (is just a symbol) or (is a tree not starting with the default operation) # if no returns nothing, rule is not applied -# if yes (2): adds the pair (default value name, default value) to the found bindings and +# if yes (3): adds the pair (default value name, default value) to the found bindings and # calls the success function like term_matcher would do function defslot_term_matcher_constructor(term) @@ -218,33 +198,64 @@ function defslot_term_matcher_constructor(term) function defslot_term_matcher(success, data, bindings) # if data is not a list, return nothing !islist(data) && return nothing + result = nothing + if iscall(car(data)) + # (1) + function loop(term, bindings′, matchers′) # Get it to compile faster + if !islist(matchers′) + if !islist(term) + return success(bindings′, 1) + end + return nothing + end + car(matchers′)(term, bindings′) do b, n + loop(drop_n(term, n), b, cdr(matchers′)) + end + end + + result = loop(car(data), bindings, matchers) # Try to eat exactly one term + # if data is of the alternative form 1/(...)^(...), it might match with negative exponent + if operation(term)==^ + alternative_form = (operation(car(data))==/) && arguments(car(data))[1]==1 && iscall(arguments(car(data))[2]) && (operation(arguments(car(data))[2])==^) + if result === nothing && alternative_form + denominator = arguments(car(data))[2] + # let's say data = a^b with a and b can be whatever + # if b is not a number then call the loop function with a^-b + if !isa(arguments(denominator)[2], Number) + frankestein = arguments(denominator)[1] ^ -(arguments(denominator)[2]) + result = loop(frankestein, bindings, matchers) + else + # if b is a number, like 3, we cant call loop with a^-3 bc it + # will automatically transform into 1/a^3. Therfore we need to + # create a matcher that flips the sign of the exponent. I created + # this matecher just for `DefSlot`s and not for terms, because if b + # is a number and not a call, certainly doesn't match a term (I hope). + if isa(arguments(term)[2], DefSlot) + matchers2 = (matcher(operation(term)), matcher(arguments(term)[1]), opposite_sign_matcher(arguments(term)[2])) # is this ok to be here or should it be outside neg_pow_term_matcher? + result = loop(denominator, bindings, matchers2) + end + end + end + end + # (2) + if result !== nothing + return result + end + end + # if data (is not a tree and is just a symbol) or (is a tree not starting with the default operation) - if !iscall(car(data)) || (iscall(car(data)) && nameof(operation(car(data))) != defslot.operation) + if ( !iscall(car(data)) || (iscall(car(data)) && nameof(operation(car(data))) != defslot.operation) ) other_part_matcher = matchers[defslot_index==2 ? 2 : 3] # find the matcher of the normal part # checks wether it matches the normal part - # <-----------------(2)-------------------------------> + # <-----------------(3)-------------------------------> bindings = other_part_matcher((b,n) -> assoc(b, defslot.name, defslot.defaultValue), data, bindings) if bindings === nothing return nothing end - return success(bindings, 1) + result = success(bindings, 1) end - - # (1) - function loop(term, bindings′, matchers′) # Get it to compile faster - if !islist(matchers′) - if !islist(term) - return success(bindings′, 1) - end - return nothing - end - car(matchers′)(term, bindings′) do b, n - loop(drop_n(term, n), b, cdr(matchers′)) - end - end - - loop(car(data), bindings, matchers) # Try to eat exactly one term + result end end diff --git a/test/rewrite.jl b/test/rewrite.jl index e0eadd2fa..3cd545f2c 100644 --- a/test/rewrite.jl +++ b/test/rewrite.jl @@ -91,6 +91,12 @@ end r2 = @rule (~x)^(~y + ~z) => (~x, ~y, ~z) # rule with term as exponent @test r2(1/a^(b+2c)) === (a, -b, -2c) # uses frankestein @test r2(1/a^3) === nothing # should use a term_matcher that flips the sign, but is not implemented + + r1defslot = @rule (~x)^(~!y) => (~x, ~y) # rule with slot as exponent + @test r1defslot(1/a^b) === (a, -b) # uses frankestein + @test r1defslot(1/a^(b+2c)) === (a, -b-2c) # uses frankestein + @test r1defslot(1/a^2) === (a, -2) # uses opposite_sign_matcher + @test r1defslot(a) === (a, 1) end using SymbolicUtils: @capture