Class: Moxml::XPath::Compiler

Inherits:
Object
  • Object
show all
Defined in:
lib/moxml/xpath/compiler.rb

Overview

Compiler for transforming XPath AST into executable Ruby code.

This class takes an XPath AST (produced by Parser) and compiles it into a Ruby Proc that can be executed against XML documents. The compilation process:

  1. Traverse the XPath AST

  2. Generate Ruby::Node AST representing Ruby code

  3. Use Ruby::Generator to convert to Ruby source string

  4. Evaluate source in Context to get a Proc

Examples:

ast = Parser.parse("//book")
proc = Compiler.compile_with_cache(ast)
result = proc.call(document)

Constant Summary collapse

CONTEXT =

Shared context for compiled Procs

Context.new
CACHE =

Expression cache

Cache.new
STAR =

Wildcard for node names/namespace prefixes

"*"
RETURN_NODESET =

Node types that require a NodeSet to push nodes into

i[path absolute_path relative_path axis
predicate].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(namespaces: nil) ⇒ Compiler

Initialize compiler

Parameters:

  • namespaces (Hash, nil) (defaults to: nil)

    Optional namespace prefix mappings



49
50
51
52
53
54
# File 'lib/moxml/xpath/compiler.rb', line 49

def initialize(namespaces: nil)
  @namespaces = namespaces
  @literal_id = 0
  @predicate_nodesets = []
  @predicate_indexes = []
end

Class Method Details

.compile_with_cache(ast, namespaces: nil) ⇒ Proc

Compiles and caches an AST

Parameters:

  • ast (AST::Node)

    XPath AST to compile

  • namespaces (Hash, nil) (defaults to: nil)

    Optional namespace prefix mappings

Returns:

  • (Proc)

    Compiled Proc that accepts a document



41
42
43
44
# File 'lib/moxml/xpath/compiler.rb', line 41

def self.compile_with_cache(ast, namespaces: nil)
  cache_key = namespaces ? [ast, namespaces] : ast
  CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) }
end

Instance Method Details

#compile(ast) ⇒ Proc

Compiles an XPath AST into a Ruby Proc

Parameters:

Returns:

  • (Proc)

    Executable Proc



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/moxml/xpath/compiler.rb', line 60

def compile(ast)
  document = literal(:node)
  matched = matched_literal
  context_var = context_literal

  # Enable debug output
  debug = ENV["DEBUG_XPATH"] == "1"
  if debug
    puts "\n#{'=' * 60}"
    puts "COMPILING XPath"
    puts "=" * 60
    puts "AST: #{ast.inspect}"
    puts
  end

  ruby_ast = if return_nodeset?(ast)
               process(ast, document) { |node| matched.push(node) }
             else
               process(ast, document)
             end

  proc_ast = literal(:lambda).add_block(document) do
    # Get context from document
    context_assign = context_var.assign(document.context)

    if return_nodeset?(ast)
      # Create NodeSet using send node: Moxml::NodeSet.new([], context)
      nodeset_class = const_ref("Moxml", "NodeSet")
      empty_array = Ruby::Node.new(:array, [])
      nodeset_new = Ruby::Node.new(:send,
                                   [nodeset_class, "new", empty_array,
                                    context_var])

      body = matched.assign(nodeset_new)
        .followed_by(ruby_ast)
        .followed_by(matched)
    else
      body = ruby_ast
    end

    context_assign.followed_by(body)
  end

  generator = Ruby::Generator.new
  source = generator.process(proc_ast)

  if debug
    puts "GENERATED RUBY CODE:"
    puts "-" * 60
    puts source
    puts "=" * 60
    puts
  end

  CONTEXT.evaluate(source)
ensure
  @literal_id = 0
  @predicate_nodesets.clear
  @predicate_indexes.clear
end

#on_binary_op(ast, input, &block) ⇒ Object

Dispatcher for generic binary operator nodes



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/moxml/xpath/compiler.rb', line 132

def on_binary_op(ast, input, &block)
  operator = ast.value # :eq, :lt, :add, :plus, :star, etc.

  # Map token names to handler method names
  method_name = case operator
                when :plus then :add
                when :minus then :sub
                when :star then :mul
                else operator # eq, lt, gt, div, mod, etc.
                end

  send(:"on_#{method_name}", ast, input, &block)
end

#on_unary_op(ast, input, &block) ⇒ Object

Dispatcher for generic unary operator nodes



147
148
149
150
# File 'lib/moxml/xpath/compiler.rb', line 147

def on_unary_op(ast, input, &block)
  operator = ast.value # :minus
  send(:"on_#{operator}", ast, input, &block)
end

#on_union(ast, input, &block) ⇒ Object

Dispatcher for union nodes (parser creates :union, compiler uses :pipe)



153
154
155
# File 'lib/moxml/xpath/compiler.rb', line 153

def on_union(ast, input, &block)
  on_pipe(ast, input, &block)
end

#process(ast, input) {|Ruby::Node| ... } ⇒ Ruby::Node

Process a single XPath AST node

Parameters:

Yields:

  • (Ruby::Node)

    Yields matched nodes if block given

Returns:



127
128
129
# File 'lib/moxml/xpath/compiler.rb', line 127

def process(ast, input, &block)
  send(:"on_#{ast.type}", ast, input, &block)
end