activefacts 1.0.2 → 1.1.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 (47) hide show
  1. checksums.yaml +5 -13
  2. data/Rakefile +2 -2
  3. data/bin/afgen +1 -1
  4. data/bin/cql +118 -27
  5. data/examples/CQL/Insurance.cql +2 -2
  6. data/examples/CQL/Metamodel.cql +3 -3
  7. data/examples/CQL/SchoolActivities.cql +1 -1
  8. data/examples/CQL/Warehousing.cql +5 -4
  9. data/lib/activefacts/cql.rb +1 -1
  10. data/lib/activefacts/cql/Language/English.treetop +2 -1
  11. data/lib/activefacts/cql/compiler.rb +6 -6
  12. data/lib/activefacts/cql/compiler/clause.rb +69 -46
  13. data/lib/activefacts/cql/compiler/constraint.rb +14 -14
  14. data/lib/activefacts/cql/compiler/entity_type.rb +24 -24
  15. data/lib/activefacts/cql/compiler/fact.rb +40 -27
  16. data/lib/activefacts/cql/compiler/fact_type.rb +16 -16
  17. data/lib/activefacts/cql/compiler/query.rb +12 -12
  18. data/lib/activefacts/cql/compiler/shared.rb +9 -0
  19. data/lib/activefacts/cql/compiler/value_type.rb +4 -4
  20. data/lib/activefacts/cql/parser.rb +9 -9
  21. data/lib/activefacts/generate/cql.rb +41 -20
  22. data/lib/activefacts/generate/helpers/oo.rb +33 -70
  23. data/lib/activefacts/generate/helpers/ordered.rb +61 -87
  24. data/lib/activefacts/generate/ruby.rb +12 -72
  25. data/lib/activefacts/generate/transform/surrogate.rb +13 -13
  26. data/lib/activefacts/input/orm.rb +72 -71
  27. data/lib/activefacts/persistence/columns.rb +66 -31
  28. data/lib/activefacts/persistence/foreignkey.rb +6 -6
  29. data/lib/activefacts/persistence/index.rb +12 -12
  30. data/lib/activefacts/persistence/object_type.rb +15 -12
  31. data/lib/activefacts/persistence/reference.rb +20 -18
  32. data/lib/activefacts/persistence/tables.rb +40 -36
  33. data/lib/activefacts/support.rb +69 -123
  34. data/lib/activefacts/version.rb +2 -2
  35. data/lib/activefacts/vocabulary/extensions.rb +42 -39
  36. data/lib/activefacts/vocabulary/metamodel.rb +11 -1
  37. data/lib/activefacts/vocabulary/verbaliser.rb +28 -28
  38. data/spec/cql/contractions_spec.rb +1 -1
  39. data/spec/cql/entity_type_spec.rb +1 -1
  40. data/spec/cql/fact_type_matching_spec.rb +3 -3
  41. data/spec/cql/role_matching_spec.rb +4 -4
  42. data/spec/cql/samples_spec.rb +2 -2
  43. data/spec/cql_cql_spec.rb +1 -1
  44. data/spec/helpers/array_matcher.rb +1 -1
  45. data/spec/norma_ruby_sql_spec.rb +2 -2
  46. data/spec/norma_tables_spec.rb +3 -2
  47. metadata +47 -68
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NzcwY2M0NGUyNTljNzZmMDBlN2ExOTMyNTI3MDE0ZTMwYjU4MWY3Yw==
5
- data.tar.gz: !binary |-
6
- Yzk1MmFlZTJhZTZiOTY0OGMyZTNjYzExM2Q5OTJhMzQwNTNhNjQ2Nw==
2
+ SHA1:
3
+ metadata.gz: 6e791a369dca69a80ccd666e7eec9441e9f00483
4
+ data.tar.gz: d894b07ab82cfdf942a8e93561fa86a8a3bedd8a
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MTI2OTFhNWU0YjA2NWNjNmJkNDI0NzJkYWZkZGVhNGNiZDIzNTBjNWQ1MjQz
10
- ODZhOWRkZDA4ZmIzNDNmYjQzZTMzMzQxYTA1Y2E5MWFkOTgyODM0NTc5MDMw
11
- YzA3OWE0ZGQ3ZmFiNTlhNjE3MzlmNzM2MGNlYjY0YTgzZmVmNmI=
12
- data.tar.gz: !binary |-
13
- YzJmNzk1OGVmMTM3Y2E0YzE4MzJhMzg2YzVmMWE2ZjVkYjA2Y2JlODAxY2M5
14
- MzE1ZGIzZDRiYzc4YjBiZTUwZDZlOWJiNjZmZjA0ZWQxMzJmNzFlODMwOTI1
15
- MjM0MjAyMDFhNjgzY2I1ZTc3ZDU0OTViYzE2MmMzMTQ0Mzg0OTI=
6
+ metadata.gz: c5f05a7b8b3b116f478b55819baba8162b12c8807122f9825474c93819a9f0f3255dd5c2040eaf54dd59e6e445c5572bcd1d82267d3b9312af140ae8b51d8ce5
7
+ data.tar.gz: e6d40c1c950bbca20c96700002c88fa640c1a019c3dfe72b4e88da5a549d401d6d5d87eead9c20d6ced53c688750f7458f2ae598181a6a9c02d84775c6407ca4
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
3
  require 'fileutils'
4
+ $: << "lib"
4
5
  require File.dirname(__FILE__) + '/lib/activefacts'
5
6
 
6
7
  def File.read_utf(path)
@@ -38,8 +39,7 @@ and object models in SQL, Ruby and other languages.
38
39
 
39
40
  gem.files = File.open("Manifest.txt"){|f| f.read.split(/\n/)}
40
41
  gem.executables = gem.files.grep(%r{^bin/}).map{|f| f.sub('bin/', '')}
