Module: LazyGraph::Node::DerivedRules

Included in:
LazyGraph::Node
Defined in:
lib/lazy_graph/node/derived_rules.rb

Constant Summary collapse

PLACEHOLDER_VAR_REGEX =
/\$\{[^}]+\}/

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extract_expr_from_source_location(source_location) ⇒ Object



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
120
121
122
123
124
125
126
127
128
129
# File 'lib/lazy_graph/node/derived_rules.rb', line 80

def self.extract_expr_from_source_location(source_location)
  @derived_proc_cache ||= {}
  mtime = File.mtime(source_location.first).to_i

  if @derived_proc_cache[source_location]&.last.to_i.< mtime
    @derived_proc_cache[source_location] = begin
      source_lines = IO.readlines(source_location.first)
      proc_line = source_location.last - 1
      first_line = source_lines[proc_line]
      until first_line =~ /(?:lambda|proc|->)/ || proc_line.zero?
        proc_line -= 1
        first_line = source_lines[proc_line]
      end
      lines = source_lines[proc_line..]
      lines[0] = lines[0][/(?:lambda|proc|->).*/]
      src_str = ''.dup
      intermediate = nil
      lines.each do |line|
        token_count = 0
        line.split(/(?=\s|;|\)|\})/).each do |token|
          src_str << token
          token_count += 1
          intermediate = Prism.parse(src_str)
          next unless intermediate.success? && token_count > 1

          break
        end
        break if intermediate.success?
      end

      raise 'Source Extraction Failed' unless intermediate.success?

      src = intermediate.value.statements.body.first.yield_self do |s|
        s.type == :call_node ? s.block : s
      end
      requireds = (src.parameters&.parameters&.requireds || []).map(&:name)
      optionals = src.parameters&.parameters&.optionals || []
      keywords =  (src.parameters&.parameters&.keywords || []).map do |kw|
        [kw.name, kw.value.slice.gsub(/^_\./, '$.')]
      end.to_h
      [src, requireds, optionals, keywords, proc_line, mtime]
    end
  end

  @derived_proc_cache[source_location]
rescue StandardError => e
  LazyGraph.logger.error(e.message)
  LazyGraph.logger.error(e.backtrace)
  raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported"
end

Instance Method Details

#build_derived_inputs(derived, helpers) ⇒ Object

Derived input rules can be provided in a wide variety of formats, this function handles them all.

  1. A simple string or symbol: ‘a.b.c’. The value at the nodes is simply set to the resolved value

  2. Alternatively, you must split the inputs and the rule.

derived[:inputs]
a. Inputs as strings or symbols, e.g. inputs: ['position', 'velocity'].
 These paths are resolved and made available within the rule by the same name
b. Inputs as a map of key-value pairs, e.g. inputs: { position: 'a.b.c', velocity: 'd.e.f' },
 These are resolved and made available within the rule by the mapped name
  1. derived

The rule can be a simple string of Ruby code OR (this way we can encode entire lazy graphs as pure JSON)
A ruby block.


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/lazy_graph/node/derived_rules.rb', line 25

def build_derived_inputs(derived, helpers)
  @resolvers = {}.compare_by_identity
  @path_cache = {}.compare_by_identity

  derived = interpret_derived_proc(derived) if derived.is_a?(Proc)
  derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol)
  derived[:inputs] = parse_derived_inputs(derived)
  @fixed_result = derived[:fixed_result]
  @copy_input = true if !derived[:calc] && derived[:inputs].size == 1
  extract_derived_src(derived) if @debug

  @inputs_optional = derived[:calc].is_a?(Proc)
  derived[:calc] = parse_rule_string(derived) if derived[:calc].is_a?(String) || derived[:calc].is_a?(Symbol)

  @node_context = create_derived_input_context(derived, helpers)
  @inputs = map_derived_inputs_to_paths(derived[:inputs])
  @conditions = derived[:conditions]
  @derived = true
end

#create_derived_input_context(derived, helpers) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/lazy_graph/node/derived_rules.rb', line 179

