Module: Parslet::Atoms::CanFlatten

Included in:
Base
Defined in:
lib/parslet/atoms/can_flatten.rb

Overview

A series of helper functions that have the common topic of flattening result values into the intermediary tree that consists of Ruby Hashes and Arrays.

This module has one main function, #flatten, that takes an annotated structure as input and returns the reduced form that users expect from Atom#parse.

NOTE: Since all of these functions are just that, functions without side effects, they are in a module and not in a class. Its hard to draw the line sometimes, but this is beyond.

Instance Method Summary collapse

Instance Method Details

#flatten(value, named = false) ⇒ Object

Takes a mixed value coming out of a parslet and converts it to a return value for the user by dropping things and merging hashes.

Named is set to true if this result will be embedded in a Hash result from naming something using .as(...). It changes the folding semantics of repetition.

[View source]

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/parslet/atoms/can_flatten.rb', line 23

def flatten(value, named=false)
  # Passes through everything that isn't an array of things
  return value unless value.instance_of? Array

  # Extracts the s-expression tag
  tag, *tail = value

  # Merges arrays:
  result = tail.
    map { |e| flatten(e) }            # first flatten each element

  case tag
    when :sequence
      return flatten_sequence(result)
    when :maybe
      return named ? result.first : result.first || ''
    when :repetition
      return flatten_repetition(result, named)
  end

  fail "BUG: Unknown tag #{tag.inspect}."
end

#flatten_repetition(list, named) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Flatten results from a repetition of a single parslet. named indicates whether the user has named the result or not. If the user has named the results, we want to leave an empty list alone - otherwise it is turned into an empty string.

[View source]

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/parslet/atoms/can_flatten.rb', line 104

def flatten_repetition(list, named)
  if list.any? { |e| e.instance_of?(Hash) }
    # If keyed subtrees are in the array, we'll want to discard all 
    # strings inbetween. To keep them, name them. 
    return list.select { |e| e.instance_of?(Hash) }
  end

  if list.any? { |e| e.instance_of?(Array) }
    # If any arrays are nested in this array, flatten all arrays to this
    # level. 
    return list.
      select { |e| e.instance_of?(Array) }.
      flatten(1)
  end

  # Consistent handling of empty lists, when we act on a named result        
  return [] if named && list.empty?

  # If there are only strings, concatenate them and return that. 
  foldl(list.compact) { |s,e| s+e }
end

#flatten_sequence(list) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Flatten results from a sequence of parslets.

[View source]

58
59
60
61
62
# File 'lib/parslet/atoms/can_flatten.rb', line 58

def flatten_sequence(list)
  foldl(list.compact) { |r, e|        # and then merge flat elements
    merge_fold(r, e)
  }
end

#foldl(list, &block) ⇒ Object

Lisp style fold left where the first element builds the basis for an inject.

[View source]

49
50
51
52
# File 'lib/parslet/atoms/can_flatten.rb', line 49

def foldl(list, &block)
  return '' if list.empty?
  list[1..-1].inject(list.first, &block)
end

#merge_fold(l, r) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

[View source]

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
# File 'lib/parslet/atoms/can_flatten.rb', line 64

def merge_fold(l, r)
  # equal pairs: merge. ----------------------------------------------------
  if l.class == r.class
    if l.is_a?(Hash)
      warn_about_duplicate_keys(l, r)
      return l.merge(r)
    else
      return l + r
    end
  end

  # unequal pairs: hoist to same level. ------------------------------------

  # Maybe classes are not equal, but both are stringlike?
  if l.respond_to?(:to_str) && r.respond_to?(:to_str)
    # if we're merging a String with a Slice, the slice wins. 
    return r if r.respond_to? :to_slice
    return l if l.respond_to? :to_slice

    fail "NOTREACHED: What other stringlike classes are there?"
  end

  # special case: If one of them is a string/slice, the other is more important 
  return l if r.respond_to? :to_str
  return r if l.respond_to? :to_str

  # otherwise just create an array for one of them to live in 
  return l + [r] if r.class == Hash
  return [l] + r if l.class == Hash

  fail "Unhandled case when foldr'ing sequence."
end

#warn_about_duplicate_keys(h1, h2) ⇒ Object

That annoying warning ‘Duplicate subtrees while merging result’ comes from here. You should add more ‘.as(…)’ names to your intermediary tree.

[View source]

129
130
131
132
133
134
135
# File 'lib/parslet/atoms/can_flatten.rb', line 129

def warn_about_duplicate_keys(h1, h2)
  d = h1.keys & h2.keys
  unless d.empty?
    warn "Duplicate subtrees while merging result of \n  #{self.inspect}\nonly the values"+
         " of the latter will be kept. (keys: #{d.inspect})"
  end
end