41
- gem.rdoc_options = ['-S'] +
42
- # RDoc used to have these options: -A has_one -A one_to_one -A maybe
42
+ gem.rdoc_options = # RDoc used to have these options: -A has_one -A one_to_one -A maybe
43
43
  %w{
44
44
  -x lib/activefacts/cql/.*.rb
45
45
  -x lib/activefacts/vocabulary/.*.rb
data/bin/afgen CHANGED
@@ -64,6 +64,6 @@ begin
64
64
  rescue => e
65
65
  $stderr.puts "#{e.message}"
66
66
  # puts "\t#{e.backtrace*"\n\t"}"
67
- $stderr.puts "\t#{e.backtrace*"\n\t"}" if debug :exception
67
+ $stderr.puts "\t#{e.backtrace*"\n\t"}" if trace :exception
68
68
  exit 1
69
69
  end
data/bin/cql CHANGED
@@ -7,16 +7,6 @@
7
7
  #
8
8
  require 'readline'
9
9
 
10
- # Load the ruby debugger before everything else, if requested
11
- #if d = ENV['DEBUG'] and d.split(/,/).include?('debug')
12
- # begin
13
- # require 'ruby-debug'
14
- # Debugger.start(:post_mortem => true) # Stop when an exception is thrown, but before it's rescued
15
- # rescue LoadError
16
- # # Ok, no debugger, tough luck.
17
- # end
18
- #end
19
-
20
10
  require 'activefacts'
21
11
  require 'activefacts/cql/compiler'
22
12
 
@@ -28,6 +18,11 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
28
18
  self.root = :definition
29
19
  end
30
20
 
21
+ def add_logger logger
22
+ @constellation.loggers << logger
23
+ @constellation.loggers.uniq!
24
+ end
25
+
31
26
  def list_instances name
32
27
  if (name.is_a?(ActiveFacts::Metamodel::ObjectType))
33
28
  name.all_instance
@@ -40,7 +35,7 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
40
35
 
41
36
  def toggle_trace words
42
37
  words.each { |word|
43
- puts "#{word} #{debug_toggle(word) ? "en" : "dis"}abled"
38
+ puts "#{word} #{trace.toggle(word) ? "en" : "dis"}abled"
44
39
  }
45
40
  end
46
41
 
@@ -79,7 +74,7 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
79
74
  case cmd = words.shift
80
75
  when "/trace"
81
76
  if words.empty?
82
- puts debug_keys*", "
77
+ puts trace_keys*", "
83
78
  else
84
79
  toggle_trace words
85
80
  end
@@ -89,6 +84,13 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
89
84
  when "/highlight"
90
85
  @show_highlight = !@show_highlight
91
86
  puts "Will #{@show_highlight ? "" : "not "}show the highlighted parse"
87
+ when "/log"
88
+ @logger ||= proc {|*a|
89
+ unless @undoing
90
+ print "LOG: "; p a
91
+ end
92
+ }
93
+ add_logger @logger
92
94
  when "/timings"
93
95
  $show_timings = !$show_timings
94
96
  puts "Will #{$show_timings ? "" : "not "}show command timings"
@@ -103,13 +105,13 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
103
105
  end
104
106
  when "/load"
105
107
  load_file(words*' ')
108
+ when "/quit"
109
+ exit
110
+ when "/help", "/"
111
+ help words
106
112
  else
107
- if cmd == "/help" or cmd == "/"
108
- help words
109
- else
110
- puts "Unknown metacommand #{line}?"
111
- help
112
- end
113
+ puts "Unknown metacommand #{line}?"
114
+ help
113
115
  end
114
116
  end
115
117
 
@@ -124,17 +126,57 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
124
126
  $stderr.puts e.message
125
127
  rescue => e
126
128
  puts e.message
127
- puts "\t#{e.backtrace*"\n\t"}" if debug :exception
129
+ puts "\t#{e.backtrace*"\n\t"}" if trace :exception
128
130
  end
129
131
  end
130
132
 
131
- def process_query(join)
132
- if join.all_join_node.size == 1 and join.all_join_step.size == 0
133
- list_instances(join.all_join_node.single.object_type)
133
+ def process_query(query)
134
+ result_projection = {} # A set of {hash of Variable to Instance}
135
+ query_block =
136
+ proc do |unbound_variables = [], bindings = {}|
137
+ # unbound_variables is an array of the remaining dimensions of the search
138
+ # bindings is a hash of Varable to Instance
139
+ next_variable = unbound_variables[0]
140
+ if next_variable
141
+ ins = next_variable.object_type.all_instance
142
+ trace :result, "Query search through #{ins.size} #{next_variable.role_name || next_variable.object_type.name}" do
143
+ next_variable.object_type.all_instance.each do |instance|
144
+ trace :result, "Considering #{instance.verbalise}"
145
+ # REVISIT: Check whether this instance satisfies the query steps that contain the bound variables
146
+ (b = bindings.dup).store(next_variable, instance)
147
+ result_projection[b] = true
148
+ query_block.call(unbound_variables[1..-1], b)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ unbound_variables = query.all_variable.to_a
155
+ bound_variables = {}
156
+ unbound_variables.reject!{|v| v.value != nil ? bound_variables[v] = v.value : nil }
157
+ print 'bound variables: '; p bound_variables.keys.map{|v| [v.object_type.name, v.value.inspect] }
158
+ print 'unbound variables: '; p unbound_variables.map{|v| v.object_type.name}
159
+
160
+ query_block.call(unbound_variables, {})
161
+ result_projection.map do |result, _|
162
+ puts(result.map{|v, i| i.verbalise }.join(', '))
163
+ end
164
+
165
+ =begin
166
+ if query.all_variable.size == 1 and
167
+ (v = query.all_variable.single).all_play.map{|p| p.all_step.to_a}.flatten.uniq.size == 0
168
+ if v.object_type.all_instance.size == 0
169
+ puts "There are no instances of #{v.object_type.name}"
170
+ else
171
+ list_instances(v.object_type)
172
+ end
134
173
  else
135
- # REVISIT: Identify all the unbound Join Nodes, and do a nested-loops iteration checking all Steps
136
- puts "Can't yet process complex queries (involving #{join.all_join_node.map{|jn|jn.object_type.name}*', '})"
174
+ # REVISIT: Identify all the unbound Variables, and do a nested-loops iteration checking all Steps
175
+ puts "Can't yet process complex queries (involving #{
176
+ query.all_variable.map{|v| v.object_type.name}
177
+ }*', '})"
137
178
  end