def create_derived_input_context(derived, helpers)
  return if @copy_input

  Struct.new(*(derived[:inputs].keys.map(&:to_sym) + %i[itself stack_ptr])) do
    def missing?(value) = value.is_a?(LazyGraph::MissingValue) || value.nil?
    helpers&.each { |h| include h }

    define_method(:process!, &derived[:calc]) if derived[:calc].is_a?(Proc)
    def method_missing(name, *args, &block)
      stack_ptr.send(name, *args, &block)
    end

    def respond_to_missing?(name, include_private = false)
      stack_ptr.respond_to?(name, include_private)
    end
  end.new
end

#extract_derived_src(derived) ⇒ Object



158
159
160
161
162
163
164
165
166
# File 'lib/lazy_graph/node/derived_rules.rb', line 158

def extract_derived_src(derived)
  return @src ||= derived[:calc].to_s.lines unless derived[:calc].is_a?(Proc)

  @src ||= begin
    extract_expr_from_source_location(derived[:calc].source_location).body.slice.lines.map(&:strip)
  rescue StandardError
    ["Failed to extract source from proc #{derived}"]
  end
end

#interpret_derived_proc(derived) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/lazy_graph/node/derived_rules.rb', line 45

def interpret_derived_proc(derived)
  src, requireds, optionals, keywords, proc_line, = DerivedRules.extract_expr_from_source_location(derived.source_location)
  src = src.body&.slice || ''
  @src = src.lines.map(&:strip)
  inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords)

  {
    inputs: inputs,
    mtime: File.mtime(derived.source_location.first),
    conditions: conditions,
    calc: instance_eval(
      "->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{src}}",
      # rubocop:disable:next-line
      derived.source_location.first,
      # rubocop:enable
      derived.source_location.last.succ.succ
    )
  }
end

#map_derived_inputs_to_paths(inputs) ⇒ Object



197
198
199
200
201
202
203
204
# File 'lib/lazy_graph/node/derived_rules.rb', line 197

def map_derived_inputs_to_paths(inputs)
  inputs.values.map.with_index do |path, idx|
    segment_indexes = path.parts.map.with_index do |segment, i|
      segment.is_a?(PathParser::PathGroup) && segment.options.length == 1 ? i : nil
    end.compact
    [path, idx, segment_indexes.any? ? segment_indexes : nil]
  end
end

#parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/lazy_graph/node/derived_rules.rb', line 65

def parse_args_with_conditions(requireds, optionals_with_conditions, keywords_with_conditions)
  keywords = requireds.map { |r| [r, r] }.to_h
  conditions = {}
  keywords_with_conditions.map do |k, v|
    path, condition = v.split('=')
    keywords[k] = path
    conditions[k] = eval(condition) if condition
  end
  optionals_with_conditions.each do |optional_with_conditions|
    keywords[optional_with_conditions.name] = optional_with_conditions.name
    conditions[optional_with_conditions.name] = eval(optional_with_conditions.value.slice)
  end
  [keywords, conditions.any? ? conditions : nil]
end

#parse_derived_inputs(derived) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/lazy_graph/node/derived_rules.rb', line 131

def parse_derived_inputs(derived)
  inputs = derived[:inputs]
  case inputs
  when Symbol, String
    if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc]
      @src ||= inputs
      input_hash = {}
      @input_mapper = {}
      derived[:calc] = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
        sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
        @input_mapper[sub.to_sym] = match[2...-1].to_sym
        sub
      end
      input_hash.invert
    else
      { inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze }
    end
  when Array
    pairs = inputs.last.is_a?(Hash) ? inputs.pop : {}
    inputs.map { |v| { v.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => v } }.reduce(pairs, :merge)
  when Hash
    inputs
  else
    {}
  end.transform_values { |v| PathParser.parse(v) }
end

#parse_rule_string(derived) ⇒ Object



168
169
170
171
172
173
174
175
176
177
# File 'lib/lazy_graph/node/derived_rules.rb', line 168

def parse_rule_string(derived)
  calc_str = derived[:calc]
  src = @src
  instance_eval(
    "->{ begin; #{calc_str}; rescue StandardError => e; LazyGraph.logger.error(\"Exception in \#{src}. \#{e.message}\"); LazyGraph.logger.error(e.backtrace.join(\"\\n\")); raise; end  }", __FILE__, __LINE__
  )
rescue SyntaxError
  missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
  -> { missing_value }
end