Module: Expressir::Express::Builder

Defined in:
lib/expressir/express/builder.rb

Overview

Builder registry for AST node type handlers. Each builder is a callable object that transforms AST data into Model objects. This is the ONLY way to build models from AST - no Transformer fallback.

Constant Summary collapse

SNAKE_CASE_CACHE =

Cache for snake_case conversions

{}
OPERATOR_TOKENS =

Build a Model object from AST data. Operator tokens that return nil (separators, punctuation) When these appear as the first key in a multi-key hash, they should be skipped in favor of the content key. This handles grammar patterns like ‘element >> (op_comma >> element).repeat` which produce => …, :element => {…}.

Returns:

Set.new(%i[
  op_comma op_colon op_decl op_delim op_leftparen op_rightparen
  op_leftbracket op_rightbracket op_left_curly_brace op_right_curly_brace
  op_period op_pipe op_double_backslash op_double_pipe op_double_asterisk
  op_asterisk op_slash op_plus op_minus op_less_equal op_greater_equal
  op_less_greater op_less_than op_greater_than op_equals
  op_colon_less_greater_colon op_colon_equals_colon
  op_query_begin op_query_end op_question_mark
]).freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.include_sourceObject (readonly)

Returns the value of attribute include_source.



10
11
12
# File 'lib/expressir/express/builder.rb', line 10

def include_source
  @include_source
end

.sourceObject (readonly)

Returns the value of attribute source.



10
11
12
# File 'lib/expressir/express/builder.rb', line 10

def source
  @source
end

Class Method Details

.build(ast, source: nil, include_source: nil) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
# File 'lib/expressir/express/builder.rb', line 44

def build(ast, source: nil, include_source: nil)
  return nil unless ast

  # Only set instance variables on first call (when they're provided)
  # Recursive calls pass nil which shouldn't override the saved values
  @source = source unless source.nil?
  @include_source = include_source unless include_source.nil?

  # Optimized: Hash is 90%+ of cases, check it first
  case ast
  when Hash
    node_type = ast.keys.first
    node_data = ast[node_type]

    handler_key = cached_snake_case(node_type)
    snake_data = fast_convert_keys(node_data)

    builder = @register[handler_key]
    if builder
      result = builder.call(snake_data)

      # Fast path: single-key hash or non-nil result
      if !result.nil? || ast.keys.length <= 1
        attach_source_info(result, node_data)
        return result
      end

      # Slow path: operator token returned nil in multi-key hash.
      # Try other keys for actual content. This handles
      # {:op_comma => ..., :element => {...}} where the first key
      # is an operator separator rather than a content key.
      if OPERATOR_TOKENS.include?(handler_key)
        ast.each_key do |key|
          next if key == node_type

          h_key = cached_snake_case(key)
          h_builder = @register[h_key]
          next unless h_builder

          n_data = ast[key]
          s_data = fast_convert_keys(n_data)
          result = h_builder.call(s_data)

          unless result.nil?
            attach_source_info(result, n_data)
            return result
          end
        end
      end
    else
      raise Error::UnknownNodeTypeError, node_type
    end
    nil
  when Array
    ast.map do |item|
      build(item)
    end
  when Parsanol::Slice
    ast.to_s
  else
    ast
  end
end

.build_children(ast_array) ⇒ Object

Build children (array of AST nodes) Optimized to avoid intermediate array allocations



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/expressir/express/builder.rb', line 155

def build_children(ast_array)
  return [] if ast_array.nil?

  # Handle Parsanol::Slice (empty Slices from optional rules)
  # Convert to empty Array
  if ast_array.is_a?(Parsanol::Slice)
    return []
  end

  # Handle single element (common case)
  unless ast_array.is_a?(Array)
    return [build(ast_array)].compact
  end

  # Build result in single pass, avoiding flatten/compact/map chain
  result = []
  ast_array.each do |item|
    next if item.nil?

    # Empty Slices from optional rules should be treated as empty arrays
    if item.is_a?(Parsanol::Slice)
      next
    end

    case item
    when Array
      item.each do |sub|
        result << build(sub) unless sub.nil?
      end
    else
      built = build(item)
      result << built if built
    end
  end
  result
end

.build_expression(data) ⇒ Object

Fast path for expression nodes



227
228
229
# File 'lib/expressir/express/builder.rb', line 227

def build_expression(data)
  build_node(:expression, data)
end

.build_factor(data) ⇒ Object

Fast path for factor nodes



212
213
214
# File 'lib/expressir/express/builder.rb', line 212

def build_factor(data)
  build_node(:factor, data)
end

.build_node(node_type, data) ⇒ Object

Call a registered builder directly with data (avoids hash wrapper allocation)

Parameters:

  • node_type (Symbol)

    The node type key

  • data

    The data to pass to the builder (already snake_case)

Raises:



199
200
201
202
203
204
# File 'lib/expressir/express/builder.rb', line 199

def build_node(node_type, data)
  builder = @register&.[](node_type)
  raise Error::UnknownNodeTypeError, node_type unless builder

  builder.call(data)
end

.build_optional(ast) ⇒ Object

Build optional (returns nil if ast is nil)



138
139
140
141
142
# File 'lib/expressir/express/builder.rb', line 138

def build_optional(ast)
  return nil unless ast

  build(ast)
end

.build_primary(data) ⇒ Object

Fast path for primary nodes



222
223
224
# File 'lib/expressir/express/builder.rb', line 222

def build_primary(data)
  build_node(:primary, data)
end

.build_simple_expression(data) ⇒ Object

Fast path for simple_expression nodes



232
233
234
# File 'lib/expressir/express/builder.rb', line 232

def build_simple_expression(data)
  build_node(:simple_expression, data)
end

.build_simple_factor(data) ⇒ Object

Fast path for simple_factor nodes



217
218
219
# File 'lib/expressir/express/builder.rb', line 217

def build_simple_factor(data)
  build_node(:simple_factor, data)
end

.build_term(data) ⇒ Object

Fast path for term nodes



207
208
209
# File 'lib/expressir/express/builder.rb', line 207

def build_term(data)
  build_node(:term, data)
end

.build_with_remarks(ast, source: nil, include_source: nil) ⇒ Object

Build with remark attachment



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/expressir/express/builder.rb', line 109

def build_with_remarks(ast, source: nil, include_source: nil)
  # Reset instance variables at the start of a top-level build
  # This ensures state from previous parses is cleared
  @source = source
  @include_source = include_source

  result = build(ast)

  # Only attach remarks if include_source is explicitly true
  # (nil means use default behavior - attach remarks)
  if source && result && include_source != false
    attacher = RemarkAttacher.new(source)
    attacher.attach(result)
  end

  result
end

.ensure_array(value) ⇒ Object

Normalize a value to an Array for iteration. Handles: nil → [], Parsanol::Slice → [], Array → Array, other → [other]



146
147
148
149
150
151
# File 'lib/expressir/express/builder.rb', line 146

def ensure_array(value)
  return [] if value.nil?
  return [] if value.is_a?(Parsanol::Slice)

  value.is_a?(Array) ? value : [value]
end

.register(node_type, builder = nil) { ... } ⇒ Object

Register a builder for a node type.

Parameters:

  • node_type (Symbol)

    The AST node type

  • builder (#call) (defaults to: nil)

    Optional callable that takes (ast_data)

Yields:

  • Block that takes (ast_data) if builder not provided



19
20
21
22
# File 'lib/expressir/express/builder.rb', line 19

def register(node_type, builder = nil, &block)
  @register ||= {}
  @register[node_type] = builder || block
end

.registered?(node_type) ⇒ Boolean

Check if a builder is registered for a node type.

Returns:

  • (Boolean)


128
129
130
# File 'lib/expressir/express/builder.rb', line 128

def registered?(node_type)
  @register&.key?(node_type)
end

.registered_typesObject

Get all registered node types.



133
134
135
# File 'lib/expressir/express/builder.rb', line 133

def registered_types
  @register&.keys || []
end