Skip to content

Commit

Permalink
Merge pull request #20 from Herb-AI/dev
Browse files Browse the repository at this point in the history
Add changes on dev to master
  • Loading branch information
THinnerichs authored Aug 21, 2023
2 parents 9099a74 + 470c554 commit bcbfd64
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 42 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Constraints.jl
# HerbConstraints.jl

This package contains the functionality to formulate, represent and use constraints within `Herb`.

Constraints are formulated as context-sensitive grammars. Further, they are divided into global and local constraints, that may be partially mapped to each other.

`HerbConstraints.jl` provides functionality to propagate constraints and match constraint patterns efficiently. Further, provides error handling through `matchfail` and `matchnode`.

[![Build Status](https://github.com/Herb-AI/HerbConstraints.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/Herb-AI/HerbConstraints.jl/actions/workflows/CI.yml?query=branch%3Amaster)

[![Build Status](https://github.com/Herb-AI/HerbConstraints.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/Herb-AI/HerbConstraints.jl/actions/workflows/CI.yml?query=branch%3Amaster)
41 changes: 41 additions & 0 deletions src/HerbConstraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,49 @@ module HerbConstraints
using HerbCore
using HerbGrammar

"""
PropagatorConstraint <: Constraint
Abstract type representing all propagator constraints.
Each propagator constraint has an implementation of a [`propagate`](@ref)-function that takes
- the [`PropagatorConstraint`](@ref)
- a [`Grammar`](@ref)
- a [`GrammarContext`](@ref), which most importantly contains the tree and the location
in the tree where propagation should take place.
- The `domain` which the [`propagate`](@ref)-function prunes.
The [`propagate`](@ref)-function returns a tuple containing
- The pruned `domain`
- A list of new [`LocalConstraint`](@ref)s
"""
abstract type PropagatorConstraint <: Constraint end

"""
abstract type LocalConstraint <: Constraint
Abstract type representing all local constraints.
Local constraints correspond to a specific (partial) [`AbstractRuleNode`](@ref) tree.
Each local constraint contains a `path` to a specific location in the tree.
Each local constraint has an implementation of a [`propagate`](@ref)-function that takes
- the [`LocalConstraint`](@ref)
- a [`Grammar`](@ref)
- a [`GrammarContext`](@ref), which most importantly contains the tree and the location
in the tree where propagation should take place.
- The `domain` which the [`propagate`](@ref)-function prunes.
The [`propagate`](@ref)-function returns a tuple containing
- The pruned `domain`
- A list of new [`LocalConstraint`](@ref)s
!!! warning
By default, [`LocalConstraint`](@ref)s are only propagated once.
Constraints that have to be propagated more frequently should return
themselves in the list of new local constraints.
"""
abstract type LocalConstraint <: Constraint end

@enum PropagateFailureReason unchanged_domain=1
Expand Down
8 changes: 7 additions & 1 deletion src/context.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""
mutable struct GrammarContext
Structure used to track the context.
Contains:
- the expression being modified
Expand All @@ -15,6 +17,8 @@ end
GrammarContext(originalExpr::AbstractRuleNode) = GrammarContext(originalExpr, [], [])

"""
addparent!(context::GrammarContext, parent::Int)
Adds a parent to the context.
The parent is defined by the grammar rule id.
"""
Expand All @@ -24,10 +28,12 @@ end


"""
copy_and_insert(old_context::GrammarContext, parent::Int)
Copies the given context and insert the parent in the node location.
"""
function copy_and_insert(old_context::GrammarContext, parent::Int)
new_context = GrammarContext(old_context.originalExpr, deepcopy(old_context.nodeLocation), deepcopy(old_context.constraints))
push!(new_context.nodeLocation, parent)
new_context
end
end
2 changes: 2 additions & 0 deletions src/localconstraints/local_forbidden.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

"""
LocalForbidden
Forbids the a subtree that matches the MatchNode tree to be generated at the location
provided by the path.
Use a `Forbidden` constraint for enforcing this throughout the entire search space.
Expand Down
4 changes: 3 additions & 1 deletion src/matchfail.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""
@enum MatchFail hardfail softfail
This enum is used for distinguishing between two types of failures when trying to
match a `RuleNode` either with another `RuleNode` or with an `AbstractMatchNode`
- Hardfail means that there is no match, and there is no way to fill in the holes to get a match.
- Softfail means that there is no match, but there *might* be a way to fill the holes that results in a match.
"""
@enum MatchFail hardfail softfail
@enum MatchFail hardfail softfail
57 changes: 53 additions & 4 deletions src/matchnode.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""
abstract type AbstractMatchNode
Tree structure to which rulenode trees can be matched.
Consists of MatchNodes, which can match a specific RuleNode,
and MatchVars, which is a variable that can be filled in with any RuleNode.
"""
abstract type AbstractMatchNode end

"""
struct MatchNode <: AbstractMatchNode
Match a specific rulenode, where the grammar rule index is `rule_ind`
and `children` matches the children of the RuleNode.
Example usage:
Expand All @@ -22,6 +26,8 @@ end
MatchNode(rule_ind::Int) = MatchNode(rule_ind, [])

"""
struct MatchVar <: AbstractMatchNode
Matches anything and assigns it to a variable.
The `ForbiddenTree` constraint will not match if identical variable symbols match to different trees.
Example usage:
Expand All @@ -34,6 +40,11 @@ struct MatchVar <: AbstractMatchNode
var_name::Symbol
end

"""
Base.show(io::IO, node::MatchNode; separator=",", last_child::Bool=true)
Prints a found [`MatchNode`](@ref) given an and the respective children to `IO`.
"""
function Base.show(io::IO, node::MatchNode; separator=",", last_child::Bool=true)
print(io, node.rule_ind)
if !isempty(node.children)
Expand All @@ -47,6 +58,11 @@ function Base.show(io::IO, node::MatchNode; separator=",", last_child::Bool=true
end
end

"""
Base.show(io::IO, node::MatchVar; separator=",", last_child::Bool=true)
Prints a matching variable assignment described by [`MatchVar`](@ref) to `IO`.
"""
function Base.show(io::IO, node::MatchVar; separator=",", last_child::Bool=true)
print(io, node.var_name)
if !last_child
Expand All @@ -62,8 +78,10 @@ contains_var(mn::MatchNode, var::Symbol) = any(contains_var(c, var) for c ∈ mn


"""
matchnode2expr(pattern::MatchNode, grammar::Grammar)
Converts a MatchNode tree into a Julia expression.
This is primarily useful for pretty-printing a pattern.
This is primarily useful for pretty-printing a pattern. Returns the corresponding expression.
"""
function matchnode2expr(pattern::MatchNode, grammar::Grammar)
root = deepcopy(grammar.rules[pattern.rule_ind])
Expand All @@ -73,10 +91,21 @@ function matchnode2expr(pattern::MatchNode, grammar::Grammar)
return root
end

"""
matchnode2expr(pattern::MatchVar, grammar::Grammar)
Converts a MatchVar into an expression by returning the variable directly.
This is primarily useful for pretty-printing a pattern.
"""
function matchnode2expr(pattern::MatchVar, ::Grammar)
return pattern.var_name
end

"""
_matchnode2expr(expr::Expr, pattern::MatchNode, grammar::Grammar, j=0)
Internal function for [`matchnode2expr`](@ref), recursively iterating over a matched pattern and converting it to an expression. This is primarily useful for pretty-printing a pattern. Returns the corresponding expression and the current child index.
"""
function _matchnode2expr(expr::Expr, pattern::MatchNode, grammar::Grammar, j=0)
for (k,arg) enumerate(expr.args)
if isa(arg, Expr)
Expand All @@ -96,6 +125,12 @@ function _matchnode2expr(expr::Expr, pattern::MatchNode, grammar::Grammar, j=0)
return expr, j
end


"""
_matchnode2expr(expr::Expr, pattern::MatchVar, grammar::Grammar, j=0)
Internal function for [`matchnode2expr`](@ref), recursively iterating over a matched variable and converting it to an expression. This is primarily useful for pretty-printing a pattern. Returns the corresponding expression and the current child index.
"""
function _matchnode2expr(expr::Expr, pattern::MatchVar, grammar::Grammar, j=0)
for (k,arg) enumerate(expr.args)
if isa(arg, Expr)
Expand All @@ -111,10 +146,12 @@ function _matchnode2expr(expr::Expr, pattern::MatchVar, grammar::Grammar, j=0)
return expr, j
end

function _matchnode2expr(typ::Symbol, pattern::MatchVar, grammar::Grammar, j=0)
return pattern.var_name, j
end

"""
_matchnode2expr(typ::Symbol, pattern::MatchNode, grammar::Grammar, j=0)
Internal function for [`matchnode2expr`](@ref), returning the matched translated symbol. This is primarily useful for pretty-printing a pattern. Returns the corresponding expression, i.e. the variable name and the current child index.
"""
function _matchnode2expr(typ::Symbol, pattern::MatchNode, grammar::Grammar, j=0)
retval = typ
if haskey(grammar.bytype, typ)
Expand All @@ -130,3 +167,15 @@ function _matchnode2expr(typ::Symbol, pattern::MatchNode, grammar::Grammar, j=0)
end
return retval, j
end


"""
_matchnode2expr(typ::Symbol, pattern::MatchVar, grammar::Grammar, j=0)
Internal function for [`matchnode2expr`](@ref). This is primarily useful for pretty-printing a pattern. Returns the corresponding expression, i.e. the variable name and the current child index.
"""
function _matchnode2expr(typ::Symbol, pattern::MatchVar, grammar::Grammar, j=0)
return pattern.var_name, j
end


43 changes: 32 additions & 11 deletions src/patternmatch.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

"""
Tries to match RuleNode `rn` with MatchNode `mn` and fill in
the domain of the hole at `hole_location`.
_pattern_match_with_hole(rn::RuleNode, mn::MatchNode, hole_location::Vector{Int}, vars::Dict{Symbol, AbstractRuleNode})::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}}
Tries to match [`RuleNode`](@ref) `rn` with [`MatchNode`](@ref) `mn` and fill in the domain of the hole at `hole_location`.
Returns if match is successful either:
- The id for the node which fills the hole
- A symbol for the variable that fills the hole
Expand Down Expand Up @@ -53,7 +54,12 @@ function _pattern_match_with_hole(
end
end

# Matching RuleNode with MatchVar
"""
_pattern_match_with_hole(rn::RuleNode, mv::MatchVar, hole_location::Vector{Int}, vars::Dict{Symbol, AbstractRuleNode})::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}}
Tries to match [`RuleNode`](@ref) `rn` with [`MatchVar`](@ref) `mv` and fill in the domain of the hole at `hole_location`.
If the variable name is already assigned in `vars`, the rulenode is matched with the hole. Otherwise the variable and the hole location are returned.
"""
function _pattern_match_with_hole(
rn::RuleNode,
mv::MatchVar,
Expand All @@ -68,12 +74,22 @@ function _pattern_match_with_hole(
end
end

# Matching Hole with MatchNode
"""
_pattern_match_with_hole(::Hole, mn::MatchNode, hole_location::Vector{Int}, ::Dict{Symbol, AbstractRuleNode})::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}}
Matches the [`Hole`](@ref) with the given [`MatchNode`](@ref).
TODO check this behaviour?
"""
_pattern_match_with_hole(::Hole, mn::MatchNode, hole_location::Vector{Int}, ::Dict{Symbol, AbstractRuleNode}
)::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}} =
hole_location == [] && mn.children == [] ? mn.rule_ind : softfail

# Matching Hole with MatchVar
"""
_pattern_match_with_hole(::Hole, mn::MatchNode, hole_location::Vector{Int}, ::Dict{Symbol, AbstractRuleNode})::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}}
Matches the [`Hole`](@ref) with the given [`MatchVar`](@ref), similar to [`_pattern_match_with_hole`](@ref).
"""
function _pattern_match_with_hole(h::Hole, mv::MatchVar, hole_location::Vector{Int}, vars::Dict{Symbol, AbstractRuleNode}
)::Union{Int, Symbol, MatchFail, Tuple{Symbol, Vector{Int}}}
@assert hole_location == []
Expand All @@ -85,13 +101,13 @@ function _pattern_match_with_hole(h::Hole, mv::MatchVar, hole_location::Vector{I
end
end

# Matching RuleNode with MatchNode
"""
Tries to match RuleNode `rn` with MatchNode `mn`.
_pattern_match(rn::RuleNode, mn::MatchNode, vars::Dict{Symbol, AbstractRuleNode})::Union{Nothing, MatchFail}
Tries to match [`RuleNode`](@ref) `rn` with [`MatchNode`](@ref) `mn`.
Modifies the variable assignment dictionary `vars`.
Returns nothing if the match is successful.
If the match is unsuccessful, it returns whether it
is a softfail or hardfail (see MatchFail docstring)
Returns `nothing` if the match is successful.
If the match is unsuccessful, it returns whether it is a softfail or hardfail (see [`MatchFail`](@ref) docstring)
"""
function _pattern_match(rn::RuleNode, mn::MatchNode, vars::Dict{Symbol, AbstractRuleNode})::Union{Nothing, MatchFail}
if rn.ind mn.rule_ind || length(rn.children) length(mn.children)
Expand All @@ -109,7 +125,12 @@ function _pattern_match(rn::RuleNode, mn::MatchNode, vars::Dict{Symbol, Abstract
return nothing
end

# Matching RuleNode with MatchVar
"""
_pattern_match(rn::RuleNode, mv::MatchVar, vars::Dict{Symbol, AbstractRuleNode})::Union{Nothing, MatchFail}
Matching [`RuleNode`](@ref) `rn` with [`MatchVar`](@ref) `mv`. If the variable is already assigned, the rulenode is matched with the specific variable value. Returns `nothing` if the match is succesful.
If the match is unsuccessful, it returns whether it is a softfail or hardfail (see [`MatchFail`](@ref) docstring)
"""
function _pattern_match(rn::RuleNode, mv::MatchVar, vars::Dict{Symbol, AbstractRuleNode})::Union{Nothing, MatchFail}
if mv.var_name keys(vars)
# If the assignments are unequal, the match is unsuccessful
Expand Down
34 changes: 31 additions & 3 deletions src/propagatorconstraints/comesafter.jl
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@

"""
Derivation rule can only appear in a derivation tree if the predecessors are in the path to the current node (in order)
ComesAfter <: PropagatorConstraint
A [`ComesAfter`](@ref) constraint is a [`PropagatorConstraint`](@ref) containing the following:
- `rule::Int`: A reference to a rule in the grammar
- `predecessors`: A list of rules in the grammar
This [`Constraint`](@ref) enforces that the `rule` can only be applied if every rule in
`predecessors` is used in the path from the root of the tree to the current hole in the order
that they are given. Even though the rules must be in order, there might be other rules inbetween.
For example, consider the tree `1(a, 2(b, 3(c, d))))`:
- `ComesAfter(4, [2, 3])` would enforce that rule `4` can only be used if `2` and `3`
are used in the path from the root in that order. Therefore, only hole `c` and `d` can be filled with `4`.
- `ComesAfter(4, [1, 3])` also allows `c` and `d` to be filled, since `1` and `3` are still used in the
correct order. It does not matter that `2` is also used in the path to the root.
- `ComesAfter(4, [3, 2])` does not allow any hole to be filled with `4`, since either the predecessors are
either not in the path or in the wrong order for each of the holes.
"""
struct ComesAfter <: PropagatorConstraint
rule::Int
predecessors::Vector{Int}
end


"""
ComesAfter(rule::Int, predecessor::Int)
Creates a [`ComesAfter`](@ref) constraint with only a single `predecessor`.
"""
ComesAfter(rule::Int, predecessor::Int) = ComesAfter(rule, [predecessor])

"""
Propagates the ComesAfter constraint.
It removes the rule from the domain if the predecessors sequence is in the ancestors.
propagate(c::ComesAfter, ::Grammar, context::GrammarContext, domain::Vector{Int})::Tuple{Vector{Int}, Vector{LocalConstraint}}
Propagates the [`ComesAfter`](@ref) [`PropagatorConstraint`](@ref).
Rules in the domain that would violate the [`ComesAfter`](@ref) constraint are removed.
"""

function propagate(
c::ComesAfter,
::Grammar,
Expand All @@ -27,6 +54,7 @@ function propagate(

if c.rule in domain # if rule is in domain, check the ancestors
ancestors = get_rulesequence(context.originalExpr, context.nodeLocation[begin:end-1]) # remove the current node from the node sequence

if containedin(c.predecessors, ancestors)
return domain, Set()
else
Expand Down
Loading

2 comments on commit bcbfd64

@nicolaefilat
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/89701

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.0 -m "<description of version>" bcbfd648390a7702fddfa3892f4563917d8301bc
git push origin v0.1.0

Please sign in to comment.