Update specificity rule matching for annotations
Change-Id: Ifa7ec5eef3583cb196f4aa1ca0cfcd65790de226
diff --git a/ast/ast.go b/ast/ast.go
index b944824..df21ba9 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -487,11 +487,11 @@
SetRewrites([]Rewrite)
}
-func (t *Term) GetRewrites() []Rewrite { return t.Rewrites }
-func (t *Term) SetRewrites(r []Rewrite) { t.Rewrites = r }
-func (tg *TermGroup) GetRewrites() []Rewrite { return tg.Rewrites }
+func (t *Term) GetRewrites() []Rewrite { return t.Rewrites }
+func (t *Term) SetRewrites(r []Rewrite) { t.Rewrites = r }
+func (tg *TermGroup) GetRewrites() []Rewrite { return tg.Rewrites }
func (tg *TermGroup) SetRewrites(r []Rewrite) { tg.Rewrites = r }
-func (t *Token) GetRewrites() []Rewrite { return t.Rewrites }
+func (t *Token) GetRewrites() []Rewrite { return t.Rewrites }
func (t *Token) SetRewrites(r []Rewrite) { t.Rewrites = r }
// AppendRewrite appends a rewrite to any Rewriteable node.
@@ -502,6 +502,36 @@
}
}
+// Specificity returns the specificity score of an AST node.
+// Specificity is the count of AND-connected leaf constraints:
+// - Term -> 1
+// - TermGroup(AND) -> sum of Specificity of all operands
+// - TermGroup(OR) -> 0 (alternatives, not additional constraints)
+// - Token -> Specificity(Wrap)
+// - CatchallNode / nil -> 0
+func Specificity(node Node) int {
+ if node == nil {
+ return 0
+ }
+ switch n := node.(type) {
+ case *Term:
+ return 1
+ case *TermGroup:
+ if n.Relation == AndRelation {
+ total := 0
+ for _, op := range n.Operands {
+ total += Specificity(op)
+ }
+ return total
+ }
+ return 0
+ case *Token:
+ return Specificity(n.Wrap)
+ default:
+ return 0
+ }
+}
+
// StripRewrites recursively removes all rewrites from an AST tree.
func StripRewrites(node Node) {
if node == nil {
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 3965f1d..4d8b97a 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -1133,6 +1133,107 @@
}
}
+func TestSpecificity(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ expected int
+ }{
+ {
+ name: "Term returns 1",
+ node: &Term{Key: "ADJ", Match: MatchEqual},
+ expected: 1,
+ },
+ {
+ name: "TermGroup(AND) with 2 terms returns 2",
+ node: &TermGroup{
+ Relation: AndRelation,
+ Operands: []Node{
+ &Term{Key: "ADJ", Match: MatchEqual},
+ &Term{Key: "Variant", Value: "Short", Match: MatchEqual},
+ },
+ },
+ expected: 2,
+ },
+ {
+ name: "TermGroup(AND) with 3 terms returns 3",
+ node: &TermGroup{
+ Relation: AndRelation,
+ Operands: []Node{
+ &Term{Key: "VERB", Match: MatchEqual},
+ &Term{Key: "Mood", Value: "Ind", Match: MatchEqual},
+ &Term{Key: "VerbForm", Value: "Fin", Match: MatchEqual},
+ },
+ },
+ expected: 3,
+ },
+ {
+ name: "TermGroup(OR) with 2 terms returns 0",
+ node: &TermGroup{
+ Relation: OrRelation,
+ Operands: []Node{
+ &Term{Key: "ADJA", Match: MatchEqual},
+ &Term{Key: "ADJD", Match: MatchEqual},
+ },
+ },
+ expected: 0,
+ },
+ {
+ name: "Nested AND containing OR returns count of AND terms only",
+ node: &TermGroup{
+ Relation: AndRelation,
+ Operands: []Node{
+ &Term{Key: "DET", Match: MatchEqual},
+ &TermGroup{
+ Relation: OrRelation,
+ Operands: []Node{
+ &Term{Key: "PronType", Value: "Ind", Match: MatchEqual},
+ &Term{Key: "PronType", Value: "Neg", Match: MatchEqual},
+ },
+ },
+ },
+ },
+ expected: 1,
+ },
+ {
+ name: "Token wrapping TermGroup(AND) with 2 terms returns 2",
+ node: &Token{
+ Wrap: &TermGroup{
+ Relation: AndRelation,
+ Operands: []Node{
+ &Term{Key: "DET", Match: MatchEqual},
+ &Term{Key: "PronType", Value: "Art", Match: MatchEqual},
+ },
+ },
+ },
+ expected: 2,
+ },
+ {
+ name: "nil node returns 0",
+ node: nil,
+ expected: 0,
+ },
+ {
+ name: "CatchallNode returns 0",
+ node: &CatchallNode{NodeType: "koral:group"},
+ expected: 0,
+ },
+ {
+ name: "Token wrapping a simple Term returns 1",
+ node: &Token{
+ Wrap: &Term{Key: "NOUN", Match: MatchEqual},
+ },
+ expected: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, Specificity(tt.node))
+ })
+ }
+}
+
func TestRestrictToObligatoryDoesNotModifyOriginal(t *testing.T) {
// Test that the original node is not modified
original := &TermGroup{