activefacts-cql 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +19 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-cql.gemspec +29 -0
  10. data/bin/setup +7 -0
  11. data/lib/activefacts/cql.rb +7 -0
  12. data/lib/activefacts/cql/.gitignore +0 -0
  13. data/lib/activefacts/cql/Rakefile +14 -0
  14. data/lib/activefacts/cql/compiler.rb +156 -0
  15. data/lib/activefacts/cql/compiler/clause.rb +1137 -0
  16. data/lib/activefacts/cql/compiler/constraint.rb +581 -0
  17. data/lib/activefacts/cql/compiler/entity_type.rb +457 -0
  18. data/lib/activefacts/cql/compiler/expression.rb +443 -0
  19. data/lib/activefacts/cql/compiler/fact.rb +390 -0
  20. data/lib/activefacts/cql/compiler/fact_type.rb +421 -0
  21. data/lib/activefacts/cql/compiler/query.rb +106 -0
  22. data/lib/activefacts/cql/compiler/shared.rb +161 -0
  23. data/lib/activefacts/cql/compiler/value_type.rb +174 -0
  24. data/lib/activefacts/cql/parser.rb +234 -0
  25. data/lib/activefacts/cql/parser/CQLParser.treetop +167 -0
  26. data/lib/activefacts/cql/parser/Context.treetop +48 -0
  27. data/lib/activefacts/cql/parser/Expressions.treetop +67 -0
  28. data/lib/activefacts/cql/parser/FactTypes.treetop +358 -0
  29. data/lib/activefacts/cql/parser/Language/English.treetop +315 -0
  30. data/lib/activefacts/cql/parser/Language/French.treetop +315 -0
  31. data/lib/activefacts/cql/parser/Language/Mandarin.treetop +304 -0
  32. data/lib/activefacts/cql/parser/LexicalRules.treetop +253 -0
  33. data/lib/activefacts/cql/parser/ObjectTypes.treetop +210 -0
  34. data/lib/activefacts/cql/parser/Terms.treetop +183 -0
  35. data/lib/activefacts/cql/parser/ValueTypes.treetop +202 -0
  36. data/lib/activefacts/cql/parser/nodes.rb +49 -0
  37. data/lib/activefacts/cql/require.rb +36 -0
  38. data/lib/activefacts/cql/verbaliser.rb +804 -0
  39. data/lib/activefacts/cql/version.rb +5 -0
  40. data/lib/activefacts/input/cql.rb +43 -0
  41. data/lib/rubygems_plugin.rb +12 -0
  42. metadata +167 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf73dfa6319dcc1633fd864489bad8b9a3a206b6
