Class: ActiveFacts::CQL::Compiler::Fact

Inherits:
Definition
  • Object
show all
Defined in:
lib/activefacts/cql/compiler/fact.rb

Instance Attribute Summary

Attributes inherited from Definition

#constellation, #tree, #vocabulary

Instance Method Summary collapse

Methods inherited from Definition

#all_bindings_in_clauses, #build_all_steps, #build_steps, #build_variables, #source

Constructor Details

#initialize(clauses, population_name = '') ⇒ Fact

Returns a new instance of Fact.



6
7
8
9
# File 'lib/activefacts/cql/compiler/fact.rb', line 6

def initialize clauses, population_name = ''
  @clauses = clauses
  @population_name = population_name
end

Instance Method Details

#all_clausesObject

Occasionally we need to search through all the clauses:



134
135
136
137
138
# File 'lib/activefacts/cql/compiler/fact.rb', line 134

def all_clauses
  @clauses.map do |clause|
    [clause] + clause.refs.map{|vr| vr.nested_clauses}
  end.flatten.compact
end

#bind_clause(clause) ⇒ Object

Try to bind this clause, and return true if it can be completed



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
# File 'lib/activefacts/cql/compiler/fact.rb', line 81

def bind_clause clause
  return true if clause.fact

  # Find the roles of this clause that do not yet have an instance
  bare_roles = clause.refs.
    select do |ref|
      next false if ref.binding.instance
      next false if ref.literal and
        ref.binding.instance = instance_identified_by_literal(ref.binding.player, ref.literal)
      true
    end

  debug :instance, "Considering '#{clause.display}' with "+
    (bare_roles.empty? ? "no bare roles" : "bare roles: #{bare_roles.map{|ref| ref.player.name}*", "}") do

    # If all the roles are in place, we can bind the rest of this clause:
    return true if bare_roles.size == 0 && bind_complete_fact(clause)

    progress = false
    if bare_roles.size == 1 &&
        (binding = bare_roles[0].binding) &&
        (et = binding.player).is_a?(ActiveFacts::Metamodel::EntityType)
      if et.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type == clause.fact_type} &&
        bind_entity_if_identifier_ready(clause, et, binding)
        progress = true
      end
    end

    return true if progress
    debug :instance, "Can't make progress on '#{clause.display}'"
    nil
  end
end

#bind_complete_fact(clause) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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
191
192
193
# File 'lib/activefacts/cql/compiler/fact.rb', line 140

def bind_complete_fact clause
  return true unless clause.fact_type  # An bare objectification
  debug :instance, "All bindings in '#{clause.display}' contain instances; create the fact type"
  instances = clause.refs.map{|vr| vr.binding.instance}
  debug :instance, "Instances are #{instances.map{|i| "#{i.object_type.name} #{i.value.inspect}"}*", "}"

  if e = clause.fact_type.entity_type and
    clause.refs[0].binding.instance.object_type == e
    fact = clause.refs[0].binding.instance.fact
  else
    # Check that this fact doesn't already exist
    fact = clause.fact_type.all_fact.detect do |f|
      # Get the role values of this fact in the order of the clause we just bound
      role_values_in_clause_order = f.all_role_value.sort_by do |rv|
        clause.reading.role_sequence.all_role_ref.detect{|rr| rr.role == rv.role}.ordinal
      end
      # If all this fact's role values are played by the bound instances, it's the same fact
      !role_values_in_clause_order.zip(instances).detect{|rv, i| rv.instance != i }
    end
  end
  if fact
    clause.fact = fact
    debug :instance, "Found existing fact type instance"
  else
    fact =
      clause.fact =
      @constellation.Fact(:new, :fact_type => clause.fact_type, :population => @population)
    @bound_facts << fact

    clause.reading.role_sequence.all_role_ref_in_order.zip(instances).each do |rr, instance|
      debug :instance, "New fact has #{instance.object_type.name} role #{instance.value.inspect}"
      # REVISIT: Any residual adjectives after the fact type matching are lost here.
      @constellation.RoleValue(:fact => fact, :instance => instance, :role => rr.role, :population => @population)
    end
  end

  if !fact.instance && clause.fact_type.entity_type
    # Objectified fact type; create the instance
    # Create the instance that objectifies this fact. We don't have the binding to assign it to though; that'll happen in our caller
    debug :instance, "Objectifying fact as #{clause.fact_type.entity_type.name}"
    instance =
      @constellation.Instance(:new, :object_type => clause.fact_type.entity_type, :fact => fact, :population => @population)
    @bound_facts << instance
  end

  if clause.fact and
    clause.objectified_as and
    instance = clause.fact.instance and
    instance.object_type == clause.objectified_as.binding.player
    clause.objectified_as.binding.instance = instance
  end

  true
