Module: Ast::Merge::NodeTyping::Normalizer

Defined in:
lib/ast/merge/node_typing/normalizer.rb

Overview

Thread-safe backend registration and type normalization for AST merge libraries.

Normalizer provides a shared, thread-safe mechanism for registering backend-specific
node type mappings and normalizing them to canonical types. This enables portable
merge rules across different parsers/backends for the same file format.

Thread Safety

All registration and lookup operations are protected by a mutex to ensure
thread-safe access to the backend mappings. This follows the same pattern
used in TreeHaver::LanguageRegistry and TreeHaver::PathValidator.

Usage Pattern

File-format-specific merge libraries (e.g., toml-merge, markdown-merge) should:

  1. Create their own NodeTypeNormalizer module
  2. Include or extend Ast::Merge::NodeTyping::Normalizer
  3. Define their canonical types and default backend mappings
  4. Call configure_normalizer with their initial mappings

Examples:

Creating a format-specific normalizer

module Toml
  module Merge
    module NodeTypeNormalizer
      extend Ast::Merge::NodeTyping::Normalizer

      configure_normalizer(
        tree_sitter_toml: {
          table_array_element: :array_of_tables,
          pair: :pair,
          # ...
        }.freeze,
        citrus_toml: {
          table_array_element: :array_of_tables,
          pair: :pair,
          # ...
        }.freeze
      )

      # Optional: Add format-specific helper methods
      def self.table_type?(type)
        %i[table array_of_tables].include?(type.to_sym)
      end
    end
  end
end

See Also:

  • TreeHaver::LanguageRegistry
  • TreeHaver::PathValidator

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(base) ⇒ Object



61
62
63
64
# File 'lib/ast/merge/node_typing/normalizer.rb', line 61

def extended(base)
  base.instance_variable_set(:@normalizer_mutex, Mutex.new)
  base.instance_variable_set(:@backend_mappings, {})
end

Instance Method Details

#backend_registered?(backend) ⇒ Boolean

Check if a backend is registered.

Parameters:

  • backend (Symbol)

    Backend identifier

Returns:

  • (Boolean)


170
171
172
173
174
# File 'lib/ast/merge/node_typing/normalizer.rb', line 170

def backend_registered?(backend)
  @normalizer_mutex.synchronize do
    @backend_mappings.key?(backend.to_sym)
  end
end

#canonical_type(backend_type, backend = nil) ⇒ Symbol?

Get the canonical type for a backend-specific type.

If no mapping exists, returns the original type unchanged (passthrough).
This allows backend-specific types to pass through for backend-specific
merge rules.

Examples:

NodeTypeNormalizer.canonical_type(:table_array_element, :tree_sitter_toml)
# => :array_of_tables

NodeTypeNormalizer.canonical_type(:unknown_type, :tree_sitter_toml)
# => :unknown_type (passthrough)

Parameters:

  • backend_type (Symbol, String, nil)

    The backend’s node type

  • backend (Symbol) (defaults to: nil)

    The backend identifier

Returns:

  • (Symbol, nil)

    Canonical type (or original if no mapping), nil if input was nil



128
129
130
131
132
133
134
135
# File 'lib/ast/merge/node_typing/normalizer.rb', line 128

def canonical_type(backend_type, backend = nil)
  return backend_type if backend_type.nil?

  type_sym = backend_type.to_sym
  @normalizer_mutex.synchronize do
    @backend_mappings.dig(backend, type_sym) || type_sym
  end
end

#canonical_typesArray<Symbol>

Get all canonical types across all backends.

Returns:

  • (Array<Symbol>)

    Unique canonical type symbols



189
190
191
192
193
# File 'lib/ast/merge/node_typing/normalizer.rb', line 189

def canonical_types
  @normalizer_mutex.synchronize do
    @backend_mappings.values.flat_map(&:values).uniq
  end
end

#configure_normalizer(**mappings) ⇒ void

This method returns an undefined value.

Configure the normalizer with initial backend mappings.

This should be called once when defining the format-specific normalizer,
providing the default backend mappings. Additional backends can be
registered later via register_backend.

Examples:

configure_normalizer(
  tree_sitter_toml: { table_array_element: :array_of_tables }.freeze,
  citrus_toml: { table_array_element: :array_of_tables }.freeze
)

Parameters:

  • mappings (Hash{Symbol => Hash{Symbol => Symbol}})

    Initial backend mappings
    Keys are backend identifiers, values are hashes mapping backend types to canonical types



82
83
84
85
86
87
88
89
# File 'lib/ast/merge/node_typing/normalizer.rb', line 82

def configure_normalizer(**mappings)
  @normalizer_mutex.synchronize do
    mappings.each do |backend, type_mappings|
      @backend_mappings[backend.to_sym] = type_mappings.frozen? ? type_mappings : type_mappings.freeze
    end
  end
  nil
end

#mappings_for(backend) ⇒ Hash{Symbol => Symbol}?

Get the mappings for a specific backend.

Parameters:

  • backend (Symbol)

    Backend identifier

Returns:

  • (Hash{Symbol => Symbol}, nil)

    The mappings or nil if not registered



180
181
182
183
184
# File 'lib/ast/merge/node_typing/normalizer.rb', line 180

def mappings_for(backend)
  @normalizer_mutex.synchronize do
    @backend_mappings[backend.to_sym]
  end
end

#register_backend(backend, mappings) ⇒ void

This method returns an undefined value.

Register type mappings for a new backend.

This allows extending the normalizer to support additional parsers
beyond those configured initially. Thread-safe for runtime registration.

Examples:

NodeTypeNormalizer.register_backend(:my_parser, {
  my_table: :table,
  my_pair: :pair,
})

Parameters:

  • backend (Symbol)

    Backend identifier (e.g., :my_parser)

  • mappings (Hash{Symbol => Symbol})

    Backend type → canonical type mappings



105
106
107
108
109
110
# File 'lib/ast/merge/node_typing/normalizer.rb', line 105

def register_backend(backend, mappings)
  @normalizer_mutex.synchronize do
    @backend_mappings[backend.to_sym] = mappings.frozen? ? mappings : mappings.freeze
  end
  nil
end

#registered_backendsArray<Symbol>

Get all registered backends.

Returns:

  • (Array<Symbol>)

    Backend identifiers



160
161
162
163
164
# File 'lib/ast/merge/node_typing/normalizer.rb', line 160

def registered_backends
  @normalizer_mutex.synchronize do
    @backend_mappings.keys
  end
end

#wrap(node, backend) ⇒ Ast::Merge::NodeTyping::Wrapper

Wrap a node with its canonical type as merge_type.

Uses Ast::Merge::NodeTyping.with_merge_type to create a wrapper
that delegates all methods to the underlying node while adding
a canonical merge_type attribute.

Examples:

wrapped = NodeTypeNormalizer.wrap(node, :tree_sitter_toml)
wrapped.type        # => :table_array_element (original)
wrapped.merge_type  # => :array_of_tables (canonical)
wrapped.unwrap      # => node (original node)

Parameters:

  • node (Object)

    The backend node to wrap (must respond to #type)

  • backend (Symbol)

    The backend identifier

Returns:



152
153
154
155
# File 'lib/ast/merge/node_typing/normalizer.rb', line 152

def wrap(node, backend)
  canonical = canonical_type(node.type, backend)
  Ast::Merge::NodeTyping.with_merge_type(node, canonical)
end