179
+ =end
138
180
  end
139
181
 
140
182
  def process(statement)
@@ -148,15 +190,64 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
148
190
  end
149
191
  rescue => e
150
192
  puts e
151
- puts "\t"+e.backtrace*"\n\t" if ENV["DEBUG"] =~ /exception/
193
+ puts "\t"+e.backtrace*"\n\t" if trace :exception
152
194
  end
153
195
  end
154
196
 
197
+ def start_log
198
+ @constellation.loggers << proc{|*a| self.log(*a) }
199
+ @logged = []
200
+ end
201
+
202
+ def log *a
203
+ if @undoing
204
+ print 'UNDO: '; p a
205
+ else
206
+ @logged << a
207
+ end
208
+ end
209
+
210
+ def undo
211
+ puts '>'*30+' UNDO '+'<'*30
212
+ @undoing = true
213
+ @logged.reverse.each do |a|
214
+ case a[0]
215
+ when :assert
216
+ action, klass, identifying_role_values = *a
217
+ # puts "retracting #{klass.basename} #{identifying_role_values.inspect}"
218
+ object = @constellation.instances[klass][identifying_role_values]
219
+ object && object.retract
220
+
221
+ when :retract
222
+ action, klass, identifying_role_values = *a
223
+ # puts "reasserting #{klass.basename}(#{identifying_role_values.inspect})"
224
+ @constellation.assert(klass, identifying_role_values)
225
+
226
+ when :assign
227
+ action, klass, role, object_id, old, new = *a
228
+ # puts "de-assigning #{klass.basename}(#{object_id.inspect}).#{role.name} from #{new.inspect} back to #{old.inspect}"
229
+ object = @constellation.instances[klass][object_id]
230
+ object.send(role.setter, old)
231
+
232
+ else raise "Unexpected log action #{a[0]}"
233
+ end
234
+ end
235
+ @undoing = false
236
+ end
237
+
155
238
  def compile_definition ast
156
239
  # Accumulate the results:
157
240
  p ast if @show_tree
158
241
  puts highlight_tree(ast.tree) if @show_highlight
159
- result = super
242
+ begin
243
+ start_log
244
+ result = super
245
+ rescue => e
246
+ undo
247
+ raise
248
+ ensure
249
+ @constellation.loggers.pop
250
+ end
160
251
  @results += Array(result)
161
252
  result
162
253
  end
@@ -196,7 +287,7 @@ Meta-commands are:
196
287
  /root rule\t\tParse just a fragment of a CQL statement, matching syntax rule only
197
288
  /timings\t\t\tDisplay the elapsed time to execute each command
198
289
  /tree\t\t\tDisplay the abstract syntax tree from each statement parsed
199
- /trace key\t\tToggle debug tracing key, or list available keys
290
+ /trace key\t\tToggle tracing key, or list available keys
200
291
  }
201
292
  else
202
293
  words.each do |word|
@@ -218,14 +218,14 @@ Person was Driving,
218
218
  Driving was by one Person;
219
219
 
220
220
  Driving Charge is where
221
- Driving resulted in one Charge;
221
+ Driving resulted in at most one Charge;
222
222
  Driving Charge is a warning;
223
223
 
224
224
  Finance Institution is a kind of Company;
225
225
  Vehicle is subject to finance with at most one Finance Institution;
226
226
 
227
227
  Hospitalization is where
228
- Driving resulted in driver taken to one Hospital;
228
+ Driving resulted in driver taken to at most one Hospital;
229
229
  Hospitalization resulted in at most one blood-Test Result;
230
230
 
231
231
  Insured is a kind of Party;
@@ -173,9 +173,9 @@ Name is of at most one Unit,
173
173
  Unit is called one Name;
174
174
  Unit has at most one Coefficient;
175
175
  Unit has at most one Offset;
176
- Unit has plural-Name; // Avoid ambiguity; this is a new fact type
177
- Unit has at most one plural-Name,
178
- plural-Name is of at most one Unit;
176
+ Unit (as Plural Named Unit) has plural-Name; // Avoid ambiguity; this is a new fact type
177
+ Unit (as Plural Named Unit) has at most one plural-Name,
178
+ plural-Name is of at most one Plural Named Unit;
179
179
  Unit is fundamental;
180
180
 
181
181
  Value is identified by Literal and Value is literal string and Unit where
@@ -22,7 +22,7 @@ Student is enrolled in one School;
22
22
 
23
23
  Student Participation is where
24
24
  Student represents School in Activity,
25
- Student participates in Activity which is sanctioned by one School;
25
+ Student participates in Activity as representative of one School;
26
26
 
27
27
  /*
28
28
  * Constraints:
@@ -82,19 +82,20 @@ Transfer Request is from one Warehouse (as From Warehouse);
82
82
  Transfer Request is to one Warehouse (as To Warehouse);
83
83
  Warehouse contains at least one Bin;
84
84
 
85
+ Back Order Allocation is where
86
+ Purchase Order Item is allocated to Sales Order Item;
87
+ Back Order Allocation is for one Quantity;
88
+
85
89
  Customer is a kind of Party;
86
90
  Customer made Sales Order,
87
91
  Sales Order was made by one Customer;
88
92
 
89
- Direct Order Match is where
90
- Purchase Order Item matches Sales Order Item;
91
-
92
93
  /*
93
94
  * Constraints:
94
95
  */
