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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +19 -0
- data/Rakefile +6 -0
- data/activefacts-cql.gemspec +29 -0
- data/bin/setup +7 -0
- data/lib/activefacts/cql.rb +7 -0
- data/lib/activefacts/cql/.gitignore +0 -0
- data/lib/activefacts/cql/Rakefile +14 -0
- data/lib/activefacts/cql/compiler.rb +156 -0
- data/lib/activefacts/cql/compiler/clause.rb +1137 -0
- data/lib/activefacts/cql/compiler/constraint.rb +581 -0
- data/lib/activefacts/cql/compiler/entity_type.rb +457 -0
- data/lib/activefacts/cql/compiler/expression.rb +443 -0
- data/lib/activefacts/cql/compiler/fact.rb +390 -0
- data/lib/activefacts/cql/compiler/fact_type.rb +421 -0
- data/lib/activefacts/cql/compiler/query.rb +106 -0
- data/lib/activefacts/cql/compiler/shared.rb +161 -0
- data/lib/activefacts/cql/compiler/value_type.rb +174 -0
- data/lib/activefacts/cql/parser.rb +234 -0
- data/lib/activefacts/cql/parser/CQLParser.treetop +167 -0
- data/lib/activefacts/cql/parser/Context.treetop +48 -0
- data/lib/activefacts/cql/parser/Expressions.treetop +67 -0
- data/lib/activefacts/cql/parser/FactTypes.treetop +358 -0
- data/lib/activefacts/cql/parser/Language/English.treetop +315 -0
- data/lib/activefacts/cql/parser/Language/French.treetop +315 -0
- data/lib/activefacts/cql/parser/Language/Mandarin.treetop +304 -0
- data/lib/activefacts/cql/parser/LexicalRules.treetop +253 -0
- data/lib/activefacts/cql/parser/ObjectTypes.treetop +210 -0
- data/lib/activefacts/cql/parser/Terms.treetop +183 -0
- data/lib/activefacts/cql/parser/ValueTypes.treetop +202 -0
- data/lib/activefacts/cql/parser/nodes.rb +49 -0
- data/lib/activefacts/cql/require.rb +36 -0
- data/lib/activefacts/cql/verbaliser.rb +804 -0
- data/lib/activefacts/cql/version.rb +5 -0
- data/lib/activefacts/input/cql.rb +43 -0
- data/lib/rubygems_plugin.rb +12 -0
- 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
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation
|
data/.travis.yml
ADDED
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,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
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
|