activefacts 0.6.0

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 (84) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +83 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +41 -0
  5. data/bin/afgen +46 -0
  6. data/bin/cql +52 -0
  7. data/examples/CQL/Address.cql +46 -0
  8. data/examples/CQL/Blog.cql +54 -0
  9. data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
  10. data/examples/CQL/Death.cql +16 -0
  11. data/examples/CQL/Genealogy.cql +95 -0
  12. data/examples/CQL/Marriage.cql +18 -0
  13. data/examples/CQL/Metamodel.cql +238 -0
  14. data/examples/CQL/MultiInheritance.cql +19 -0
  15. data/examples/CQL/OilSupply.cql +47 -0
  16. data/examples/CQL/Orienteering.cql +108 -0
  17. data/examples/CQL/PersonPlaysGame.cql +17 -0
  18. data/examples/CQL/SchoolActivities.cql +31 -0
  19. data/examples/CQL/SimplestUnary.cql +12 -0
  20. data/examples/CQL/SubtypePI.cql +32 -0
  21. data/examples/CQL/Warehousing.cql +99 -0
  22. data/examples/CQL/WindowInRoomInBldg.cql +22 -0
  23. data/lib/activefacts.rb +10 -0
  24. data/lib/activefacts/api.rb +25 -0
  25. data/lib/activefacts/api/concept.rb +384 -0
  26. data/lib/activefacts/api/constellation.rb +106 -0
  27. data/lib/activefacts/api/entity.rb +239 -0
  28. data/lib/activefacts/api/instance.rb +54 -0
  29. data/lib/activefacts/api/numeric.rb +158 -0
  30. data/lib/activefacts/api/role.rb +94 -0
  31. data/lib/activefacts/api/standard_types.rb +67 -0
  32. data/lib/activefacts/api/support.rb +59 -0
  33. data/lib/activefacts/api/value.rb +122 -0
  34. data/lib/activefacts/api/vocabulary.rb +120 -0
  35. data/lib/activefacts/cql.rb +31 -0
  36. data/lib/activefacts/cql/CQLParser.treetop +104 -0
  37. data/lib/activefacts/cql/Concepts.treetop +112 -0
  38. data/lib/activefacts/cql/DataTypes.treetop +66 -0
  39. data/lib/activefacts/cql/Expressions.treetop +113 -0
  40. data/lib/activefacts/cql/FactTypes.treetop +185 -0
  41. data/lib/activefacts/cql/Language/English.treetop +92 -0
  42. data/lib/activefacts/cql/LexicalRules.treetop +169 -0
  43. data/lib/activefacts/cql/Rakefile +6 -0
  44. data/lib/activefacts/cql/parser.rb +88 -0
  45. data/lib/activefacts/generate/absorption.rb +87 -0
  46. data/lib/activefacts/generate/cql.rb +441 -0
  47. data/lib/activefacts/generate/cql/html.rb +397 -0
  48. data/lib/activefacts/generate/null.rb +19 -0
  49. data/lib/activefacts/generate/ordered.rb +557 -0
  50. data/lib/activefacts/generate/ruby.rb +326 -0
  51. data/lib/activefacts/generate/sql/server.rb +164 -0
  52. data/lib/activefacts/generate/text.rb +21 -0
  53. data/lib/activefacts/input/cql.rb +1268 -0
  54. data/lib/activefacts/input/orm.rb +926 -0
  55. data/lib/activefacts/persistence.rb +1 -0
  56. data/lib/activefacts/persistence/composition.rb +653 -0
  57. data/lib/activefacts/support.rb +51 -0
  58. data/lib/activefacts/version.rb +3 -0
  59. data/lib/activefacts/vocabulary.rb +6 -0
  60. data/lib/activefacts/vocabulary/extensions.rb +343 -0
  61. data/lib/activefacts/vocabulary/metamodel.rb +303 -0
  62. data/script/txt2html +71 -0
  63. data/spec/absorption_spec.rb +95 -0
  64. data/spec/api/autocounter.rb +82 -0
  65. data/spec/api/constellation.rb +130 -0
  66. data/spec/api/entity_type.rb +101 -0
  67. data/spec/api/instance.rb +428 -0
  68. data/spec/api/roles.rb +122 -0
  69. data/spec/api/value_type.rb +112 -0
  70. data/spec/api_spec.rb +14 -0
  71. data/spec/cql_cql_spec.rb +58 -0
  72. data/spec/cql_parse_spec.rb +31 -0
  73. data/spec/cql_ruby_spec.rb +60 -0
  74. data/spec/cql_sql_spec.rb +54 -0
  75. data/spec/cql_symbol_tables_spec.rb +259 -0
  76. data/spec/cql_unit_spec.rb +336 -0
  77. data/spec/cqldump_spec.rb +169 -0
  78. data/spec/norma_cql_spec.rb +48 -0
  79. data/spec/norma_ruby_spec.rb +50 -0
  80. data/spec/norma_sql_spec.rb +45 -0
  81. data/spec/norma_tables_spec.rb +94 -0
  82. data/spec/spec.opts +1 -0
  83. data/spec/spec_helper.rb +10 -0
  84. metadata +173 -0