95
96
  either Dispatch Item is for Transfer Request or Dispatch Item is for Sales Order Item but not both;
96
97
  either Received Item is for Purchase Order Item or Received Item is for Transfer Request but not both;
97
- Purchase Order Item matches Sales Order Item
98
+ Purchase Order Item is allocated to Sales Order Item
98
99
  only if Purchase Order Item is for Product that is in Sales Order Item;
99
100
  each Bin occurs at most one time in
100
101
  Warehouse contains Bin;
@@ -19,7 +19,7 @@ module ActiveFacts
19
19
  # require 'activefacts/cql'
20
20
  class CQLLoader
21
21
  def self.load(file) #:nodoc:
22
- debug "Loading #{file}" do
22
+ trace "Loading #{file}" do
23
23
  vocabulary = ActiveFacts::Input::CQL.readfile(file)
24
24
 
25
25
  ruby = StringIO.new
@@ -155,6 +155,7 @@ module ActiveFacts
155
155
  # "that" on the other hand means that this role player was *previously* designated using "some".
156
156
  # These distinctions are lost here
157
157
  / that s { def value; nil; end }
158
+ / which s { def value; nil; end }
158
159
  / one s { def value; [1, 1]; end }
159
160
  / no s { def value; [0, 0]; end }
160
161
  / exactly s quantity { def value; q = quantity.value; [q, q]; end }
@@ -264,7 +265,7 @@ module ActiveFacts
264
265
  rule in 'in' !alphanumeric end
265
266
  rule import 'import' !alphanumeric end
266
267
  rule independent 'independent' !alphanumeric end
267
- rule stronglyintransitive 'stronglyintransitive' !alphanumeric end
268
+ rule stronglyintransitive 'strongly' s 'intransitive' !alphanumeric end
268
269
  rule intransitive 'intransitive' !alphanumeric end
269
270
  rule irreflexive 'irreflexive' !alphanumeric end
270
271
  rule is 'is' !alphanumeric end
@@ -29,7 +29,7 @@ module ActiveFacts
29
29
  super *a
30
30
  @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
31
31
  @language = nil
32
- debug :file, "Parsing '#{@filename}'"
32
+ trace :file, "Parsing '#{@filename}'"
33
33
  end
34
34
 
35
35
  def compile_file filename
@@ -64,16 +64,16 @@ module ActiveFacts
64
64
  # The syntax tree created from each parsed CQL statement gets passed to the block.
65
65
  # parse_all returns an array of the block's non-nil return values.
66
66
  ok = parse_all(@string, :definition) do |node|
67
- debug :parse, "Parsed '#{node.text_value.gsub(/\s+/,' ').strip}'" do
67
+ trace :parse, "Parsed '#{node.text_value.gsub(/\s+/,' ').strip}'" do
68
68
  begin
69
69
  ast = node.ast
70
70
  next unless ast
71
- debug :ast, ast.inspect
71
+ trace :ast, ast.inspect
72
72
  ast.tree = node
73
73
  ast.constellation = @constellation
74
74
  ast.vocabulary = @vocabulary
75
75
  value = compile_definition ast
76
- debug :definition, "Compiled to #{value.is_a?(Array) ? value.map{|v| v.verbalise}*', ' : value.verbalise}" if value
76
+ trace :definition, "Compiled to #{value.is_a?(Array) ? value.map{|v| v.verbalise}*', ' : value.verbalise}" if value
77
77
  @vocabulary = value if ast.is_a?(Compiler::Vocabulary)
78
78
  rescue => e
79
79
  # Augment the exception message, but preserve the backtrace
@@ -122,8 +122,8 @@ module ActiveFacts
122
122
 
123
123
  def unit? s
124
124
  name = @constellation.Name[s]
125
- units = (!name ? [] : Array(name.unit) + Array(name.unit_as_plural_name)).uniq
126
- debug :units, "Looking for unit #{s}, got #{units.map{|u|u.name}.inspect}"
125
+ units = (!name ? [] : Array(name.unit) + Array(name.plural_named_unit)).uniq
126
+ trace :units, "Looking for unit #{s}, got #{units.map{|u|u.name}.inspect}"
127
127
  units.size > 0
128
128
  end
129
129
 
@@ -108,6 +108,8 @@ module ActiveFacts
108
108
  end
109
109
  end
110
110
 
111
+ # This method is used in matching unary fact types in entity identification
112
+ # It disregards literals, which are not allowed in this context.
111
113
  def phrases_match(phrases)
112
114
  @phrases.zip(phrases).each do |mine, theirs|
113
115
  return false if mine.is_a?(Reference) != theirs.is_a?(Reference)
@@ -135,7 +137,7 @@ module ActiveFacts
135
137
  changed_conjunction = (lcc = context.left_contraction_conjunction) && lcc != conjunction
136
138
  if context.left_contraction_allowed && (new_conjunction || changed_conjunction)
137
139
  # Conjunctions are that/who, where, comparison-operator, ','
138
- debug :matching, "A left contraction will be against #{self.inspect}, conjunction is #{conjunction.inspect}"
140
+ trace :matching, "A left contraction will be against #{self.inspect}, conjunction is #{conjunction.inspect}"
139
141
  context.left_contractable_clause = self
140
142
  left_contract_this_onto = nil # Can't left-contract this clause
141
143
  end
@@ -162,7 +164,7 @@ module ActiveFacts
162
164
  vrs.unshift contracted_role
