activefacts-cql 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|