activefacts-cql 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +19 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-cql.gemspec +29 -0
  10. data/bin/setup +7 -0
  11. data/lib/activefacts/cql.rb +7 -0
  12. data/lib/activefacts/cql/.gitignore +0 -0
  13. data/lib/activefacts/cql/Rakefile +14 -0
  14. data/lib/activefacts/cql/compiler.rb +156 -0
  15. data/lib/activefacts/cql/compiler/clause.rb +1137 -0
  16. data/lib/activefacts/cql/compiler/constraint.rb +581 -0
  17. data/lib/activefacts/cql/compiler/entity_type.rb +457 -0
  18. data/lib/activefacts/cql/compiler/expression.rb +443 -0
  19. data/lib/activefacts/cql/compiler/fact.rb +390 -0
  20. data/lib/activefacts/cql/compiler/fact_type.rb +421 -0
  21. data/lib/activefacts/cql/compiler/query.rb +106 -0
  22. data/lib/activefacts/cql/compiler/shared.rb +161 -0
  23. data/lib/activefacts/cql/compiler/value_type.rb +174 -0
  24. data/lib/activefacts/cql/parser.rb +234 -0
  25. data/lib/activefacts/cql/parser/CQLParser.treetop +167 -0
  26. data/lib/activefacts/cql/parser/Context.treetop +48 -0
  27. data/lib/activefacts/cql/parser/Expressions.treetop +67 -0
  28. data/lib/activefacts/cql/parser/FactTypes.treetop +358 -0
  29. data/lib/activefacts/cql/parser/Language/English.treetop +315 -0
  30. data/lib/activefacts/cql/parser/Language/French.treetop +315 -0
  31. data/lib/activefacts/cql/parser/Language/Mandarin.treetop +304 -0
  32. data/lib/activefacts/cql/parser/LexicalRules.treetop +253 -0
  33. data/lib/activefacts/cql/parser/ObjectTypes.treetop +210 -0
  34. data/lib/activefacts/cql/parser/Terms.treetop +183 -0
  35. data/lib/activefacts/cql/parser/ValueTypes.treetop +202 -0
  36. data/lib/activefacts/cql/parser/nodes.rb +49 -0
  37. data/lib/activefacts/cql/require.rb +36 -0
  38. data/lib/activefacts/cql/verbaliser.rb +804 -0
  39. data/lib/activefacts/cql/version.rb +5 -0
  40. data/lib/activefacts/input/cql.rb +43 -0
  41. data/lib/rubygems_plugin.rb +12 -0
  42. metadata +167 -0