163
165
  contracted_left = true
164
166
  phrases = [contracted_role]+phrases
165
- debug :matching, "Failed to match #{inspect}. Trying again using left contraction onto #{contraction_player.name}"
167
+ trace :matching, "Failed to match #{inspect}. Trying again using left contraction onto #{contraction_player.name}"
166
168
  end
167
169
 
168
170
  contract_right = proc do
@@ -176,7 +178,7 @@ module ActiveFacts
176
178
  contracted_role.bind(context)
177
179
  vrs.push contracted_role
178
180
  phrases = phrases+[contracted_role]
179
- debug :matching, "Failed to match #{inspect}. Trying again using right contraction onto #{contraction_player.name}"
181
+ trace :matching, "Failed to match #{inspect}. Trying again using right contraction onto #{contraction_player.name}"
180
182
  end
181
183
 
182
184
  begin
@@ -193,8 +195,8 @@ module ActiveFacts
193
195
 
194
196
  player_names = players.map{|p| p.name}
195
197
 
196
- debug :matching, "Looking for existing #{players.size}-ary fact types matching '#{inspect(phrases)}'" do
197
- debug :matching, "Players are '#{player_names.inspect}'"
198
+ trace :matching, "Looking for existing #{players.size}-ary fact types matching '#{inspect(phrases)}'" do
199
+ trace :matching, "Players are '#{player_names.inspect}'"
198
200
 
199
201
  # Match existing fact types in nested clauses first:
200
202
  # (not for contractions) REVISIT: Why not?
@@ -223,7 +225,7 @@ module ActiveFacts
223
225
  player.subtypes_transitive).uniq
224
226
  end
225
227
 
226
- debug :matching, "Players must match '#{player_related_types.map{|pa| pa.map{|p|p.name}}.inspect}'"
228
+ trace :matching, "Players must match '#{player_related_types.map{|pa| pa.map{|p|p.name}}.inspect}'"
227
229
 
228
230
  start_obj = player_related_types[0] || [left_contract_this_onto.refs[-1].player]
229
231
  # The candidate fact types have the right number of role players of related types.
@@ -238,7 +240,7 @@ module ActiveFacts
238
240
 
239
241
  compatible_readings = role.fact_type.compatible_readings(player_related_types)
240
242
  next unless compatible_readings.size > 0
241
- debug :matching_fails, "These readings are compatible: #{compatible_readings.map(&:expand).inspect}"
243
+ trace :matching_fails, "These readings are compatible: #{compatible_readings.map(&:expand).inspect}"
242
244
  true
243
245
  end.
244
246
  map{ |role| role.fact_type}
@@ -246,7 +248,7 @@ module ActiveFacts
246
248
 
247
249
  # If there is more than one possible exact match (same adjectives) with different subyping, the implicit query is ambiguous and is not allowed
248
250
 
249
- debug :matching, "Looking amongst #{candidate_fact_types.size} existing fact types for one matching #{left_insertion}'#{inspect}'#{right_insertion}" do
251
+ trace :matching, "Looking amongst #{candidate_fact_types.size} existing fact types for one matching #{left_insertion}'#{inspect}'#{right_insertion}" do
250
252
  matches = {}
251
253
  candidate_fact_types.map do |fact_type|
252
254
  fact_type.all_reading.map do |reading|
@@ -266,7 +268,7 @@ module ActiveFacts
266
268
  # Between equivalents, prefer the one without steps on the first role
267
269
  (m = matches[match]).cost*2 + ((!(e = m.role_side_effects[0]) || e.cost) == 0 ? 0 : 1)
268
270
  }
269
- debug :matching_fails, "Found #{matches.size} valid matches#{matches.size > 0 ? ', best is '+best_matches[0].expand : ''}"
271
+ trace :matching_fails, "Found #{matches.size} valid matches#{matches.size > 0 ? ', best is '+best_matches[0].expand : ''}"
270
272
 
271
273
  if matches.size > 1
272
274
  first = matches[best_matches[0]]
@@ -284,14 +286,14 @@ module ActiveFacts
284
286
  @reading = best_matches[0]
285
287
  @side_effects = matches[@reading]
286
288
  @fact_type = @side_effects.fact_type
287
- debug :matching, "Matched '#{@fact_type.default_reading}'"
289
+ trace :matching, "Matched '#{@fact_type.default_reading}'"
288
290
  @phrases = phrases
289
291
  apply_side_effects(context, @side_effects)
290
292
  return @fact_type
291
293
  end
292
294
 
293
295
  end
294
- debug :matching, "No fact type matched, candidates were '#{candidate_fact_types.map{|ft| ft.default_reading}*"', '"}'"
296
+ trace :matching, "No fact type matched, candidates were '#{candidate_fact_types.map{|ft| ft.default_reading}*"', '"}'"
295
297
  end
296
298
  if left_contract_this_onto
297
299
  if !contracted_left
@@ -341,21 +343,21 @@ module ActiveFacts
341
343
  # REVISIT: the verbaliser will need to know about a negated step to a free variable
342
344
  implicitly_negated = true if refs.detect{|ref| q = ref.quantifier and q.is_zero }
343
345
 
344
- debug :matching_fails, "Does '#{phrases.inspect}' match '#{reading.expand}'" do
346
+ trace :matching_fails, "Does '#{phrases.inspect}' match '#{reading.expand}'" do
345
347
  phrase_num = 0
346
348
  reading_parts = reading.text.split(/\s+/)
347
349
  reading_parts.each do |element|
348
350
  phrase = phrases[phrase_num]
349
351
  if phrase == 'not'
350
352
  raise "Stop playing games with your double negatives: #{phrases.inspect}" if implicitly_negated
351
- debug :matching, "Negation detected"
353
+ trace :matching, "Negation detected"
352
354
  implicitly_negated = true