end

#bind_entity_if_identifier_ready(clause, entity_type, binding) ⇒ Object

If we have one bare role (no literal or instance) played by an entity type, and the bound fact type participates in the identifier, we might now be able to create the entity instance.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/activefacts/cql/compiler/fact.rb', line 198

def bind_entity_if_identifier_ready clause, entity_type, binding
  # Check this instance doesn't already exist already:
  identifying_binding = (clause.refs.map{|vr| vr.binding}-[binding])[0]
  return false unless identifying_binding # This happens when we have a bare objectification
  identifying_instance = identifying_binding.instance
  preferred_identifier = entity_type.preferred_identifier

  debug :instance, "This clause associates a new #{binding.player.name} with a #{identifying_binding.player.name}#{identifying_instance ? " which exists" : ""}"

  identifying_role_ref = preferred_identifier.role_sequence.all_role_ref.detect { |rr|
      rr.role.fact_type == clause.fact_type && rr.role.object_type == identifying_binding.player
    }
  unless identifying_role_ref
    # This shold never happen; we already bound all refs
    debug :instance, "Failed to find a #{identifying_instance.object_type.name}"
    return false # We can't do this yet
  end
  role_value = identifying_instance.all_role_value.detect do |rv|
    rv.fact.fact_type == identifying_role_ref.role.fact_type
  end
  if role_value
    instance = (role_value.fact.all_role_value.to_a-[role_value])[0].instance
    debug :instance, "Found an existing instance (of #{instance.object_type.name}) from a previous definition"
    binding.instance = instance
    return true  # Done with this clause
  end

  pi_role_refs = preferred_identifier.role_sequence.all_role_ref
  # For each pi role, we have to find the fact clause, which contains the binding we need.
  # Then we have to create an instance of each fact
  identifiers =
    pi_role_refs.map do |rr|
      # Find a clause that provides the identifying_ref for this player:
      identifying_clause = all_clauses.detect do |clause|
        rr.role.fact_type == clause.fact_type &&
          clause.refs.detect{|vr| vr.binding == binding}
      end
      return false unless identifying_clause
      identifying_ref = identifying_clause.refs.select{|ref| ref.binding != binding}[0]
      identifying_binding = identifying_ref ? identifying_ref.binding : nil
      identifying_instance = identifying_binding.instance

      [rr, identifying_clause, identifying_binding, identifying_instance]
    end
  if identifiers.detect{ |i| !i[3] }  # Not all required facts are bound yet
    debug :instance, "Can't go through with creating #{binding.player.name}; not all the identifying facts are in"
    return false
  end

  debug :instance, "Going ahead with creating #{binding.player.name} using #{identifiers.size} roles" do
    instance = @constellation.Instance(:new, :object_type => entity_type, :population => @population)
    binding.instance = instance
    @bound_facts << instance
    identifiers.each do |rr, identifying_clause, identifying_binding, identifying_instance|
      # This clause provides the identifying literal for the entity_type
      id_fact =
        identifying_clause.fact =
        @constellation.Fact(:new, :fact_type => rr.role.fact_type, :population => @population)
      @bound_facts << id_fact
      role = (rr.role.fact_type.all_role.to_a-[rr.role])[0]
      @constellation.RoleValue(:instance => instance, :fact => id_fact, :population => @population, :role => role)
      @constellation.RoleValue(:instance => identifying_instance, :fact => id_fact, :role => rr.role, :population => @population)
    end
  end

  true  # Done with this clause