@@ -0,0 +1,49 @@
1
+ module ActiveFacts
2
+ module CQL
3
+ class Parser
4
+ class TermNode < Treetop::Runtime::SyntaxNode
5
+ def ast quantifier = nil, function_call = nil, role_name = nil, value_constraint = nil, literal = nil, nested_clauses = nil
6
+ t = x.context[:term]
7
+ gt = x.context[:global_term]
8
+ if t.size > gt.size and t[-gt.size..-1] == gt
9
+ leading_adjective = t[0...-gt.size-1]
10
+ leading_adjective.sub!(/ /, '-') if !tail.elements[0].dbl.empty?
11
+ end
12
+ if t.size > gt.size and t[0...gt.size] == gt
13
+ trailing_adjective = t[gt.size+1..-1]
14
+ trailing_adjective.sub!(/ (\S*)\Z/, '-\1') if !tail.elements[-1].dbl.empty?
15
+ end
16
+ Compiler::Reference.new(gt, leading_adjective, trailing_adjective, quantifier, function_call, role_name, value_constraint, literal, nested_clauses)
17
+ end
18
+
19
+ def value # Sometimes we just want the full term name
20
+ x.context[:term]
21
+ end
22
+
23
+ def node_type
24
+ :term
25
+ end
26
+ end
27
+
28
+ class TermLANode < TermNode
29
+ def ast quantifier = nil, function_call = nil, role_name = nil, value_constraint = nil, literal = nil, nested_clauses = nil
30
+ ast = term.ast(quantifier, function_call, role_name, value_constraint, literal, nested_clauses)
31
+ ast.leading_adjective = head.text_value
32
+ ast
33
+ end
34
+ end
35
+
36
+ class TermDefinitionNameNode < TermNode
37
+ def value
38
+ t.elements.inject([
39
+ id.value
40
+ ]){|a, e| a << e.id.value}*' '
41
+ end
42
+
43
+ def node_type
44
+ :term
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+ # ActiveFacts CQL loader.
3
+ # Use Polyglot to patch things so you can *require* a CQL file and have it define a Ruby module.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'polyglot'
8
+ require 'stringio'
9
+
10
+ require 'activefacts/support'
11
+ require 'activefacts/input/cql'
12
+
13
+ if (require 'activefacts/generators/ruby' rescue nil)
14
+ module ActiveFacts
15
+ # This class has a load method for Polyglot to tell it how to _require_ a CQL file.
16
+ # The CQL file is parsed to a vocabulary constellation, which is generated
17
+ # to Ruby code and eval'd, making the generated classes available.
18
+ # To make this Loader available, simply
19
+ # require 'activefacts/cql'
20
+ class CQLLoader
21
+ def self.load(file) #:nodoc:
22
+ trace "Loading #{file}" do
23
+ vocabulary = ActiveFacts::Input::CQL.readfile(file)
24
+
25
+ ruby = StringIO.new
26
+ @dumper = ActiveFacts::Generators::RUBY.new(vocabulary.constellation)
27
+ @dumper.generate(ruby)
28
+ ruby.rewind
29
+ eval ruby.read, ::TOPLEVEL_BINDING
30
+ end
31
+ end
32
+ end
33
+
34
+ Polyglot.register('cql', CQLLoader)
35
+ end
36
+ end
@@ -0,0 +1,804 @@
1
+ #
2
+ # ActiveFacts Vocabulary Metamodel.
3
+ # Verbaliser for the ActiveFacts Vocabulary
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module CQL
9
+ #
10
+ # The Verbaliser fulfils two roles:
11
+ # * Maintains verbalisation context to expand readings using subscripting where needed
12
+ # * Verbalises Queries by iteratively choosing a Step and expanding readings
13
+ #
14
+ # The verbalisation context consists of a set of Players, each for one ObjectType.
15
+ # There may be more than one Player for the same ObjectType. If adjectives or role
16
+ # names don't make such duplicates unambiguous, subscripts will be generated.
17
+ # Thus, the verbalisation context must be completely populated before subscript
18
+ # generation, which must be before any Player name gets verbalised.
19
+ #
20
+ # When a Player occurs in a Query, it corresponds to one Variable of that Query.
21
+ # Each such Player has one or more Plays, which refer to roles played by
22
+ # that ObjectType. Where a query traverses two roles of a ternary fact type, there
23
+ # will be a residual node that has only a single Play with no other meaning.
24
+ # A Play must be for exactly one Player, so is used to identify a Player.
25
+ #
26
+ # When a Player occurs outside a Query, it's identified by a projected RoleRef.
27
+ # REVISIT: This is untrue when a uniqueness constraint is imported from NORMA.
28
+ # In this case no query will be constructed to project the roles of the constrained
29
+ # object type (only the constrained roles will be projected) - this will be fixed.
30
+ #
31
+ # Each constraint (except Ring Constraints) has one or more RoleSequence containing
32
+ # the projected RoleRefs. Each constrained RoleSequence may have an associated Query.
33
+ # If it has a Query, each RoleRef is projected from a Play, otherwise none are.
34
+ #
35
+ # The only type of query possible in a Ring Constraint is a subtyping query, which
36
+ # is always implicit and unambiguous, so is never instantiated.
37
+ #
38
+ # A constrained RoleSequence that has no explicit Query may have an implicit query,
39
+ # as per ORM2, when the roles aren't in the same fact type. These implicit queries
40
+ # are over only one ObjectType, by traversing a single FactType (and possibly,
41
+ # multiple TypeInheritance FactTypes) for each RoleRef. Note however that when
42
+ # the ObjectType is an objectified Fact Type, the FactType traversed might be a
43
+ # phantom of the objectification. In the case of implicit queries, each Player is
44
+ # identified by the projected RoleRef, except for the joined-over ObjectType whose
45
+ # Player is... well, read the next paragraph!
46
+ #
47
+ # REVISIT: I believe that the foregoing paragraph is out of date, except with
48
+ # respect to PresenceConstraints imported from NORMA (both external mandatory
49
+ # and external uniqueness constraints). The joined-over Player in a UC is
50
+ # identified by its RoleRefs in the RoleSequence of the Fact Type's preferred
51
+ # reading. Subtyping steps in a mandatory constraint will probably malfunction.
52
+ # However, all other such queries are explicit, and these should be also.
53
+ #
54
+ # For a SetComparisonConstraint, there are two or more constrained RoleSequences.
55
+ # The matching RoleRefs (by Ordinal position) are for joined players, that is,
56
+ # one individual instance plays both roles. The RoleRefs must (now) be for the
57
+ # same ObjectType (no implicit subtyping step is allowed). Instead, the input modules
58
+ # find the closest common supertype and create explicit Steps so its roles
59
+ # can be projected.
60
+ #
61
+ # When expanding Reading text however, the RoleRefs in the reading's RoleSequence
62
+ # may be expected not to be attached to the Players for that reading. Instead,
63
+ # the set of one or more RoleRefs which caused that reading to be expanded must
64
+ # be passed in, and the corresponding roles matched with Players to determine
65
+ # the need to emit a subscript.
66
+ #
67
+ class Verbaliser
68
+ # Verbalisation context:
69
+ attr_reader :players
70
+ attr_reader :player_by_play # Used for each query
71
+ attr_reader :player_joined_over # Used when there's an implicit query
72
+ attr_reader :player_by_role_ref # Used when a constrained role sequence has no query
73
+
74
+ # The projected role references over which we're verbalising
75
+ attr_reader :role_refs
76
+
77
+ # Query Verbaliser context:
78
+ attr_reader :query
79
+ attr_reader :variables # All Variables
80
+ attr_reader :steps # All remaining unemitted Steps
81
+ attr_reader :steps_by_variable # A Hash by Variable containing an array of remaining steps
82
+
83
+ def initialize role_refs = nil
84
+ @role_refs = role_refs
85
+
86
+ # Verbalisation context:
87
+ @players = []
88
+ @player_by_play = {}
89
+ @player_by_role_ref = {}
90
+ @player_joined_over = nil
91
+
92
+ # Query Verbaliser context:
93
+ @query = nil
94
+ @variables = []
95
+ @steps = []
96
+ @steps_by_variable = {}
97
+
98
+ add_role_refs role_refs if role_refs
99
+ end
100
+
101
+ class Player
102
+ attr_accessor :object_type, :variables_by_query, :subscript, :plays, :role_refs
103
+ def initialize object_type
104
+ @object_type = object_type
105
+ @variables_by_query = {}
106
+ @subscript = nil
107
+ @plays = []
108
+ @role_refs = []
109
+ end
110
+
111
+ # What words are used (across all roles) for disambiguating the references to this player?
112
+ # If more than one set of adjectives was used, this player must have been subject to loose binding.
113
+ # This method is used to decide when subscripts aren't needed.
114
+ def role_adjuncts matching
115
+ if matching == :loose
116
+ adjuncts = []
117
+ else
118
+ adjuncts = @role_refs.map{|rr|
119
+ [
120
+ rr.leading_adjective,
121
+ matching == :rolenames ? rr.role.role_name : nil,
122
+ rr.trailing_adjective
123
+ ].compact}.uniq.sort
124
+ end
125
+ adjuncts += [@variables_by_query.values.map{|jn| jn.role_name}.compact[0]].compact
126
+ adjuncts.flatten*"_"
127
+ end
128
+
129
+ def describe
130
+ @object_type.name + (@variables_by_query.size > 0 ? " (in #{@variables_by_query.size} variables)" : "")
131
+ end
132
+ end
133
+
134
+ # Find or create a Player to which we can add this role_ref
135
+ def player(ref)
136
+ existing_player = if ref.is_a?(ActiveFacts::Metamodel::Play)
137
+ @player_by_play[ref]
138
+ else
139
+ @player_by_role_ref[ref] or ref.play && @player_by_play[ref.play]
140
+ end
141
+ if existing_player
142
+ trace :player, "Using existing player for #{ref.role.object_type.name} #{ref.respond_to?(:role_sequence) && ref.role_sequence.all_reading.size > 0 ? ' in reading' : ''}in '#{ref.role.fact_type.default_reading}'"
143
+ return existing_player
144
+ else
145
+ trace :player, "Adding new player for #{ref.role.object_type.name} #{ref.respond_to?(:role_sequence) && ref.role_sequence.all_reading.size > 0 ? ' in reading' : ''}in '#{ref.role.fact_type.default_reading}'"
146
+ p = Player.new(ref.role.object_type)
147
+ @players.push(p)
148
+ p
149
+ end
150
+ end
151
+
152
+ def add_play player, play
153
+ return if player.plays.include?(play)
154
+ jn = play.variable
155
+ if jn1 = player.variables_by_query[jn.query] and jn1 != jn
156
+ raise "Player for #{player.object_type.name} may only have one variable per query, not #{jn1.object_type.name} and #{jn.object_type.name}"
157
+ end
158
+ player.variables_by_query[jn.query] = jn
159
+ @player_by_play[play] = player
160
+ player.plays << play
161
+ end
162
+
163
+ # Add a RoleRef to an existing Player
164
+ def add_role_player player, role_ref
165
+ #trace :subscript, "Adding role_ref #{role_ref.object_id} to player #{player.object_id}"
166
+ if play = role_ref.play
167
+ add_play(player, play)
168
+ elsif !player.role_refs.include?(role_ref)
169
+ trace :subscript, "Adding reference to player #{player.object_id} for #{role_ref.role.object_type.name} in #{role_ref.role_sequence.describe} with #{role_ref.role_sequence.all_reading.size} readings"
170
+ player.role_refs.push(role_ref)
171
+ @player_by_role_ref[role_ref] = player
172
+ end
173
+ end
174
+
175
+ def add_role_ref role_ref
176
+ add_role_player(player(role_ref), role_ref)
177
+ end
178
+
179
+ # Add RoleRefs to one or more Players, creating players where needed
180
+ def add_role_refs role_refs
181
+ role_refs.each{|rr| add_role_ref(rr) }
182
+ end
183
+
184
+ # Return an array of the names of these identifying_roles.
185
+ def identifying_role_names identifying_role_refs
186
+ identifying_role_refs.map do |role_ref|
187
+ preferred_role_ref = role_ref.role.fact_type.preferred_reading.role_sequence.all_role_ref.detect{|reading_rr|
188
+ reading_rr.role == role_ref.role
189
+ }
190
+
191
+ if (role_ref.role.fact_type.all_role.size == 1)
192
+ role_ref.role.fact_type.default_reading # Need whole reading for a unary.
193
+ elsif role_name = role_ref.role.role_name and role_name != ''
194
+ role_name
195
+ else
196
+ role_name = preferred_role_ref.cql_name
197
+ if p = player(preferred_role_ref) and p.subscript
198
+ role_name += "(#{p.subscript})"
199
+ end
200
+ role_name
201
+ end
202
+ end
203
+ end
204
+
205
+ # All these readings are for the same fact type, and all will be emitted, so the roles cover the same players
206
+ # This is used when verbalising fact types and entity types.
207
+ def alternate_readings readings
208
+ readings.map do |reading|
209
+ reading.role_sequence.all_role_ref.sort_by{|rr| rr.role.ordinal}
210
+ end.transpose.each do |role_refs|
211
+ role_refs_have_same_player role_refs
212
+ end
213
+ end
214
+
215
+ def plays_have_same_player plays
216
+ return if plays.empty?
217
+
218
+ # If any of these plays are for a known player, use that, else make a new player.
219
+ existing_players = plays.map{|play| @player_by_play[play] }.compact.uniq
220
+ if existing_players.size > 1
221
+ raise "At most one existing player can play these roles: #{existing_players.map{|p|p.object_type.name}*', '}!"
222
+ end
223
+ p = existing_players[0] || player(plays[0])
224
+ trace :subscript, "roles are playes of #{p.describe}" do
225
+ plays.each do |play|
226
+ trace :subscript, "#{play.describe}" do
227
+ add_play p, play
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ # These RoleRefs are all for the same player. Find whether any of them has a player already
234
+ def role_refs_have_same_player role_refs
235
+ role_refs = role_refs.is_a?(Array) ? role_refs : role_refs.all_role_ref.to_a
236
+ return if role_refs.empty?
237
+
238
+ # If any of these role_refs are for a known player, use that, else make a new player.
239
+ existing_players =
240
+ role_refs.map{|rr| @player_by_role_ref[rr] || @player_by_play[rr.play] }.compact.uniq
241
+ if existing_players.size > 1
242
+ raise "At most one existing player can play these roles: #{existing_players.map{|p|p.object_type.name}*', '}!"
243
+ end
244
+ p = existing_players[0] || player(role_refs[0])
245
+
246
+ trace :subscript, "#{existing_players[0] ? 'Adding to existing' : 'Creating new'} player for #{role_refs.map{|rr| rr.role.object_type.name}.uniq*', '}" do
247
+ role_refs.each do |rr|
248
+ unless p.object_type == rr.role.object_type
249
+ # This happens in SubtypePI because uniqueness constraint is built without its implicit subtyping step.
250
+ # For now, explode only if there's no common supertype:
251
+ if 0 == (p.object_type.supertypes_transitive & rr.role.object_type.supertypes_transitive).size
252
+ raise "REVISIT: Internal error, trying to add role of #{rr.role.object_type.name} to player #{p.object_type.name}"
253
+ end
254
+ end
255
+ add_role_player(p, rr)
256
+ end
257
+ end
258
+ end
259
+
260
+ # REVISIT: include_rolenames is a bit of a hack. Role names generally serve to disambiguate players,
261
+ # so subscripts wouldn't be needed, but where a constraint refers to a fact type which is defined with
262
+ # role names, those are considered. We should instead consider only the role names that are defined
263
+ # within the constraint, not in the underlying fact types. For now, this parameter is passed as true
264
+ # from all the object type verbalisations, and not from constraints.
265
+ def create_subscripts(matching = :normal)
266
+ # Create subscripts, where necessary
267
+ @players.each { |p| p.subscript = nil } # Wipe subscripts
268
+ @players.
269
+ map{|p| [p, p.object_type] }.
270
+ each do |player, object_type|
271
+ next if player.subscript # Done previously
272
+ dups = @players.select do |p|
273
+ p.object_type == object_type &&
274
+ p.role_adjuncts(matching) == player.role_adjuncts(matching)
275
+ end
276
+ if dups.size == 1
277
+ trace :subscript, "No subscript needed for #{object_type.name}"
278
+ next
279
+ end
280
+ trace :subscript, "Applying subscripts to #{dups.size} occurrences of #{object_type.name}" do
281
+ s = 0
282
+ dups.
283
+ sort_by do |p| # Guarantee stable numbering
284
+ p.role_adjuncts(:role_name) + ' ' +
285
+ # Tie-breaker:
286
+ p.role_refs.map{|rr| rr.role.fact_type.preferred_reading.text}.sort.to_s
287
+ end.
288
+ each do |player|
289
+ jrname = player.plays.map{|play| play.role_ref && play.role_ref.role.role_name}.compact[0]
290
+ rname = (rr = player.role_refs[0]) && rr.role.role_name
291
+ if jrname and !rname
292
+ # puts "Oops: rolename #{rname.inspect} != #{jrname.inspect}" if jrname != rname
293
+ player.variables_by_query.values.each{|jn| jn.role_name = jrname }
294
+ else
295
+ player.subscript = s+1
296
+ s += 1
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ # Expand a reading for an entity type or fact type definition. Unlike expansions in constraints,
304
+ # these expansions include frequency constraints, role names and value constraints as passed-in,
305
+ # and also define adjectives by using the hyphenated form (on at least the first occurrence).
306
+ def expand_reading(reading, frequency_constraints = [], define_role_names = nil, value_constraints = [], &subscript_block)
307
+ reading.expand(frequency_constraints, define_role_names, value_constraints) do |role_ref, *parts|
308
+ parts + [
309
+ (!(role_ref.role.role_name and define_role_names != nil) and p = player(role_ref) and p.subscript) ? "(#{p.subscript})" : nil
310
+ ]
311
+ end
312
+ end
313
+
314
+ # Where no explicit Query has been created, a query is still sometimes present (e.g. in a constraint from NORMA)
315
+ # REVISIT: This probably doesn't produce the required result. Need to fix the NORMA importer to create the query.
316
+ def role_refs_have_subtype_steps roles
317
+ role_refs = roles.is_a?(Array) ? roles : roles.all_role_ref.to_a
318
+ role_refs_by_object_type = role_refs.inject({}) { |h, r| (h[r.role.object_type] ||= []) << r; h }
319
+ role_refs_by_object_type.values.each { |rrs| role_refs_have_same_player(rrs) }
320
+ end
321
+
322
+ # These roles are the players in an implicit counterpart join in a Presence Constraint.
323
+ # REVISIT: It's not clear that we can safely use the preferred_reading's RoleRefs here.
324
+ # Fix the CQL compiler to create proper queries for these presence constraints instead.
325
+ def roles_have_same_player roles
326
+ role_refs = roles.map do |role|
327
+ role.fact_type.all_reading.map{|reading|
328
+ reading.role_sequence.all_role_ref.detect{|rr| rr.role == role}
329
+ } +
330
+ role.all_role_ref.select{|rr| rr.role_sequence.all_reading.size == 0 }
331
+ end.flatten.uniq
332
+ role_refs_have_same_player(role_refs)
333
+ end
334
+
335
+ def prepare_role_sequence role_sequence, join_over = nil
336
+ @role_refs = role_sequence.is_a?(Array) ? role_sequence : role_sequence.all_role_ref.to_a
337
+
338
+ if jrr = @role_refs.detect{|rr| rr.play && rr.play.variable}
339
+ return prepare_query_players(jrr.play.variable.query)
340
+ end
341
+
342
+ # Ensure that all the joined-over role_refs are indexed for subscript generation.
343
+ role_refs_by_fact_type =
344
+ @role_refs.inject({}) { |hash, rr| (hash[rr.role.fact_type] ||= []) << rr; hash }
345
+ role_refs_by_fact_type.each do |fact_type, role_refs|
346
+ role_refs.each { |rr| role_refs_have_same_player([rr]) }
347
+
348
+ # Register the role_refs in the preferred reading which refer to roles not covered in the role sequence.
349
+ prrs = fact_type.preferred_reading.role_sequence.all_role_ref
350
+ residual_roles = fact_type.all_role.select{|r| !@role_refs.detect{|rr| rr.role == r} }
351
+ residual_roles.each do |role|
352
+ trace :subscript, "Adding residual role for #{role.object_type.name} (in #{fact_type.default_reading}) not covered in role sequence"
353
+ preferred_role_ref = prrs.detect{|rr| rr.role == role}
354
+ if p = @player_by_role_ref[preferred_role_ref] and !p.role_refs.include?(preferred_role_ref)
355
+ raise "Adding DUPLICATE residual role for #{role.object_type.name}"
356
+ end
357
+ role_refs_have_same_player([prrs.detect{|rr| rr.role == role}])
358
+ end
359
+ end
360
+ end
361
+
362
+ def prepare_query_players query
363
+ trace :subscript, "Indexing roles of fact types in #{query.all_step.size} steps" do
364
+ steps = []
365
+ # Register all references to each variable as being for the same player:
366
+ query.all_variable.to_a.sort_by(&:ordinal).each do |variable|
367
+ trace :subscript, "Adding Roles of #{variable.describe}" do
368
+ plays_have_same_player(variable.all_play.to_a)
369
+ steps = steps | variable.all_step
370
+ end
371
+ end
372
+
373
+ =begin
374
+ # For each fact type traversed, register a player for each role *not* linked to this query
375
+ # REVISIT: Using the preferred_reading role_ref is wrong here; the same preferred_reading might occur twice,
376
+ # so the respective object_type will need more than one Player and will be subscripted to keep them from being joined.
377
+ # Accordingly, there must be a step for each such role, and to enforce that, I raise an exception here on duplication.
378
+ # This isn't needed now all Variables have at least one Play
379
+
380
+ steps.map do |js|
381
+ if js.fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
382
+ js.fact_type.implying_role.fact_type
383
+ else
384
+ js.fact_type
385
+ end
386
+ end.uniq.each do |fact_type|
387
+ #steps.map{|js|js.fact_type}.uniq.each do |fact_type|
388
+ next if fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
389
+
390
+ trace :subscript, "Residual roles in '#{fact_type.default_reading}' are" do
391
+ prrs = fact_type.preferred_reading.role_sequence.all_role_ref
392
+ residual_roles = fact_type.all_role.select{|r| !r.all_role_ref.detect{|rr| rr.variable && rr.variable.query == query} }
393
+ residual_roles.each do |r|
394
+ trace :subscript, "Adding residual role for #{r.object_type.name} (in #{fact_type.default_reading}) not covered in query"
395
+ preferred_role_ref = prrs.detect{|rr| rr.role == r}
396
+ if p = @player_by_role_ref[preferred_role_ref] and !p.role_refs.include?(preferred_role_ref)
397
+ raise "Adding DUPLICATE residual role for #{r.object_type.name} not covered in query"
398
+ end
399
+ role_refs_have_same_player([preferred_role_ref])
400
+ end
401
+ end
402
+ end
403
+ =end
404
+ end
405
+ end
406
+
407
+ def verbalise_over_role_sequence role_sequence, conjunction = ' and ', role_proximity = :both
408
+ @role_refs = role_sequence.is_a?(Array) ? role_sequence : role_sequence.all_role_ref.to_a
409
+
410
+ if jrr = role_refs.detect{|rr| rr.play}
411
+ return verbalise_query(jrr.play.variable.query)
412
+ end
413
+
414
+ # First, figure out whether there's a query:
415
+ join_over, joined_roles = *Metamodel.plays_over(role_sequence.all_role_ref.map{|rr|rr.role}, role_proximity)
416
+
417
+ role_by_fact_type = {}
418
+ fact_types = @role_refs.map{|rr| ft = rr.role.fact_type; role_by_fact_type[ft] ||= rr.role; ft}.uniq
419
+ readings = fact_types.map do |fact_type|
420
+ name_substitutions = []
421
+ # Choose a reading that start with the (first) role which caused us to emit this fact type:
422
+ reading = fact_type.reading_preferably_starting_with_role(role_by_fact_type[fact_type])
423
+ if join_over and # Find a reading preferably starting with the joined_over role:
424
+ joined_role = fact_type.all_role.select{|r| join_over.subtypes_transitive.include?(r.object_type)}[0]
425
+ reading = fact_type.reading_preferably_starting_with_role joined_role
426
+
427
+ # Use the name of the joined_over object, not the role player, in case of a subtype step:
428
+ rrrs = reading.role_sequence.all_role_ref_in_order
429
+ role_index = (0..rrrs.size).detect{|i| rrrs[i].role == joined_role }
430
+ name_substitutions[role_index] = [nil, join_over.name]
431
+ end
432
+ reading.role_sequence.all_role_ref.each do |rr|
433
+ next unless player = @player_by_role_ref[rr]
434
+ next unless subscript = player.subscript
435
+ trace :subscript, "Need to apply subscript #{subscript} to #{rr.role.object_type.name}"
436
+ end
437
+ player_by_role = {}
438
+ @player_by_role_ref.keys.each{|rr| player_by_role[rr.role] = @player_by_role_ref[rr] if rr.role.fact_type == fact_type }
439
+ expand_reading_text(nil, reading.text, reading.role_sequence, player_by_role)
440
+ end
441
+ conjunction ? readings*conjunction : readings
442
+ end
443
+
444
+ # Expand this reading (or partial reading, during contraction)
445
+ def expand_reading_text(step, text, role_sequence, player_by_role = {})
446
+ if !player_by_role.empty? and !player_by_role.is_a?(Hash) || player_by_role.keys.detect{|k| !k.is_a?(ActiveFacts::Metamodel::Role)}
447
+ raise "Need to change this call to expand_reading_text to pass a role->variable hash"
448
+ end
449
+ rrs = role_sequence.all_role_ref_in_order
450
+ variable_by_role = {}
451
+ if step
452
+ plays = step.all_play
453
+ variable_by_role = plays.inject({}) { |h, play| h[play.role] = play.variable; h }
454
+ end
455
+ trace :subscript, "expanding '#{text}' with #{role_sequence.describe}" do
456
+ text.gsub(/\{(\d)\}/) do
457
+ role_ref = rrs[$1.to_i]
458
+ # REVISIT: We may need to use the step's role_refs to expand the role players here, not the reading's one (extra adjectives?)
459
+ player = player_by_role[role_ref.role]
460
+ variable = variable_by_role[role_ref.role]
461
+
462
+ play_name = variable && variable.role_name
463
+ raise hell if player && player.is_a?(ActiveFacts::Metamodel::EntityType) && player.fact_type && !variable
464
+ subscripted_player(role_ref, player && player.subscript, play_name, variable && variable.value) +
465
+ objectification_verbalisation(variable)
466
+ end
467
+ end
468
+ end
469
+
470
+ def subscripted_player role_ref, subscript = nil, play_name = nil, value = nil
471
+ prr = @player_by_role_ref[role_ref]
472
+ subscript ||= prr.subscript if prr
473
+ trace :subscript, "Need to apply subscript #{subscript} to #{role_ref.role.object_type.name}" if subscript
474
+ object_type = role_ref.role.object_type
475
+ (play_name ||
476
+ [
477
+ role_ref.leading_adjective,
478
+ object_type.name,
479
+ role_ref.trailing_adjective
480
+ ].compact*' '
481
+ ) +
482
+ (value ? ' '+value.inspect : '') +
483
+ (subscript ? "(#{subscript})" : '')
484
+ end
485
+
486
+ def expand_contracted_text(step, reading, role_refs = [])
487
+ ' that ' +
488
+ expand_reading_text(step, reading.text.sub(/\A\{\d\} /,''), reading.role_sequence, role_refs)
489
+ end
490
+
491
+ # Each query we wish to verbalise must first have had its players prepared.
492
+ # Then, this prepares the query for verbalising:
493
+ def prepare_query query
494
+ @query = query
495
+ return unless query
496
+
497
+ @variables = query.all_variable.to_a.sort_by(&:ordinal)
498
+
499
+ @steps = @variables.map(&:all_step).flatten.uniq
500
+ @steps_by_variable = @variables.
501
+ inject({}) do |h, var|
502
+ var.all_step.each{|step| (h[var] ||= []) << step}
503
+ h
504
+ end
505
+ end
506
+
507
+ # De-index this step now that we've processed it:
508
+ def step_completed(step)
509
+ @steps.delete(step)
510
+
511
+ step.all_play.each do |play|
512
+ var = play.variable
513
+ steps = @steps_by_variable[var]
514
+ steps.delete(step)
515
+ @steps_by_variable.delete(var) if steps.empty?
516
+ end
517
+ end
518
+
519
+ def choose_step(next_var)
520
+ next_steps = @steps_by_variable[next_var]
521
+
522
+ # We need to emit each objectification before mentioning an object that plays a role in one, if possible
523
+ # so that we don't wind up with an objectification as the only way to mention its name.
524
+
525
+ # If we don't have a next_var against which we can contract,
526
+ # so just use any step involving this node, or just any step.
527
+ if next_steps
528
+ if next_step = next_steps.detect { |ns| !ns.is_objectification_step }
529
+ trace :query, "Chose new non-objectification step: #{next_step.describe}"
530
+ return next_step
531
+ end
532
+ end
533
+
534
+ if next_step = @steps.detect { |ns| !ns.is_objectification_step }
535
+ trace :query, "Chose random non-objectification step: #{next_step.describe}"
536
+ return next_step
537
+ end
538
+
539
+ next_step = @steps[0]
540
+ if next_step
541
+ trace :query, "Chose new random step from #{steps.size}: #{next_step.describe}"
542
+ if next_step.is_objectification_step
543
+ # if this objectification plays any roles (other than its FT roles) in remaining steps, use one of those first:
544
+ fact_type = next_step.fact_type.implying_role.fact_type
545
+ jn = [next_step.input_play.variable, next_step.output_play.variable].detect{|jn| jn.object_type == fact_type.entity_type}
546
+ sr = @steps_by_variable[jn].reject{|t| r = t.fact_type.implying_role and r.fact_type == fact_type}
547
+ next_step = sr[0] if sr.size > 0
548
+ end
549
+ return next_step
550
+ end
551
+ raise "Internal error: There are more steps here, but we failed to choose one"
552
+ end
553
+
554
+ # The step we just emitted (using the reading given) is contractable iff
555
+ # the reading has the next_var's role player as the final text
556
+ def node_contractable_against_reading(next_var, reading)
557
+ reading &&
558
+ # Find whether last role has no following text, and its ordinal
559
+ (reading.text =~ /\{([0-9])\}$/) &&
560
+ # This reading's RoleRef for that role:
561
+ (role_ref = reading.role_sequence.all_role_ref_in_order[$1.to_i]) &&
562
+ # was that RoleRef for the upcoming node?
563
+ role_ref.role.object_type == next_var.object_type
564
+ end
565
+
566
+ def reading_starts_with_node(reading, next_var)
567
+ reading.text =~ /^\{([0-9])\}/ and
568
+ role_ref = reading.role_sequence.all_role_ref.detect{|rr| rr.ordinal == $1.to_i} and
569
+ role_ref.role.object_type == next_var.object_type
570
+ end
571
+
572
+ # The last reading we emitted ended with the object type name for next_var.
573
+ # Choose a step and a reading that can be contracted against that name
574
+ def contractable_step(next_steps, next_var)
575
+ next_reading = nil
576
+ next_step =
577
+ next_steps.detect do |js|
578
+ next false if js.is_objectification_step or js.is_disallowed
579
+ # If we find a reading here, it can be contracted against the previous one
580
+ next_reading =
581
+ js.fact_type.all_reading_by_ordinal.detect do |reading|
582
+ # This step is contractable iff the FactType has a reading that starts with the role of next_var (no preceding text)
583
+ reading_starts_with_node(reading, next_var)
584
+ end
585
+ next_reading
586
+ end
587
+ trace :query, "#{next_reading ? "'"+next_reading.expand+"'" : "No reading"} contracts against last node '#{next_var.object_type.name}'"
588
+ return [next_step, next_reading]
589
+ end
590
+
591
+ def objectification_verbalisation(variable)
592
+ return '' unless variable
593
+ raise "Not fully re-implemented, should pass the variable instead of #{variable.inspect}" unless variable.is_a?(ActiveFacts::Metamodel::Variable)
594
+ objectified_node = nil
595
+ object_type = variable.object_type
596
+ return '' unless object_type.is_a?(Metamodel::EntityType) # Not a entity type
597
+ return '' unless object_type.fact_type # Not objectified
598
+
599
+ objectification_step = variable.step
600
+ return '' unless objectification_step
601
+
602
+ steps = [objectification_step]
603
+ step_completed(objectification_step)
604
+
605
+ =begin
606
+ while other_step =
607
+ @steps.
608
+ detect{|step|
609
+ step.is_objectification_step and
610
+ step.input_play.variable.object_type == object_type || step.output_play.variable.object_type == object_type
611
+ }
612
+ steps << other_step
613
+ trace :query, "Emitting objectification step allows deleting #{other_step.describe}"
614
+ step_completed(other_step)
615
+ end
616
+ =end
617
+
618
+ # Find all references to roles in this objectified fact type which are relevant to the variables of these steps:
619
+ player_by_role = {}
620
+ steps.each do |step|
621
+ step.all_play.to_a.map do |play|
622
+ player_by_role[play.role] = @player_by_play[play]
623
+ end
624
+ end
625
+
626
+ # role_refs = steps.map{|step| [step.input_play.variable, step.output_play.variable].map{|jn| jn.all_role_ref.detect{|rr| rr.role.fact_type == object_type.fact_type}}}.flatten.compact.uniq
627
+
628
+ reading = object_type.fact_type.preferred_reading
629
+ " (in which #{expand_reading_text(objectification_step, reading.text, reading.role_sequence, player_by_role)})"
630
+ end
631
+
632
+ def elided_objectification(next_step, fact_type, last_is_contractable, next_var)
633
+ if last_is_contractable
634
+ # Choose a reading that's contractable against the previous step, if possible
635
+ reading = fact_type.all_reading_by_ordinal.
636
+ detect do |reading|
637
+ # Only contract a negative reading if we want one
638
+ (!next_step.is_disallowed || !reading.is_negative == !next_step.is_disallowed) and
639
+ reading_starts_with_node(reading, next_var)
640
+ end
641
+ end
642
+ last_is_contractable = false unless reading
643
+ reading ||= fact_type.preferred_reading(next_step.is_disallowed) || fact_type.preferred_reading
644
+
645
+ # Find which role occurs last in the reading, and which Variable is attached
646
+ reading.text =~ /\{(\d)\}[^{]*\Z/
647
+ last_role_ref = reading.role_sequence.all_role_ref_in_order[$1.to_i]
648
+ exit_node = @variables.detect{|jn| jn.all_play.detect{|play| play.role == last_role_ref.role}}
649
+ exit_step = nil
650
+
651
+ trace :query, "Stepping over an objectification to #{exit_node.object_type.name} requires eliding the other implied steps" do
652
+ count = 0
653
+ while other_step =
654
+ @steps.
655
+ detect{|js|
656
+ trace :query, "Considering step '#{js.fact_type.default_reading}'"
657
+ next unless js.is_objectification_step
658
+
659
+ # REVISIT: This test is too weak: We need to ensure that the same variables are involved, not just the same object types:
660
+ next unless js.input_play.variable.object_type == fact_type.entity_type || js.output_play.variable.object_type == fact_type.entity_type
661
+ exit_step = js if js.output_play.variable == exit_node
662
+ true
663
+ }
664
+ trace :query, "Emitting objectified FT allows deleting #{other_step.describe}"
665
+ step_completed(other_step)
666
+ # raise "The objectification of '#{fact_type.default_reading}' should not cause the deletion of more than #{fact_type.all_role.size} other steps" if (count += 1) > fact_type.all_role.size
667
+ end
668
+ end
669
+
670
+ [ reading, exit_step ? exit_step.input_play.variable : exit_node, exit_step, last_is_contractable]
671
+ end
672
+
673
+ def verbalise_query query
674
+ prepare_query query
675
+ readings = ''
676
+ next_var = @role_refs[0].play.variable # Choose a place to start
677
+ last_is_contractable = false
678
+
679
+ trace :query, "Verbalising query" do
680
+ if trace(:query)
681
+ trace :query, "variables:" do
682
+ @variables.each do |var|
683
+ trace :query, var.describe
684
+ end
685
+ end
686
+ trace :query, "steps:" do
687
+ @steps.each do |step|
688
+ trace :query, step.describe
689
+ end
690
+ end
691
+ end
692
+
693
+ until @steps.empty?
694
+ next_reading = nil
695
+ # Choose amongst all remaining steps we can take from the next node, if any
696
+ next_steps = @steps_by_variable[next_var]
697
+ trace :query, "Next Steps from #{next_var.describe} are #{(next_steps||[]).map{|js| js.describe }.inspect}"
698
+
699
+ # See if we can find a next step that contracts against the last (if any):
700
+ next_step = nil
701
+ if last_is_contractable && next_steps
702
+ next_step, next_reading = *contractable_step(next_steps, next_var)
703
+ end
704
+
705
+ if next_step
706
+ trace :query, "Chose #{next_step.describe} because it's contractable against last node #{next_var.object_type.name} using #{next_reading.expand}"
707
+
708
+ player_by_role =
709
+ next_step.all_play.inject({}) {|h, play| h[play.role] = @player_by_play[play]; h }
710
+ raise "REVISIT: Needed a negated reading here" if !next_reading.is_negative != !next_step.is_disallowed
711
+ raise "REVISIT: Need to emit 'maybe' here" if next_step.is_optional
712
+ readings += expand_contracted_text(next_step, next_reading, player_by_role)
713
+ step_completed(next_step)
714
+ else
715
+ next_step = choose_step(next_var) if !next_step
716
+
717
+ player_by_role =
718
+ next_step.all_play.inject({}) {|h, play| h[play.role] = @player_by_play[play]; h }
719
+
720
+ if next_step.is_unary_step
721
+ # Objectified unaries get emitted as unaries, not as objectifications:
722
+ role = next_step.input_play.role
723
+ role = role.fact_type.implying_role if role.fact_type.is_a?(LinkFactType)
724
+ next_reading = role.fact_type.preferred_reading(next_step.is_disallowed) || role.fact_type.preferred_reading
725
+ readings += " and " unless readings.empty?
726
+ readings += "it is not the case that " if !next_step.is_disallowed != !next_reading.is_negative
727
+ raise "REVISIT: Need to emit 'maybe' here" if next_step.is_optional
728
+ readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
729
+ step_completed(next_step)
730
+ elsif next_step.is_objectification_step
731
+ fact_type = next_step.fact_type.implying_role.fact_type
732
+
733
+ # This objectification step is over an implicit fact type, so player_by_role won't have all the players
734
+ # Add the players of other roles associated with steps from this objectified player.
735
+ objectified_node = next_step.input_play.variable
736
+ raise "Assumption violated that the objectification is the input play" unless objectified_node.object_type.fact_type
737
+ objectified_node.all_step.map do |other_step|
738
+ other_step.all_play.map do |play|
739
+ player_by_role[play.role] = @player_by_play[play]
740
+ end
741
+ end
742
+
743
+ if last_is_contractable and next_var.object_type.is_a?(EntityType) and next_var.object_type.fact_type == fact_type
744
+ # The last reading we emitted ended with the name of the objectification of this fact type, so we can contract the objectification
745
+ # REVISIT: Do we need to use player_by_role here (if this objectification is traversed twice and so is subscripted)
746
+ readings += objectification_verbalisation(fact_type.entity_type)
747
+ else
748
+ # This objectified fact type does not need to be made explicit.
749
+ negation = next_step.is_disallowed
750
+ next_reading, next_var, next_step, last_is_contractable =
751
+ *elided_objectification(next_step, fact_type, last_is_contractable, next_var)
752
+ if last_is_contractable
753
+ raise "REVISIT: Need to emit 'maybe' here" if next_step and next_step.is_optional
754
+ readings += expand_contracted_text(next_step, next_reading, player_by_role)
755
+ else
756
+ readings += " and " unless readings.empty?
757
+ readings += "it is not the case that " if !!negation != !!next_reading.is_negative
758
+ raise "REVISIT: Need to emit 'maybe' here" if next_step and next_step.is_optional
759
+ readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
760
+ end
761
+ # No need to continue if we just deleted the last step
762
+ break if @steps.empty?
763
+
764
+ end
765
+ else
766
+ fact_type = next_step.fact_type
767
+ # Prefer a reading that starts with the player of next_var
768
+ next_reading = fact_type.all_reading_by_ordinal.
769
+ detect do |reading|
770
+ (!next_step.is_disallowed || !reading.is_negative == !next_step.is_disallowed) and
771
+ reading_starts_with_node(reading, next_var)
772
+ end || fact_type.preferred_reading(next_step.is_disallowed)
773
+ # REVISIT: If this step and reading has role references with adjectives, we need to expand using those
774
+ readings += " and " unless readings.empty?
775
+ readings += "it is not the case that " if !next_step.is_disallowed != !next_reading.is_negative
776
+ raise "REVISIT: Need to emit 'maybe' here" if next_step and next_step.is_optional
777
+ readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
778
+ step_completed(next_step)
779
+ end
780
+ end
781
+
782
+ if next_step
783
+ # Continue from this step with the node having the most steps remaining
784
+ input_steps = @steps_by_variable[input_var = next_step.input_play.variable] || []
785
+ output_play = next_step.output_plays.last
786
+ output_steps = (output_play && (output_var = output_play.variable) && @steps_by_variable[output_var]) || []
787
+
788
+ next_var = input_steps.size > output_steps.size ? input_var : output_var
789
+ # Prepare for possible contraction following:
790
+ last_is_contractable = next_reading && node_contractable_against_reading(next_var, next_reading)
791
+ else
792
+ # This shouldn't happen, but an elided objectification that had missing steps can cause it. Survive:
793
+ next_var = (steps[0].input_play || steps[0].output_plays.last).variable
794
+ last_is_contractable = false
795
+ end
796
+
797
+ end
798
+ end
799
+ readings
800
+ end
801
+ end
802
+
803
+ end
804
+ end