353
355
  phrase = phrases[phrase_num += 1]
354
356
  end
355
357
  if element !~ /\{(\d+)\}/
356
358
  # Just a word; it must match
357
359
  unless phrase == element
358
- debug :matching_fails, "Mismatched ordinary word #{phrases[phrase_num].inspect} (wanted #{element})"
360
+ trace :matching_fails, "Mismatched ordinary word #{phrases[phrase_num].inspect} (wanted #{element})"
359
361
  return nil
360
362
  end
361
363
  phrase_num += 1
@@ -388,11 +390,11 @@ module ActiveFacts
388
390
  # This relies on the supertypes being in breadth-first order:
389
391
  common_supertype = (next_player.supertypes_transitive & player.supertypes_transitive)[0]
390
392
  if !common_supertype
391
- debug :matching_fails, "Reading discounted because next player #{player.name} doesn't match #{next_player.name}"
393
+ trace :matching_fails, "Reading discounted because next player #{player.name} doesn't match #{next_player.name}"
392
394
  return nil
393
395
  end
394
396
 
395
- debug :matching_fails, "Subtype step is required between #{player.name} and #{next_player_phrase.player.name} via common supertype #{common_supertype.name}"
397
+ trace :matching_fails, "Subtype step is required between #{player.name} and #{next_player_phrase.player.name} via common supertype #{common_supertype.name}"
396
398
  else
397
399
  if !next_player_phrase
398
400
  next # Contraction succeeded so far
@@ -460,7 +462,7 @@ module ActiveFacts
460
462
  if a = (!phrase.role_name.is_a?(Integer) && phrase.role_name) and
461
463
  e = role_ref.role.role_name and
462
464
  a != e
463
- debug :matching, "Role names #{e.inspect} for #{player.name} and #{a.inspect} for #{next_player_phrase.player.name} don't match"
465
+ trace :matching, "Role names #{e.inspect} for #{player.name} and #{a.inspect} for #{next_player_phrase.player.name} don't match"
464
466
  return nil
465
467
  end
466
468
  =end
@@ -469,7 +471,7 @@ module ActiveFacts
469
471
  if residual_adjectives && next_player_phrase.binding.refs.size == 1
470
472
  # This makes matching order-dependent, because there may be no "other purpose"
471
473
  # until another reading has been matched and the roles rebound.
472
- debug :matching_fails, "Residual adjectives have no other purpose, so this match fails"
474
+ trace :matching_fails, "Residual adjectives have no other purpose, so this match fails"
473
475
  return nil
474
476
  end
475
477
 
@@ -478,7 +480,7 @@ module ActiveFacts
478
480
  end
479
481
 
480
482
  if phrase_num != phrases.size || !intervening_words.empty?
481
- debug :matching_fails, "Extra words #{(intervening_words + phrases[phrase_num..-1]).inspect}"
483
+ trace :matching_fails, "Extra words #{(intervening_words + phrases[phrase_num..-1]).inspect}"
482
484
  return nil
483
485
  end
484
486
 
@@ -486,7 +488,7 @@ module ActiveFacts
486
488
  # There may be only one subtyping step on a TypeInheritance fact type.
487
489
  ti_steps = side_effects.select{|side_effect| side_effect.common_supertype}
488
490
  if ti_steps.size > 1 # Not allowed
489
- debug :matching_fails, "Can't have more than one subtyping step on a TypeInheritance fact type"
491
+ trace :matching_fails, "Can't have more than one subtyping step on a TypeInheritance fact type"
490
492
  return nil
491
493
  end
492
494
 
@@ -496,13 +498,13 @@ module ActiveFacts
496
498
  fact_type.subtype.supertypes_transitive :
497
499
  fact_type.supertype.subtypes_transitive
498
500
  if !allowed.include?(ti.common_supertype)
499
- debug :matching_fails, "Implicit subtyping step extends in the wrong direction"
501
+ trace :matching_fails, "Implicit subtyping step extends in the wrong direction"
500
502
  return nil
501
503
  end
502
504
  end
503
505
  end
504
506
 
505
- debug :matching, "Matched reading '#{reading.expand}' (with #{
507
+ trace :matching, "Matched reading '#{reading.expand}' (with #{
506
508
  side_effects.map{|side_effect|
507
509
  side_effect.absorbed_precursors+side_effect.absorbed_followers + (side_effect.common_supertype ? 1 : 0)
508
510
  }.inspect
@@ -518,12 +520,12 @@ module ActiveFacts
518
520
  @applied_side_effects = side_effects
519
521
  # Enact the side-effects of this match (delete the consumed adjectives):
520
522
  # Since this deletes words from the phrases, we do it in reverse order.
521
- debug :matching, "Apply side-effects" do
523
+ trace :matching, "Apply side-effects" do
522
524
  side_effects.apply_all do |side_effect|
523
525
  phrase = side_effect.phrase
524
526
 
525
527
  # We re-use the role_ref if possible (no extra adjectives were used, no rolename or step, etc).
526
- debug :matching, "side-effect means binding #{phrase.inspect} matches role ref #{side_effect.role_ref.role.object_type.name}"
528
+ trace :matching, "side-effect means binding #{phrase.inspect} matches role ref #{side_effect.role_ref.role.object_type.name}"
527
529
  phrase.role_ref = side_effect.role_ref
528
530
 
529
531
  changed = false
@@ -532,7 +534,7 @@ module ActiveFacts
532
534
  # the role_ref, those must be local, and we'll need to extract them.
533
535
 
534
536
  if rra = side_effect.role_ref.trailing_adjective
