Optimize mappings
diff --git a/ast/ast.go b/ast/ast.go
index dc7d7f3..e47a78e 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -29,6 +29,7 @@
// Node represents a node in the AST
type Node interface {
Type() NodeType
+ Clone() Node
}
// Rewrite represents a koral:rewrite
@@ -88,6 +89,18 @@
return RewriteNode
}
+// Clone creates a deep copy of the Rewrite node
+func (r *Rewrite) Clone() Node {
+ return &Rewrite{
+ Editor: r.Editor,
+ Operation: r.Operation,
+ Scope: r.Scope,
+ Src: r.Src,
+ Comment: r.Comment,
+ Original: r.Original, // Note: this is a shallow copy of the Original field
+ }
+}
+
// MarshalJSON implements custom JSON marshaling to ensure clean output
func (r *Rewrite) MarshalJSON() ([]byte, error) {
// Create a map with only the modern field names
@@ -128,6 +141,27 @@
return TokenNode
}
+// Clone creates a deep copy of the Token node
+func (t *Token) Clone() Node {
+ var clonedWrap Node
+ if t.Wrap != nil {
+ clonedWrap = t.Wrap.Clone()
+ }
+ tc := &Token{
+ Wrap: clonedWrap,
+ }
+
+ if t.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(t.Rewrites))
+ for i, rewrite := range t.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tc.Rewrites = clonedRewrites
+ }
+
+ return tc
+}
+
// TermGroup represents a koral:termGroup
type TermGroup struct {
Operands []Node `json:"operands"`
@@ -139,6 +173,27 @@
return TermGroupNode
}
+// Clone creates a deep copy of the TermGroup node
+func (tg *TermGroup) Clone() Node {
+ clonedOperands := make([]Node, len(tg.Operands))
+ for i, operand := range tg.Operands {
+ clonedOperands[i] = operand.Clone()
+ }
+ tgc := &TermGroup{
+ Operands: clonedOperands,
+ Relation: tg.Relation,
+ }
+ if tg.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(tg.Rewrites))
+ for i, rewrite := range tg.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tgc.Rewrites = clonedRewrites
+ }
+
+ return tgc
+}
+
// Term represents a koral:term
type Term struct {
Foundry string `json:"foundry"`
@@ -153,6 +208,27 @@
return TermNode
}
+// Clone creates a deep copy of the Term node
+func (t *Term) Clone() Node {
+
+ tc := &Term{
+ Foundry: t.Foundry,
+ Key: t.Key,
+ Layer: t.Layer,
+ Match: t.Match,
+ Value: t.Value,
+ }
+
+ if t.Rewrites != nil {
+ clonedRewrites := make([]Rewrite, len(t.Rewrites))
+ for i, rewrite := range t.Rewrites {
+ clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
+ }
+ tc.Rewrites = clonedRewrites
+ }
+ return tc
+}
+
// Pattern represents a pattern to match in the AST
type Pattern struct {
Root Node
@@ -174,3 +250,61 @@
func (c *CatchallNode) Type() NodeType {
return NodeType(c.NodeType)
}
+
+// Clone creates a deep copy of the CatchallNode
+func (c *CatchallNode) Clone() Node {
+ newNode := &CatchallNode{
+ NodeType: c.NodeType,
+ }
+
+ // Handle RawContent properly - preserve nil if it's nil
+ if c.RawContent != nil {
+ newNode.RawContent = make(json.RawMessage, len(c.RawContent))
+ copy(newNode.RawContent, c.RawContent)
+ }
+
+ if c.Wrap != nil {
+ newNode.Wrap = c.Wrap.Clone()
+ }
+
+ if len(c.Operands) > 0 {
+ newNode.Operands = make([]Node, len(c.Operands))
+ for i, operand := range c.Operands {
+ newNode.Operands[i] = operand.Clone()
+ }
+ }
+
+ return newNode
+}
+
+// ApplyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
+func ApplyFoundryAndLayerOverrides(node Node, foundry, layer string) {
+ if node == nil {
+ return
+ }
+
+ switch n := node.(type) {
+ case *Term:
+ if foundry != "" {
+ n.Foundry = foundry
+ }
+ if layer != "" {
+ n.Layer = layer
+ }
+ case *TermGroup:
+ for _, op := range n.Operands {
+ ApplyFoundryAndLayerOverrides(op, foundry, layer)
+ }
+ case *Token:
+ if n.Wrap != nil {
+ ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
+ }
+ case *CatchallNode:
+ if n.Wrap != nil {
+ ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
+ }
+ for _, op := range n.Operands {
+ ApplyFoundryAndLayerOverrides(op, foundry, layer)
+ }
+ }
+}
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 0daaf7c..49fdbe3 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -343,49 +343,39 @@
}
func TestComplexNestedStructures(t *testing.T) {
- // Create a complex nested structure
- innerGroup1 := &TermGroup{
+ // Test nested tokens and term groups
+ termGroup := &TermGroup{
Operands: []Node{
- &Term{Foundry: "f1", Key: "k1", Layer: "l1", Match: MatchEqual},
- &Term{Foundry: "f2", Key: "k2", Layer: "l2", Match: MatchNotEqual},
+ &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
},
Relation: AndRelation,
}
- innerGroup2 := &TermGroup{
- Operands: []Node{
- &Term{Foundry: "f3", Key: "k3", Layer: "l3", Match: MatchEqual},
- &Term{Foundry: "f4", Key: "k4", Layer: "l4", Match: MatchEqual, Value: "test"},
- },
- Relation: OrRelation,
+ token := &Token{
+ Wrap: termGroup,
}
- topGroup := &TermGroup{
- Operands: []Node{
- innerGroup1,
- innerGroup2,
- &Token{Wrap: &Term{Foundry: "f5", Key: "k5", Layer: "l5", Match: MatchEqual}},
- },
- Relation: AndRelation,
- }
-
- assert.Equal(t, TermGroupNode, topGroup.Type())
- assert.Len(t, topGroup.Operands, 3)
- assert.Equal(t, AndRelation, topGroup.Relation)
-
- // Test inner groups
- group1 := topGroup.Operands[0].(*TermGroup)
- assert.Len(t, group1.Operands, 2)
- assert.Equal(t, AndRelation, group1.Relation)
-
- group2 := topGroup.Operands[1].(*TermGroup)
- assert.Len(t, group2.Operands, 2)
- assert.Equal(t, OrRelation, group2.Relation)
-
- // Test token wrapping
- token := topGroup.Operands[2].(*Token)
+ assert.Equal(t, TokenNode, token.Type())
assert.NotNil(t, token.Wrap)
- assert.Equal(t, TermNode, token.Wrap.Type())
+ assert.Equal(t, TermGroupNode, token.Wrap.Type())
+
+ // Test that the nested structure is correct
+ if tg, ok := token.Wrap.(*TermGroup); ok {
+ assert.Equal(t, 2, len(tg.Operands))
+ assert.Equal(t, AndRelation, tg.Relation)
+ }
}
func TestEdgeCases(t *testing.T) {
@@ -462,3 +452,402 @@
t.Run(tt.name, tt.test)
}
}
+
+func TestCloneMethod(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ }{
+ {
+ name: "Clone Term",
+ node: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ Value: "test",
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "foundry",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone Token",
+ node: &Token{
+ Wrap: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "layer",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone TermGroup",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ Relation: AndRelation,
+ Rewrites: []Rewrite{
+ {
+ Editor: "test",
+ Scope: "foundry",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone CatchallNode",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ RawContent: []byte(`{"@type":"koral:unknown","test":"value"}`),
+ Wrap: &Term{
+ Foundry: "opennlp",
+ Key: "DET",
+ Layer: "p",
+ Match: MatchEqual,
+ },
+ Operands: []Node{
+ &Term{
+ Foundry: "opennlp",
+ Key: "AdjType",
+ Layer: "m",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ },
+ {
+ name: "Clone Rewrite",
+ node: &Rewrite{
+ Editor: "termMapper",
+ Operation: "injection",
+ Scope: "foundry",
+ Src: "test",
+ Comment: "test comment",
+ Original: "original_value",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cloned := tt.node.Clone()
+
+ // Check that the clone is not the same instance
+ assert.NotSame(t, tt.node, cloned)
+
+ // Check that the clone has the same type
+ assert.Equal(t, tt.node.Type(), cloned.Type())
+
+ // Check that nodes are equal (deep comparison)
+ assert.True(t, NodesEqual(tt.node, cloned))
+
+ // Test that modifying the clone doesn't affect the original
+ switch original := tt.node.(type) {
+ case *Term:
+ clonedTerm := cloned.(*Term)
+ clonedTerm.Foundry = "modified"
+ assert.NotEqual(t, original.Foundry, clonedTerm.Foundry)
+
+ case *Token:
+ clonedToken := cloned.(*Token)
+ if clonedToken.Wrap != nil {
+ if termWrap, ok := clonedToken.Wrap.(*Term); ok {
+ termWrap.Foundry = "modified"
+ if originalWrap, ok := original.Wrap.(*Term); ok {
+ assert.NotEqual(t, originalWrap.Foundry, termWrap.Foundry)
+ }
+ }
+ }
+
+ case *TermGroup:
+ clonedGroup := cloned.(*TermGroup)
+ clonedGroup.Relation = OrRelation
+ assert.NotEqual(t, original.Relation, clonedGroup.Relation)
+
+ case *CatchallNode:
+ clonedCatchall := cloned.(*CatchallNode)
+ clonedCatchall.NodeType = "modified"
+ assert.NotEqual(t, original.NodeType, clonedCatchall.NodeType)
+
+ case *Rewrite:
+ clonedRewrite := cloned.(*Rewrite)
+ clonedRewrite.Editor = "modified"
+ assert.NotEqual(t, original.Editor, clonedRewrite.Editor)
+ }
+ })
+ }
+}
+
+func TestCloneNilNodes(t *testing.T) {
+ // Test cloning nodes with nil fields
+ tests := []struct {
+ name string
+ node Node
+ }{
+ {
+ name: "Token with nil wrap",
+ node: &Token{Wrap: nil},
+ },
+ {
+ name: "TermGroup with empty operands",
+ node: &TermGroup{
+ Operands: []Node{},
+ Relation: AndRelation,
+ },
+ },
+ {
+ name: "CatchallNode with nil wrap and operands",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ RawContent: nil,
+ Wrap: nil,
+ Operands: nil,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cloned := tt.node.Clone()
+ assert.NotSame(t, tt.node, cloned)
+ assert.Equal(t, tt.node.Type(), cloned.Type())
+ assert.True(t, NodesEqual(tt.node, cloned))
+ })
+ }
+}
+
+func TestApplyFoundryAndLayerOverrides(t *testing.T) {
+ tests := []struct {
+ name string
+ node Node
+ foundry string
+ layer string
+ expectedChanges func(t *testing.T, node Node)
+ }{
+ {
+ name: "Apply foundry and layer to Term",
+ node: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ term := node.(*Term)
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ },
+ },
+ {
+ name: "Apply only foundry to Term",
+ node: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ foundry: "new_foundry",
+ layer: "",
+ expectedChanges: func(t *testing.T, node Node) {
+ term := node.(*Term)
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "original", term.Layer) // Should remain unchanged
+ },
+ },
+ {
+ name: "Apply to TermGroup",
+ node: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "original1",
+ Key: "DET",
+ Layer: "original1",
+ Match: MatchEqual,
+ },
+ &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ Relation: AndRelation,
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ termGroup := node.(*TermGroup)
+ for _, operand := range termGroup.Operands {
+ if term, ok := operand.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ }
+ },
+ },
+ {
+ name: "Apply to Token with wrapped Term",
+ node: &Token{
+ Wrap: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ token := node.(*Token)
+ if term, ok := token.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ },
+ },
+ {
+ name: "Apply to CatchallNode",
+ node: &CatchallNode{
+ NodeType: "koral:unknown",
+ Wrap: &Term{
+ Foundry: "original",
+ Key: "DET",
+ Layer: "original",
+ Match: MatchEqual,
+ },
+ Operands: []Node{
+ &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ catchall := node.(*CatchallNode)
+ if term, ok := catchall.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ for _, operand := range catchall.Operands {
+ if term, ok := operand.(*Term); ok {
+ assert.Equal(t, "new_foundry", term.Foundry)
+ assert.Equal(t, "new_layer", term.Layer)
+ }
+ }
+ },
+ },
+ {
+ name: "Apply to nested structure",
+ node: &Token{
+ Wrap: &TermGroup{
+ Operands: []Node{
+ &Term{
+ Foundry: "original1",
+ Key: "DET",
+ Layer: "original1",
+ Match: MatchEqual,
+ },
+ &Token{
+ Wrap: &Term{
+ Foundry: "original2",
+ Key: "AdjType",
+ Layer: "original2",
+ Match: MatchEqual,
+ Value: "Pdt",
+ },
+ },
+ },
+ Relation: AndRelation,
+ },
+ },
+ foundry: "new_foundry",
+ layer: "new_layer",
+ expectedChanges: func(t *testing.T, node Node) {
+ token := node.(*Token)
+ if termGroup, ok := token.Wrap.(*TermGroup); ok {
+ for _, operand := range termGroup.Operands {
+ switch op := operand.(type) {
+ case *Term:
+ assert.Equal(t, "new_foundry", op.Foundry)
+ assert.Equal(t, "new_layer", op.Layer)
+ case *Token:
+ if innerTerm, ok := op.Wrap.(*Term); ok {
+ assert.Equal(t, "new_foundry", innerTerm.Foundry)
+ assert.Equal(t, "new_layer", innerTerm.Layer)
+ }
+ }
+ }
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Clone the node to avoid modifying the original test data
+ cloned := tt.node.Clone()
+
+ // Apply the overrides
+ ApplyFoundryAndLayerOverrides(cloned, tt.foundry, tt.layer)
+
+ // Check the expected changes
+ tt.expectedChanges(t, cloned)
+ })
+ }
+}
+
+func TestApplyFoundryAndLayerOverridesNilNode(t *testing.T) {
+ // Test that applying overrides to a nil node doesn't panic
+ assert.NotPanics(t, func() {
+ ApplyFoundryAndLayerOverrides(nil, "foundry", "layer")
+ })
+}
+
+func TestApplyFoundryAndLayerOverridesEmptyValues(t *testing.T) {
+ // Test applying empty foundry and layer values
+ term := &Term{
+ Foundry: "original_foundry",
+ Key: "DET",
+ Layer: "original_layer",
+ Match: MatchEqual,
+ }
+
+ ApplyFoundryAndLayerOverrides(term, "", "")
+
+ // Values should remain unchanged
+ assert.Equal(t, "original_foundry", term.Foundry)
+ assert.Equal(t, "original_layer", term.Layer)
+}
diff --git a/ast/compare.go b/ast/compare.go
index fec22b1..617e8f0 100644
--- a/ast/compare.go
+++ b/ast/compare.go
@@ -21,7 +21,8 @@
n1.Key == n2.Key &&
n1.Layer == n2.Layer &&
n1.Match == n2.Match &&
- n1.Value == n2.Value
+ n1.Value == n2.Value &&
+ rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *TermGroup:
if n2, ok := b.(*TermGroup); ok {
@@ -33,22 +34,62 @@
return false
}
}
- return true
+ return rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *Token:
if n2, ok := b.(*Token); ok {
- return NodesEqual(n1.Wrap, n2.Wrap)
+ return NodesEqual(n1.Wrap, n2.Wrap) &&
+ rewritesEqual(n1.Rewrites, n2.Rewrites)
}
case *CatchallNode:
if n2, ok := b.(*CatchallNode); ok {
- return n1.NodeType == n2.NodeType &&
- reflect.DeepEqual(n1.RawContent, n2.RawContent) &&
- NodesEqual(n1.Wrap, n2.Wrap)
+ if n1.NodeType != n2.NodeType ||
+ !reflect.DeepEqual(n1.RawContent, n2.RawContent) ||
+ !NodesEqual(n1.Wrap, n2.Wrap) {
+ return false
+ }
+ // Compare operands
+ if len(n1.Operands) != len(n2.Operands) {
+ return false
+ }
+ for i := range n1.Operands {
+ if !NodesEqual(n1.Operands[i], n2.Operands[i]) {
+ return false
+ }
+ }
+ return true
+ }
+ case *Rewrite:
+ if n2, ok := b.(*Rewrite); ok {
+ return n1.Editor == n2.Editor &&
+ n1.Operation == n2.Operation &&
+ n1.Scope == n2.Scope &&
+ n1.Src == n2.Src &&
+ n1.Comment == n2.Comment &&
+ reflect.DeepEqual(n1.Original, n2.Original)
}
}
return false
}
+// rewritesEqual compares two slices of Rewrite structs for equality
+func rewritesEqual(a, b []Rewrite) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i].Editor != b[i].Editor ||
+ a[i].Operation != b[i].Operation ||
+ a[i].Scope != b[i].Scope ||
+ a[i].Src != b[i].Src ||
+ a[i].Comment != b[i].Comment ||
+ !reflect.DeepEqual(a[i].Original, b[i].Original) {
+ return false
+ }
+ }
+ return true
+}
+
// IsTermNode checks if a node is a Term node
func IsTermNode(node Node) bool {
_, ok := node.(*Term)
diff --git a/mapper/mapper.go b/mapper/mapper.go
index 424896e..0bbe39f 100644
--- a/mapper/mapper.go
+++ b/mapper/mapper.go
@@ -148,18 +148,30 @@
// Store original node for rewrite if needed
var originalNode ast.Node
if opts.AddRewrites {
- originalBytes, err := parser.SerializeToJSON(node)
- if err != nil {
- return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
- }
- originalNode, err = parser.ParseJSON(originalBytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse original node for rewrite: %w", err)
- }
+ originalNode = node.Clone()
}
+ // Pre-check foundry/layer overrides to optimize processing
+ var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
+ if opts.Direction { // true means AtoB
+ patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
+ replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
+ } else {
+ patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
+ replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
+ }
+
+ // Create a pattern cache key for memoization
+ type patternCacheKey struct {
+ ruleIndex int
+ foundry string
+ layer string
+ isReplacement bool
+ }
+ patternCache := make(map[patternCacheKey]ast.Node)
+
// Apply each rule to the AST
- for _, rule := range rules {
+ for i, rule := range rules {
// Create pattern and replacement based on direction
var pattern, replacement ast.Node
if opts.Direction { // true means AtoB
@@ -178,40 +190,55 @@
replacement = token.Wrap
}
- // Create deep copies of pattern and replacement to avoid modifying the original parsed rules
- patternBytes, err := parser.SerializeToJSON(pattern)
- if err != nil {
- return nil, fmt.Errorf("failed to serialize pattern for copying: %w", err)
- }
- patternCopy, err := parser.ParseJSON(patternBytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse pattern copy: %w", err)
+ // First, quickly check if the pattern could match without creating a full matcher
+ // This is a lightweight pre-check to avoid expensive operations
+ if !m.couldPatternMatch(node, pattern) {
+ continue
}
- replacementBytes, err := parser.SerializeToJSON(replacement)
- if err != nil {
- return nil, fmt.Errorf("failed to serialize replacement for copying: %w", err)
- }
- replacementCopy, err := parser.ParseJSON(replacementBytes)
- if err != nil {
- return nil, fmt.Errorf("failed to parse replacement copy: %w", err)
+ // Get or create pattern with overrides
+ patternKey := patternCacheKey{ruleIndex: i, foundry: patternFoundry, layer: patternLayer, isReplacement: false}
+ processedPattern, exists := patternCache[patternKey]
+ if !exists {
+ // Clone pattern only when needed
+ processedPattern = pattern.Clone()
+ // Apply foundry and layer overrides only if they're non-empty
+ if patternFoundry != "" || patternLayer != "" {
+ ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
+ }
+ patternCache[patternKey] = processedPattern
}
- // Apply foundry and layer overrides to the copies
- if opts.Direction { // true means AtoB
- applyFoundryAndLayerOverrides(patternCopy, opts.FoundryA, opts.LayerA)
- applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryB, opts.LayerB)
- } else {
- applyFoundryAndLayerOverrides(patternCopy, opts.FoundryB, opts.LayerB)
- applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryA, opts.LayerA)
+ // Create a temporary matcher to check for actual matches
+ tempMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: &ast.Term{}})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temporary matcher: %w", err)
}
- // Create matcher and apply replacement using the copies
- m, err := matcher.NewMatcher(ast.Pattern{Root: patternCopy}, ast.Replacement{Root: replacementCopy})
+ // Only proceed if there's an actual match
+ if !tempMatcher.Match(node) {
+ continue
+ }
+
+ // Get or create replacement with overrides (lazy evaluation)
+ replacementKey := patternCacheKey{ruleIndex: i, foundry: replacementFoundry, layer: replacementLayer, isReplacement: true}
+ processedReplacement, exists := patternCache[replacementKey]
+ if !exists {
+ // Clone replacement only when we have a match
+ processedReplacement = replacement.Clone()
+ // Apply foundry and layer overrides only if they're non-empty
+ if replacementFoundry != "" || replacementLayer != "" {
+ ast.ApplyFoundryAndLayerOverrides(processedReplacement, replacementFoundry, replacementLayer)
+ }
+ patternCache[replacementKey] = processedReplacement
+ }
+
+ // Create the actual matcher and apply replacement
+ actualMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: processedReplacement})
if err != nil {
return nil, fmt.Errorf("failed to create matcher: %w", err)
}
- node = m.Replace(node)
+ node = actualMatcher.Replace(node)
}
// Wrap the result in a token if the input was a token
@@ -374,34 +401,87 @@
return true
}
-// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
-func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
+// couldPatternMatch performs a lightweight check to see if a pattern could potentially match a node
+// This is an optimization to avoid expensive operations when there's clearly no match possible
+func (m *Mapper) couldPatternMatch(node, pattern ast.Node) bool {
+ if pattern == nil {
+ return true
+ }
if node == nil {
- return
+ return false
+ }
+
+ // Handle Token wrappers
+ if token, ok := pattern.(*ast.Token); ok {
+ pattern = token.Wrap
+ }
+ if token, ok := node.(*ast.Token); ok {
+ node = token.Wrap
+ }
+
+ // For simple terms, check basic compatibility
+ if patternTerm, ok := pattern.(*ast.Term); ok {
+ // Check if there's any term in the node structure that could match
+ return m.hasMatchingTerm(node, patternTerm)
+ }
+
+ // For TermGroups, we need to check all possible matches
+ if patternGroup, ok := pattern.(*ast.TermGroup); ok {
+ if patternGroup.Relation == ast.OrRelation {
+ // For OR relations, any operand could match
+ for _, op := range patternGroup.Operands {
+ if m.couldPatternMatch(node, op) {
+ return true
+ }
+ }
+ return false
+ } else {
+ // For AND relations, all operands must have potential matches
+ for _, op := range patternGroup.Operands {
+ if !m.couldPatternMatch(node, op) {
+ return false
+ }
+ }
+ return true
+ }
+ }
+
+ // For other cases, assume they could match (conservative approach)
+ return true
+}
+
+// hasMatchingTerm checks if there's any term in the node structure that could match the pattern term
+func (m *Mapper) hasMatchingTerm(node ast.Node, patternTerm *ast.Term) bool {
+ if node == nil {
+ return false
}
switch n := node.(type) {
case *ast.Term:
- if foundry != "" {
- n.Foundry = foundry
- }
- if layer != "" {
- n.Layer = layer
- }
+ // Check if this term could match the pattern
+ // We only check key as that's the most distinctive attribute
+ return n.Key == patternTerm.Key
case *ast.TermGroup:
+ // Check all operands
for _, op := range n.Operands {
- applyFoundryAndLayerOverrides(op, foundry, layer)
+ if m.hasMatchingTerm(op, patternTerm) {
+ return true
+ }
}
+ return false
case *ast.Token:
- if n.Wrap != nil {
- applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
- }
+ return m.hasMatchingTerm(n.Wrap, patternTerm)
case *ast.CatchallNode:
- if n.Wrap != nil {
- applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
+ if n.Wrap != nil && m.hasMatchingTerm(n.Wrap, patternTerm) {
+ return true
}
for _, op := range n.Operands {
- applyFoundryAndLayerOverrides(op, foundry, layer)
+ if m.hasMatchingTerm(op, patternTerm) {
+ return true
+ }
}
+ return false
+ default:
+ return false
}
}