end

#bind_literal_or_fact_type(clause) ⇒ 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
# File 'lib/activefacts/cql/compiler/fact.rb', line 44

def bind_literal_or_fact_type clause
  # Every bound word (term) in the phrases must have a literal
  # OR be bound to an entity type identified by the phrases

  # Any clause that has one binding and no other word is
  # either a value instance or a simply-identified entity.
  clause.refs.each do |ref|
    next unless l = ref.literal    # No literal
    next if ref.binding.instance   # Already bound
    player = ref.binding.player
    # raise "A literal may not be an objectification" if ref.role_ref.nested_clauses
    # raise "Not processing facts involving nested clauses yet" if ref.role_ref
    debug :instance, "Making #{player.class.basename} #{player.name} using #{l.inspect}" do
      ref.binding.instance = instance_identified_by_literal(player, l)
    end
    ref
  end

  if clause.phrases.size == 1 and (ref = clause.phrases[0]).is_a?(Compiler::Reference)
    if ref.nested_clauses
      # Assign the objectified fact type as this clause's fact type?
      clause.fact_type = ref.player.fact_type
      clause
    else
      # This is an existential fact (like "Name 'foo'", or "Company 'Microsoft'")
      nil # Nothing to see here, move along
    end
  else
    raise "Fact Type not found: '#{clause.display}'" unless clause.fact_type
    # This instance will be associated with its binding by our caller
    clause
  end
end

#bind_more_factsObject

Take one pass through the @unbound_clauses, processing (and removing) any that have all pre-requisites



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/activefacts/cql/compiler/fact.rb', line 116

def bind_more_facts
  return false unless @unbound_clauses.size > 0
  @pass += 1

  progress = false
  debug :instance, "Pass #{@pass} with #{@unbound_clauses.size} clauses to consider" do
    @unbound_clauses =
      @unbound_clauses.select do |clause|
        action = bind_clause(clause)
        progress = true if action
        !action
      end
    debug :instance, "end of pass, unbound clauses are #{@unbound_clauses.map(&:display)*', '}"
  end # debug
  progress
end

#compileObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/activefacts/cql/compiler/fact.rb', line 11

def compile
  @population = @constellation.Population[[@vocabulary.identifying_role_values, @population_name]] ||
	      @constellation.Population(@vocabulary, @population_name, :concept => :new)

  @context = CompilationContext.new(@vocabulary)
  @context.bind @clauses
  @context.left_contraction_allowed = true
  @clauses.each do |clause|
	    ft = clause.match_existing_fact_type @context
	    if clause.certainty == false
	      raise "Negated fact #{clause.inspect} is not supported"
	    end
	  end

  # Figure out the simple existential facts and find fact types:
  @bound_facts = []
  @unbound_clauses = all_clauses.
    map do |clause|
      bind_literal_or_fact_type clause
    end.
    compact

  # Because the fact types may include forward references, we must
  # process the list repeatedly until we make no further progress.
  @pass = 0 # Repeat until we make no more progress:
  true while bind_more_facts

  # Any remaining unbound facts are a problem we can bitch about:
  complain_incomplete unless @unbound_clauses.empty?

  @bound_facts.uniq # N.B. this includes Instance objects (existential facts)
end

#complain_incompleteObject



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/activefacts/cql/compiler/fact.rb', line 331