535
- debug :matching, "Deleting matched trailing adjective '#{rra}'#{side_effect.absorbed_followers>0 ? " in #{side_effect.absorbed_followers} followers" : ""}, cost is #{side_effect.cost}"
537
+ trace :matching, "Deleting matched trailing adjective '#{rra}'#{side_effect.absorbed_followers>0 ? " in #{side_effect.absorbed_followers} followers" : ""}, cost is #{side_effect.cost}"
536
538
  side_effect.cancel_cost side_effect.absorbed_followers
537
539
 
538
540
  # These adjective(s) matched either an adjective here, or a follower word, or both.
@@ -552,7 +554,7 @@ module ActiveFacts
552
554
  end
553
555
 
554
556
  if rra = side_effect.role_ref.leading_adjective
555
- debug :matching, "Deleting matched leading adjective '#{rra}'#{side_effect.absorbed_precursors>0 ? " in #{side_effect.absorbed_precursors} precursors" : ""}, cost is #{side_effect.cost}"
557
+ trace :matching, "Deleting matched leading adjective '#{rra}'#{side_effect.absorbed_precursors>0 ? " in #{side_effect.absorbed_precursors} precursors" : ""}, cost is #{side_effect.cost}"
556
558
  side_effect.cancel_cost side_effect.absorbed_precursors
557
559
 
558
560
  # These adjective(s) matched either an adjective here, or a precursor word, or both.
@@ -582,7 +584,7 @@ module ActiveFacts
582
584
  # Don't assign @fact_type; that will happen when the reading is added
583
585
  def make_fact_type vocabulary
584
586
  fact_type = vocabulary.constellation.FactType(:new)
585
- debug :matching, "Making new fact type for #{@phrases.inspect}" do
587
+ trace :matching, "Making new fact type for #{@phrases.inspect}" do
586
588
  @phrases.each do |phrase|
587
589
  next unless phrase.respond_to?(:player)
588
590
  phrase.role = vocabulary.constellation.Role(fact_type, fact_type.all_role.size, :object_type => phrase.player, :concept => :new)
@@ -598,7 +600,7 @@ module ActiveFacts
598
600
  @role_sequence = constellation.RoleSequence(:new)
599
601
  reading_words = @phrases.clone
600
602
  index = 0
601
- debug :matching, "Making new reading for #{@phrases.inspect}" do
603
+ trace :matching, "Making new reading for #{@phrases.inspect}" do
602
604
  reading_words.map! do |phrase|
603
605
  if phrase.respond_to?(:player)
604
606
  # phrase.role will be set if this reading was used to make_fact_type.
@@ -662,7 +664,7 @@ module ActiveFacts
662
664
  if phrase.role_name != phrase.role_ref.role.role_name ||
663
665
  phrase.leading_adjective ||
664
666
  phrase.trailing_adjective
665
- debug :matching, "phrase in matched clause has residual adjectives or role name, so needs a new role_sequence" if @fact_type.all_reading.size > 0
667
+ trace :matching, "phrase in matched clause has residual adjectives or role name, so needs a new role_sequence" if @fact_type.all_reading.size > 0
666
668
  new_role_sequence_needed = true
667
669
  end
668
670
  else
@@ -671,7 +673,7 @@ module ActiveFacts
671
673
  end
672
674
  end
673
675
 
674
- debug :matching, "Clause '#{reading_words*' '}' #{new_role_sequence_needed ? 'requires' : 'does not require'} a new Role Sequence"
676
+ trace :matching, "Clause '#{reading_words*' '}' #{new_role_sequence_needed ? 'requires' : 'does not require'} a new Role Sequence"
675
677
 
676
678
  constellation = @fact_type.constellation
677
679
  reading_text = reading_words*" "
@@ -693,15 +695,15 @@ module ActiveFacts
693
695
  extra_adjectives << "(as #{a})"
694
696
  end
695
697
  end
696
- debug :matching, "Making new role sequence for new reading #{reading_text} due to #{extra_adjectives.inspect}"
698
+ trace :matching, "Making new role sequence for new reading #{reading_text} due to #{extra_adjectives.inspect}"
697
699
  else
698
700
  # Use existing RoleSequence
699
701
  @role_sequence = role_phrases[0].role_ref.role_sequence
700
702
  if @role_sequence.all_reading.detect{|r| r.text == reading_text }
701
- debug :matching, "No need to re-create identical reading for #{reading_text}"
703
+ trace :matching, "No need to re-create identical reading for #{reading_text}"
702
704
  return @role_sequence
703
705
  else
704
- debug :matching, "Using existing role sequence for new reading '#{reading_text}'"
706
+ trace :matching, "Using existing role sequence for new reading '#{reading_text}'"
705
707
  end
706
708
  end
707
709
  if @fact_type.all_reading.
@@ -738,7 +740,7 @@ module ActiveFacts
738
740
  end
739
741
 
740
742
  # REVISIT: Check maybe and other qualifiers:
741
- debug :constraint, "Need to make constraints for #{@qualifiers.inspect}" if @qualifiers.size > 0 or @certainty != true
743
+ trace :constraint, "Need to make constraints for #{@qualifiers.inspect}" if @qualifiers.size > 0 or @certainty != true
742
744
  end
743
745
  end
744
746
 
@@ -762,7 +764,7 @@ module ActiveFacts
762
764
  @common_supertype = common_supertype
763
765
  @residual_adjectives = residual_adjectives
764
766
  @cancelled_cost = 0
765
- debug :matching_fails, "Saving side effects for #{@phrase.term}, absorbs #{@absorbed_precursors}/#{@absorbed_followers}#{@common_supertype ? ', step over supertype '+ @common_supertype.name : ''}" if @absorbed_precursors+@absorbed_followers+(@common_supertype ? 1 : 0) > 0
767
+ trace :matching_fails, "Saving side effects for #{@phrase.term}, absorbs #{@absorbed_precursors}/#{@absorbed_followers}#{@common_supertype ? ', step over supertype '+ @common_supertype.name : ''}" if @absorbed_precursors+@absorbed_followers+(@common_supertype ? 1 : 0) > 0
766
768
  end
