activefacts-cql 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +19 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-cql.gemspec +29 -0
  10. data/bin/setup +7 -0
  11. data/lib/activefacts/cql.rb +7 -0
  12. data/lib/activefacts/cql/.gitignore +0 -0
  13. data/lib/activefacts/cql/Rakefile +14 -0
  14. data/lib/activefacts/cql/compiler.rb +156 -0
  15. data/lib/activefacts/cql/compiler/clause.rb +1137 -0
  16. data/lib/activefacts/cql/compiler/constraint.rb +581 -0
  17. data/lib/activefacts/cql/compiler/entity_type.rb +457 -0
  18. data/lib/activefacts/cql/compiler/expression.rb +443 -0
  19. data/lib/activefacts/cql/compiler/fact.rb +390 -0
  20. data/lib/activefacts/cql/compiler/fact_type.rb +421 -0
  21. data/lib/activefacts/cql/compiler/query.rb +106 -0
  22. data/lib/activefacts/cql/compiler/shared.rb +161 -0
  23. data/lib/activefacts/cql/compiler/value_type.rb +174 -0
  24. data/lib/activefacts/cql/parser.rb +234 -0
  25. data/lib/activefacts/cql/parser/CQLParser.treetop +167 -0
  26. data/lib/activefacts/cql/parser/Context.treetop +48 -0
  27. data/lib/activefacts/cql/parser/Expressions.treetop +67 -0
  28. data/lib/activefacts/cql/parser/FactTypes.treetop +358 -0
  29. data/lib/activefacts/cql/parser/Language/English.treetop +315 -0
  30. data/lib/activefacts/cql/parser/Language/French.treetop +315 -0
  31. data/lib/activefacts/cql/parser/Language/Mandarin.treetop +304 -0
  32. data/lib/activefacts/cql/parser/LexicalRules.treetop +253 -0
  33. data/lib/activefacts/cql/parser/ObjectTypes.treetop +210 -0
  34. data/lib/activefacts/cql/parser/Terms.treetop +183 -0
  35. data/lib/activefacts/cql/parser/ValueTypes.treetop +202 -0
  36. data/lib/activefacts/cql/parser/nodes.rb +49 -0
  37. data/lib/activefacts/cql/require.rb +36 -0
  38. data/lib/activefacts/cql/verbaliser.rb +804 -0
  39. data/lib/activefacts/cql/version.rb +5 -0
  40. data/lib/activefacts/input/cql.rb +43 -0
  41. data/lib/rubygems_plugin.rb +12 -0
  42. metadata +167 -0
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