def complain_incomplete
  if @unbound_clauses.size > 0
    # Provide a readable description of the problem here, by showing each binding with no instance
    missing_bindings = @unbound_clauses.
      map do |clause|
        clause.refs.
          select do |refs|
            !refs.binding.instance
          end.
          map do |ref|
            ref.binding
          end
      end.
      flatten.
      uniq

    raise "Not enough facts are given to identify #{
        missing_bindings.
          sort_by{|b| b.key}.
          map do |b|
            player_identifier =
              if b.player.is_a?(ActiveFacts::Metamodel::EntityType)
                "lacking " +
                  b.player.preferred_identifier.role_sequence.all_role_ref.map do |rr|
                    [ rr.leading_adjective, rr.role.role_name || rr.role.object_type.name, rr.trailing_adjective ].compact*" "
                  end*", "
              else
                "needs a value"
              end
            [
              b.refs[0].leading_adjective, b.player.name, b.refs[0].trailing_adjective
            ].compact*" " +
              " (#{player_identifier})"
          end*" or "
      }"
  end
end

#entity_identified_by_literal(object_type, literal) ⇒ Object



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/activefacts/cql/compiler/fact.rb', line 296

def entity_identified_by_literal object_type, literal
  # A literal that identifies an entity type means the entity type has only one identifying role
  # That role is played either by a value type, or by another similarly single-identified entity type
  debug "Making EntityType #{object_type.name} identified by '#{literal}' #{@population.name.size>0 ? " in "+@population.name.inspect : ''}" do
    identifying_role_refs = object_type.preferred_identifier.role_sequence.all_role_ref
    raise "Single literal cannot satisfy multiple identifying roles for #{object_type.name}" if identifying_role_refs.size > 1
    role = identifying_role_refs.single.role
    # This instance has no binding; the binding is of the entity type not the identifying value type
    identifying_instance = instance_identified_by_literal role.object_type, literal
    existing_instance = nil
    instance_rv = identifying_instance.all_role_value.detect { |rv|
      next false unless rv.population == @population         # Not this population
      next false unless rv.fact.fact_type == role.fact_type # Not this fact type
      other_role_value = (rv.fact.all_role_value-[rv])[0]
      existing_instance = other_role_value.instance
      other_role_value.instance.object_type == object_type          # Is it this object_type?
    }
    if instance_rv
      instance = existing_instance
      debug :instance, "This #{object_type.name} entity already exists"
    else
      # This fact has no clause.
      fact = @constellation.Fact(:new, :fact_type => role.fact_type, :population => @population)
      @bound_facts << fact
      # This instance will be associated with its binding by our caller
      instance = @constellation.Instance(:new, :object_type => object_type, :population => @population)
      @bound_facts << instance
      # The identifying fact type has two roles; create both role instances:
      @constellation.RoleValue(:instance => identifying_instance, :fact => fact, :population => @population, :role => role)
      @constellation.RoleValue(:instance => instance, :fact => fact, :population => @population, :role => (role.fact_type.all_role-[role])[0])
    end
    instance
  end
end

#instance_identified_by_literal(object_type, literal) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/activefacts/cql/compiler/fact.rb', line 266

def instance_identified_by_literal object_type, literal
  if object_type.is_a?(ActiveFacts::Metamodel::EntityType)
    entity_identified_by_literal object_type, literal
  else
    debug :instance, "Making ValueType #{object_type.name} #{literal.inspect} #{@population.name.size>0 ? " in "+@population.name.inspect : ''}" do

      is_literal_string = literal.literal.is_a?(String)
      instance = @constellation.Instance.detect do |key, i|
          # REVISIT: And same unit
          i.population == @population &&
            i.value &&
            i.value.literal == literal &&
            i.value.is_literal_string == is_literal_string
        end
      #instance = object_type.all_instance.detect { |instance|
      #  instance.population == @population && instance.value == literal
      #}
      debug :instance, "This #{object_type.name} value already exists" if instance
      unless instance
        instance = @constellation.Instance(:new)
        instance.object_type = object_type
        instance.population = @population
        instance.value = [literal.to_s, is_literal_string, nil]
        @bound_facts << instance
      end
      instance
    end
  end
end

#to_sObject



369
370
371
# File 'lib/activefacts/cql/compiler/fact.rb', line 369

def to_s
  super+@clauses.map(&:to_s)*', '
end