767
769
 
768
770
  def cost
@@ -921,8 +923,9 @@ module ActiveFacts
921
923
  l = @leading_adjective
922
924
  t = @trailing_adjective
923
925
  key = [!l || l.empty? ? nil : l, @term, !t || t.empty? ? nil : t]
924
- key
925
926
  end
927
+ key += [:literal, literal.literal] if @literal
928
+ key
926
929
  end
927
930
 
928
931
  def bind context
@@ -940,14 +943,34 @@ module ActiveFacts
940
943
  role_name = @term
941
944
  end
942
945
  end
943
- @binding = (context.bindings[key] ||= Binding.new(@player, role_name))
944
- @binding.refs << self
946
+ k = key
947
+ @binding = context.bindings[k]
948
+ if !@binding
949
+ if !literal
950
+ # Find a binding that has a literal, and bind to it if it's the only one
951
+ candidates = context.bindings.map do |binding_key, binding|
952
+ binding_key[0...k.size] == k &&
953
+ binding_key[-2] == :literal ? binding : nil
954
+ end.compact
955
+ raise "Uncertain binding reference for #{to_s}, could be any of #{candidates.inspect}" if candidates.size > 1
956
+ @binding = candidates[0]
957
+ else
958
+ # New binding has a literal, look for one without:
959
+ @binding = context.bindings[k[0...-2]]
960
+ end
961
+ end
962
+
963
+ if !@binding
964
+ @binding = Binding.new(@player, role_name)
965
+ context.bindings[k] = @binding
966
+ end
967
+ @binding.add_ref self
945
968
  @binding
946
969
  end
947
970
 
948
971
  def unbind context
949
972
  # The key has changed.
950
- @binding.refs.delete(self)
973
+ @binding.delete_ref self
951
974
  if @binding.refs.empty?
952
975
  # Remove the binding from the context if this was the last reference
953
976
  context.bindings.delete_if {|k,v| v == @binding }
@@ -961,7 +984,7 @@ module ActiveFacts
961
984
  end
962
985
 
963
986
  def rebind_to(context, other_ref)
964
- debug :binding, "Rebinding #{inspect} to #{other_ref.inspect}"
987
+ trace :binding, "Rebinding #{inspect} to #{other_ref.inspect}"
965
988
 
966
989
  old_binding = binding # Remember to move all refs across
967
990
  unbind(context)
@@ -969,7 +992,7 @@ module ActiveFacts
969
992
  new_binding = other_ref.binding
970
993
  [self, *old_binding.refs].each do |ref|
971
994
  ref.binding = new_binding
972
- new_binding.refs << ref
995
+ new_binding.add_ref ref
973
996
  end
974
997
  old_binding.rebound_to = new_binding
975
998
  end
@@ -999,20 +1022,20 @@ module ActiveFacts
999
1022
  fact_type = @role_ref.role.fact_type
1000
1023
  constellation = vocabulary.constellation
1001
1024
 
1002
- debug :constraint, "Processing embedded constraint #{@quantifier.inspect} on #{@role_ref.role.object_type.name} in #{fact_type.describe}" do
1025
+ trace :constraint, "Processing embedded constraint #{@quantifier.inspect} on #{@role_ref.role.object_type.name} in #{fact_type.describe}" do
1003
1026
  # Preserve the role order of the clause, excluding this role:
1004
1027
  constrained_roles = (@clause.refs-[self]).map{|vr| vr.role_ref.role}
1005
1028
  if constrained_roles.empty?
1006
- debug :constraint, "Quantifier over unary role has no effect"
1029
+ trace :constraint, "Quantifier over unary role has no effect"
1007
1030
  return
1008
1031
  end
1009
1032
  constraint = find_pc_over_roles(constrained_roles)
1010
1033
  if constraint
1011
1034
  raise "Conflicting maximum frequency for constraint" if constraint.max_frequency && constraint.max_frequency != @quantifier.max
1012
- debug :constraint, "Setting max frequency to #{@quantifier.max} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}" unless constraint.max_frequency
1035
+ trace :constraint, "Setting max frequency to #{@quantifier.max} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}" unless constraint.max_frequency
1013
1036
  constraint.max_frequency = @quantifier.max
1014
1037
  raise "Conflicting minimum frequency for constraint" if constraint.min_frequency && constraint.min_frequency != @quantifier.min
1015
- debug :constraint, "Setting min frequency to #{@quantifier.min} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}" unless constraint.min_frequency
1038
+ trace :constraint, "Setting min frequency to #{@quantifier.min} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}" unless constraint.min_frequency
1016
1039
  constraint.min_frequency = @quantifier.min
1017
1040
  else
1018
1041
  role_sequence = constellation.RoleSequence(:new)
@@ -1027,7 +1050,7 @@ module ActiveFacts
1027
1050
  :max_frequency => @quantifier.max,
1028
1051
  :min_frequency => @quantifier.min
1029
1052
  )
1030
- debug :constraint, "Made new embedded PC GUID=#{constraint.concept.guid} min=#{@quantifier.min.inspect} max=#{@quantifier.max.inspect} over #{(e = fact_type.entity_type) ? e.name : role_sequence.describe} in #{fact_type.describe}"
1053
+ trace :constraint, "Made new embedded PC GUID=#{constraint.concept.guid} min=#{@quantifier.min.inspect} max=#{@quantifier.max.inspect} over #{(e = fact_type.entity_type) ? e.name : role_sequence.describe} in #{fact_type.describe}"
1031
1054
  @quantifier.enforcement.compile(constellation, constraint) if @quantifier.enforcement
1032
1055
  @embedded_presence_constraint = constraint
1033
1056
  end