@@ -0,0 +1,21 @@
1
+ #
2
+ # Generate text output for ActiveFacts vocabularies.
3
+ #
4
+ # Copyright (c) 2007 Clifford Heath. Read the LICENSE file.
5
+ # Author: Clifford Heath.
6
+ #
7
+ module ActiveFacts
8
+ module Generate
9
+ class TEXT
10
+ def initialize(vocabulary)
11
+ @vocabulary = vocabulary
12
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::Constellation === @vocabulary
13
+ end
14
+
15
+ def generate(out = $>)
16
+ out.puts @vocabulary.constellation.verbalise
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,1268 @@
1
+ #
2
+ # Compile a CQL file into an ActiveFacts vocabulary.
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ require 'activefacts/vocabulary'
6
+ require 'activefacts/cql/parser'
7
+
8
+ require 'ruby-debug'
9
+
10
+ module ActiveFacts
11
+ module Input #:nodoc:
12
+ # The CQL Input module is activated whenever afgen is called upon to process a file
13
+ # whose name ends in .cql. The file is parsed to a constellation and the vocabulary
14
+ # object defined in that file is returned.
15
+ class CQL
16
+ include ActiveFacts
17
+ include ActiveFacts::Metamodel
18
+
19
+ class SymbolTable; end #:nodoc:
20
+
21
+ RingTypes = %w{acyclic intransitive symmetric asymmetric transitive antisymmetric irreflexive reflexive}
22
+ RingPairs = {
23
+ :intransitive => [:acyclic, :asymmetric, :symmetric],
24
+ :irreflexive => [:symmetric]
25
+ }
26
+
27
+ # Open the specified file and read it:
28
+ def self.readfile(filename)
29
+ File.open(filename) {|file|
30
+ self.read(file, filename)
31
+ }
32
+ rescue => e
33
+ puts e.message+"\n\t"+e.backtrace*"\n\t" if debug :exception
34
+ raise "In #{filename} #{e.message.strip}"
35
+ end
36
+
37
+ # Read the specified input stream:
38
+ def self.read(file, filename = "stdin")
39
+ CQL.new(file.read, filename).read
40
+ end
41
+
42
+ def initialize(file, filename = "stdin")
43
+ @file = file
44
+ @filename = filename
45
+ end
46
+
47
+ # Read the input, returning a new Vocabulary:
48
+ def read
49
+ @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
50
+
51
+ @parser = ActiveFacts::CQLParser.new
52
+
53
+ # The syntax tree created from each parsed CQL statement gets passed to the block.
54
+ # parse_all returns an array of the block's non-nil return values.
55
+ result = @parser.parse_all(@file, :definition) do |node|
56
+ begin
57
+ kind, *value = @parser.definition(node)
58
+ #print "Parsed '#{node.text_value}'"
59
+ #print " to "; p value
60
+ raise "Definition of #{kind} must be in a vocabulary" if kind != :vocabulary and !@vocabulary
61
+ case kind
62
+ when :vocabulary
63
+ @vocabulary = @constellation.Vocabulary(value[0])
64
+ when :data_type
65
+ value_type *value
66
+ when :entity_type
67
+ entity_type *value
68
+ when :fact_type
69
+ fact_type *value
70
+ when :constraint
71
+ constraint *value
72
+ else
73
+ print "="*20+" unhandled declaration type: "; p kind, value
74
+ end
75
+ rescue => e
76
+ puts e.message+"\n\t"+e.backtrace*"\n\t" if debug :exception
77
+ start_line = @file.line_of(node.interval.first)
78
+ end_line = @file.line_of(node.interval.last-1)
79
+ lines = start_line != end_line ? "s #{start_line}-#{end_line}" : " #{start_line.to_s}"
80
+ raise "at line#{lines} #{e.message.strip}"
81
+ end
82
+
83
+ nil
84
+ end
85
+ raise @parser.failure_reason unless result
86
+ @vocabulary
87
+ end
88
+
89
+ def value_type(name, base_type_name, parameters, unit, ranges)
90
+ length, scale = *parameters
91
+
92
+ # Create the base type:
93
+ base_type = nil
94
+ if (base_type_name != name)
95
+ unless base_type = @constellation.ValueType[[@constellation.Name(base_type_name), @vocabulary]]
96
+ #puts "REVISIT: Creating base ValueType #{base_type_name} in #{@vocabulary.inspect}"
97
+ base_type = @constellation.ValueType(base_type_name, @vocabulary)
98
+ return if base_type_name == name
99
+ end
100
+ end
101
+
102
+ # Create and initialise the ValueType:
103
+ vt = @constellation.ValueType(name, @vocabulary)
104
+ vt.supertype = base_type if base_type
105
+ vt.length = length if length
106
+ vt.scale = scale if scale
107
+
108
+ # REVISIT: Find and apply the units
109
+
110
+ if ranges.size != 0
111
+ vt.value_restriction = @constellation.ValueRestriction(:new)
112
+ ranges.each do |range|
113
+ min, max = Array === range ? range : [range, range]
114
+ v_range = @constellation.ValueRange(
115
+ min ? [min.to_s, true] : nil,
116
+ max ? [max.to_s, true] : nil
117
+ )
118
+ ar = @constellation.AllowedRange(v_range, vt.value_restriction)
119
+ end
120
+ end
121
+ end
122
+
123
+ def entity_type(name, supertypes, identification, clauses)
124
+ #puts "Entity Type #{name}, supertypes #{supertypes.inspect}, id #{identification.inspect}, clauses = #{clauses.inspect}"
125
+ debug :entity, "Defining Entity Type #{name}" do
126
+ # Assert the entity:
127
+ # If this entity was forward referenced, this won't be a new object, and will subsume its roles
128
+ entity_type = @constellation.EntityType(name, @vocabulary)
129
+
130
+ # Set up its supertypes:
131
+ supertypes.each do |supertype_name|
132
+ add_supertype(entity_type, supertype_name, !identification && supertype_name == supertypes[0])
133
+ end
134
+
135
+ # Use a two-pass algorithm for entity fact types...
136
+ # The first step is to find all role references and definitions in the clauses
137
+ # After bind_roles, each item in the reading part of each clause is either:
138
+ # * a string, which is a linking word, or
139
+ # * the phrase hash augmented with a :binding=>Binding
140
+ @symbols = SymbolTable.new(@constellation, @vocabulary)
141
+ @symbols.bind_roles_in_clauses(clauses, identification ? identification[:roles] : nil)
142
+
143
+ # Next arrange the readings according to what fact they belong to,
144
+ # then process each fact type using normal fact type processing.
145
+ # That way if we find a fact type here having none of the players being the
146
+ # entity type, we know it's an objectified fact type. The CQL syntax might make
147
+ # us come here with such a case when the fact type is a subtype of some entity type,
148
+ # such as occurs in the Metamodel with TypeInheritance.
149
+
150
+ # N.B. This doesn't allow forward identification by roles with adjectives (see the i[0]):
151
+ @symbols.allowed_forward = (ir = identification && identification[:roles]) ? ir.inject({}){|h, i| h[i[0]] = true; h} : {}
152
+
153
+ # If we're using a common identification mode, find or create the necessary ValueTypes first:
154
+ vt_name = vt = nil
155
+ if identification && identification[:mode]
156
+ mode = identification[:mode] # An identification mode
157
+
158
+ # Find or Create an appropriate ValueType called "#{name}#{mode}", of the supertype "#{mode}"
159
+ vt_name = "#{name}#{mode}"
160
+ unless vt = @constellation.ValueType[[vt_name, @vocabulary]]
161
+ base_vt = @constellation.ValueType(mode, @vocabulary)
162
+ vt = @constellation.ValueType(vt_name, @vocabulary, :supertype => base_vt)
163
+ end
164
+ end
165
+
166
+ identifying_fact_types = {}
167
+ clauses_by_fact_type(clauses).each do |clauses_for_fact_type|
168
+ fact_type = nil
169
+ @symbols.embedded_presence_constraints = [] # Clear embedded_presence_constraints for each fact type
170
+ debug "New Fact Type for entity #{name}" do
171
+ clauses_for_fact_type.each do |clause|
172
+ type, qualifiers, reading = *clause
173
+ debug :reading, "Clause: #{clause.inspect}" do
174
+ f = bind_fact_reading(fact_type, qualifiers, reading)
175
+ identifying_fact_types[f] = true
176
+ fact_type ||= f
177
+ end
178
+ end
179
+ end
180
+
181
+ # Find the role that this entity type plays in the fact type, if any:
182
+ player_roles = fact_type.all_role.select{|role| role.concept == entity_type }
183
+ raise "#{role.concept.name} may only play one role in each identifying fact type" if player_roles.size > 1
184
+ if player_role = player_roles[0]
185
+ non_player_roles = fact_type.all_role-[player_role]
186
+
187
+ raise "#{name} cannot be identified by a role in a non-binary fact type" if non_player_roles.size > 1
188
+ elsif identification
189
+ # This situation occurs when an objectified fact type has an entity identifier
190
+ raise "Entity type #{name} may only objectify a single fact type" if entity_type.fact_type
191
+
192
+ entity_type.fact_type = fact_type
193
+ fact_type_identification(fact_type, name, false)
194
+ else
195
+ # it's an objectified fact type, such as a subtype
196
+ entity_type.fact_type = fact_type
197
+ end
198
+ end
199
+
200
+ # Finally, create the identifying uniqueness constraint, or mark it as preferred
201
+ # if it's already been created. The identifying roles have been defined already.
202
+
203
+ if identification
204
+ debug :identification, "Handling identification" do
205
+ if id_role_names = identification[:roles] # A list of identifying roles
206
+ debug "Identifying roles: #{id_role_names.inspect}"
207
+
208
+ # Pick out the identifying_roles in the order they were declared,
209
+ # not the order the fact types were defined:
210
+ identifying_roles = id_role_names.map do |names|
211
+ unless (role = bind_unary_fact_type(entity_type, names))
212
+ player, binding = @symbols.bind(names)
213
+ role = @symbols.roles_by_binding[binding]
214
+ raise "identifying role #{names*"-"} not found in fact types for #{name}" unless role
215
+ end
216
+ role
217
+ end
218
+
219
+ # Find a uniqueness constraint as PI, or make one
220
+ pc = find_pc_over_roles(identifying_roles)
221
+ if (pc)
222
+ debug "Existing PC #{pc.verbalise} is now PK for #{name} #{pc.class.roles.keys.map{|k|"#{k} => "+pc.send(k).verbalise}*", "}"
223
+ pc.is_preferred_identifier = true
224
+ pc.name = "#{name}PK" unless pc.name
225
+ else
226
+ debug "Adding PK for #{name} using #{identifying_roles.map{|r| r.concept.name}.inspect}"
227
+
228
+ role_sequence = @constellation.RoleSequence(:new)
229
+ # REVISIT: Need to sort the identifying_roles to match the identification parameter array
230
+ identifying_roles.each_with_index do |identifying_role, index|
231
+ @constellation.RoleRef(role_sequence, index, :role => identifying_role)
232
+ end
233
+
234
+ # Add a unique constraint over all identifying roles
235
+ pc = @constellation.PresenceConstraint(
236
+ :new,
237
+ :vocabulary => @vocabulary,
238
+ :name => "#{name}PK", # Is this a useful name?
239
+ :role_sequence => role_sequence,
240
+ :is_preferred_identifier => true,
241
+ :max_frequency => 1 # Unique
242
+ #:is_mandatory => true,
243
+ #:min_frequency => 1,
244
+ )
245
+ end
246
+
247
+ elsif identification[:mode]
248
+ mode = identification[:mode] # An identification mode
249
+
250
+ raise "Entity definition using reference mode may only have one identifying fact type" if identifying_fact_types.size > 1
251
+ mode_fact_type = identifying_fact_types.keys[0]
252
+
253
+ # If the entity type is an objectified fact type, don't use the objectified fact type!
254
+ mode_fact_type = nil if mode_fact_type && mode_fact_type.entity_type == entity_type
255
+
256
+ debug :mode, "Processing Reference Mode for #{name}#{mode_fact_type ? " with existing '#{mode_fact_type.default_reading}'" : ""}"
257
+
258
+ # WARNING: Several things here depend on the method and order of creation of roles and role sequences above
259
+ # But heck, that's what tests are for, right?
260
+
261
+ # Fact Type:
262
+ if (ft = mode_fact_type)
263
+ entity_role = ft.all_role[n = (ft.all_role[0].concept == entity_type ? 0 : 1)]
264
+ value_role = ft.all_role[1-n]
265
+ else
266
+ ft = @constellation.FactType(:new)
267
+ entity_role = @constellation.Role(ft, 0, entity_type)
268
+ value_role = @constellation.Role(ft, 1, vt)
269
+ debug :mode, "Creating new fact type to identify #{name}"
270
+ end
271
+
272
+ # Forward reading, if it doesn't already exist:
273
+ rss = entity_role.all_role_ref.map{|rr| rr.role_sequence.all_role_ref.size == 2 ? rr.role_sequence : nil }.compact
274
+ rs01 = rss.select{|rs| rs.all_role_ref[1].role == value_role }[0]
275
+ rs10 = rss.select{|rs| rs.all_role_ref[1].role == entity_role }[0]
276
+ if !rs01
277
+ rs01 = @constellation.RoleSequence(:new)
278
+ @constellation.RoleRef(rs01, 0, :role => entity_role)
279
+ @constellation.RoleRef(rs01, 1, :role => value_role)
280
+ @constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs01, :reading_text => "{0} has {1}")
281
+ debug :mode, "Creating new forward reading '#{name} has #{vt.name}'"
282
+ else
283
+ debug :mode, "Using existing forward reading #{rs01.all_reading[0].expand}"
284
+ end
285
+
286
+ # Reverse reading:
287
+ if !rs10
288
+ rs10 = @constellation.RoleSequence(:new)
289
+ @constellation.RoleRef(rs10, 0, :role => value_role)
290
+ @constellation.RoleRef(rs10, 1, :role => entity_role)
291
+ @constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs10, :reading_text => "{0} is of {1}")
292
+ debug :mode, "Creating new reverse reading '#{vt.name} is of #{name}'"
293
+ else
294
+ debug :mode, "Using existing reverse reading"
295
+ end
296
+
297
+ # Entity Type must have a value type. Find or create the role sequence, then create a PC if necessary
298
+ debug :mode, "entity_role has #{entity_role.all_role_ref.size} attached sequences"
299
+ debug :mode, "entity_role has #{entity_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}.size} unary sequences"
300
+ rs0 = entity_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1 ? rr.role_sequence : nil }.compact[0]
301
+ if !rs0
302
+ rs0 = @constellation.RoleSequence(:new)
303
+ @constellation.RoleRef(rs0, 0, :role => entity_role)
304
+ debug :mode, "Creating new EntityType role sequence"
305
+ else
306
+ rs0 = rs0.role_sequence
307
+ debug :mode, "Using existing EntityType role sequence"
308
+ end
309
+ if (rs0.all_presence_constraint.size == 0)
310
+ @constellation.PresenceConstraint(
311
+ :new,
312
+ :name => '',
313
+ :enforcement => '',
314
+ :vocabulary => @vocabulary,
315
+ :role_sequence => rs0,
316
+ :min_frequency => 1,
317
+ :max_frequency => 1,
318
+ :is_preferred_identifier => false,
319
+ :is_mandatory => true
320
+ )
321
+ debug :mode, "Creating new EntityType PresenceConstraint"
322
+ else
323
+ debug :mode, "Using existing EntityType PresenceConstraint"
324
+ end
325
+
326
+ # Value Type must have a value type. Find or create the role sequence, then create a PC if necessary
327
+ debug :mode, "value_role has #{value_role.all_role_ref.size} attached sequences"
328
+ debug :mode, "value_role has #{value_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}.size} unary sequences"
329
+ rs1 = value_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1 ? rr.role_sequence : nil }.compact[0]
330
+ if (!rs1)
331
+ rs1 = @constellation.RoleSequence(:new)
332
+ @constellation.RoleRef(rs1, 0, :role => value_role)
333
+ debug :mode, "Creating new ValueType role sequence"
334
+ else
335
+ rs1 = rs1.role_sequence
336
+ debug :mode, "Using existing ValueType role sequence"
337
+ end
338
+ if (rs1.all_presence_constraint.size == 0)
339
+ @constellation.PresenceConstraint(
340
+ :new,
341
+ :name => '',
342
+ :enforcement => '',
343
+ :vocabulary => @vocabulary,
344
+ :role_sequence => rs1,
345
+ :min_frequency => 0,
346
+ :max_frequency => 1,
347
+ :is_preferred_identifier => true,
348
+ :is_mandatory => false
349
+ )
350
+ debug :mode, "Creating new ValueType PresenceConstraint"
351
+ else
352
+ debug :mode, "Marking existing ValueType PresenceConstraint as preferred"
353
+ rs1.all_presence_constraint[0].is_preferred_identifier = true
354
+ end
355
+ end
356
+ end
357
+ else
358
+ # identification must be inherited.
359
+ debug "Identification is inherited"
360
+ end
361
+ end
362
+ end
363
+
364
+ def add_supertype(entity_type, supertype_name, identifying_supertype)
365
+ debug :supertype, "Supertype #{supertype_name}"
366
+ supertype = @constellation.EntityType(supertype_name, @vocabulary)
367
+ inheritance_fact = @constellation.TypeInheritance(entity_type, supertype, :fact_type_id => :new)
368
+
369
+ # Create a reading:
370
+ sub_role = @constellation.Role(inheritance_fact, 0, entity_type)
371
+ super_role = @constellation.Role(inheritance_fact, 1, supertype)
372
+ rs = @constellation.RoleSequence(:new)
373
+ @constellation.RoleRef(rs, 0, :role => sub_role)
374
+ @constellation.RoleRef(rs, 1, :role => super_role)
375
+ @constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :reading_text => "{0} is a subtype of {1}")
376
+
377
+ if identifying_supertype
378
+ inheritance_fact.provides_identification = true
379
+ end
380
+
381
+ # Create uniqueness constraints over the subtyping fact type
382
+ p1rs = @constellation.RoleSequence(:new)
383
+ @constellation.RoleRef(p1rs, 0).role = sub_role
384
+ pc1 = @constellation.PresenceConstraint(:new)
385
+ pc1.name = "#{entity_type.name}MustHaveSupertype#{supertype.name}"
386
+ pc1.vocabulary = @vocabulary
387
+ pc1.role_sequence = p1rs
388
+ pc1.is_mandatory = true # A subtype instance must have a supertype instance
389
+ pc1.min_frequency = 1
390
+ pc1.max_frequency = 1
391
+ pc1.is_preferred_identifier = false
392
+
393
+ # The supertype role often identifies the subtype:
394
+ p2rs = @constellation.RoleSequence(:new)
395
+ @constellation.RoleRef(p2rs, 0).role = super_role
396
+ pc2 = @constellation.PresenceConstraint(:new)
397
+ pc2.name = "#{supertype.name}MayBeA#{entity_type.name}"
398
+ pc2.vocabulary = @vocabulary
399
+ pc2.role_sequence = p2rs
400
+ pc2.is_mandatory = false
401
+ pc2.min_frequency = 0
402
+ pc2.max_frequency = 1
403
+ pc2.is_preferred_identifier = inheritance_fact.provides_identification
404
+ end
405
+
406
+ # If one of the words is the name of the entity type, and the other
407
+ # words consist of a unary fact type reading, return the role it plays.
408
+ def bind_unary_fact_type(entity_type, words)
409
+ return nil unless i = words.index(entity_type.name)
410
+
411
+ to_match = words.clone
412
+ to_match[i] = '{0}'
413
+ to_match = to_match*' '
414
+
415
+ # See if any unary fact type of this or any supertype matches these words:
416
+ entity_type.supertypes_transitive.each do |supertype|
417
+ supertype.all_role.each do |role|
418
+ role.fact_type.all_role.size == 1 &&
419
+ role.fact_type.all_reading.each do |reading|
420
+ if reading.reading_text == to_match
421
+ debug :identification, "Bound identification to unary role '#{to_match.sub(/\{0\}/, entity_type.name)}'"
422
+ return role
423
+ end
424
+ end
425
+ end
426
+ end
427
+ nil
428
+ end
429
+
430
+ def fact_type(name, clauses, conditions)
431
+ debug "Processing clauses for fact type" do
432
+ fact_type = nil
433
+
434
+ #
435
+ # The first step is to find all role references and definitions in the reading clauses.
436
+ # This also:
437
+ # * deletes any adjectives that were used but not hyphenated
438
+ # * changes each linking word phrase into a simple String
439
+ # * adds a :binding key to each bound role
440
+ #
441
+ @symbols = SymbolTable.new(@constellation, @vocabulary)
442
+ @symbols.bind_roles_in_clauses(clauses)
443
+
444
+ clauses.each do |clause|
445
+ kind, qualifiers, reading = *clause
446
+
447
+ fact_type = bind_fact_reading(fact_type, qualifiers, reading)
448
+ end
449
+
450
+ # The fact type has a name iff it's objectified as an entity type
451
+ #puts "============= Creating entity #{name} to nominalize fact type #{fact_type.default_reading} ======================" if name
452
+ fact_type.entity_type = @constellation.EntityType(name, @vocabulary) if name
453
+
454
+ # Add the identifying PresenceConstraint for this fact type:
455
+ if fact_type.all_role.size == 1 && !fact_type.entity_type
456
+ # All is well, unaries don't need an identifying PC unless objectified
457
+ else
458
+ fact_type_identification(fact_type, name, true)
459
+ end
460
+
461
+ # REVISIT: Process the fact derivation conditions, if any
462
+ end
463
+ end
464
+
465
+ def constraint *value
466
+ case type = value.shift
467
+ when :presence
468
+ presence_constraint *value
469
+ when :subset
470
+ subset_constraint *value
471
+ when :set
472
+ set_constraint *value
473
+ when :equality
474
+ equality_constraint *value
475
+ else
476
+ $stderr.puts "REVISIT: external #{type} constraints aren't yet handled:\n\t"+value.map{|a| a.inspect }*"\n\t"
477
+ end
478
+ end
479
+
480
+ # The readings list is an array of an array of fact types.
481
+ # The fact types contain roles played by concepts, where each
482
+ # concept plays more than one role. In fact, a concept may
483
+ # occur in more than one binding, and each binding plays more
484
+ # than one role. The bindings that are common to all fact types
485
+ # in each array in the readings list form the constrained role
486
+ # sequences. Each binding that isn't common at this top level
487
+ # must occur more than once in each group of fact types where
488
+ # it appears, and it forms a join between those fact types.
489
+ def bind_join_paths_as_role_sequences(readings_list)
490
+ @symbols = SymbolTable.new(@constellation, @vocabulary)
491
+ fact_roles_list = []
492
+ bindings_list = []
493
+ readings_list.each_with_index do |readings, index|
494
+ # readings is an array of readings
495
+ @symbols.bind_roles_in_readings(readings)
496
+
497
+ # Was:
498
+ # fact_roles_list[index] = invoked_fact_roles(readings[0])
499
+ # raise "Fact type reading not found for #{readings[0].inspect}" unless fact_roles_list[index]
500
+ # bindings_list[index] = readings[0].map{|phrase| Hash === phrase ? phrase[:binding] : nil}.compact
501
+
502
+ fact_roles_list << readings.map do |reading|
503
+ ifr = invoked_fact_roles(reading)
504
+ raise "Fact type reading not found for #{reading.inspect}" unless ifr
505
+ ifr
506
+ end
507
+ bindings_list << readings.map do |reading|
508
+ reading.map{ |phrase| Hash === phrase ? phrase[:binding] : nil}.compact
509
+ end
510
+ end
511
+
512
+ # Each set of binding arrays in the list must share at least one common binding
513
+ bindings_by_join_path = bindings_list.map{|join_path| join_path.flatten}
514
+ common_bindings = bindings_by_join_path[1..-1].inject(bindings_by_join_path[0]) { |c, b| c & b }
515
+ # Was:
516
+ # common_bindings = bindings_list.inject(bindings_list[0]) { |common, bindings| common & bindings }
517
+ raise "Set constraints must have at least one common role between the sets" unless common_bindings.size > 0
518
+
519
+ # REVISIT: Do we need to constrain things such that each join path only includes *one* instance of each common binding?
520
+
521
+ # For each set of binding arrays, if there's more than one binding array in the set,
522
+ # it represents a join path. Here we check that each join path is complete, i.e. linked up.
523
+ # Each element of a join path is the array of bindings for a fact type invocation.
524
+ # Each invocation must share a binding (not one of the globally common ones) with
525
+ # another invocation in that join path.
526
+ bindings_list.each_with_index do |join_path, jpnum|
527
+ # Check that this bindings array creates a complete join path:
528
+ join_path.each_with_index do |bindings, i|
529
+ fact_type_roles = fact_roles_list[jpnum][i]
530
+ fact_type = fact_type_roles[0].fact_type
531
+
532
+ # The bindings are for one fact type invocation.
533
+ # These bindings must be joined to some later fact type by a common binding that isn't a globally-common one:
534
+ local_bindings = bindings-common_bindings
535
+ next if local_bindings.size == 0 # No join path is required, as only one fact type is invoked.
536
+ next if i == join_path.size-1 # We already checked that the last fact type invocation is joined
537
+ ok = local_bindings.detect do |local_binding|
538
+ j = i+1
539
+ join_path[j..-1].detect do |other_bindings|
540
+ other_fact_type_roles = fact_roles_list[jpnum][j]
541
+ other_fact_type = other_fact_type_roles[0].fact_type
542
+ j += 1
543
+ # These next two lines allow joining from/to an objectified fact type:
544
+ fact_type_roles.detect{|r| r.concept == other_fact_type.entity_type } ||
545
+ other_fact_type_roles.detect{|r| r.concept == fact_type.entity_type } ||
546
+ other_bindings.include?(local_binding)
547
+ end
548
+ end
549
+ raise "Incomplete join path; one of the bindings #{local_bindings.inspect} must re-occur to establish a join" unless ok
550
+ end
551
+ end
552
+
553
+ # Create the role sequences and their role references.
554
+ # Each role sequence contain one RoleRef for each common binding
555
+ role_sequences = readings_list.map{|r| @constellation.RoleSequence(:new) }
556
+ common_bindings.each_with_index do |binding, index|
557
+ role_sequences.each_with_index do |rs, rsi|
558
+ join_path = bindings_list[rsi]
559
+ fact_pos = nil
560
+ join_pos = (0...join_path.size).detect do |i|
561
+ fact_pos = join_path[i].index(binding)
562
+ end
563
+ @constellation.RoleRef(rs, index).role = fact_roles_list[rsi][join_pos][fact_pos]
564
+ end
565
+ end
566
+
567
+ role_sequences
568
+ end
569
+
570
+ def subset_constraint(subset_readings, superset_readings)
571
+ role_sequences = bind_join_paths_as_role_sequences([subset_readings, superset_readings])
572
+
573
+ #puts "subset_constraint:\n\t#{subset_readings.inspect}\n\t#{superset_readings.inspect}"
574
+ #puts "\t#{role_sequences.map{|rs| rs.describe}.inspect}"
575
+
576
+ # create the constraint:
577
+ constraint = @constellation.SubsetConstraint(:new)
578
+ constraint.vocabulary = @vocabulary
579
+ #constraint.name = nil
580
+ #constraint.enforcement =
581
+ constraint.subset_role_sequence = role_sequences[0]
582
+ constraint.superset_role_sequence = role_sequences[1]
583
+ end
584
+
585
+ def set_constraint(constrained_roles, quantifier, *readings_list)
586
+ # Exactly one or at most one, nothing else will do
587
+ raise "Set comparison constraint must use 'at most' or 'exactly' one" if quantifier[1] != 1
588
+
589
+ role_sequences = bind_join_paths_as_role_sequences(readings_list)
590
+
591
+ # Create the constraint:
592
+ constraint = @constellation.SetExclusionConstraint(:new)
593
+ constraint.vocabulary = @vocabulary
594
+ role_sequences.each do |rs|
595
+ @constellation.SetComparisonRoles(constraint, rs)
596
+ end
597
+ constraint.is_mandatory = quantifier[0] == 1
598
+ end
599
+
600
+ def equality_constraint(*readings_list)
601
+ #puts "REVISIT: equality\n\t#{readings_list.map{|rl| rl.inspect}*"\n\tif and only if\n\t"}"
602
+
603
+ role_sequences = bind_join_paths_as_role_sequences(readings_list)
604
+
605
+ # Create the constraint:
606
+ constraint = @constellation.SetEqualityConstraint(:new)
607
+ constraint.vocabulary = @vocabulary
608
+ role_sequences.each do |rs|
609
+ @constellation.SetComparisonRoles(constraint, rs)
610
+ end
611
+ end
612
+
613
+ def presence_constraint(constrained_role_names, quantifier, readings)
614
+ raise "REVISIT: Join presence constraints not supported yet" if readings[0].size > 1
615
+ readings = readings.map{|r| r[0] }
616
+ #p readings
617
+
618
+ @symbols = SymbolTable.new(@constellation, @vocabulary)
619
+
620
+ # Find players for all constrained_role_names. These may use leading or trailing adjective forms...
621
+ constrained_players = []
622
+ constrained_bindings = []
623
+ constrained_role_names.each do |role_name|
624
+ player, binding = @symbols.bind(role_name)
625
+ constrained_players << player
626
+ constrained_bindings << binding
627
+ end
628
+ #puts "Constrained bindings are #{constrained_bindings.inspect}"
629
+ #puts "Constrained bindings object_id's are #{constrained_bindings.map{|b|b.object_id.to_s}*","}"
630
+
631
+ # Find players for all the concepts in all readings:
632
+ @symbols.bind_roles_in_readings(readings)
633
+
634
+ constrained_roles = []
635
+ unmatched_roles = constrained_role_names.clone
636
+ readings.each do |reading|
637
+ # puts reading.inspect
638
+
639
+ # If this succeeds, the reading found matches the roles in our phrases
640
+ fact_roles = invoked_fact_roles(reading)
641
+ raise "Fact type reading not found for #{reading.inspect}" unless fact_roles
642
+
643
+ # Look for the constrained role(s); the bindings will be the same
644
+ matched_bindings = reading.select{|p| Hash === p}.map{|p| p[:binding]}
645
+ #puts "matched_bindings = #{matched_bindings.inspect}"
646
+ #puts "matched_bindings object_id's are #{matched_bindings.map{|b|b.object_id.to_s}*","}}"
647
+ matched_bindings.each_with_index{|b, pos|
648
+ i = constrained_bindings.index(b)
649
+ next unless i
650
+ unmatched_roles[i] = nil
651
+ #puts "found #{constrained_bindings[i].inspect} found as #{b.inspect} in position #{i.inspect}"
652
+ role = fact_roles[pos]
653
+ constrained_roles << role unless constrained_roles.include?(role)
654
+ }
655
+ end
656
+
657
+ # Check that all constrained roles were matched at least once:
658
+ unmatched_roles.compact!
659
+ raise "Constrained roles #{unmatched_roles.map{|ur| ur*"-"}*", "} not found in fact types" if unmatched_roles.size != 0
660
+
661
+ rs = @constellation.RoleSequence(:new)
662
+ #puts "constrained_roles: #{constrained_roles.map{|r| r.concept.name}.inspect}"
663
+ constrained_roles.each_with_index do |role, index|
664
+ raise "Constrained role #{constrained_role_names[index]} not found" unless role
665
+ rr = @constellation.RoleRef(rs, index)
666
+ rr.role = role
667
+ end
668
+ #puts "New external PresenceConstraint with quantifier = #{quantifier.inspect} over #{rs.describe}"
669
+ @constellation.PresenceConstraint(
670
+ :new,
671
+ :name => '',
672
+ :enforcement => '',
673
+ :vocabulary => @vocabulary,
674
+ :role_sequence => rs,
675
+ :min_frequency => quantifier[0],
676
+ :max_frequency => quantifier[1],
677
+ :is_preferred_identifier => false,
678
+ :is_mandatory => quantifier[0] && quantifier[0] > 0
679
+ )
680
+ end
681
+
682
+ def inheritance_path(subtype, supertype)
683
+ direct_inheritance = subtype.all_supertype_inheritance.select{|ti| ti.supertype == supertype}
684
+ return direct_inheritance if (direct_inheritance[0])
685
+ subtype.all_supertype_inheritance.each{|ti|
686
+ ip = inheritance_path(ti.supertype, supertype)
687
+ return ip+[ti] if (ip)
688
+ }
689
+ return nil
690
+ end
691
+
692
+ # For a given reading from the parser, find the matching declared reading, and return
693
+ # the array of Role object in the same order as they occur in the reading.
694
+ def invoked_fact_roles(reading)
695
+ if (reading[0] == "!SUBTYPE!")
696
+ subtype = reading[1][:binding].concept
697
+ supertype = reading[2][:binding].concept
698
+ raise "#{subtype.name} is not a subtype of #{supertype.name}" unless subtype.supertypes_transitive.include?(supertype)
699
+ ip = inheritance_path(subtype, supertype)
700
+ return [ip[0].all_role[0], ip[-1].all_role[1]]
701
+ end
702
+
703
+ bindings = reading.select{|p| Hash === p}
704
+ players = bindings.map{|p| p[:binding].concept }
705
+ invoked_fact_roles_by_players(reading, players)
706
+ end
707
+
708
+ def invoked_fact_roles_by_players(reading, players)
709
+ players[0].all_role.each do |role|
710
+ # Does this fact type have the right number of roles?
711
+ next if role.fact_type.all_role.size != players.size
712
+
713
+ # Does this fact type include the correct other players?
714
+ # REVISIT: Might need subtype/supertype matching here, with an implied subtyping join invocation
715
+ next if role.fact_type.all_role.detect{|r| !players.include?(r.concept)}
716
+
717
+ # Oooh, a real candidate. Check the reading words.
718
+ debug "Considering "+role.fact_type.describe do
719
+ next unless role.fact_type.all_reading.detect do |candidate_reading|
720
+ debug "Considering reading"+candidate_reading.reading_text do
721
+ to_match = reading.clone
722
+ players_to_match = players.clone
723
+ candidate_reading.words_and_role_refs.each do |wrr|
724
+ if (RoleRef === wrr)
725
+ break unless Hash === to_match.first
726
+ break unless binding = to_match[0][:binding]
727
+ # REVISIT: May need to match super- or sub-types here too!
728
+ break unless players_to_match[0] == wrr.role.concept
729
+ break if wrr.leading_adjective && binding.leading_adjective != wrr.leading_adjective
730
+ break if wrr.trailing_adjective && binding.trailing_adjective != wrr.trailing_adjective
731
+
732
+ # All matched.
733
+ to_match.shift
734
+ players_to_match.shift
735
+ # elsif # REVISIT: Match "not" and "none" here as negating the fact type invocation
736
+ else
737
+ break unless String === to_match[0]
738
+ break unless to_match[0] == wrr
739
+ to_match.shift
740
+ end
741
+ end
742
+
743
+ # This is the first matching candidate.
744
+ # REVISIT: Since we do sub/supertype matching (and will do more!),
745
+ # we need to accumulate all possible matches to be sure
746
+ # there's only one, or the match is exact, or risk ambiguity.
747
+ debug "Reading match was #{to_match.size == 0 ? "ok" : "bad"}"
748
+ return candidate_reading.role_sequence.all_role_ref.map{|rr| rr.role} if to_match.size == 0
749
+ end
750
+ end
751
+ end
752
+ end
753
+
754
+ # Hmm, that didn't work, try the subtypes of the first player.
755
+ # When a fact type matches like this, there is an implied join to the subtype.
756
+ players[0].subtypes.map do |subtype|
757
+ players[0] = subtype
758
+ fr = invoked_fact_roles_by_players(reading, players)
759
+ return fr if fr
760
+ end
761
+
762
+ # REVISIT: Do we need to do this again for the supertypes of the first player?
763
+
764
+ nil
765
+ end
766
+
767
+ def bind_fact_reading(fact_type, qualifiers, reading)
768
+ debug :reading, "Processing reading #{reading.inspect}" do
769
+ role_phrases = reading.select do |phrase|
770
+ Hash === phrase && phrase[:binding]
771
+ end
772
+
773
+ # All readings for a fact type must have the same number of roles.
774
+ # This might be relaxed later for fact clauses, where readings might
775
+ # be concatenated if the adjacent items are the same concept.
776
+ if (fact_type && fact_type.all_reading.size > 0 && role_phrases.size != fact_type.all_role.size)
777
+ raise "#{
778
+ role_phrases.size > fact_type.all_role.size ? "Too many" : "Not all"
779
+ } roles found for non-initial reading of #{fact_type.describe}"
780
+ end
781
+
782
+ # REVISIT: If the first reading is a re-iteration of an existing fact type, find and use the existing fact type
783
+ # This will require loading the @symbols.roles_by_binding using a SymbolTable
784
+
785
+ fact_type ||= @constellation.FactType(:new)
786
+
787
+ # Create the roles on the first reading, or look them up on subsequent readings.
788
+ # If the player occurs twice, we must find one with matching adjectives.
789
+
790
+ role_sequence = @constellation.RoleSequence(:new) # RoleSequence for RoleRefs of this reading
791
+ roles = []
792
+ role_phrases.each_with_index do |role_phrase, index|
793
+ binding = role_phrase[:binding]
794
+ role_name = role_phrase[:role_name]
795
+ player = binding.concept
796
+ if (fact_type.all_reading.size == 0) # First reading
797
+ # Assert this role of the fact type:
798
+ role = @constellation.Role(fact_type, fact_type.all_role.size, player)
799
+ role.role_name = role_name if role_name
800
+ debug "Concept #{player.name} found, created role #{role.describe} by binding #{binding.inspect}"
801
+ @symbols.roles_by_binding[binding] = role
802
+ else # Subsequent readings
803
+ #debug "Looking for role #{binding.inspect} in bindings #{@symbols.roles_by_binding.inspect}"
804
+ role = @symbols.roles_by_binding[binding]
805
+ raise "Role #{binding.inspect} not found in prior readings" if !role
806
+ player = role.concept
807
+ end
808
+ roles << role
809
+
810
+ # Create the RoleRefs for the RoleSequence
811
+
812
+ role_ref = @constellation.RoleRef(role_sequence, index, :role => roles[index])
813
+ leading_adjective = role_phrase[:leading_adjective]
814
+ role_ref.leading_adjective = leading_adjective if leading_adjective
815
+ trailing_adjective = role_phrase[:trailing_adjective]
816
+ role_ref.trailing_adjective = trailing_adjective if trailing_adjective
817
+ end
818
+
819
+ # Create any embedded constraints:
820
+ debug "Creating embedded presence constraints for #{fact_type.describe}" do
821
+ create_embedded_presence_constraints(fact_type, role_phrases, roles)
822
+ end
823
+
824
+ process_qualifiers(role_sequence, qualifiers)
825
+
826
+ # Save the first role sequence to be used for a default PresenceConstraint
827
+ add_reading(fact_type, role_sequence, reading)
828
+ end
829
+ fact_type
830
+ end
831
+
832
+ def fact_type_identification(fact_type, name, prefer)
833
+ if !@symbols.embedded_presence_constraints.detect{|pc| pc.max_frequency == 1}
834
+ first_role_sequence = fact_type.all_reading[0].role_sequence
835
+ identifier = @constellation.PresenceConstraint(
836
+ :new,
837
+ :vocabulary => @vocabulary,
838
+ :name => "#{name}PK", # Is this a useful name?
839
+ :role_sequence => first_role_sequence,
840
+ :is_preferred_identifier => prefer,
841
+ :max_frequency => 1 # Unique
842
+ )
843
+ # REVISIT: The UC might be provided later as an external constraint, relax this rule:
844
+ #raise "'#{fact_type.default_reading}': non-unary fact types having no uniqueness constraints must be objectified (named)" unless fact_type.entity_type
845
+ debug "Made default fact type identifier #{identifier.object_id} over #{first_role_sequence.describe} in #{fact_type.describe}"
846
+ elsif prefer
847
+ #debug "Made fact type identifier #{identifier.object_id} preferred over #{@symbols.embedded_presence_constraints[0].role_sequence.describe} in #{fact_type.describe}"
848
+ @symbols.embedded_presence_constraints[0].is_preferred_identifier = true
849
+ end
850
+ end
851
+
852
+ # Categorise the fact type clauses according to the set of role player names
853
+ # Return an array where each element is an array of clauses, the clauses having
854
+ # matching players, and otherwise preserving the order of definition.
855
+ def clauses_by_fact_type(clauses)
856
+ clause_group_by_role_players = {}
857
+ clauses.inject([]) do |clause_groups, clause|
858
+ type, qualifiers, reading = *clause
859
+
860
+ debug "Clause: #{clause.inspect}"
861
+ roles = reading.map do |phrase|
862
+ Hash === phrase ? phrase[:binding] : nil
863
+ end.compact
864
+
865
+ # Look for an existing clause group involving these players, or make one:
866
+ clause_group = clause_group_by_role_players[key = roles.sort]
867
+ if clause_group # Another clause for an existing clause group
868
+ clause_group << clause
869
+ else # A new clause group
870
+ clause_groups << (clause_group_by_role_players[key] = [clause])
871
+ end
872
+ clause_groups
873
+ end
874
+ end
875
+
876
+ # For each fact reading there may be embedded mandatory, uniqueness or frequency constraints:
877
+ def create_embedded_presence_constraints(fact_type, role_phrases, roles)
878
+ embedded_presence_constraints = []
879
+ role_phrases.zip(roles).each_with_index do |role_pair, index|
880
+ role_phrase, role = *role_pair
881
+
882
+ next unless quantifier = role_phrase[:quantifier]
883
+
884
+ debug "Processing embedded constraint #{quantifier.inspect} on #{role.concept.name} in #{fact_type.describe}" do
885
+ constrained_roles = roles.clone
886
+ constrained_roles.delete_at(index)
887
+ constraint = find_pc_over_roles(constrained_roles)
888
+ if constraint
889
+ debug "Setting max frequency to #{quantifier[1]} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}"
890
+ constraint.max_frequency = quantifier[1]
891
+ else
892
+ role_sequence = @constellation.RoleSequence(:new)
893
+ constrained_roles.each_with_index do |constrained_role, i|
894
+ role_ref = @constellation.RoleRef(role_sequence, i, :role => constrained_role)
895
+ end
896
+ constraint = @constellation.PresenceConstraint(
897
+ :new,
898
+ :vocabulary => @vocabulary,
899
+ :role_sequence => role_sequence,
900
+ :is_mandatory => quantifier[0] && quantifier[0] > 0, # REVISIT: Check "maybe" qualifier?
901
+ :max_frequency => quantifier[1],
902
+ :min_frequency => quantifier[0]
903
+ )
904
+ embedded_presence_constraints << constraint
905
+ debug "Made new PC min=#{quantifier[0].inspect} max=#{quantifier[1].inspect} constraint #{constraint.object_id} over #{(e = fact_type.entity_type) ? e.name : role_sequence.describe} in #{fact_type.describe}"
906
+ end
907
+ end
908
+ end
909
+ @symbols.embedded_presence_constraints += embedded_presence_constraints
910
+ end
911
+
912
+ def process_qualifiers(role_sequence, qualifiers)
913
+ return unless qualifiers.size > 0
914
+ qualifiers.sort!
915
+
916
+ # Process the ring constraints:
917
+ ring_constraints, qualifiers = qualifiers.partition{|q| RingTypes.include?(q) }
918
+ unless ring_constraints.empty?
919
+ # A Ring may be over a supertype/subtype pair, and this won't find that.
920
+ role_refs = role_sequence.all_role_ref
921
+ role_pairs = []
922
+ player_supertypes_by_role = role_refs.map{|rr|
923
+ concept = rr.role.concept
924
+ EntityType === concept ? supertypes(concept) : [concept]
925
+ }
926
+ role_refs.each_with_index{|rr1, i|
927
+ player1 = rr1.role.concept
928
+ (i+1...role_refs.size).each{|j|
929
+ rr2 = role_refs[j]
930
+ player2 = rr2.role.concept
931
+ if player_supertypes_by_role[i] - player_supertypes_by_role[j] != player_supertypes_by_role[i]
932
+ role_pairs << [rr1.role, rr2.role]
933
+ end
934
+ }
935
+ }
936
+ raise "ring constraint (#{ring_constraints*" "}) role pair not found" if role_pairs.size == 0
937
+ raise "ring constraint (#{ring_constraints*" "}) is ambiguous over roles of #{role_pairs.map{|rp| rp.map{|r| r.concept.name}}.inspect}" if role_pairs.size > 1
938
+ roles = role_pairs[0]
939
+
940
+ # Ensure that the keys in RingPairs follow others:
941
+ ring_constraints = ring_constraints.partition{|rc| !RingPairs.keys.include?(rc.downcase.to_sym) }.flatten
942
+
943
+ if ring_constraints.size > 1 and !RingPairs[ring_constraints[-1].to_sym].include?(ring_constraints[0].to_sym)
944
+ raise "incompatible ring constraint types (#{ring_constraints*", "})"
945
+ end
946
+ ring_type = ring_constraints.map{|c| c.capitalize}*""
947
+
948
+ ring = @constellation.RingConstraint(
949
+ :new,
950
+ :vocabulary => @vocabulary,
951
+ # :name => name, # REVISIT: Create a name for Ring Constraints?
952
+ :role => roles[0],
953
+ :other_role => roles[1],
954
+ :ring_type => ring_type
955
+ )
956
+
957
+ debug "Added #{ring.verbalise} #{ring.class.roles.keys.map{|k|"#{k} => "+ring.send(k).verbalise}*", "}"
958
+ end
959
+
960
+ return unless qualifiers.size > 0
961
+
962
+ # Process the remaining qualifiers:
963
+ puts "REVISIT: Qualifiers #{qualifiers.inspect} over #{role_sequence.describe}"
964
+ end
965
+
966
+ def find_pc_over_roles(roles)
967
+ return nil if roles.size == 0 # Safeguard; this would chuck an exception otherwise
968
+ roles[0].all_role_ref.each do |role_ref|
969
+ next if role_ref.role_sequence.all_role_ref.map(&:role) != roles
970
+ pc = role_ref.role_sequence.all_presence_constraint[0]
971
+ #puts "Existing PresenceConstraint matches those roles!" if pc
972
+ return pc if pc
973
+ end
974
+ nil
975
+ end
976
+
977
+ def add_reading(fact_type, role_sequence, reading)
978
+ ordinal = (fact_type.all_reading.map(&:ordinal).max||-1) + 1 # Use the next unused ordinal
979
+ defined_reading = @constellation.Reading(fact_type, ordinal, :role_sequence => role_sequence)
980
+ role_num = -1
981
+ defined_reading.reading_text = reading.map {|phrase|
982
+ Hash === phrase ? "{#{role_num += 1}}" : phrase
983
+ }*" "
984
+ raise "Wrong number of players (#{role_num+1}) found in reading #{defined_reading.reading_text} over #{fact_type.describe}" if role_num+1 != fact_type.all_role.size
985
+ debug "Added reading #{defined_reading.reading_text}"
986
+ end
987
+
988
+ # Return an array of this entity type and all its supertypes, transitively:
989
+ def supertypes(o)
990
+ ([o] + o.all_supertype_inheritance.map{|ti| supertypes(ti.supertype)}.flatten).uniq
991
+ end
992
+
993
+ def concept_by_name(name)
994
+ player = @constellation.Concept[[name, @vocabulary.identifying_role_values]]
995
+
996
+ # REVISIT: Hack to allow facts to refer to standard types that will be imported from standard vocabulary:
997
+ if !player && %w{Date DateAndTime Time}.include?(name)
998
+ player = @constellation.ValueType(name, @vocabulary.identifying_role_values)
999
+ end
1000
+
1001
+ if (!player && @symbols.allowed_forward[name])
1002
+ player = @constellation.EntityType(name, @vocabulary)
1003
+ end
1004
+ player
1005
+ end
1006
+
1007
+ class SymbolTable #:nodoc:all
1008
+ # Externally built tables used in this binding context:
1009
+ attr_reader :roles_by_binding
1010
+ attr_accessor :embedded_presence_constraints
1011
+ attr_accessor :allowed_forward
1012
+ attr_reader :constellation
1013
+ attr_reader :vocabulary
1014
+ attr_reader :bindings_by_concept
1015
+ attr_reader :role_names
1016
+
1017
+ # A Binding here is a form of reference to a concept, being a name and optional adjectives, possibly designated by a role name:
1018
+ Binding = Struct.new("Binding", :concept, :name, :leading_adjective, :trailing_adjective, :role_name)
1019
+ class Binding
1020
+ def inspect
1021
+ "Binding(#{concept.class.basename} #{concept.name}, #{[leading_adjective, name, trailing_adjective].compact*"-"}#{role_name ? " (as #{role_name})" : ""})"
1022
+ end
1023
+
1024
+ # Any ordering works to allow a hash to be keyed by a set (unordered array) of Bindings:
1025
+ def <=>(other)
1026
+ object_id <=> other.object_id
1027
+ end
1028
+ end
1029
+
1030
+ def initialize(constellation, vocabulary)
1031
+ @constellation = constellation
1032
+ @vocabulary = vocabulary
1033
+ @bindings_by_concept = Hash.new {|h, k| h[k] = [] } # Indexed by Binding#name, maybe multiple entries for each name
1034
+ @role_names = {}
1035
+
1036
+ @embedded_presence_constraints = []
1037
+ @roles_by_binding = {} # Build a hash of allowed bindings on first reading (check against it on subsequent ones)
1038
+ @allowed_forward = {} # No roles may be forward-referenced
1039
+ end
1040
+
1041
+ #
1042
+ # This method is the guts of role matching.
1043
+ # "words" may be a single word (and then the adjectives may also be used) or two words.
1044
+ # In either case a word is expected to be a defined concept or role name.
1045
+ # If a role_name is provided here, that's a *definition* and will only be accepted if legal
1046
+ # If allowed_forward is true, words is a single word and is not defined, create a forward Entity
1047
+ # If leading_speculative or trailing_speculative is true, the adjectives may not apply. If they do apply, use them.
1048
+ # If loose_binding_except is true, it's a hash containing names that may *not* be loose-bound... else none may.
1049
+ #
1050
+ # Loose binding is when a word without an adjective matches a role with, or vice verse.
1051
+ #
1052
+ def bind(words, leading_adjective = nil, trailing_adjective = nil, role_name = nil, allowed_forward = false, leading_speculative = false, trailing_speculative = false, loose_binding_except = nil)
1053
+ words = Array(words)
1054
+ if (words.size > 2 or words.size == 2 && (leading_adjective or trailing_adjective or allowed_forward))
1055
+ raise "role has too many adjectives '#{[leading_adjective, words, trailing_adjective].flatten.compact*" "}'"
1056
+ end
1057
+
1058
+ # Check for use of a role name, valid if they haven't used any adjectives or tried to define a role_name:
1059
+ binding = @role_names[words[0]]
1060
+ if binding && words.size == 1 # If ok, this is it.
1061
+ raise "May not use existing role name '#{words[0]}' to define a new role name" if role_name
1062
+ if (leading_adjective && !leading_speculative) || (trailing_adjective && !trailing_speculative)
1063
+ raise "May not use existing role name '#{words[0]}' with adjectives"
1064
+ end
1065
+ return binding.concept, binding
1066
+ end
1067
+
1068
+ # Look for an existing definition
1069
+ # If we have more than one word that might be the concept name, find which it is:
1070
+ words.each do |w|
1071
+ # Find the existing defined binding that matches this one:
1072
+ bindings = @bindings_by_concept[w]
1073
+ best_match = nil
1074
+ matched_adjectives = 0
1075
+ bindings.each do |binding|
1076
+ # Adjectives defined on the binding must be matched unless loose binding is allowed.
1077
+ loose_ok = loose_binding_except and !loose_binding_except[binding.concept.name]
1078
+
1079
+ # Don't allow binding a new role name to an existing one:
1080
+ next if role_name and role_name != binding.role_name
1081
+
1082
+ quality = 0
1083
+ if binding.leading_adjective != leading_adjective
1084
+ next if binding.leading_adjective && leading_adjective # Both set, but different
1085
+ next if !loose_ok && (!leading_speculative || !leading_adjective)
1086
+ quality += 1
1087
+ end
1088
+
1089
+ if binding.trailing_adjective != trailing_adjective
1090
+ next if binding.trailing_adjective && trailing_adjective # Both set, but different
1091
+ next if !loose_ok && (!trailing_speculative || !trailing_adjective)
1092
+ quality += 1
1093
+ end
1094
+
1095
+ quality += 1 unless binding.role_name # A role name that was not matched... better if there wasn't one
1096
+
1097
+ if (quality > matched_adjectives || !best_match)
1098
+ best_match = binding # A better match than we had before
1099
+ matched_adjectives = quality
1100
+ break unless loose_ok || leading_speculative || trailing_speculative
1101
+ end
1102
+ end
1103
+
1104
+ if best_match
1105
+ # We've found the best existing definition
1106
+
1107
+ # Indicate which speculative adjectives were used so the clauses can be deleted:
1108
+ leading_adjective.replace("") if best_match.leading_adjective and leading_adjective and leading_speculative
1109
+ trailing_adjective.replace("") if best_match.trailing_adjective and trailing_adjective and trailing_speculative
1110
+
1111
+ return best_match.concept, best_match
1112
+ end
1113
+
1114
+ # No existing defined binding. Look up an existing concept of this name:
1115
+ player = concept(w, allowed_forward)
1116
+ next unless player
1117
+
1118
+ # Found a new binding for this player, save it.
1119
+
1120
+ # Check that a trailing adjective isn't an existing role name or concept:
1121
+ trailing_word = words[1] if w == words[0]
1122
+ if trailing_word
1123
+ raise "May not use existing role name '#{trailing_word}' with a new name or with adjectives" if @role_names[trailing_word]
1124
+ raise "ambiguous concept reference #{words*" '"}'" if concept(trailing_word)
1125
+ end
1126
+ leading_word = words[0] if w != words[0]
1127
+
1128
+ raise "may not redefine existing concept '#{role_name}' as a role name" if role_name and concept(role_name)
1129
+
1130
+ binding = Binding.new(
1131
+ player,
1132
+ w,
1133
+ (!leading_speculative && leading_adjective) || leading_word,
1134
+ (!trailing_speculative && trailing_adjective) || trailing_word,
1135
+ role_name
1136
+ )
1137
+ @bindings_by_concept[binding.name] << binding
1138
+ @role_names[binding.role_name] = binding if role_name
1139
+ return binding.concept, binding
1140
+ end
1141
+
1142
+ # Not found.
1143
+ return nil
1144
+ end
1145
+
1146
+ # return the EntityType or ValueType this name refers to:
1147
+ def concept(name, allowed_forward = false)
1148
+ # See if the name is a defined concept in this vocabulary:
1149
+ player = @constellation.Concept[[name, virv = @vocabulary.identifying_role_values]]
1150
+
1151
+ # REVISIT: Hack to allow facts to refer to standard types that will be imported from standard vocabulary:
1152
+ if !player && %w{Date DateAndTime Time}.include?(name)
1153
+ player = @constellation.ValueType(name, virv)
1154
+ end
1155
+
1156
+ if !player && allowed_forward
1157
+ player = @constellation.EntityType(name, @vocabulary)
1158
+ end
1159
+
1160
+ player
1161
+ end
1162
+
1163
+ def bind_roles_in_clauses(clauses, identification = [])
1164
+ identification ||= []
1165
+ bind_roles_in_readings(
1166
+ clauses.map{|clause| clause[2]}, # Extract the readings
1167
+ single_word_identifiers = identification.map{|i| i.size == 1 ? i[0] : nil}.compact.uniq
1168
+ )
1169
+ end
1170
+
1171
+ #
1172
+ # Walk through all phrases of all readings identifying role players.
1173
+ # Each role player phrase gets a :binding key added to it.
1174
+ #
1175
+ # Any adjectives that the parser didn't recognise are merged with their players here,
1176
+ # as long as they're indicated as adjectives of that player somewhere in the readings.
1177
+ #
1178
+ # Other words are turned from phrases (hashes) into simple strings.
1179
+ #
1180
+ def bind_roles_in_readings(readings, allowed_forwards = [])
1181
+ disallow_loose_binding = allowed_forwards.inject({}) { |h, v| h[v] = true; h }
1182
+ readings.each do |reading|
1183
+ debug :bind, "Binding a reading"
1184
+
1185
+ if (reading.size == 1 && reading[0][:subtype])
1186
+ # REVISIT: Handle a subtype reading here
1187
+ subtype_name = reading[0][:subtype]
1188
+ supertype_name = reading[0][:supertype]
1189
+ subtype, subtype_binding = bind(subtype_name)
1190
+ supertype, supertype_binding = bind(supertype_name)
1191
+ reading.replace([
1192
+ "!SUBTYPE!",
1193
+ {:word => subtype, :binding => subtype_binding },
1194
+ {:word => supertype, :binding => supertype_binding }
1195
+ ]
1196
+ )
1197
+ next
1198
+ end
1199
+
1200
+ phrase_numbers_used_speculatively = []
1201
+ disallow_loose_binding_this_reading = disallow_loose_binding.clone
1202
+ reading.each_with_index do |phrase, index|
1203
+ la = phrase[:leading_adjective]
1204
+ player_name = phrase[:word]
1205
+ ta = phrase[:trailing_adjective]
1206
+ role_name = phrase[:role_name]
1207
+
1208
+ # We use the preceeding phrase and/or following phrase speculatively if they're simple words:
1209
+ preceeding_phrase = nil
1210
+ following_phrase = nil
1211
+ if !la && index > 0 && (preceeding_phrase = reading[index-1])
1212
+ preceeding_phrase = nil unless String === preceeding_phrase || preceeding_phrase.keys == [:word]
1213
+ la = preceeding_phrase[:word] if Hash === preceeding_phrase
1214
+ end
1215
+ if !ta && (following_phrase = reading[index+1])
1216
+ following_phrase = nil unless following_phrase.keys == [:word]
1217
+ ta = following_phrase[:word] if following_phrase
1218
+ end
1219
+
1220
+ # If the identification includes this player name as a single word, it's allowed to be forward referenced:
1221
+ allowed_forward = allowed_forwards.include?(player_name)
1222
+
1223
+ debug :bind, "Binding a role: #{[player_name, la, ta, role_name, allowed_forward, !!preceeding_phrase, !!following_phrase].inspect}"
1224
+ player, binding = bind(
1225
+ player_name,
1226
+ la, ta,
1227
+ role_name,
1228
+ allowed_forward,
1229
+ !!preceeding_phrase, !!following_phrase,
1230
+ reading == readings[0] ? nil : disallow_loose_binding_this_reading # Never allow loose binding on the first reading
1231
+ )
1232
+ disallow_loose_binding_this_reading[player.name] = true if player
1233
+
1234
+ # Arrange to delete the speculative adjectives that were used:
1235
+ if preceeding_phrase && preceeding_phrase[:word] == ""
1236
+ debug :bind, "binding consumed a speculative leading_adjective #{la}"
1237
+ # The numbers are adjusted to allow for prior deletions.
1238
+ phrase_numbers_used_speculatively << index-1-phrase_numbers_used_speculatively.size
1239
+ end
1240
+ if following_phrase && following_phrase[:word] == ""
1241
+ debug :bind, "binding consumed a speculative trailing_adjective #{ta}"
1242
+ phrase_numbers_used_speculatively << index+1-phrase_numbers_used_speculatively.size
1243
+ end
1244
+
1245
+ if player
1246
+ # Replace the words used to identify the role by a reference to the role itself,
1247
+ # leaving :quantifier, :function, :restriction and :literal intact
1248
+ phrase[:binding] = binding
1249
+ binding
1250
+ else
1251
+ raise "Internal error; role #{phrase.inspect} not matched" unless phrase.keys == [:word]
1252
+ # Just a linking word
1253
+ reading[index] = phrase[:word]
1254
+ end
1255
+ debug :bind, "Bound phrase: #{phrase.inspect}" + " -> " + (player ? player.name+", "+binding.inspect : phrase[:word].inspect)
1256
+
1257
+ end
1258
+ phrase_numbers_used_speculatively.each do |index|
1259
+ reading.delete_at(index)
1260
+ end
1261
+ debug :bind, "Bound reading: #{reading.inspect}"
1262
+ end
1263
+ end
1264
+ end # of SymbolTable class
1265
+
1266
+ end
1267
+ end
1268
+ end