4
+ data.tar.gz: 293bef927e2e02a2b136a8c23e5ade074a0efea8
5
+ SHA512:
6
+ metadata.gz: a0f0136f2c2c8bd84ddc0e00ca76a8f3bb00e10056e4fae8fd576cc105e89de64ee08a37077307ec876ab96bd41076565a032c3f53d88939feb1c5516ac9ab55
7
+ data.tar.gz: 8cac3ac7f967093b426cc026e9a09d691fdae0d262d80fbead60f1deda0e8ee944199f61a4b672ac7ecc09db113da2155504383852fed29c6c00f58d75f2700d
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
11
+ clj
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ before_install: gem install bundler -v 1.10.0.rc
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activefacts-cql.gemspec
4
+ gemspec
5
+
6
+ if ENV['PWD'] =~ %r{\A/Users/cjh/work/activefacts}
7
+ gem 'activefacts-api', path: '/Users/cjh/work/activefacts/api'
8
+ gem 'activefacts-metamodel', path: '/Users/cjh/work/activefacts/metamodel'
9
+ # gem 'activefacts-metamodel', git: 'git://github.com/cjheath/activefacts-metamodel.git'
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Clifford Heath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # ActiveFacts::CQL
2
+
3
+ The compiler and verbaliser for the Constellaton Query Language.
4
+ Part of the ActiveFacts fact modeling tool set.
5
+
6
+ ## Usage
7
+
8
+ CQL is normally used from the activefacts generator, afgen.
9
+
10
+ ## Development
11
+
12
+ ## Contributing
13
+
14
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cjheath/activefacts-cql.
15
+
16
+ ## License
17
+
18
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
19
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activefacts/cql/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activefacts-cql"
8
+ spec.version = ActiveFacts::CQL::VERSION
9
+ spec.authors = ["Clifford Heath"]
10
+ spec.email = ["clifford.heath@gmail.com"]
11
+
12
+ spec.summary = %q{Compiler for the Constellation Query Language}
13
+ spec.description = %q{Compiler for the Constellation Query Language, part of the ActiveFacts suite for Fact Modeling}
14
+ spec.homepage = "http://github.com/cjheath/activefacts-cql"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10.a"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+
26
+ spec.add_runtime_dependency(%q<activefacts-metamodel>, [">= 1.7.0", "~> 1.7"])
27
+ spec.add_runtime_dependency(%q<treetop>, [">= 1.4.14", "~> 1.4"])
28
+ end
29
+
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ #
2
+ # ActiveFacts Constellation Query Language compiler
3
+ #
4
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
5
+ #
6
+ require 'activefacts/cql/version'
7
+ require 'activefacts/cql/compiler'
File without changes
@@ -0,0 +1,14 @@
1
+ #
2
+ # ActiveFacts CQL Parser.
3
+ # A Rakefile to run Treetop when the ActiveFacts gem is installed.
4
+ # This isn't mandatory but makes it much faster to start the parser.
5
+ # Delete the generated files during parser development.
6
+ #
7
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
8
+ #
9
+ task :default do
10
+ pattern = File.dirname(__FILE__) + '**/*.treetop'
11
+ files = Dir[pattern]
12
+ # Hopefully this quoting will work where there are spaces in filenames, and even maybe on Windows?
13
+ exec "tt '#{files*"' '"}'"
14
+ end
@@ -0,0 +1,156 @@
1
+ # Compile a CQL string into an ActiveFacts vocabulary.
2
+ #
3
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ require 'activefacts/metamodel'
6
+ require 'activefacts/cql/parser'
7
+ require 'activefacts/cql/compiler/shared'
8
+ require 'activefacts/cql/compiler/value_type'
9
+ require 'activefacts/cql/compiler/entity_type'
10
+ require 'activefacts/cql/compiler/clause'
11
+ require 'activefacts/cql/compiler/fact_type'
12
+ require 'activefacts/cql/compiler/expression'
13
+ require 'activefacts/cql/compiler/fact'
14
+ require 'activefacts/cql/compiler/constraint'
15
+ require 'activefacts/cql/compiler/query'
16
+
17
+ module ActiveFacts
18
+ module CQL
19
+ class Compiler < ActiveFacts::CQL::Parser
20
+ LANGUAGES = {
21
+ 'en' => 'English',
22
+ 'fr' => 'French',
23
+ 'cn' => 'Mandarin'
24
+ }
25
+ attr_reader :vocabulary
26
+
27
+ def initialize *a
28
+ @filename = a.shift || "stdio"
29
+ super *a
30
+ @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
31
+ @language = nil
32
+ trace :file, "Parsing '#{@filename}'"
33
+ end
34
+
35
+ def compile_file filename
36
+ old_filename = @filename
37
+ @filename = filename
38
+ File.open(filename) do |f|
39
+ compile(f.read)
40
+ end
41
+ @filename = old_filename
42
+ @vocabulary
43
+ end
44
+
45
+ # Load the appropriate natural language module
46
+ def detect_language
47
+ @filename =~ /.*\.(..)\.cql$/i
48
+ language_code = $1
49
+ @language = LANGUAGES[language_code] || 'English'
50
+ end
51
+
52
+ def include_language
53
+ detect_language unless @langage
54
+ require 'activefacts/cql/parser/Language/'+@language
55
+ language_module = ActiveFacts::CQL.const_get(@language)
56
+ extend language_module
57
+ end
58
+
59
+ # Mark any new Concepts as belonging to this topic
60
+ def topic_flood
61
+ @constellation.Concept.each do |key, concept|
62
+ next if concept.topic
63
+ trace :topic, "Colouring #{concept.describe} with #{@topic.topic_name}"
64
+ concept.topic = @topic
65
+ end
66
+ end
67
+
68
+ def compile input
69
+ include_language
70
+
71
+ @string = input
72
+
73
+ # The syntax tree created from each parsed CQL statement gets passed to the block.
74
+ # parse_all returns an array of the block's non-nil return values.
75
+ ok = parse_all(@string, :definition) do |node|
76
+ trace :parse, "Parsed '#{node.text_value.gsub(/\s+/,' ').strip}'" do
77
+ trace :lex, (proc { node.inspect })
78
+ begin
79
+ ast = node.ast
80
+ next unless ast
81
+ trace :ast, ast.inspect
82
+ ast.tree = node
83
+ ast.constellation = @constellation
84
+ ast.vocabulary = @vocabulary
85
+ value = compile_definition ast
86
+ trace :definition, "Compiled to #{value.is_a?(Array) ? value.map{|v| v.verbalise}*', ' : value.verbalise}" if value
87
+ if value.is_a?(ActiveFacts::Metamodel::Topic)
88
+ topic_flood if @topic
89
+ @topic = value
90
+ elsif ast.is_a?(Compiler::Vocabulary)
91
+ topic_flood if @topic
92
+ @vocabulary = value
93
+ @topic = @constellation.Topic(@vocabulary.name)
94
+ end
95
+ rescue => e
96
+ # Augment the exception message, but preserve the backtrace
97
+ start_line = @string.line_of(node.interval.first)
98
+ end_line = @string.line_of(node.interval.last-1)
99
+ lines = start_line != end_line ? "s #{start_line}-#{end_line}" : " #{start_line.to_s}"
100
+ ne = StandardError.new("at line#{lines} #{e.message.strip}")
101
+ ne.set_backtrace(e.backtrace)
102
+ raise ne
103
+ end
104
+ end
105
+ topic_flood if @topic
106
+ end
107
+ raise failure_reason unless ok
108
+ vocabulary
109
+ end
110
+
111
+ def compile_import file, aliases
112
+ saved_index = @index
113
+ saved_block = @block
114
+ saved_string = @string
115
+ saved_input_length = @input_length
116
+ saved_topic = @topic
117
+ old_filename = @filename
118
+ @filename = File.dirname(old_filename)+'/'+file+'.cql'
119
+
120
+ # REVISIT: Save and use another @vocabulary for this file?
121
+ File.open(@filename) do |f|
122
+ topic_flood if @topic
123
+ @topic = @constellation.Topic(File.basename(@filename, '.cql'))
124
+ trace :import, "Importing #{@filename} as #{@topic.topic_name}" do
125
+ ok = parse_all(f.read, nil, &@block)
126
+ end
127
+ @topic = saved_topic
128
+ end
129
+
130
+ rescue => e
131
+ ne = StandardError.new("In #{@filename} #{e.message.strip}")
132
+ ne.set_backtrace(e.backtrace)
133
+ raise ne
134
+ ensure
135
+ @block = saved_block
136
+ @index = saved_index
137
+ @input_length = saved_input_length
138
+ @string = saved_string
139
+ @filename = old_filename
140
+ nil
141
+ end
142
+
143
+ def compile_definition ast
144
+ ast.compile
145
+ end
146
+
147
+ def unit? s
148
+ name = @constellation.Name[s]
149
+ units = (!name ? [] : Array(name.unit) + Array(name.plural_named_unit)).uniq
150
+ trace :units, "Looking for unit #{s}, got #{units.map{|u|u.name}.inspect}"
151
+ units.size > 0
152
+ end
153
+
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,1137 @@
1
+ module ActiveFacts
2
+ module CQL
3
+ class Compiler < ActiveFacts::CQL::Parser
4
+
5
+ class Clause
6
+ attr_reader :phrases
7
+ attr_accessor :qualifiers, :context_note
8
+ attr_accessor :certainty # nil, true, false -> maybe, definitely, not
9
+ attr_accessor :conjunction # one of {nil, 'and', ',', 'or', 'where'}
10
+ attr_accessor :fact_type
11
+ attr_reader :reading, :role_sequence # These are the Metamodel objects
12
+ attr_reader :side_effects # How to adjust the phrases if this fact_type match is accepted
13
+ attr_accessor :fact # When binding fact instances the fact goes here
14
+ attr_accessor :objectified_as # The Reference which objectified this fact type
15
+
16
+ def initialize phrases, qualifiers = [], context_note = nil
17
+ @phrases = phrases
18
+ refs.each { |ref| ref.clause = self }
19
+ @certainty = true
20
+ @qualifiers = qualifiers
21
+ @context_note = context_note
22
+ end
23
+
24
+ def refs
25
+ @phrases.select{|r| r.respond_to?(:player)}
26
+ end
27
+
28
+ # A clause that contains only the name of a ObjectType and no literal or reading text
29
+ # refers only to the existence of that ObjectType (as opposed to an instance of the object_type).
30
+ def is_existential_type
31
+ @phrases.size == 1 and
32
+ @phrases[0].is_a?(Reference) and
33
+ !@phrases[0].literal
34
+ end
35
+
36
+ def display
37
+ to_s
38
+ end
39
+
40
+ def inspect
41
+ to_s
42
+ end
43
+
44
+ def to_s phrases = nil
45
+ phrases ||= @phrases
46
+ "#{
47
+ @qualifiers && @qualifiers.size > 0 ? @qualifiers.sort.inspect+' ' : nil
48
+ }#{
49
+ case @certainty
50
+ when nil; 'maybe '
51
+ when false; 'negated '
52
+ # else 'definitely '
53
+ end
54
+ }#{
55
+ (
56
+ phrases.map do |phrase|
57
+ case phrase
58
+ when String
59
+ '"' + phrase.to_s + '"'
60
+ when Reference
61
+ phrase.to_s +
62
+ if phrase.nested_clauses
63
+ ' (in which ' +
64
+ phrase.nested_clauses.map do |c|
65
+ ((j = c.conjunction) ? j+' ' : '') +
66
+ c.to_s
67
+ end*' ' +
68
+ ')'
69
+ else
70
+ ''
71
+ end
72
+ when Operation
73
+ phrase.inspect
74
+ when Literal
75
+ phrase.inspect
76
+ #when FunctionCallChain # REVISIT: Add something here when I re-add functions
77
+ # phrase.inspect
78
+ else
79
+ raise "Unexpected phrase type in clause: #{phrase.class}"
80
+ end
81
+ end * ' '
82
+ ).gsub(/" "/, ' ')
83
+ }#{
84
+ @context_note && ' ' + @context_note.inspect
85
+ }"
86
+ end
87
+
88
+ def identify_players_with_role_name context
89
+ refs.each do |ref|
90
+ ref.identify_players_with_role_name(context)
91
+ end
92
+ end
93
+
94
+ def identify_other_players context
95
+ refs.each do |ref|
96
+ ref.identify_other_players(context)
97
+ # Include players in nested clauses, if any
98
+ ref.nested_clauses.each{|clause| clause.identify_other_players(context)} if ref.nested_clauses
99
+ end
100
+ end
101
+
102
+ def includes_literals
103
+ refs.detect{|ref| ref.literal || (ja = ref.nested_clauses and ja.detect{|jr| jr.includes_literals })}
104
+ end
105
+
106
+ def is_equality_comparison
107
+ false
108
+ end
109
+
110
+ def bind context
111
+ role_names = refs.map{ |ref| ref.role_name }.compact
112
+
113
+ # Check uniqueness of role names and subscripts within this clause:
114
+ role_names.each do |rn|
115
+ next if role_names.select{|rn2| rn2 == rn}.size == 1
116
+ raise "Duplicate role #{rn.is_a?(Integer) ? "subscript" : "name"} '#{rn}' in clause"
117
+ end
118
+
119
+ refs.each do |ref|
120
+ ref.bind context
121
+ end
122
+ end
123
+
124
+ # This method is used in matching unary fact types in entity identification
125
+ # It disregards literals, which are not allowed in this context.
126
+ def phrases_match(phrases)
127
+ @phrases.zip(phrases).each do |mine, theirs|
128
+ return false if mine.is_a?(Reference) != theirs.is_a?(Reference)
129
+ if mine.is_a?(Reference)
130
+ return false unless mine.key == theirs.key
131
+ else
132
+ return false unless mine == theirs
133
+ end
134
+ end
135
+ true
136
+ end
137
+
138
+ # This method chooses the existing fact type which matches most closely.
139
+ # It returns nil if there is none, or a ClauseMatchSideEffects object if matched.
140
+ #
141
+ # As this match may not necessarily be used (depending on the side effects),
142
+ # no change is made to this Clause object - those will be done later.
143
+ #
144
+ def match_existing_fact_type context, options = {}
145
+ raise "Cannot match a clause that contains no object types" if refs.size == 0
146
+ raise "Internal error, clause already matched, should not match again" if @fact_type
147
+
148
+ if is_naked_object_type
149
+ ref = refs[0] # "There can be only one"
150
+ return true unless ref.nested_clauses
151
+ ref.nested_clauses.each do |nested|
152
+ ft = nested.match_existing_fact_type(context)
153
+ raise "Unrecognised fact type #{nested.display} nested under #{inspect}" unless ft
154
+ if (ft.entity_type == ref.player)
155
+ ref.objectification_of = ft
156
+ nested.objectified_as = ref
157
+ end
158
+ end
159
+ raise "#{ref.inspect} contains objectification steps that do not objectify it" unless ref.objectification_of
160
+ return true
161
+ end
162
+
163
+ # If we fail to match, try a left contraction (or save this for a subsequent left contraction):
164
+ left_contract_this_onto = context.left_contractable_clause
165
+ new_conjunction = (conjunction == nil || conjunction == ',')
166
+ changed_conjunction = (lcc = context.left_contraction_conjunction) && lcc != conjunction
167
+ if context.left_contraction_allowed && (new_conjunction || changed_conjunction)
168
+ # Conjunctions are that/who, where, comparison-operator, ','
169
+ trace :matching, "A left contraction will be against #{self.inspect}, conjunction is #{conjunction.inspect}"
170
+ context.left_contractable_clause = self
171
+ left_contract_this_onto = nil # Can't left-contract this clause
172
+ end
173
+ context.left_contraction_conjunction = new_conjunction ? nil : @conjunction
174
+
175
+ phrases = @phrases
176
+ vrs = []+refs
177
+
178
+ # A left contraction is where the first player in the previous clause continues as first player of this clause
179
+ contracted_left = false
180
+ can_contract_right = false
181
+ left_insertion = nil
182
+ right_insertion = nil
183
+ supposed_roles = [] # Arrange to unbind incorrect references supposed due to contraction
184
+ contract_left = proc do
185
+ contracted_from = left_contract_this_onto.refs[0]
186
+ contraction_player = contracted_from.player
187
+ contracted_role = Reference.new(contraction_player.name)
188
+ supposed_roles << contracted_role
189
+ left_insertion = contracted_role.inspect+' '
190
+ contracted_role.player = contracted_from.player
191
+ contracted_role.role_name = contracted_from.role_name
192
+ contracted_role.bind(context)
193
+ vrs.unshift contracted_role
194
+ contracted_left = true
195
+ phrases = [contracted_role]+phrases
196
+ trace :matching, "Failed to match #{inspect}. Trying again using left contraction onto #{contraction_player.name}"
197
+ end
198
+
199
+ contract_right = proc do
200
+ contracted_from = left_contract_this_onto.refs[-1]
201
+ contraction_player = contracted_from.player
202
+ contracted_role = Reference.new(contraction_player.name)
203
+ supposed_roles << contracted_role
204
+ right_insertion = ' '+contracted_role.inspect
205
+ contracted_role.player = contracted_from.player
206
+ contracted_role.role_name = contracted_from.role_name
207
+ contracted_role.bind(context)
208
+ vrs.push contracted_role
209
+ phrases = phrases+[contracted_role]
210
+ trace :matching, "Failed to match #{inspect}. Trying again using right contraction onto #{contraction_player.name}"
211
+ end
212
+
213
+ begin
214
+ players = vrs.map{|vr| vr.player}
215
+
216
+ if players.size == 0
217
+ can_contract_right = left_contract_this_onto.refs.size == 2
218
+ contract_left.call
219
+ redo
220
+ end
221
+
222
+ raise "Must identify players before matching fact types" if players.include? nil
223
+ raise "A fact type must involve at least one object type, but there are none in '#{inspect}'" if players.size == 0 && !left_contract_this_onto
224
+
225
+ player_names = players.map{|p| p.name}
226
+
227
+ trace :matching, "Looking for existing #{players.size}-ary fact types matching '#{inspect}'" do
228
+ trace :matching, "Players are '#{player_names.inspect}'"
229
+
230
+ # Match existing fact types in nested clauses first:
231
+ # (not for contractions) REVISIT: Why not?
232
+ if !contracted_left
233
+ vrs.each do |ref|
234
+ next if ref.is_a?(Operation)
235
+ next unless steps = ref.nested_clauses and !steps.empty?
236
+ ref.nested_clauses.each do |nested|
237
+ ft = nested.match_existing_fact_type(context)
238
+ raise "Unrecognised fact type #{nested.display}" unless ft
239
+ if (ft && ft.entity_type == ref.player)
240
+ ref.objectification_of = ft
241
+ nested.objectified_as = ref
242
+ end
243
+ end
244
+ raise "#{ref.inspect} contains objectification steps that do not objectify it" unless ref.objectification_of
245
+ end
246
+ end
247
+
248
+ # For each role player, find the compatible types (the set of all subtypes and supertypes).
249
+ # For a player that's an objectification, we don't allow implicit supertype steps
250
+ player_related_types =
251
+ vrs.zip(players).map do |ref, player|
252
+ disallow_subtyping = ref && ref.objectification_of || options[:exact_type]
253
+ ((disallow_subtyping ? [] : player.supertypes_transitive) +
254
+ player.subtypes_transitive).uniq
255
+ end
256
+
257
+ trace :matching, "Players must match '#{player_related_types.map{|pa| pa.map{|p|p.name}}.inspect}'"
258
+
259
+ start_obj = player_related_types[0] || [left_contract_this_onto.refs[-1].player]
260
+ # The candidate fact types have the right number of role players of related types.
261
+ # If any role is played by a supertype or subtype of the required type, there's an implicit subtyping steps
262
+ # REVISIT: A double contraction results in player_related_types being empty here
263
+ candidate_fact_types =
264
+ start_obj.map do |related_type|
265
+ related_type.all_role.select do |role|
266
+ # next if role.fact_type.all_reading.size == 0
267
+ next if role.fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
268
+ next if role.fact_type.all_role.size != players.size # Wrong number of players
269
+
270
+ compatible_readings = role.fact_type.compatible_readings(player_related_types)
271
+ next unless compatible_readings.size > 0
272
+ trace :matching_fails, "These readings are compatible: #{compatible_readings.map(&:expand).inspect}"
273
+ true
274
+ end.
275
+ map{ |role| role.fact_type}
276
+ end.flatten.uniq
277
+
278
+ # If there is more than one possible exact match (same adjectives) with different subyping, the implicit query is ambiguous and is not allowed
279
+
280
+ trace :matching, "Looking amongst #{candidate_fact_types.size} existing fact types for one matching #{left_insertion}'#{inspect}'#{right_insertion}" do
281
+ matches = {}
282
+ candidate_fact_types.map do |fact_type|
283
+ fact_type.all_reading.map do |reading|
284
+ next unless side_effects = clause_matches(fact_type, reading, phrases)
285
+ matches[reading] = side_effects if side_effects
286
+ end
287
+ end
288
+
289
+ # REVISIT: Side effects that leave extra adjectives should only be allowed if the
290
+ # same extra adjectives exist in some other clause in the same declaration.
291
+ # The extra adjectives are then necessary to associate the two role players
292
+ # when consumed adjectives were required to bind to the underlying fact types.
293
+ # This requires the final decision on fact type matching to be postponed until
294
+ # the whole declaration has been processed and the extra adjectives can be matched.
295
+
296
+ best_matches = matches.keys.sort_by{|match|
297
+ # Between equivalents, prefer the one without steps on the first role
298
+ (m = matches[match]).cost*2 + ((!(e = m.role_side_effects[0]) || e.cost) == 0 ? 0 : 1)
299
+ }
300
+ trace :matching_fails, "Found #{matches.size} valid matches#{matches.size > 0 ? ', best is '+best_matches[0].expand : ''}"
301
+
302
+ if matches.size > 1
303
+ first = matches[best_matches[0]]
304
+ cost = first.cost
305
+ equal_best = matches.select{|k,m| m.cost == cost}
306
+
307
+ if equal_best.size > 1 and equal_best.detect{|k,m| !m.fact_type.is_a?(Metamodel::TypeInheritance)}
308
+ # Complain if there's more than one equivalent cost match (unless all are TypeInheritance):
309
+ raise "#{@phrases.inspect} could match any of the following:\n\t"+
310
+ best_matches.map { |reading| reading.expand + " with " + matches[reading].describe } * "\n\t"
311
+ end
312
+ end
313
+
314
+ if matches.size >= 1
315
+ @reading = best_matches[0]
316
+ @side_effects = matches[@reading]
317
+ @fact_type = @side_effects.fact_type
318
+ trace :matching, "Matched '#{@fact_type.default_reading}'"
319
+ @phrases = phrases
320
+ apply_side_effects(context, @side_effects)
321
+ return @fact_type
322
+ end
323
+
324
+ end
325
+ trace :matching, "No fact type matched, candidates were '#{candidate_fact_types.map{|ft| ft.default_reading}*"', '"}'"
326
+ end
327
+ if left_contract_this_onto
328
+ if !contracted_left
329
+ contract_left.call
330
+ redo
331
+ elsif can_contract_right
332
+ contract_right.call
333
+ can_contract_right = false
334
+ redo
335
+ end
336
+ end
337
+ end until true # Once through, unless we hit a redo
338
+ supposed_roles.each do |role|
339
+ role.unbind context
340
+ end
341
+ @fact_type = nil
342
+ end
343
+
344
+ # The Reading passed has the same players as this Clause. Does it match?
345
+ # Twisty curves. This is a complex bit of code!
346
+ # Find whether the phrases of this clause match the fact type reading,
347
+ # which may require absorbing unmarked adjectives.
348
+ #
349
+ # If it does match, make the required changes and set @ref to the matching role ref.
350
+ # Adjectives that were used to match are removed (and leaving any additional adjectives intact).
351
+ #
352
+ # Approach:
353
+ # Match each element where element means:
354
+ # a role player phrase (perhaps with adjectives)
355
+ # Our phrase must either be
356
+ # a player that contains the same adjectives as in the reading.
357
+ # a word (unmarked leading adjective) that introduces a sequence
358
+ # of adjectives leading up to a matching player
359
+ # trailing adjectives, both marked and unmarked, are absorbed too.
360
+ # a word that matches the reading's
361
+ #
362
+ def clause_matches(fact_type, reading, phrases = @phrases)
363
+ implicitly_negated = false
364
+ side_effects = [] # An array of items for each role, describing any side-effects of the match.
365
+ intervening_words = nil
366
+ residual_adjectives = false
367
+
368
+ # The following form of negation is, e.g., where "Person was invited to no Party",
369
+ # as opposed to where "Person was not invited to that Party". Quite different meaning,
370
+ # because a free Party variable is required, but the join step is still disallowed.
371
+ # REVISIT: I'll create the free variable when I implement some/that binding
372
+ # REVISIT: the verbaliser will need to know about a negated step to a free variable
373
+ implicitly_negated = true if refs.detect{|ref| q = ref.quantifier and q.is_zero }
374
+
375
+ trace :matching_fails, "Does '#{phrases.inspect}' match '#{reading.expand}'" do
376
+ phrase_num = 0
377
+ reading_parts = reading.text.split(/\s+/)
378
+ reading_parts.each do |element|
379
+ phrase = phrases[phrase_num]
380
+ if phrase == 'not'
381
+ raise "Stop playing games with your double negatives: #{phrases.inspect}" if implicitly_negated
382
+ trace :matching, "Negation detected"
383
+ implicitly_negated = true
384
+ phrase = phrases[phrase_num += 1]
385
+ end
386
+ if element !~ /\{(\d+)\}/
387
+ # Just a word; it must match
388
+ unless phrase == element
389
+ trace :matching_fails, "Mismatched ordinary word #{phrases[phrase_num].inspect} (wanted #{element})"
390
+ return nil
391
+ end
392
+ phrase_num += 1
393
+ next
394
+ else
395
+ role_ref = reading.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}[$1.to_i]
396
+ end
397
+
398
+ player = role_ref.role.object_type
399
+
400
+ # Figure out what's next in this phrase (the next player and the words leading up to it)
401
+ next_player_phrase = nil
402
+ intervening_words = []
403
+ while (phrase = phrases[phrase_num])
404
+ phrase_num += 1
405
+ if phrase.respond_to?(:player)
406
+ next_player_phrase = phrase
407
+ next_player_phrase_num = phrase_num-1
408
+ break
409
+ else
410
+ intervening_words << phrase
411
+ end
412
+ end
413
+ return nil unless next_player_phrase # reading has more players than we do.
414
+ next_player = next_player_phrase.player
415
+
416
+ # The next player must match:
417
+ common_supertype = nil
418
+ if next_player != player
419
+ # This relies on the supertypes being in breadth-first order:
420
+ common_supertype = (next_player.supertypes_transitive & player.supertypes_transitive)[0]
421
+ if !common_supertype
422
+ trace :matching_fails, "Reading discounted because next player #{player.name} doesn't match #{next_player.name}"
423
+ return nil
424
+ end
425
+
426
+ trace :matching_fails, "Subtype step is required between #{player.name} and #{next_player_phrase.player.name} via common supertype #{common_supertype.name}"
427
+ else
428
+ if !next_player_phrase
429
+ next # Contraction succeeded so far
430
+ end
431
+ end
432
+
433
+ # It's the right player. Do the adjectives match? This must include the intervening_words, if any.
434
+
435
+ role_has_residual_adjectives = false
436
+ absorbed_precursors = 0
437
+ if la = role_ref.leading_adjective and !la.empty?
438
+ # The leading adjectives must match, one way or another
439
+ la = la.split(/\s+/)
440
+ if (la[0, intervening_words.size] == intervening_words) # Exact match
441
+ iw = intervening_words
442
+ else
443
+ # We may have hyphenated adjectives. Break them up to check:
444
+ iw = intervening_words.map{|w| w.split(/-/)}.flatten
445
+ return nil unless la[0,iw.size] == iw
446
+ end
447
+
448
+ # Any intervening_words matched, see what remains
449
+ la.slice!(0, iw.size)
450
+
451
+ # If there were intervening_words, the remaining reading adjectives must match the phrase's leading_adjective exactly.
452
+ phrase_la = (next_player_phrase.leading_adjective||'').split(/\s+/)
453
+ return nil if !iw.empty? && la != phrase_la
454
+ # If not, the phrase's leading_adjectives must *end* with the reading's
455
+ return nil if phrase_la[-la.size..-1] != la
456
+ role_has_residual_adjectives = true if phrase_la.size > la.size
457
+ # The leading adjectives and the player matched! Check the trailing adjectives.
458
+ absorbed_precursors = intervening_words.size
459
+ intervening_words = []
460
+ elsif intervening_words.size > 0 || next_player_phrase.leading_adjective
461
+ role_has_residual_adjectives = true
462
+ end
463
+
464
+ absorbed_followers = 0
465
+ if ta = role_ref.trailing_adjective and !ta.empty?
466
+ ta = ta.split(/\s+/) # These are the trailing adjectives to match
467
+
468
+ phrase_ta = (next_player_phrase.trailing_adjective||'').split(/\s+/)
469
+ i = 0 # Pad the phrases up to the size of the trailing_adjectives
470
+ while phrase_ta.size < ta.size
471
+ break unless (word = phrases[phrase_num+i]).is_a?(String)
472
+ phrase_ta << word
473
+ i += 1
474
+ end
475
+ # ta is the adjectives in the fact type being matched
476
+ # phrase_ta is the explicit adjectives augmented with implicit ones to the same size
477
+ return nil if ta != phrase_ta[0,ta.size]
478
+ role_has_residual_adjectives = true if phrase_ta.size > ta.size
479
+ absorbed_followers = i
480
+ phrase_num += i # Skip following words that were consumed as trailing adjectives
481
+ elsif next_player_phrase.trailing_adjective
482
+ role_has_residual_adjectives = true
483
+ end
484
+
485
+ # REVISIT: I'm not even sure I should be caring about role names here.
486
+ # Role names are on roles, and are only useful within the fact type definition.
487
+ # At some point, we need to worry about role names on clauses within fact type derivations,
488
+ # which means they'll move to the Role Ref class; but even then they only match within the
489
+ # definition that creates that Role Ref.
490
+ =begin
491
+ if a = (!phrase.role_name.is_a?(Integer) && phrase.role_name) and
492
+ e = role_ref.role.role_name and
493
+ a != e
494
+ trace :matching, "Role names #{e.inspect} for #{player.name} and #{a.inspect} for #{next_player_phrase.player.name} don't match"
495
+ return nil
496
+ end
497
+ =end
498
+
499
+ residual_adjectives ||= role_has_residual_adjectives
500
+ if residual_adjectives && next_player_phrase.binding.refs.size == 1
501
+ # This makes matching order-dependent, because there may be no "other purpose"
502
+ # until another reading has been matched and the roles rebound.
503
+ trace :matching_fails, "Residual adjectives have no other purpose, so this match fails"
504
+ return nil
505
+ end
506
+
507
+ # The phrases matched this reading's next role_ref, save data to apply the side-effects:
508
+ side_effects << ClauseMatchSideEffect.new(next_player_phrase, role_ref, next_player_phrase_num, absorbed_precursors, absorbed_followers, common_supertype, role_has_residual_adjectives)
509
+ end
510
+
511
+ if phrase_num != phrases.size || !intervening_words.empty?
512
+ trace :matching_fails, "Extra words #{(intervening_words + phrases[phrase_num..-1]).inspect}"
513
+ return nil
514
+ end
515
+
516
+ if fact_type.is_a?(Metamodel::TypeInheritance)
517
+ # There may be only one subtyping step on a TypeInheritance fact type.
518
+ ti_steps = side_effects.select{|side_effect| side_effect.common_supertype}
519
+ if ti_steps.size > 1 # Not allowed
520
+ trace :matching_fails, "Can't have more than one subtyping step on a TypeInheritance fact type"
521
+ return nil
522
+ end
523
+
524
+ if ti = ti_steps[0]
525
+ # The Type Inheritance step must continue in the same direction as this reading.
526
+ allowed = fact_type.supertype == ti.role_ref.role.object_type ?
527
+ fact_type.subtype.supertypes_transitive :
528
+ fact_type.supertype.subtypes_transitive
529
+ if !allowed.include?(ti.common_supertype)
530
+ trace :matching_fails, "Implicit subtyping step extends in the wrong direction"
531
+ return nil
532
+ end
533
+ end
534
+ end
535
+
536
+ trace :matching, "Matched reading '#{reading.expand}' (with #{
537
+ side_effects.map{|side_effect|
538
+ side_effect.absorbed_precursors+side_effect.absorbed_followers + (side_effect.common_supertype ? 1 : 0)
539
+ }.inspect
540
+ } side effects)#{residual_adjectives ? ' and residual adjectives' : ''}"
541
+ end
542
+ # There will be one side_effects for each role player
543
+ @certainty = !@certainty if implicitly_negated
544
+ @certainty = !@certainty if reading.is_negative
545
+ ClauseMatchSideEffects.new(fact_type, self, residual_adjectives, side_effects, implicitly_negated)
546
+ end
547
+
548
+ def apply_side_effects(context, side_effects)
549
+ @applied_side_effects = side_effects
550
+ # Enact the side-effects of this match (delete the consumed adjectives):
551
+ # Since this deletes words from the phrases, we do it in reverse order.
552
+ trace :matching, "Apply side-effects" do
553
+ side_effects.apply_all do |side_effect|
554
+ phrase = side_effect.phrase
555
+
556
+ # We re-use the role_ref if possible (no extra adjectives were used, no rolename or step, etc).
557
+ trace :matching, "side-effect means binding #{phrase.inspect} matches role ref #{side_effect.role_ref.role.object_type.name}"
558
+ phrase.role_ref = side_effect.role_ref
559
+
560
+ changed = false
561
+
562
+ # Where this phrase has leading or trailing adjectives that are in excess of those of
563
+ # the role_ref, those must be local, and we'll need to extract them.
564
+
565
+ if rra = side_effect.role_ref.trailing_adjective
566
+ trace :matching, "Deleting matched trailing adjective '#{rra}'#{side_effect.absorbed_followers>0 ? " in #{side_effect.absorbed_followers} followers" : ""}, cost is #{side_effect.cost}"
567
+ side_effect.cancel_cost side_effect.absorbed_followers
568
+
569
+ # These adjective(s) matched either an adjective here, or a follower word, or both.
570
+ if a = phrase.trailing_adjective
571
+ if a.size >= rra.size
572
+ a = a[rra.size+1..-1]
573
+ phrase.trailing_adjective = a == '' ? nil : a
574
+ changed = true
575
+ end
576
+ elsif side_effect.absorbed_followers > 0
577
+ # The following statement is incorrect. The absorbed adjective is what caused the match.
578
+ # This phrase is absorbing non-hyphenated adjective(s), which changes its binding
579
+ # phrase.trailing_adjective =
580
+ @phrases.slice!(side_effect.num+1, side_effect.absorbed_followers)*' '
581
+ changed = true
582
+ end
583
+ end
584
+
585
+ if rra = side_effect.role_ref.leading_adjective
586
+ trace :matching, "Deleting matched leading adjective '#{rra}'#{side_effect.absorbed_precursors>0 ? " in #{side_effect.absorbed_precursors} precursors" : ""}, cost is #{side_effect.cost}"
587
+ side_effect.cancel_cost side_effect.absorbed_precursors
588
+
589
+ # These adjective(s) matched either an adjective here, or a precursor word, or both.
590
+ if a = phrase.leading_adjective
591
+ if a.size >= rra.size
592
+ a = a[0...-rra.size]
593
+ phrase.leading_adjective = a == '' ? nil : a
594
+ changed = true
595
+ end
596
+ elsif side_effect.absorbed_precursors > 0
597
+ # The following statement is incorrect. The absorbed adjective is what caused the match.
598
+ # This phrase is absorbing non-hyphenated adjective(s), which changes its binding
599
+ #phrase.leading_adjective =
600
+ @phrases.slice!(side_effect.num-side_effect.absorbed_precursors, side_effect.absorbed_precursors)*' '
601
+ changed = true
602
+ end
603
+ end
604
+ if changed
605
+ phrase.rebind context
606
+ end
607
+
608
+ end
609
+ end
610
+ end
611
+
612
+ # Make a new fact type with roles for this clause.
613
+ # Don't assign @fact_type; that will happen when the reading is added
614
+ def make_fact_type vocabulary
615
+ fact_type = vocabulary.constellation.FactType(:new)
616
+ trace :matching, "Making new fact type for #{@phrases.inspect}" do
617
+ @phrases.each do |phrase|
618
+ next unless phrase.respond_to?(:player)
619
+ phrase.role = vocabulary.constellation.Role(fact_type, fact_type.all_role.size, :object_type => phrase.player, :concept => :new)
620
+ phrase.role.role_name = phrase.role_name if phrase.role_name && phrase.role_name.is_a?(String)
621
+ end
622
+ end
623
+ fact_type
624
+ end
625
+
626
+ def make_reading vocabulary, fact_type
627
+ @fact_type = fact_type
628
+ constellation = vocabulary.constellation
629
+ @role_sequence = constellation.RoleSequence(:new)
630
+ reading_words = @phrases.clone
631
+ index = 0
632
+ trace :matching, "Making new reading for #{@phrases.inspect}" do
633
+ reading_words.map! do |phrase|
634
+ if phrase.respond_to?(:player)
635
+ # phrase.role will be set if this reading was used to make_fact_type.
636
+ # Otherwise we have to find the existing role via the Binding. This is pretty ugly.
637
+ unless phrase.role
638
+ # Find another binding for this phrase which already has a role_ref to the same fact type:
639
+ ref = phrase.binding.refs.detect{|ref| ref.role_ref && ref.role_ref.role.fact_type == fact_type}
640
+ role_ref = ref.role_ref
641
+ phrase.role = role_ref.role
642
+ end
643
+ rr = constellation.RoleRef(@role_sequence, index, :role => phrase.role)
644
+ phrase.role_ref = rr
645
+
646
+ if la = phrase.leading_adjective
647
+ # If we have used one or more adjective to match an existing reading, that has already been removed.
648
+ rr.leading_adjective = la
649
+ end
650
+ if ta = phrase.trailing_adjective
651
+ rr.trailing_adjective = ta
652
+ end
653
+
654
+ if phrase.value_constraint
655
+ raise "The role #{rr.inspect} already has a value constraint" if rr.role.role_value_constraint
656
+ phrase.value_constraint.constellation = fact_type.constellation
657
+ rr.role.role_value_constraint = phrase.value_constraint.compile
658
+ end
659
+
660
+ index += 1
661
+ "{#{index-1}}"
662
+ else
663
+ phrase
664
+ end
665
+ end
666
+ if existing = @fact_type.all_reading.detect{|r|
667
+ r.text == reading_words*' ' and
668
+ r.role_sequence.all_role_ref_in_order.map{|rr| rr.role.object_type} ==
669
+ role_sequence.all_role_ref_in_order.map{|rr| rr.role.object_type}
670
+ }
671
+ existing
672
+ #raise "Reading '#{existing.expand}' already exists, so why are we creating a duplicate?"
673
+ end
674
+ r = constellation.Reading(@fact_type, @fact_type.all_reading.size, :role_sequence => @role_sequence, :text => reading_words*" ", :is_negative => (certainty == false))
675
+ r
676
+ end
677
+ end
678
+
679
+ # When we match an existing reading, we might have matched using additional adjectives.
680
+ # These adjectives have been removed from the phrases. If there are any remaining adjectives,
681
+ # we need to make a new RoleSequence, otherwise we can use the existing one.
682
+ def adjust_for_match
683
+ return unless @applied_side_effects
684
+ new_role_sequence_needed = @applied_side_effects.residual_adjectives
685
+
686
+ role_phrases = []
687
+ reading_words = []
688
+ new_role_sequence_needed = false
689
+ @phrases.each do |phrase|
690
+ if phrase.respond_to?(:player)
691
+ role_phrases << phrase
692
+ reading_words << "{#{phrase.role_ref.ordinal}}"
693
+ if phrase.role_name != phrase.role_ref.role.role_name ||
694
+ phrase.leading_adjective ||
695
+ phrase.trailing_adjective
696
+ 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
697
+ new_role_sequence_needed = true
698
+ end
699
+ else
700
+ reading_words << phrase
701
+ false
702
+ end
703
+ end
704
+
705
+ trace :matching, "Clause '#{reading_words*' '}' #{new_role_sequence_needed ? 'requires' : 'does not require'} a new Role Sequence"
706
+
707
+ constellation = @fact_type.constellation
708
+ reading_text = reading_words*" "
709
+ if new_role_sequence_needed
710
+ @role_sequence = constellation.RoleSequence(:new)
711
+ extra_adjectives = []
712
+ role_phrases.each_with_index do |rp, i|
713
+ role_ref = constellation.RoleRef(@role_sequence, i, :role => rp.role_ref.role)
714
+ if a = rp.leading_adjective
715
+ role_ref.leading_adjective = a
716
+ extra_adjectives << a+"-"
717
+ end
718
+ if a = rp.trailing_adjective
719
+ role_ref.trailing_adjective = a
720
+ extra_adjectives << "-"+a
721
+ end
722
+ if (a = rp.role_name) && (e = rp.role_ref.role.role_name) && a != e
723
+ raise "Can't create new reading '#{reading_text}' for '#{reading.expand}' with alternate role name #{a}"
724
+ extra_adjectives << "(as #{a})"
725
+ end
726
+ end
727
+ trace :matching, "Making new role sequence for new reading #{reading_text} due to #{extra_adjectives.inspect}"
728
+ else
729
+ # Use existing RoleSequence
730
+ @role_sequence = role_phrases[0].role_ref.role_sequence
731
+ if @role_sequence.all_reading.detect{|r| r.text == reading_text }
732
+ trace :matching, "No need to re-create identical reading for #{reading_text}"
733
+ return @role_sequence
734
+ else
735
+ trace :matching, "Using existing role sequence for new reading '#{reading_text}'"
736
+ end
737
+ end
738
+ if @fact_type.all_reading.
739
+ detect do |r|
740
+ r.text == reading_text and
741
+ r.role_sequence.all_role_ref_in_order.map{|rr| rr.role.object_type} ==
742
+ @role_sequence.all_role_ref_in_order.map{|rr| rr.role.object_type}
743
+ end
744
+ # raise "Reading '#{@reading.expand}' already exists, so why are we creating a duplicate (with #{extra_adjectives.inspect})?"
745
+ else
746
+ constellation.Reading(@fact_type, @fact_type.all_reading.size, :role_sequence => @role_sequence, :text => reading_text, :is_negative => (certainty == false))
747
+ end
748
+ @role_sequence
749
+ end
750
+
751
+ def make_embedded_constraints vocabulary
752
+ refs.each do |ref|
753
+ next unless ref.quantifier
754
+ # puts "Quantifier #{ref.inspect} not implemented as a presence constraint"
755
+ ref.make_embedded_presence_constraint vocabulary
756
+ end
757
+
758
+ if @qualifiers && @qualifiers.size > 0
759
+ # We shouldn't make a new ring constraint if there's already one over this ring.
760
+ existing_rcs =
761
+ @role_sequence.all_role_ref.map{|rr| rr.role.all_ring_constraint.to_a }.flatten.uniq
762
+ unless existing_rcs[0]
763
+ rc = RingConstraint.new(@role_sequence, @qualifiers)
764
+ rc.vocabulary = vocabulary
765
+ rc.constellation = vocabulary.constellation
766
+ rc.compile
767
+ else
768
+ # Ignore the fact that the ring might be of a different type.
769
+ end
770
+
771
+ # REVISIT: Check maybe and other qualifiers:
772
+ trace :constraint, "Need to make constraints for #{@qualifiers.inspect}" if @qualifiers.size > 0 or @certainty != true
773
+ end
774
+ end
775
+
776
+ def is_naked_object_type
777
+ @phrases.size == 1 && refs.size == 1
778
+ end
779
+
780
+ end
781
+
782
+ # An instance of ClauseMatchSideEffects is created when the compiler matches an existing fact type.
783
+ # It captures the details that have to be adjusted for the match to be regarded a success.
784
+ class ClauseMatchSideEffect
785
+ attr_reader :phrase, :role_ref, :num, :absorbed_precursors, :absorbed_followers, :common_supertype, :residual_adjectives
786
+
787
+ def initialize phrase, role_ref, num, absorbed_precursors, absorbed_followers, common_supertype, residual_adjectives
788
+ @phrase = phrase
789
+ @role_ref = role_ref
790
+ @num = num
791
+ @absorbed_precursors = absorbed_precursors
792
+ @absorbed_followers = absorbed_followers
793
+ @common_supertype = common_supertype
794
+ @residual_adjectives = residual_adjectives
795
+ @cancelled_cost = 0
796
+ 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
797
+ end
798
+
799
+ def cost
800
+ absorbed_precursors + absorbed_followers + (common_supertype ? 1 : 0) - @cancelled_cost
801
+ end
802
+
803
+ def cancel_cost c
804
+ @cancelled_cost += c
805
+ end
806
+
807
+ def to_s
808
+ "#{@phrase.inspect} absorbs #{@absorbed_precursors||0}/#{@absorbed_followers||0} at #{@num}#{@common_supertype && ' super '+@common_supertype.name}#{@residual_adjectives ? ' with residual adjectives' : ''}"
809
+ end
810
+ end
811
+
812
+ class ClauseMatchSideEffects
813
+ attr_reader :residual_adjectives
814
+ attr_reader :fact_type
815
+ attr_reader :role_side_effects # One array of values per Reference matched, in order
816
+ attr_reader :negated
817
+ attr_reader :optional
818
+
819
+ def initialize fact_type, clause, residual_adjectives, role_side_effects, negated = false
820
+ @fact_type = fact_type
821
+ @clause = clause
822
+ @residual_adjectives = residual_adjectives
823
+ @role_side_effects = role_side_effects
824
+ @negated = negated
825
+ end
826
+
827
+ def inspect
828
+ 'side-effects are [' +
829
+ @role_side_effects.map{|r| r.to_s}*', ' +
830
+ ']' +
831
+ "#{@negated ? ' negated' : ''}" +
832
+ "#{@residual_adjectives ? ' with residual adjectives' : ''}"
833
+ end
834
+
835
+ def apply_all &b
836
+ @role_side_effects.reverse.each{ |role_side_effect| b.call(*role_side_effect) }
837
+ end
838
+
839
+ def cost
840
+ c = 0
841
+ @role_side_effects.each do |side_effect|
842
+ c += side_effect.cost
843
+ end
844
+ c += 1 if @residual_adjectives
845
+ c += 2 if @negated
846
+ c
847
+ end
848
+
849
+ def describe
850
+ actual_effects =
851
+ @role_side_effects.map do |side_effect|
852
+ ( [side_effect.common_supertype ? "supertype step over #{side_effect.common_supertype.name}" : nil] +
853
+ [side_effect.absorbed_precursors > 0 ? "absorbs #{side_effect.absorbed_precursors} preceding words" : nil] +
854
+ [side_effect.absorbed_followers > 0 ? "absorbs #{side_effect.absorbed_followers} following words" : nil] +
855
+ [@negated ? 'implicitly negated' : nil]
856
+ )
857
+ end.flatten.compact*','
858
+ actual_effects.empty? ? "no side effects" : actual_effects
859
+ end
860
+ end
861
+
862
+ class Reference
863
+ attr_reader :term, :quantifier, :function_call, :value_constraint, :literal, :nested_clauses
864
+ attr_accessor :leading_adjective, :trailing_adjective, :role_name
865
+ attr_accessor :player # What ObjectType does the Binding denote
866
+ attr_accessor :binding # What Binding for that ObjectType
867
+ attr_accessor :role # Which Role of this ObjectType
868
+ attr_accessor :role_ref # Which RoleRef to that Role
869
+ attr_accessor :clause # The clause that this Reference is part of
870
+ attr_accessor :objectification_of # If nested_clauses is set, this is the fact type it objectifies
871
+ attr_reader :embedded_presence_constraint # This refers to the ActiveFacts::Metamodel::PresenceConstraint
872
+
873
+ def initialize term, leading_adjective = nil, trailing_adjective = nil, quantifier = nil, function_call = nil, role_name = nil, value_constraint = nil, literal = nil, nested_clauses = nil
874
+ @term = term
875
+ @leading_adjective = leading_adjective
876
+ @trailing_adjective = trailing_adjective
877
+ @quantifier = quantifier
878
+ # @function_call = function_call # Not used or implemented
879
+ @role_name = role_name
880
+ @value_constraint = value_constraint
881
+ @literal = literal
882
+ @nested_clauses = nested_clauses
883
+ end
884
+
885
+ def inspect
886
+ to_s
887
+ end
888
+
889
+ def to_s
890
+ "{#{
891
+ @quantifier && @quantifier.inspect+' '
892
+ }#{
893
+ @leading_adjective && @leading_adjective.sub(/ |$/,'- ').sub(/ *$/,' ')
894
+ }#{
895
+ @term
896
+ }#{
897
+ @trailing_adjective && ' '+@trailing_adjective.sub(/(.* |^)/, '\1-')
898
+ }#{
899
+ @role_name and @role_name.is_a?(Integer) ? "(#{@role_name})" : " (as #{@role_name})"
900
+ }#{
901
+ @literal && ' '+@literal.inspect
902
+ }#{
903
+ @value_constraint && ' '+@value_constraint.to_s
904
+ }}"
905
+ end
906
+
907
+ def <=>(other)
908
+ ( 4*(@term <=> other.term) +
909
+ 2*((@leading_adjective||'') <=> (other.leading_adjective||'')) +
910
+ 1*((@trailing_adjective||'') <=> (other.trailing_adjective||''))
911
+ ) <=> 0
912
+ end
913
+
914
+ def includes_literals
915
+ @nested_clauses && @nested_clauses.detect{|nested| nested.includes_literals}
916
+ end
917
+
918
+ # We create value types for the results of arithmetic expressions, and they get assigned here:
919
+ def player=(player)
920
+ @player = player
921
+ end
922
+
923
+ def identify_players_with_role_name(context)
924
+ identify_player(context) if role_name
925
+ # Include players in nested clauses, if any
926
+ nested_clauses.each{|clause| clause.identify_players_with_role_name(context)} if nested_clauses
927
+ end
928
+
929
+ def identify_other_players context
930
+ identify_player context
931
+ end
932
+
933
+ def identify_player context
934
+ @player || begin
935
+ @player = context.object_type @term
936
+ raise "ObjectType #{@term} unrecognised" unless @player
937
+ context.player_by_role_name[@role_name] = player if @role_name
938
+ @player
939
+ end
940
+ end
941
+
942
+ def uses_role_name?
943
+ @term != @player.name
944
+ end
945
+
946
+ def key
947
+ if @role_name
948
+ key = [@term, @role_name] # Defines a role name
949
+ elsif uses_role_name?
950
+ key = [@player.name, @term] # Uses a role name
951
+ else
952
+ l = @leading_adjective
953
+ t = @trailing_adjective
954
+ key = [!l || l.empty? ? nil : l, @term, !t || t.empty? ? nil : t]
955
+ end
956
+ key += [:literal, literal.literal] if @literal
957
+ key
958
+ end
959
+
960
+ def bind context
961
+ @nested_clauses.each{|c| c.bind context} if @nested_clauses
962
+ if role_name = @role_name
963
+ # Omit these tests to see if anything evil eventuates:
964
+ #if @leading_adjective || @trailing_adjective
965
+ # raise "Role reference may not have adjectives if it defines a role name or uses a subscript: #{inspect}"
966
+ #end
967
+ else
968
+ if uses_role_name?
969
+ if @leading_adjective || @trailing_adjective
970
+ raise "Role reference may not have adjectives if it uses a role name: #{inspect}"
971
+ end
972
+ role_name = @term
973
+ end
974
+ end
975
+ k = key
976
+ @binding = context.bindings[k]
977
+ if !@binding
978
+ if !literal
979
+ # Find a binding that has a literal, and bind to it if it's the only one
980
+ candidates = context.bindings.map do |binding_key, binding|
981
+ binding_key[0...k.size] == k &&
982
+ binding_key[-2] == :literal ? binding : nil
983
+ end.compact
984
+ raise "Uncertain binding reference for #{to_s}, could be any of #{candidates.inspect}" if candidates.size > 1
985
+ @binding = candidates[0]
986
+ else
987
+ # New binding has a literal, look for one without:
988
+ @binding = context.bindings[k[0...-2]]
989
+ end
990
+ end
991
+
992
+ if !@binding
993
+ @binding = Binding.new(@player, role_name)
994
+ context.bindings[k] = @binding
995
+ end
996
+ @binding.add_ref self
997
+ @binding
998
+ end
999
+
1000
+ def unbind context
1001
+ # The key has changed.
1002
+ @binding.delete_ref self
1003
+ if @binding.refs.empty?
1004
+ # Remove the binding from the context if this was the last reference
1005
+ context.bindings.delete_if {|k,v| v == @binding }
1006
+ end
1007
+ @binding = nil
1008
+ end
1009
+
1010
+ def rebind(context)
1011
+ unbind context
1012
+ bind context
1013
+ end
1014
+
1015
+ def rebind_to(context, other_ref)
1016
+ trace :binding, "Rebinding #{inspect} to #{other_ref.inspect}"
1017
+
1018
+ old_binding = binding # Remember to move all refs across
1019
+ unbind(context)
1020
+
1021
+ new_binding = other_ref.binding
1022
+ [self, *old_binding.refs].each do |ref|
1023
+ ref.binding = new_binding
1024
+ new_binding.add_ref ref
1025
+ end
1026
+ old_binding.rebound_to = new_binding
1027
+ end
1028
+
1029
+ # These are called when we successfully match a fact type reading that has relevant adjectives:
1030
+ def wipe_leading_adjective
1031
+ @leading_adjective = nil
1032
+ end
1033
+
1034
+ def wipe_trailing_adjective
1035
+ @trailing_adjective = nil
1036
+ end
1037
+
1038
+ def find_pc_over_roles(roles)
1039
+ return nil if roles.size == 0 # Safeguard; this would chuck an exception otherwise
1040
+ roles[0].all_role_ref.each do |role_ref|
1041
+ next if role_ref.role_sequence.all_role_ref.map(&:role) != roles
1042
+ pc = role_ref.role_sequence.all_presence_constraint.single # Will return nil if there's more than one.
1043
+ #puts "Existing PresenceConstraint matches those roles!" if pc
1044
+ return pc if pc
1045
+ end
1046
+ nil
1047
+ end
1048
+
1049
+ def make_embedded_presence_constraint vocabulary
1050
+ raise "No Role for embedded_presence_constraint" unless @role_ref
1051
+ fact_type = @role_ref.role.fact_type
1052
+ constellation = vocabulary.constellation
1053
+
1054
+ trace :constraint, "Processing embedded constraint #{@quantifier.inspect} on #{@role_ref.role.object_type.name} in #{fact_type.describe}" do
1055
+ # Preserve the role order of the clause, excluding this role:
1056
+ constrained_roles = (@clause.refs-[self]).map{|vr| vr.role_ref.role}
1057
+ if constrained_roles.empty?
1058
+ trace :constraint, "Quantifier over unary role has no effect"
1059
+ return
1060
+ end
1061
+ constraint = find_pc_over_roles(constrained_roles)
1062
+ if constraint
1063
+ raise "Conflicting maximum frequency for constraint" if constraint.max_frequency && constraint.max_frequency != @quantifier.max
1064
+ 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
1065
+ constraint.max_frequency = @quantifier.max
1066
+ raise "Conflicting minimum frequency for constraint" if constraint.min_frequency && constraint.min_frequency != @quantifier.min
1067
+ 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
1068
+ constraint.min_frequency = @quantifier.min
1069
+ else
1070
+ role_sequence = constellation.RoleSequence(:new)
1071
+ constrained_roles.each_with_index do |constrained_role, i|
1072
+ role_ref = constellation.RoleRef(role_sequence, i, :role => constrained_role)
1073
+ end
1074
+ constraint = constellation.PresenceConstraint(
1075
+ :new,
1076
+ :vocabulary => vocabulary,
1077
+ :role_sequence => role_sequence,
1078
+ :is_mandatory => @quantifier.min && @quantifier.min > 0, # REVISIT: Check "maybe" qualifier?
1079
+ :max_frequency => @quantifier.max,
1080
+ :min_frequency => @quantifier.min
1081
+ )
1082
+ if @quantifier.pragmas
1083
+ @quantifier.pragmas.each do |p|
1084
+ constellation.ConceptAnnotation(:concept => constraint.concept, :mapping_annotation => p)
1085
+ end
1086
+ end
1087
+ 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}"
1088
+ @quantifier.enforcement.compile(constellation, constraint) if @quantifier.enforcement
1089
+ @embedded_presence_constraint = constraint
1090
+ end
1091
+ constraint
1092
+ end
1093
+
1094
+ end
1095
+
1096
+ def result(context = nil)
1097
+ self
1098
+ end
1099
+ end
1100
+
1101
+ # REVISIT: This needs to handle annotations for some/that/which, etc.
1102
+ class Quantifier
1103
+ attr_accessor :enforcement
1104
+ attr_accessor :context_note
1105
+ attr_accessor :pragmas
1106
+ attr_reader :min, :max
1107
+
1108
+ def initialize min, max, enforcement = nil, context_note = nil, pragmas = nil
1109
+ @min = min
1110
+ @max = max
1111
+ @enforcement = enforcement
1112
+ @context_note = context_note
1113
+ @pragmas = pragmas
1114
+ end
1115
+
1116
+ def is_unique
1117
+ @max and @max == 1
1118
+ end
1119
+
1120
+ def is_mandatory
1121
+ @min and @min >= 1
1122
+ end
1123
+
1124
+ def is_zero
1125
+ @min == 0 and @max == 0
1126
+ end
1127
+
1128
+ def inspect
1129
+ "[#{@min}..#{@max}]#{
1130
+ @context_note && ' ' + @context_note.inspect
1131
+ }"
1132
+ end
1133
+ end
1134
+
1135
+ end
1136
+ end
1137
+ end