activefacts 0.7.3 → 0.8.5
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.
- data/LICENSE +19 -0
- data/Manifest.txt +24 -2
- data/Rakefile +25 -3
- data/bin/afgen +1 -1
- data/bin/cql +13 -2
- data/css/offline.css +3 -0
- data/css/orm2.css +24 -0
- data/css/print.css +8 -0
- data/css/style-print.css +357 -0
- data/css/style.css +387 -0
- data/download.html +85 -0
- data/examples/CQL/Address.cql +3 -3
- data/examples/CQL/Blog.cql +13 -14
- data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
- data/examples/CQL/Death.cql +3 -2
- data/examples/CQL/Genealogy.cql +13 -11
- data/examples/CQL/Marriage.cql +2 -2
- data/examples/CQL/Metamodel.cql +136 -93
- data/examples/CQL/MultiInheritance.cql +2 -2
- data/examples/CQL/OilSupply.cql +14 -10
- data/examples/CQL/Orienteering.cql +22 -19
- data/examples/CQL/PersonPlaysGame.cql +3 -2
- data/examples/CQL/SchoolActivities.cql +4 -2
- data/examples/CQL/SimplestUnary.cql +1 -1
- data/examples/CQL/SubtypePI.cql +6 -7
- data/examples/CQL/Warehousing.cql +16 -19
- data/examples/CQL/unit.cql +584 -0
- data/examples/index.html +276 -0
- data/examples/intro.html +497 -0
- data/examples/local.css +20 -0
- data/index.html +96 -0
- data/lib/activefacts/api/concept.rb +48 -46
- data/lib/activefacts/api/constellation.rb +43 -23
- data/lib/activefacts/api/entity.rb +2 -2
- data/lib/activefacts/api/instance.rb +6 -2
- data/lib/activefacts/api/instance_index.rb +5 -0
- data/lib/activefacts/api/value.rb +8 -2
- data/lib/activefacts/api/vocabulary.rb +15 -10
- data/lib/activefacts/cql/CQLParser.treetop +109 -88
- data/lib/activefacts/cql/Concepts.treetop +32 -10
- data/lib/activefacts/cql/Context.treetop +34 -0
- data/lib/activefacts/cql/Expressions.treetop +9 -9
- data/lib/activefacts/cql/FactTypes.treetop +30 -31
- data/lib/activefacts/cql/Language/English.treetop +50 -0
- data/lib/activefacts/cql/LexicalRules.treetop +2 -1
- data/lib/activefacts/cql/Terms.treetop +117 -0
- data/lib/activefacts/cql/ValueTypes.treetop +152 -0
- data/lib/activefacts/cql/compiler.rb +1718 -0
- data/lib/activefacts/cql/parser.rb +124 -57
- data/lib/activefacts/generate/absorption.rb +1 -1
- data/lib/activefacts/generate/cql.rb +111 -100
- data/lib/activefacts/generate/cql/html.rb +5 -5
- data/lib/activefacts/generate/oo.rb +3 -3
- data/lib/activefacts/generate/ordered.rb +51 -19
- data/lib/activefacts/generate/ruby.rb +10 -8
- data/lib/activefacts/generate/sql/mysql.rb +14 -10
- data/lib/activefacts/generate/sql/server.rb +29 -24
- data/lib/activefacts/input/cql.rb +9 -1264
- data/lib/activefacts/input/orm.rb +213 -200
- data/lib/activefacts/persistence/columns.rb +11 -10
- data/lib/activefacts/persistence/index.rb +15 -18
- data/lib/activefacts/persistence/reference.rb +17 -17
- data/lib/activefacts/persistence/tables.rb +50 -51
- data/lib/activefacts/version.rb +1 -1
- data/lib/activefacts/vocabulary/extensions.rb +79 -8
- data/lib/activefacts/vocabulary/metamodel.rb +183 -114
- data/spec/absorption_ruby_spec.rb +99 -0
- data/spec/absorption_spec.rb +3 -4
- data/spec/api/constellation.rb +1 -1
- data/spec/api/entity_type.rb +3 -1
- data/spec/api/instance.rb +4 -2
- data/spec/api/roles.rb +8 -6
- data/spec/api_spec.rb +1 -2
- data/spec/cql/context_spec.rb +71 -0
- data/spec/cql/samples_spec.rb +154 -0
- data/spec/cql/unit_spec.rb +375 -0
- data/spec/cql_cql_spec.rb +31 -21
- data/spec/cql_mysql_spec.rb +70 -0
- data/spec/cql_parse_spec.rb +15 -9
- data/spec/cql_ruby_spec.rb +27 -13
- data/spec/cql_sql_spec.rb +42 -16
- data/spec/cql_symbol_tables_spec.rb +2 -3
- data/spec/cqldump_spec.rb +7 -7
- data/spec/helpers/file_matcher.rb +39 -0
- data/spec/norma_cql_spec.rb +20 -12
- data/spec/norma_ruby_spec.rb +6 -3
- data/spec/norma_sql_spec.rb +6 -3
- data/spec/norma_tables_spec.rb +6 -4
- data/spec/spec_helper.rb +27 -8
- data/status.html +69 -0
- data/why.html +60 -0
- metadata +34 -11
- data/lib/activefacts/cql/DataTypes.treetop +0 -81
- data/spec/cql_unit_spec.rb +0 -330
@@ -47,21 +47,67 @@ module ActiveFacts
|
|
47
47
|
/ number s { def value; number.value; end }
|
48
48
|
end
|
49
49
|
|
50
|
+
rule according_to
|
51
|
+
'according' S to
|
52
|
+
end
|
53
|
+
|
54
|
+
rule because
|
55
|
+
'because' S
|
56
|
+
end
|
57
|
+
|
58
|
+
rule as_opposed_to
|
59
|
+
as S 'opposed' S to
|
60
|
+
end
|
61
|
+
|
62
|
+
rule so_that
|
63
|
+
'so' S that
|
64
|
+
end
|
65
|
+
|
66
|
+
rule to_avoid
|
67
|
+
to s 'avoid' !alphanumeric
|
68
|
+
end
|
69
|
+
|
70
|
+
rule as_agreed_by
|
71
|
+
s as s 'agreed' s d:('on' S date)? by s people
|
72
|
+
{ def value; [ d.empty? ? nil : d.date.value, people.value ]; end }
|
73
|
+
end
|
74
|
+
|
75
|
+
rule date
|
76
|
+
s d:(!(by/')') .)+
|
77
|
+
{ def value; d.text_value.strip; end }
|
78
|
+
end
|
79
|
+
|
80
|
+
rule people
|
81
|
+
s h:person s t:(',' s !context_type person s)*
|
82
|
+
{ def value; [h.text_value] + t.elements.map{|e| e.person.text_value }; end }
|
83
|
+
end
|
84
|
+
|
85
|
+
rule person
|
86
|
+
id (s id)*
|
87
|
+
end
|
88
|
+
|
50
89
|
rule acyclic 'acyclic' !alphanumeric end
|
51
90
|
rule alias 'alias' !alphanumeric end
|
52
91
|
rule all 'all' !alphanumeric end
|
53
92
|
rule and 'and' !alphanumeric end
|
93
|
+
rule approximately 'approximately' !alphanumeric end
|
54
94
|
rule as 'as' !alphanumeric end
|
55
95
|
rule at 'at' !alphanumeric end
|
96
|
+
rule both 'both' !alphanumeric end
|
97
|
+
rule but 'but' !alphanumeric end
|
56
98
|
rule by 'by' !alphanumeric end
|
99
|
+
rule converts 'converts' !alphanumeric end
|
57
100
|
rule definitely 'definitely' !alphanumeric end
|
58
101
|
rule each 'each' !alphanumeric end
|
59
102
|
rule either 'either' !alphanumeric end
|
60
103
|
rule entity 'entity' !alphanumeric end
|
104
|
+
rule ephemeral 'ephemeral' !alphanumeric end
|
61
105
|
rule exactly 'exactly' !alphanumeric end
|
62
106
|
rule false 'false' !alphanumeric end
|
107
|
+
rule feminine 'feminine' !alphanumeric end
|
63
108
|
rule from 'from' !alphanumeric end
|
64
109
|
rule identified ('known'/'identified') !alphanumeric end
|
110
|
+
rule independent 'independent' !alphanumeric end
|
65
111
|
rule if 'if' !alphanumeric end
|
66
112
|
rule import 'import' !alphanumeric end
|
67
113
|
rule includes 'includes' !alphanumeric end
|
@@ -69,6 +115,7 @@ module ActiveFacts
|
|
69
115
|
rule is 'is' !alphanumeric end
|
70
116
|
rule its 'its' !alphanumeric end
|
71
117
|
rule least 'least' !alphanumeric end
|
118
|
+
rule masculine 'masculine' !alphanumeric end
|
72
119
|
rule matches 'matches' !alphanumeric end
|
73
120
|
rule maybe 'maybe' !alphanumeric end
|
74
121
|
rule most 'most' !alphanumeric end
|
@@ -79,8 +126,11 @@ module ActiveFacts
|
|
79
126
|
rule one 'one' !alphanumeric end
|
80
127
|
rule only 'only' !alphanumeric end
|
81
128
|
rule or 'or' !alphanumeric end
|
129
|
+
rule partitioned 'partitioned' !alphanumeric end
|
130
|
+
rule personal 'personal' !alphanumeric end
|
82
131
|
rule restricted 'restricted' !alphanumeric end
|
83
132
|
rule returning 'returning' !alphanumeric end
|
133
|
+
rule separate 'separate' !alphanumeric end
|
84
134
|
rule some 'some' !alphanumeric end
|
85
135
|
rule static 'static' !alphanumeric end
|
86
136
|
rule symmetric 'symmetric' !alphanumeric end
|
@@ -123,7 +123,7 @@ module ActiveFacts
|
|
123
123
|
end
|
124
124
|
|
125
125
|
rule exponent
|
126
|
-
( [Ee]
|
126
|
+
( [Ee] [-+]? [0-9]+ )
|
127
127
|
end
|
128
128
|
|
129
129
|
rule hexnumber
|
@@ -140,6 +140,7 @@ module ActiveFacts
|
|
140
140
|
|
141
141
|
rule id
|
142
142
|
alpha alphanumeric*
|
143
|
+
{ def value; text_value; end }
|
143
144
|
end
|
144
145
|
|
145
146
|
rule alpha
|
@@ -0,0 +1,117 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts CQL Parser.
|
3
|
+
# Parse rules relating to Term names
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module CQL
|
9
|
+
grammar Terms
|
10
|
+
rule term_definition_name
|
11
|
+
id s
|
12
|
+
t:(!non_term_def id s)*
|
13
|
+
{ def value
|
14
|
+
t.elements.inject([
|
15
|
+
id.value
|
16
|
+
]){|a, e| a << e.id.value}*' '
|
17
|
+
end
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
rule non_term_def
|
22
|
+
entity_prefix
|
23
|
+
/ written_as # Value type
|
24
|
+
/ is s mapping_pragmas where # Objectified type
|
25
|
+
/ non_role_word
|
26
|
+
end
|
27
|
+
|
28
|
+
rule entity_prefix
|
29
|
+
is s identified_by / subtype_prefix
|
30
|
+
end
|
31
|
+
|
32
|
+
rule prescan
|
33
|
+
s (
|
34
|
+
term_definition_name s entity_prefix
|
35
|
+
&{|e| input.context.entity_type(e[0].value); true }
|
36
|
+
/
|
37
|
+
term_definition_name written_as
|
38
|
+
&{|e| input.context.value_type(e[0].value); true }
|
39
|
+
/
|
40
|
+
term_definition_name s is s mapping_pragmas where
|
41
|
+
&{|e| input.context.objectified_fact_type(e[0].value); true }
|
42
|
+
)?
|
43
|
+
prescan_rest
|
44
|
+
&{|s|
|
45
|
+
@terminal_failures = []
|
46
|
+
@max_terminal_failure_index = start_index
|
47
|
+
|
48
|
+
# puts "========== prescan is complete on #{(s.map{|e|e.text_value}*" ").inspect} =========="
|
49
|
+
false # REVISIT: Wipe any terminal failures that were added? Can there be anything relevant?
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Do a first-pass mainly lexical analysis, looking only for role name definitions
|
54
|
+
# For use in detecting terms later.
|
55
|
+
# REVISIT: and adjectives
|
56
|
+
rule prescan_rest
|
57
|
+
&{ input.context.reset_role_names }
|
58
|
+
(
|
59
|
+
context # Context notes have different lexical conventions
|
60
|
+
/ role_name # A role name definition
|
61
|
+
&{|s| input.context.role_name(s[0].value) }
|
62
|
+
# Adjective definitions
|
63
|
+
/ new_adjective_term
|
64
|
+
/ global_term # If we see A B - C D, don't recognise B as a new adjective for C D.
|
65
|
+
/ id
|
66
|
+
/ range # Covers all numbers and strings
|
67
|
+
/ comparator # handle two-character operators
|
68
|
+
/ S # White space and comments, must precede / and *
|
69
|
+
/ [-+{}\[\].,:^/%*()] # All other punctuation and operators
|
70
|
+
)* [?;] s
|
71
|
+
end
|
72
|
+
|
73
|
+
rule new_adjective_term
|
74
|
+
!global_term adj:id '-' s global_term
|
75
|
+
&{|s| input.context.new_leading_adjective_term(s[1].value, s[4].value) } # Definitely a new leading adjective for this term
|
76
|
+
/
|
77
|
+
global_term s '-' !global_term adj:id
|
78
|
+
&{|s| input.context.new_trailing_adjective_term(s[4].value, s[1].value) } # Definitely a new trailing adjective for this term
|
79
|
+
end
|
80
|
+
|
81
|
+
# This is the rule to use after the prescan; it only succeeds on a complete term or role reference
|
82
|
+
rule term
|
83
|
+
s head:id &{|s| w = s[1].text_value; input.context.term_starts(w) }
|
84
|
+
tail:(s '-'? s w:id &{|s| w = s[3].text_value; input.context.term_continues(w) } )*
|
85
|
+
{ def value
|
86
|
+
name = tail.elements.inject(head.value) { |t, e| "#{t} #{e.w.value}" }
|
87
|
+
#puts "Detected term #{name.inspect}"
|
88
|
+
name
|
89
|
+
end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
rule global_term
|
94
|
+
# This rule shouldn't be used outside the prescan, it will memoize the wrong things.
|
95
|
+
head:id &{|s| input.context.term_starts(s[0].text_value) }
|
96
|
+
tail:(s w:id &{|s| input.context.term_continues(s[1].text_value) } )*
|
97
|
+
{ def value
|
98
|
+
tail.elements.inject(head.value) { |t, e| "#{t} #{e.w.value}" }
|
99
|
+
end
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
rule non_role_word
|
104
|
+
# These words are illegal in (but maybe ok following) a reading where a role word is expected:
|
105
|
+
and
|
106
|
+
/ where
|
107
|
+
/ if
|
108
|
+
/ only
|
109
|
+
/ or
|
110
|
+
/ quantifier
|
111
|
+
/ restriction
|
112
|
+
/ but
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts CQL Parser.
|
3
|
+
# Parse rules relating to ValueType definitions.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module CQL
|
9
|
+
grammar ValueTypes
|
10
|
+
rule value_type
|
11
|
+
s term_definition_name
|
12
|
+
written_as
|
13
|
+
&{|e| input.context.value_type(e[1].value); true }
|
14
|
+
base:id s
|
15
|
+
value_type_parameters
|
16
|
+
units
|
17
|
+
r:restriction?
|
18
|
+
mapping_pragmas
|
19
|
+
s ';' s
|
20
|
+
{
|
21
|
+
def defined_type
|
22
|
+
[
|
23
|
+
:value_type,
|
24
|
+
base.value,
|
25
|
+
value_type_parameters.values,
|
26
|
+
units.value,
|
27
|
+
(!r.empty? ? r.ranges : []),
|
28
|
+
mapping_pragmas.value
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def value
|
33
|
+
[ term_definition_name.value,
|
34
|
+
defined_type
|
35
|
+
]
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
rule value_type_parameters
|
41
|
+
'(' s tpl:type_parameter_list? ')' s
|
42
|
+
{ def values; tpl.empty? ? [] : tpl.values; end }
|
43
|
+
/ s
|
44
|
+
{ def values; []; end }
|
45
|
+
end
|
46
|
+
|
47
|
+
rule type_parameter_list
|
48
|
+
head:number s tail:( ',' s number s )*
|
49
|
+
{
|
50
|
+
def values
|
51
|
+
[head.value] + tail.elements.map{|i| i.number.value}
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
rule unit_definition
|
57
|
+
u:(
|
58
|
+
unit_coefficient units s o:unit_offset?
|
59
|
+
converts s a:(approximately s)? to s
|
60
|
+
singular:id s plural:('/' s p:id s)?
|
61
|
+
/
|
62
|
+
singular:id s plural:('/' s p:id s)?
|
63
|
+
converts s a:(approximately s)? to s
|
64
|
+
unit_coefficient units s o:unit_offset?
|
65
|
+
)
|
66
|
+
q:(approximately / ephemeral)? s
|
67
|
+
';' s
|
68
|
+
{ def value
|
69
|
+
r = { :coefficient => u.unit_coefficient.value,
|
70
|
+
:offset => u.o.text_value.empty? ? 0 : u.o.value,
|
71
|
+
:base => u.units.value,
|
72
|
+
:singular => u.singular.text_value,
|
73
|
+
}
|
74
|
+
r[:plural] = u.plural.p.text_value unless u.plural.text_value.empty?
|
75
|
+
r[:approximately] = true if q.text_value == 'approximately' || !u.a.text_value.empty?
|
76
|
+
r[:ephemeral] = true if q.text_value == 'ephemeral'
|
77
|
+
[ nil, [ :unit, r ] ]
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
rule unit_coefficient
|
83
|
+
numerator:number denominator:(s '/' s number)? s
|
84
|
+
{ def value
|
85
|
+
{ :numerator => numerator.text_value,
|
86
|
+
:denominator => (denominator.text_value.empty? ? "1" : denominator.number.text_value)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
rule unit_offset
|
93
|
+
sign:[-+] s number s
|
94
|
+
{ def value
|
95
|
+
sign.text_value == '-' ? "-"+number.text_value : number.text_value
|
96
|
+
end
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
rule units
|
101
|
+
'' u:(!non_unit unit s tail:(!non_unit unit s)* div:('/' s unit s tail:(!non_unit unit s)*)?)?
|
102
|
+
{ def value
|
103
|
+
if u.text_value.empty?
|
104
|
+
[]
|
105
|
+
else
|
106
|
+
u.tail.elements.inject([u.unit.value]) { |a, e| a << e.unit.value } +
|
107
|
+
(u.div.text_value.empty? ? [] : u.div.tail.elements.inject([u.div.unit.inverse]) { |a, e| a << e.unit.inverse })
|
108
|
+
end
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
rule non_unit
|
114
|
+
restricted / converts / approximately / ephemeral
|
115
|
+
end
|
116
|
+
|
117
|
+
rule unit
|
118
|
+
unit_name:id pow:('^' '-'? [0-9])?
|
119
|
+
{ def value
|
120
|
+
[unit_name.text_value, pow.text_value.empty? ? 1 : Integer(pow.text_value[1..-1])]
|
121
|
+
end
|
122
|
+
def inverse
|
123
|
+
a = value
|
124
|
+
a[1] = -a[1]
|
125
|
+
a
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
rule restriction
|
131
|
+
restricted s to s range_list s units
|
132
|
+
{
|
133
|
+
def ranges
|
134
|
+
range_list.ranges # REVISIT: Use the units here
|
135
|
+
end
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
rule range_list
|
140
|
+
'{' s
|
141
|
+
head:range s tail:( ',' s range )*
|
142
|
+
'}' s
|
143
|
+
{
|
144
|
+
def ranges
|
145
|
+
[head.value] + tail.elements.map{|e| e.range.value }
|
146
|
+
end
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,1718 @@
|
|
1
|
+
# Compile a CQL string into an ActiveFacts vocabulary.
|
2
|
+
#
|
3
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
4
|
+
#
|
5
|
+
require 'activefacts/vocabulary'
|
6
|
+
require 'activefacts/cql/parser'
|
7
|
+
|
8
|
+
module ActiveFacts
|
9
|
+
module CQL
|
10
|
+
class Compiler < ActiveFacts::CQL::Parser
|
11
|
+
attr_reader :vocabulary
|
12
|
+
|
13
|
+
class SymbolTable; end #:nodoc:
|
14
|
+
|
15
|
+
RingTypes = %w{acyclic intransitive symmetric asymmetric transitive antisymmetric irreflexive reflexive}
|
16
|
+
RingPairs = {
|
17
|
+
:intransitive => [:acyclic, :asymmetric, :symmetric],
|
18
|
+
:irreflexive => [:symmetric]
|
19
|
+
}
|
20
|
+
|
21
|
+
def initialize(string, filename = "stdin")
|
22
|
+
@string = string # The contents of the input string
|
23
|
+
@filename = filename
|
24
|
+
|
25
|
+
@constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
|
26
|
+
|
27
|
+
# The syntax tree created from each parsed CQL statement gets passed to the block.
|
28
|
+
# parse_all returns an array of the block's non-nil return values.
|
29
|
+
result = parse_all(@string, :definition) do |node|
|
30
|
+
begin
|
31
|
+
kind, *value = d = definition(node)
|
32
|
+
#print "Parsed '#{node.text_value}' (#{kind.inspect})"
|
33
|
+
#print " to "; p value
|
34
|
+
raise "Definition of #{kind} must be in a vocabulary" if kind != :vocabulary and !@vocabulary
|
35
|
+
case kind
|
36
|
+
when :vocabulary
|
37
|
+
@vocabulary = @constellation.Vocabulary(value[0])
|
38
|
+
when :value_type
|
39
|
+
value_type *value
|
40
|
+
when :entity_type
|
41
|
+
entity_type *value
|
42
|
+
when :fact_type
|
43
|
+
fact_type *value
|
44
|
+
when :constraint
|
45
|
+
constraint *value
|
46
|
+
when :fact
|
47
|
+
fact *value
|
48
|
+
when :unit
|
49
|
+
unit *value
|
50
|
+
else
|
51
|
+
print "="*20+" unhandled declaration type: "; p kind, value
|
52
|
+
end
|
53
|
+
rescue => e
|
54
|
+
puts e.message+"\n\t"+e.backtrace*"\n\t" if debug :exception
|
55
|
+
start_line = @string.line_of(node.interval.first)
|
56
|
+
end_line = @string.line_of(node.interval.last-1)
|
57
|
+
lines = start_line != end_line ? "s #{start_line}-#{end_line}" : " #{start_line.to_s}"
|
58
|
+
raise "at line#{lines} #{e.message.strip}"
|
59
|
+
end
|
60
|
+
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
raise failure_reason unless result
|
64
|
+
@vocabulary
|
65
|
+
end
|
66
|
+
|
67
|
+
def context
|
68
|
+
@context ||= Context.new(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def value_type(name, base_type_name, parameters, unit, ranges, mapping_pragmas)
|
73
|
+
length, scale = *parameters
|
74
|
+
|
75
|
+
# Create the base type:
|
76
|
+
base_type = nil
|
77
|
+
if (base_type_name != name)
|
78
|
+
unless base_type = @constellation.ValueType[[@vocabulary, @constellation.Name(base_type_name)]]
|
79
|
+
#puts "REVISIT: Creating base ValueType #{base_type_name} in #{@vocabulary.inspect}"
|
80
|
+
base_type = @constellation.ValueType(@vocabulary, base_type_name)
|
81
|
+
return if base_type_name == name
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create and initialise the ValueType:
|
86
|
+
vt = @constellation.ValueType(@vocabulary, name)
|
87
|
+
vt.supertype = base_type if base_type
|
88
|
+
vt.length = length if length
|
89
|
+
vt.scale = scale if scale
|
90
|
+
|
91
|
+
# REVISIT: Find and apply the units
|
92
|
+
|
93
|
+
if ranges.size != 0
|
94
|
+
vt.value_restriction = value_restriction ranges
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def value_restriction(ranges)
|
99
|
+
vr = @constellation.ValueRestriction(:new)
|
100
|
+
ranges.each do |range|
|
101
|
+
min, max = Array === range ? range : [range, range]
|
102
|
+
v_range = @constellation.ValueRange(
|
103
|
+
min ? [[String === min ? eval(min) : min.to_s, String === min, nil], true] : nil,
|
104
|
+
max ? [[String === max ? eval(max) : max.to_s, String === max, nil], true] : nil
|
105
|
+
)
|
106
|
+
ar = @constellation.AllowedRange(vr, v_range)
|
107
|
+
end
|
108
|
+
vr
|
109
|
+
end
|
110
|
+
|
111
|
+
def entity_type(name, supertypes, identification, mapping_pragmas, clauses)
|
112
|
+
#puts "Entity Type #{name}, supertypes #{supertypes.inspect}, id #{identification.inspect}, clauses = #{clauses.inspect}"
|
113
|
+
debug :entity, "Defining Entity Type #{name}" do
|
114
|
+
# Assert the entity:
|
115
|
+
# If this entity was forward referenced, this won't be a new object, and will subsume its roles
|
116
|
+
entity_type = @constellation.EntityType(@vocabulary, name)
|
117
|
+
entity_type.is_independent = true if (mapping_pragmas.include? 'independent')
|
118
|
+
|
119
|
+
# Set up its supertypes:
|
120
|
+
supertypes.each do |supertype_name|
|
121
|
+
add_supertype(entity_type, supertype_name, !identification && supertype_name == supertypes[0], mapping_pragmas)
|
122
|
+
end
|
123
|
+
|
124
|
+
# If we're using a common identification mode, find or create the necessary ValueTypes first:
|
125
|
+
vt_name = vt = nil
|
126
|
+
if identification && identification[:mode]
|
127
|
+
mode = identification[:mode] # An identification mode
|
128
|
+
|
129
|
+
# Find or Create an appropriate ValueType called "#{name}#{mode}", of the supertype "#{mode}"
|
130
|
+
vt_name = "#{name}#{mode}"
|
131
|
+
unless vt = @constellation.ValueType[[@vocabulary, vt_name]]
|
132
|
+
base_vt = @constellation.ValueType(@vocabulary, mode)
|
133
|
+
vt = @constellation.ValueType(@vocabulary, vt_name, :supertype => base_vt)
|
134
|
+
if parameters = identification[:parameters]
|
135
|
+
length, scale = *parameters
|
136
|
+
vt.length = length if length
|
137
|
+
vt.scale = scale if scale
|
138
|
+
end
|
139
|
+
end
|
140
|
+
# REVISIT: If we do this, it gets emitted twice when we generate CQL. The generator should detect that the restriction is the same and not emit it.
|
141
|
+
#if (ranges = identification[:restriction])
|
142
|
+
# vt.value_restriction = value_restriction(ranges)
|
143
|
+
#end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Use a two-pass algorithm for entity fact types...
|
147
|
+
# The first step is to find all role references and definitions in the clauses
|
148
|
+
# After bind_roles, each phrase in each clause is either:
|
149
|
+
# * a string, which is a linking word, or
|
150
|
+
# * the phrase hash augmented with a :binding=>Binding
|
151
|
+
@symbols = SymbolTable.new(@constellation, @vocabulary)
|
152
|
+
@symbols.bind_roles_in_clauses(clauses, identification ? identification[:roles] : nil)
|
153
|
+
|
154
|
+
# Next arrange the clauses according to what fact they belong to,
|
155
|
+
# then process each fact type using normal fact type processing.
|
156
|
+
# That way if we find a fact type here having none of the players being the
|
157
|
+
# entity type, we know it's an objectified fact type. The CQL syntax might make
|
158
|
+
# us come here with such a case when the fact type is a subtype of some entity type,
|
159
|
+
# such as occurs in the Metamodel with TypeInheritance.
|
160
|
+
|
161
|
+
# N.B. This doesn't allow forward identification by roles with adjectives (see the i[0]):
|
162
|
+
@symbols.allowed_forward = (ir = identification && identification[:roles]) ? ir.inject({}){|h, i| h[i[0]] = true; h} : {}
|
163
|
+
|
164
|
+
identifying_fact_types = {}
|
165
|
+
clauses_by_fact_type(clauses).each do |clauses_for_fact_type|
|
166
|
+
fact_type = nil
|
167
|
+
@symbols.embedded_presence_constraints = [] # Clear embedded_presence_constraints for each fact type
|
168
|
+
debug :entity, "New Fact Type for entity #{name}" do
|
169
|
+
clauses_for_fact_type.each do |clause|
|
170
|
+
type, qualifiers, phrases, context = *clause
|
171
|
+
debug :reading, "Clause: #{clause.inspect}" do
|
172
|
+
f, r = *bind_fact_reading(fact_type, qualifiers, phrases)
|
173
|
+
identifying_fact_types[f] = true
|
174
|
+
fact_type ||= f
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Find the role that this entity type plays in the fact type, if any:
|
180
|
+
debug :reading, "Roles are: #{fact_type.all_role.map{|role| (role.concept == entity_type ? "*" : "") + role.concept.name }*", "}"
|
181
|
+
player_roles = fact_type.all_role.select{|role| role.concept == entity_type }
|
182
|
+
raise "#{role.concept.name} may only play one role in each identifying fact type" if player_roles.size > 1
|
183
|
+
if player_role = player_roles[0]
|
184
|
+
non_player_roles = fact_type.all_role-[player_role]
|
185
|
+
|
186
|
+
raise "#{name} cannot be identified by a role in a non-binary fact type" if non_player_roles.size > 1
|
187
|
+
elsif identification
|
188
|
+
# This situation occurs when an objectified fact type has an entity identifier
|
189
|
+
#raise "Entity type #{name} cannot objectify fact type #{fact_type.describe}, it already objectifies #{entity_type.fact_type.describe}" if entity_type.fact_type
|
190
|
+
raise "Entity type #{name} cannot objectify fact type #{identification.inspect}, it already objectifies #{entity_type.fact_type.describe}" if entity_type.fact_type
|
191
|
+
debug :entity, "Entity type #{name} objectifies fact type #{fact_type.describe} with distinct identifier"
|
192
|
+
|
193
|
+
entity_type.fact_type = fact_type
|
194
|
+
fact_type_identification(fact_type, name, false)
|
195
|
+
else
|
196
|
+
debug :entity, "Entity type #{name} objectifies fact type #{fact_type.describe}"
|
197
|
+
# it's an objectified fact type, such as a subtype
|
198
|
+
entity_type.fact_type = fact_type
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Finally, create the identifying uniqueness constraint, or mark it as preferred
|
203
|
+
# if it's already been created. The identifying roles have been defined already.
|
204
|
+
|
205
|
+
if identification
|
206
|
+
debug :identification, "Handling identification" do
|
207
|
+
if id_role_names = identification[:roles] # A list of identifying roles
|
208
|
+
debug "Identifying roles: #{id_role_names.inspect}"
|
209
|
+
|
210
|
+
# Pick out the identifying_roles in the order they were declared,
|
211
|
+
# not the order the fact types were defined:
|
212
|
+
identifying_roles = id_role_names.map do |names|
|
213
|
+
unless (role = bind_unary_fact_type(entity_type, names))
|
214
|
+
player, binding = @symbols.bind(names)
|
215
|
+
role = @symbols.roles_by_binding[binding]
|
216
|
+
raise "identifying role #{names*"-"} not found in fact types for #{name}" unless role
|
217
|
+
end
|
218
|
+
role
|
219
|
+
end
|
220
|
+
|
221
|
+
# Find a uniqueness constraint as PI, or make one
|
222
|
+
pc = find_pc_over_roles(identifying_roles)
|
223
|
+
if (pc)
|
224
|
+
debug "Existing PC #{pc.verbalise} is now PK for #{name} #{pc.class.roles.keys.map{|k|"#{k} => "+pc.send(k).verbalise}*", "}"
|
225
|
+
pc.is_preferred_identifier = true
|
226
|
+
pc.name = "#{name}PK" unless pc.name
|
227
|
+
else
|
228
|
+
debug "Adding PK for #{name} using #{identifying_roles.map{|r| r.concept.name}.inspect}"
|
229
|
+
|
230
|
+
role_sequence = @constellation.RoleSequence(:new)
|
231
|
+
# REVISIT: Need to sort the identifying_roles to match the identification parameter array
|
232
|
+
identifying_roles.each_with_index do |identifying_role, index|
|
233
|
+
@constellation.RoleRef(role_sequence, index, :role => identifying_role)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Add a unique constraint over all identifying roles
|
237
|
+
pc = @constellation.PresenceConstraint(
|
238
|
+
:new,
|
239
|
+
:vocabulary => @vocabulary,
|
240
|
+
:name => "#{name}PK", # Is this a useful name?
|
241
|
+
:role_sequence => role_sequence,
|
242
|
+
:is_preferred_identifier => true,
|
243
|
+
:max_frequency => 1 # Unique
|
244
|
+
#:is_mandatory => true,
|
245
|
+
#:min_frequency => 1,
|
246
|
+
)
|
247
|
+
end
|
248
|
+
|
249
|
+
elsif identification[:mode]
|
250
|
+
mode = identification[:mode] # An identification mode
|
251
|
+
|
252
|
+
raise "Entity definition using reference mode may only have one identifying fact type" if identifying_fact_types.size > 1
|
253
|
+
mode_fact_type = identifying_fact_types.keys[0]
|
254
|
+
|
255
|
+
# If the entity type is an objectified fact type, don't use the objectified fact type!
|
256
|
+
mode_fact_type = nil if mode_fact_type && mode_fact_type.entity_type == entity_type
|
257
|
+
|
258
|
+
debug :mode, "Processing Reference Mode for #{name}#{mode_fact_type ? " with existing '#{mode_fact_type.default_reading}'" : ""}"
|
259
|
+
|
260
|
+
# Fact Type:
|
261
|
+
if (ft = mode_fact_type)
|
262
|
+
entity_role, value_role = ft.all_role.partition{|role| role.concept == entity_type}.flatten
|
263
|
+
else
|
264
|
+
ft = @constellation.FactType(:new)
|
265
|
+
entity_role = @constellation.Role(ft, 0, :concept => entity_type)
|
266
|
+
value_role = @constellation.Role(ft, 1, :concept => vt)
|
267
|
+
debug :mode, "Creating new fact type to identify #{name}"
|
268
|
+
end
|
269
|
+
|
270
|
+
# REVISIT: The restriction applies only to the value role. There is good reason to apply it above to the value type as well.
|
271
|
+
if (ranges = identification[:restriction])
|
272
|
+
value_role.role_value_restriction = value_restriction(ranges)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Forward reading, if it doesn't already exist:
|
276
|
+
rss = entity_role.all_role_ref.map{|rr| rr.role_sequence.all_role_ref.size == 2 ? rr.role_sequence : nil }.compact
|
277
|
+
# Find or create RoleSequences for the forward and reverse readings:
|
278
|
+
rs01 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [entity_role, value_role] }[0]
|
279
|
+
if !rs01
|
280
|
+
rs01 = @constellation.RoleSequence(:new)
|
281
|
+
@constellation.RoleRef(rs01, 0, :role => entity_role)
|
282
|
+
@constellation.RoleRef(rs01, 1, :role => value_role)
|
283
|
+
end
|
284
|
+
if rs01.all_reading.empty?
|
285
|
+
@constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs01, :text => "{0} has {1}")
|
286
|
+
debug :mode, "Creating new forward reading '#{name} has #{vt.name}'"
|
287
|
+
else
|
288
|
+
debug :mode, "Using existing forward reading"
|
289
|
+
end
|
290
|
+
|
291
|
+
# Reverse reading:
|
292
|
+
rs10 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [value_role, entity_role] }[0]
|
293
|
+
if !rs10
|
294
|
+
rs10 = @constellation.RoleSequence(:new)
|
295
|
+
@constellation.RoleRef(rs10, 0, :role => value_role)
|
296
|
+
@constellation.RoleRef(rs10, 1, :role => entity_role)
|
297
|
+
end
|
298
|
+
if rs10.all_reading.empty?
|
299
|
+
@constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs10, :text => "{0} is of {1}")
|
300
|
+
debug :mode, "Creating new reverse reading '#{vt.name} is of #{name}'"
|
301
|
+
else
|
302
|
+
debug :mode, "Using existing reverse reading"
|
303
|
+
end
|
304
|
+
|
305
|
+
# Entity Type must have a value type. Find or create the role sequence, then create a PC if necessary
|
306
|
+
debug :mode, "entity_role has #{entity_role.all_role_ref.size} attached sequences"
|
307
|
+
debug :mode, "entity_role has #{entity_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}.size} unary sequences"
|
308
|
+
rs0 = entity_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1 ? rr.role_sequence : nil }.compact[0]
|
309
|
+
if !rs0
|
310
|
+
rs0 = @constellation.RoleSequence(:new)
|
311
|
+
@constellation.RoleRef(rs0, 0, :role => entity_role)
|
312
|
+
debug :mode, "Creating new EntityType role sequence"
|
313
|
+
else
|
314
|
+
rs0 = rs0.role_sequence
|
315
|
+
debug :mode, "Using existing EntityType role sequence"
|
316
|
+
end
|
317
|
+
if (rs0.all_presence_constraint.size == 0)
|
318
|
+
@constellation.PresenceConstraint(
|
319
|
+
:new,
|
320
|
+
:name => '',
|
321
|
+
:enforcement => '',
|
322
|
+
:vocabulary => @vocabulary,
|
323
|
+
:role_sequence => rs0,
|
324
|
+
:min_frequency => 1,
|
325
|
+
:max_frequency => 1,
|
326
|
+
:is_preferred_identifier => false,
|
327
|
+
:is_mandatory => true
|
328
|
+
)
|
329
|
+
debug :mode, "Creating new EntityType PresenceConstraint"
|
330
|
+
else
|
331
|
+
debug :mode, "Using existing EntityType PresenceConstraint"
|
332
|
+
end
|
333
|
+
|
334
|
+
# Value Type must have a value type. Find or create the role sequence, then create a PC if necessary
|
335
|
+
debug :mode, "value_role has #{value_role.all_role_ref.size} attached sequences"
|
336
|
+
debug :mode, "value_role has #{value_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}.size} unary sequences"
|
337
|
+
rs1 = value_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1 ? rr.role_sequence : nil }.compact[0]
|
338
|
+
if (!rs1)
|
339
|
+
rs1 = @constellation.RoleSequence(:new)
|
340
|
+
@constellation.RoleRef(rs1, 0, :role => value_role)
|
341
|
+
debug :mode, "Creating new ValueType role sequence"
|
342
|
+
else
|
343
|
+
rs1 = rs1.role_sequence
|
344
|
+
debug :mode, "Using existing ValueType role sequence"
|
345
|
+
end
|
346
|
+
if (rs1.all_presence_constraint.size == 0)
|
347
|
+
@constellation.PresenceConstraint(
|
348
|
+
:new,
|
349
|
+
:name => '',
|
350
|
+
:enforcement => '',
|
351
|
+
:vocabulary => @vocabulary,
|
352
|
+
:role_sequence => rs1,
|
353
|
+
:min_frequency => 0,
|
354
|
+
:max_frequency => 1,
|
355
|
+
:is_preferred_identifier => true,
|
356
|
+
:is_mandatory => false
|
357
|
+
)
|
358
|
+
debug :mode, "Creating new ValueType PresenceConstraint"
|
359
|
+
else
|
360
|
+
debug :mode, "Marking existing ValueType PresenceConstraint as preferred"
|
361
|
+
rs1.all_presence_constraint[0].is_preferred_identifier = true
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
else
|
366
|
+
# identification must be inherited.
|
367
|
+
debug "Identification is inherited"
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def add_supertype(entity_type, supertype_name, identifying_supertype, mapping_pragmas)
|
373
|
+
debug :supertype, "Supertype #{supertype_name}"
|
374
|
+
supertype = @constellation.EntityType(@vocabulary, supertype_name)
|
375
|
+
inheritance_fact = @constellation.TypeInheritance(entity_type, supertype, :fact_type_id => :new)
|
376
|
+
|
377
|
+
assimilations = mapping_pragmas.select { |p| ['absorbed', 'separate', 'partitioned'].include? p}
|
378
|
+
raise "Conflicting assimilation pragmas #{assimilations*", "}" if assimilations.size > 1
|
379
|
+
inheritance_fact.assimilation = assimilations[0]
|
380
|
+
|
381
|
+
# Create a reading:
|
382
|
+
sub_role = @constellation.Role(inheritance_fact, 0, :concept => entity_type)
|
383
|
+
super_role = @constellation.Role(inheritance_fact, 1, :concept => supertype)
|
384
|
+
|
385
|
+
rs = @constellation.RoleSequence(:new)
|
386
|
+
@constellation.RoleRef(rs, 0, :role => sub_role)
|
387
|
+
@constellation.RoleRef(rs, 1, :role => super_role)
|
388
|
+
@constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :text => "{0} is a kind of {1}")
|
389
|
+
@constellation.Reading(inheritance_fact, 1, :role_sequence => rs, :text => "{0} is a subtype of {1}")
|
390
|
+
|
391
|
+
rs2 = @constellation.RoleSequence(:new)
|
392
|
+
@constellation.RoleRef(rs2, 0, :role => super_role)
|
393
|
+
@constellation.RoleRef(rs2, 1, :role => sub_role)
|
394
|
+
n = 'aeiouh'.include?(sub_role.concept.name.downcase[0]) ? 1 : 0
|
395
|
+
@constellation.Reading(inheritance_fact, 2+n, :role_sequence => rs2, :text => "{0} is a {1}")
|
396
|
+
@constellation.Reading(inheritance_fact, 3-n, :role_sequence => rs2, :text => "{0} is an {1}")
|
397
|
+
|
398
|
+
if identifying_supertype
|
399
|
+
inheritance_fact.provides_identification = true
|
400
|
+
end
|
401
|
+
|
402
|
+
# Create uniqueness constraints over the subtyping fact type
|
403
|
+
p1rs = @constellation.RoleSequence(:new)
|
404
|
+
@constellation.RoleRef(p1rs, 0).role = sub_role
|
405
|
+
pc1 = @constellation.PresenceConstraint(:new)
|
406
|
+
pc1.name = "#{entity_type.name}MustHaveSupertype#{supertype.name}"
|
407
|
+
pc1.vocabulary = @vocabulary
|
408
|
+
pc1.role_sequence = p1rs
|
409
|
+
pc1.is_mandatory = true # A subtype instance must have a supertype instance
|
410
|
+
pc1.min_frequency = 1
|
411
|
+
pc1.max_frequency = 1
|
412
|
+
pc1.is_preferred_identifier = false
|
413
|
+
|
414
|
+
# The supertype role often identifies the subtype:
|
415
|
+
p2rs = @constellation.RoleSequence(:new)
|
416
|
+
@constellation.RoleRef(p2rs, 0).role = super_role
|
417
|
+
pc2 = @constellation.PresenceConstraint(:new)
|
418
|
+
pc2.name = "#{supertype.name}MayBeA#{entity_type.name}"
|
419
|
+
pc2.vocabulary = @vocabulary
|
420
|
+
pc2.role_sequence = p2rs
|
421
|
+
pc2.is_mandatory = false
|
422
|
+
pc2.min_frequency = 0
|
423
|
+
pc2.max_frequency = 1
|
424
|
+
pc2.is_preferred_identifier = inheritance_fact.provides_identification
|
425
|
+
end
|
426
|
+
|
427
|
+
def unit params
|
428
|
+
singular = params[:singular]
|
429
|
+
plural = params[:plural]
|
430
|
+
base_units = params[:base]
|
431
|
+
denominator = params[:coefficient][:denominator]
|
432
|
+
numerator = params[:coefficient][:numerator]
|
433
|
+
offset = params[:offset]
|
434
|
+
approximately = params[:approximately]
|
435
|
+
ephemeral = params[:ephemeral]
|
436
|
+
|
437
|
+
if (numerator.to_f / denominator.to_i != 1.0)
|
438
|
+
coefficient = @constellation.Coefficient(
|
439
|
+
:numerator => numerator.to_f,
|
440
|
+
:denominator => denominator.to_i,
|
441
|
+
:is_precise => !approximately
|
442
|
+
)
|
443
|
+
else
|
444
|
+
coefficient = nil
|
445
|
+
end
|
446
|
+
offset = offset.to_f
|
447
|
+
offset = nil if offset == 0
|
448
|
+
debug :units, "Defining new unit #{singular}#{plural ? "/"+plural : ""}" do
|
449
|
+
debug :units, "Coefficient is #{coefficient.numerator}#{coefficient.denominator != 1 ? "/#{coefficient.denominator}" : ""} #{coefficient.is_precise ? "exactly" : "approximately"}" if coefficient
|
450
|
+
debug :units, "Offset is #{offset}" if offset
|
451
|
+
raise "Redefinition of unit #{singular}" if @constellation.Unit.values.detect{|u| u.name == singular}
|
452
|
+
raise "Redefinition of unit #{plural}" if @constellation.Unit.values.detect{|u| u.name == plural}
|
453
|
+
unit = @constellation.Unit(:new,
|
454
|
+
:name => singular,
|
455
|
+
# :plural => plural,
|
456
|
+
:coefficient => coefficient,
|
457
|
+
:offset => offset,
|
458
|
+
:is_fundamental => base_units.empty?,
|
459
|
+
:is_ephemeral => ephemeral,
|
460
|
+
:vocabulary => @vocabulary
|
461
|
+
)
|
462
|
+
base_units.each do |base_unit, exponent|
|
463
|
+
base = @constellation.Unit.values.detect{|u| u.name == base_unit}
|
464
|
+
debug :units, "Base unit #{base_unit}^#{exponent} #{base ? "" : "(implicitly fundamental)"}"
|
465
|
+
base ||= @constellation.Unit(:new, :name => base_unit, :is_fundamental => true, :vocabulary => @vocabulary)
|
466
|
+
@constellation.Derivation(:derived_unit => unit, :base_unit => base, :exponent => exponent)
|
467
|
+
end
|
468
|
+
if plural
|
469
|
+
plural_unit = @constellation.Unit(:new,
|
470
|
+
:name => plural,
|
471
|
+
:is_fundamental => false,
|
472
|
+
:vocabulary => @vocabulary
|
473
|
+
)
|
474
|
+
@constellation.Derivation(:derived_unit => plural_unit, :base_unit => unit, :exponent => 1)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
# If one of the words is the name of the entity type, and the other
|
480
|
+
# words consist of a unary fact type reading, return the role it plays.
|
481
|
+
def bind_unary_fact_type(entity_type, words)
|
482
|
+
return nil unless i = words.index(entity_type.name)
|
483
|
+
|
484
|
+
to_match = words.clone
|
485
|
+
to_match[i] = '{0}'
|
486
|
+
to_match = to_match*' '
|
487
|
+
|
488
|
+
# See if any unary fact type of this or any supertype matches these words:
|
489
|
+
entity_type.supertypes_transitive.each do |supertype|
|
490
|
+
supertype.all_role.each do |role|
|
491
|
+
role.fact_type.all_role.size == 1 &&
|
492
|
+
role.fact_type.all_reading.each do |reading|
|
493
|
+
if reading.text == to_match
|
494
|
+
debug :identification, "Bound identification to unary role '#{to_match.sub(/\{0\}/, entity_type.name)}'"
|
495
|
+
return role
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
nil
|
501
|
+
end
|
502
|
+
|
503
|
+
def fact_type(name, clauses, conditions)
|
504
|
+
debug "Processing clauses for fact type" do
|
505
|
+
fact_type = nil
|
506
|
+
|
507
|
+
#
|
508
|
+
# The first step is to find all role references and definitions in the phrases
|
509
|
+
# This also:
|
510
|
+
# * deletes any adjectives that were used but not hyphenated
|
511
|
+
# * changes each linking word phrase into a simple String
|
512
|
+
# * adds a :binding key to each bound role
|
513
|
+
#
|
514
|
+
@symbols = SymbolTable.new(@constellation, @vocabulary)
|
515
|
+
@symbols.bind_roles_in_clauses(clauses)
|
516
|
+
|
517
|
+
clauses.each do |clause|
|
518
|
+
kind, qualifiers, phrases, context = *clause
|
519
|
+
|
520
|
+
fact_type, r = *bind_fact_reading(fact_type, qualifiers, phrases)
|
521
|
+
end
|
522
|
+
|
523
|
+
# The fact type has a name iff it's objectified as an entity type
|
524
|
+
#puts "============= Creating entity type #{name} to nominalize fact type #{fact_type.default_reading} ======================" if name
|
525
|
+
fact_type.entity_type = @constellation.EntityType(@vocabulary, name) if name
|
526
|
+
|
527
|
+
# Add the identifying PresenceConstraint for this fact type:
|
528
|
+
if fact_type.all_role.size == 1 && !fact_type.entity_type
|
529
|
+
# All is well, unaries don't need an identifying PC unless objectified
|
530
|
+
else
|
531
|
+
fact_type_identification(fact_type, name, true)
|
532
|
+
end
|
533
|
+
|
534
|
+
# REVISIT: Process the fact derivation conditions, if any
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
def fact(population_name, clauses)
|
539
|
+
debug "Processing clauses for fact" do
|
540
|
+
population_name ||= ''
|
541
|
+
population = @constellation.Population(@vocabulary, population_name)
|
542
|
+
@symbols = SymbolTable.new(@constellation, @vocabulary)
|
543
|
+
@symbols.bind_roles_in_clauses(clauses)
|
544
|
+
|
545
|
+
bound_instances = {} # Instances indexed by binding
|
546
|
+
facts =
|
547
|
+
clauses.map do |clause|
|
548
|
+
kind, qualifiers, phrases, context = *clause
|
549
|
+
# Every bound word (term) in the phrases must have a literal
|
550
|
+
# OR be bound to an entity type identified by the phrases
|
551
|
+
# Any clause that has one binding and no other word is a value instance or simply-identified entity type
|
552
|
+
phrases.map! do |phrase|
|
553
|
+
next phrase unless l = phrase[:literal]
|
554
|
+
binding = phrase[:binding]
|
555
|
+
debug :instance, "Making #{binding.concept.class.basename} #{binding.concept.name} using #{l.inspect}" do
|
556
|
+
bound_instances[binding] =
|
557
|
+
instance_identified_by_literal(population, binding.concept, l)
|
558
|
+
end
|
559
|
+
phrase
|
560
|
+
end
|
561
|
+
|
562
|
+
if phrases.size == 1 && Hash === (phrase = phrases[0])
|
563
|
+
binding = phrase[:binding]
|
564
|
+
l = phrase[:literal]
|
565
|
+
debug :instance, "Making(2) #{binding.concept.class.basename} #{binding.concept.name} using #{l.inspect}" do
|
566
|
+
bound_instances[binding] =
|
567
|
+
instance_identified_by_literal(population, binding.concept, l)
|
568
|
+
end
|
569
|
+
else
|
570
|
+
[phrases, *bind_fact_reading(nil, qualifiers, phrases)]
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
# Because the fact types may include forward references, we must process the list repeatedly
|
575
|
+
# until we make no further progress. Any remaining
|
576
|
+
progress = true
|
577
|
+
pass = 0
|
578
|
+
while progress
|
579
|
+
progress = false
|
580
|
+
pass += 1
|
581
|
+
debug :instance, "Pass #{pass}" do
|
582
|
+
facts.map! do |fact|
|
583
|
+
next fact unless fact.is_a?(Array)
|
584
|
+
phrases, fact_type, reading = *fact
|
585
|
+
|
586
|
+
# This is a fact type we bound; see if we can create the fact instance yet
|
587
|
+
|
588
|
+
bare_roles = phrases.select{|w| w.is_a?(Hash) && !w[:literal] && !bound_instances[w[:binding]]}
|
589
|
+
# REVISIT: Bare bindings might be bound to instances we created
|
590
|
+
|
591
|
+
debug :instance, "Considering '#{fact_type.preferred_reading.expand}' with bare roles: #{bare_roles.map{|role| role[:binding].concept.name}*", "} "
|
592
|
+
|
593
|
+
case
|
594
|
+
when bare_roles.size == 0
|
595
|
+
debug :instance, "All bindings in '#{fact_type.preferred_reading.expand}' contain instances; create the fact type"
|
596
|
+
instances = phrases.select{|p| p.is_a?(Hash)}.map{|p| bound_instances[p[:binding]]}
|
597
|
+
debug :instance, "Instances are #{instances.map{|i| "#{i.concept.name} #{i.value.inspect}"}*", "}"
|
598
|
+
|
599
|
+
# Check that this fact doesn't already exist
|
600
|
+
fact = fact_type.all_fact.detect{|f|
|
601
|
+
# Get the role values of this fact in the order of the reading we just bound
|
602
|
+
role_values_in_reading_order = f.all_role_value.sort_by do |rv|
|
603
|
+
reading.role_sequence.all_role_ref.detect{|rr| rr.role == rv.role}.ordinal
|
604
|
+
end
|
605
|
+
# If all this fact's role values are played by the bound instances, it's the same fact
|
606
|
+
!role_values_in_reading_order.zip(instances).detect{|rv, i| rv.instance != i }
|
607
|
+
}
|
608
|
+
unless fact
|
609
|
+
fact = @constellation.Fact(:new, :fact_type => fact_type, :population => population)
|
610
|
+
@constellation.Instance(:new, :concept => fact_type.entity_type, :fact => fact, :population => population)
|
611
|
+
reading.role_sequence.all_role_ref.zip(instances).each do |rr, instance|
|
612
|
+
debug :instance, "New fact has #{instance.concept.name} role #{instance.value.inspect}"
|
613
|
+
@constellation.RoleValue(:fact => fact, :instance => instance, :role => rr.role, :population => population)
|
614
|
+
end
|
615
|
+
else
|
616
|
+
debug :instance, "Found existing fact type instance"
|
617
|
+
end
|
618
|
+
progress = true
|
619
|
+
next fact
|
620
|
+
|
621
|
+
# If we have one bare role (no literal or instance) played by an entity type,
|
622
|
+
# and the bound fact type participates in the identifier, we might now be able
|
623
|
+
# to create the entity instance.
|
624
|
+
when bare_roles.size == 1 &&
|
625
|
+
(binding = bare_roles[0][:binding]) &&
|
626
|
+
(e = binding.concept).is_a?(ActiveFacts::Metamodel::EntityType) &&
|
627
|
+
e.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type == fact_type}
|
628
|
+
|
629
|
+
# Check this instance doesn't already exist already:
|
630
|
+
identifying_binding = (phrases.select{|p| Hash === p}.map{|p|p[:binding]}-[binding])[0]
|
631
|
+
identifying_instance = bound_instances[identifying_binding]
|
632
|
+
|
633
|
+
debug :instance, "This clause associates a new #{binding.concept.name} with a #{identifying_binding.concept.name}#{identifying_instance ? " which exists" : ""}"
|
634
|
+
|
635
|
+
identifying_role_ref = e.preferred_identifier.role_sequence.all_role_ref.detect { |rr|
|
636
|
+
rr.role.fact_type == fact_type && rr.role.concept == identifying_binding.concept
|
637
|
+
}
|
638
|
+
unless identifying_role_ref
|
639
|
+
debug :instance, "Failed to find a #{identifying_instance.concept.name}"
|
640
|
+
next fact # We can't do this yet
|
641
|
+
end
|
642
|
+
role_value = identifying_instance.all_role_value.detect do |rv|
|
643
|
+
rv.fact.fact_type == identifying_role_ref.role.fact_type
|
644
|
+
end
|
645
|
+
if role_value
|
646
|
+
instance = (role_value.fact.all_role_value.to_a-[role_value])[0].instance
|
647
|
+
debug :instance, "Found existing instance (of #{instance.concept.name}) from a previous definition"
|
648
|
+
bound_instances[binding] = instance
|
649
|
+
progress = true
|
650
|
+
next role_value.instance
|
651
|
+
end
|
652
|
+
|
653
|
+
pi_role_refs = e.preferred_identifier.role_sequence.all_role_ref
|
654
|
+
# For each pi role, we have to find the fact clause, which contains the binding we need.
|
655
|
+
# Then we have to create an instance of each fact
|
656
|
+
identifiers =
|
657
|
+
pi_role_refs.map do |rr|
|
658
|
+
fact_a = facts.detect{|f| f.is_a?(Array) && f[1] == rr.role.fact_type}
|
659
|
+
identifying_binding = fact_a[0].detect{|phrase| phrase.is_a?(Hash) && phrase[:binding] != binding}[:binding]
|
660
|
+
identifying_instance = bound_instances[identifying_binding]
|
661
|
+
|
662
|
+
[rr, fact_a, identifying_binding, identifying_instance]
|
663
|
+
end
|
664
|
+
if identifiers.detect{ |i| !i[3] } # Not all required facts are bound yet
|
665
|
+
debug :instance, "Can't go through with creating #{binding.concept.name}; not all the facts are in"
|
666
|
+
next fact
|
667
|
+
end
|
668
|
+
|
669
|
+
debug :instance, "Going ahead with creating #{binding.concept.name} using #{identifiers.size} roles"
|
670
|
+
instance = @constellation.Instance(:new, :concept => e, :population => population)
|
671
|
+
bound_instances[binding] = instance
|
672
|
+
identifiers.each do |rr, fact_a, identifying_binding, identifying_instance|
|
673
|
+
# This reading provides the identifying literal for the EntityType e
|
674
|
+
id_fact = @constellation.Fact(:new, :fact_type => rr.role.fact_type, :population => population)
|
675
|
+
role = (rr.role.fact_type.all_role.to_a-[rr.role])[0]
|
676
|
+
@constellation.RoleValue(:instance => instance, :fact => id_fact, :population => population, :role => role)
|
677
|
+
@constellation.RoleValue(:instance => identifying_instance, :fact => id_fact, :role => rr.role, :population => population)
|
678
|
+
true
|
679
|
+
end
|
680
|
+
|
681
|
+
progress = true
|
682
|
+
end
|
683
|
+
fact
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|
687
|
+
incomplete = facts.select{|ft| !ft.is_a?(ActiveFacts::Metamodel::Instance) && !ft.is_a?(ActiveFacts::Metamodel::Fact)}
|
688
|
+
if incomplete.size > 0
|
689
|
+
# Provide a readable description of the problem here, by showing each binding with no instance
|
690
|
+
missing_bindings = incomplete.map do |f|
|
691
|
+
phrases = f[0]
|
692
|
+
phrases.select{|p|
|
693
|
+
p.is_a?(Hash) and binding = p[:binding] and !bound_instances[binding]
|
694
|
+
}.map{|phrase| phrase[:binding]}
|
695
|
+
end.flatten.uniq
|
696
|
+
raise "Not enough facts are given to identify #{
|
697
|
+
missing_bindings.map do |b|
|
698
|
+
[ b.leading_adjective, b.concept.name, b.trailing_adjective ].compact*" " +
|
699
|
+
" (need #{b.concept.preferred_identifier.role_sequence.all_role_ref.map do |rr|
|
700
|
+
[ rr.leading_adjective, rr.role.role_name || rr.role.concept.name, rr.trailing_adjective ].compact*" "
|
701
|
+
end*", "
|
702
|
+
})"
|
703
|
+
end*", "
|
704
|
+
}"
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
def entity_identified_by_literal(population, concept, literal)
|
710
|
+
# A literal that identifies an entity type means the entity type has only one identifying role
|
711
|
+
# That role is played either by a value type, or by another similarly single-identified entity type
|
712
|
+
debug "Making EntityType #{concept.name} identified by '#{literal}' #{population.name.size>0 ? " in "+population.name.inspect : ''}" do
|
713
|
+
identifying_role_refs = concept.preferred_identifier.role_sequence.all_role_ref
|
714
|
+
raise "Single literal cannot satisfy multiple identifying roles for #{concept.name}" if identifying_role_refs.size > 1
|
715
|
+
role = identifying_role_refs.single.role
|
716
|
+
identifying_instance = instance_identified_by_literal(population, role.concept, literal)
|
717
|
+
existing_instance = nil
|
718
|
+
instance_rv = identifying_instance.all_role_value.detect { |rv|
|
719
|
+
next false unless rv.population == population # Not this population
|
720
|
+
next false unless rv.fact.fact_type == role.fact_type # Not this fact type
|
721
|
+
other_role_value = (rv.fact.all_role_value-[rv])[0]
|
722
|
+
existing_instance = other_role_value.instance
|
723
|
+
other_role_value.instance.concept == concept # Is it this concept?
|
724
|
+
}
|
725
|
+
if instance_rv
|
726
|
+
instance = existing_instance
|
727
|
+
debug :instance, "This #{concept.name} entity already exists"
|
728
|
+
else
|
729
|
+
fact = @constellation.Fact(:new, :fact_type => role.fact_type, :population => population)
|
730
|
+
instance = @constellation.Instance(:new, :concept => concept, :population => population)
|
731
|
+
# The identifying fact type has two roles; create both role instances:
|
732
|
+
@constellation.RoleValue(:instance => identifying_instance, :fact => fact, :population => population, :role => role)
|
733
|
+
@constellation.RoleValue(:instance => instance, :fact => fact, :population => population, :role => (role.fact_type.all_role-[role])[0])
|
734
|
+
end
|
735
|
+
instance
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
def instance_identified_by_literal(population, concept, literal)
|
740
|
+
if concept.is_a?(ActiveFacts::Metamodel::EntityType)
|
741
|
+
entity_identified_by_literal(population, concept, literal)
|
742
|
+
else
|
743
|
+
debug :instance, "Making ValueType #{concept.name} #{literal.inspect} #{population.name.size>0 ? " in "+population.name.inspect : ''}" do
|
744
|
+
|
745
|
+
is_a_string = String === literal
|
746
|
+
instance = @constellation.Instance.detect do |key, i|
|
747
|
+
# REVISIT: And same unit
|
748
|
+
i.population == population &&
|
749
|
+
i.value &&
|
750
|
+
i.value.literal == literal &&
|
751
|
+
i.value.is_a_string == is_a_string
|
752
|
+
end
|
753
|
+
#instance = concept.all_instance.detect { |instance|
|
754
|
+
# instance.population == population && instance.value == literal
|
755
|
+
#}
|
756
|
+
debug :instance, "This #{concept.name} value already exists" if instance
|
757
|
+
unless instance
|
758
|
+
instance = @constellation.Instance(
|
759
|
+
:new,
|
760
|
+
:concept => concept,
|
761
|
+
:population => population,
|
762
|
+
:value => [literal.to_s, is_a_string, nil]
|
763
|
+
)
|
764
|
+
end
|
765
|
+
instance
|
766
|
+
end
|
767
|
+
end
|
768
|
+
end
|
769
|
+
|
770
|
+
def constraint *value
|
771
|
+
case type = value.shift
|
772
|
+
when :presence
|
773
|
+
presence_constraint *value
|
774
|
+
when :set
|
775
|
+
set_constraint *value
|
776
|
+
when :subset
|
777
|
+
subset_constraint *value
|
778
|
+
when :equality
|
779
|
+
equality_constraint *value
|
780
|
+
else
|
781
|
+
$stderr.puts "REVISIT: external #{type} constraints aren't yet handled:\n\t"+value.map{|a| a.inspect }*"\n\t"
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
# The joins list is an array of an array of fact types.
|
786
|
+
# The fact types contain roles played by concepts, where each
|
787
|
+
# concept plays more than one role. In fact, a concept may
|
788
|
+
# occur in more than one binding, and each binding plays more
|
789
|
+
# than one role. The bindings that are common to all fact types
|
790
|
+
# in each array in the joins list form the constrained role
|
791
|
+
# sequences. Each binding that isn't common at this top level
|
792
|
+
# must occur more than once in each group of fact types where
|
793
|
+
# it appears, and it forms a join between those fact types.
|
794
|
+
def bind_joins_as_role_sequences(joins_list)
|
795
|
+
@symbols = SymbolTable.new(@constellation, @vocabulary)
|
796
|
+
fact_roles_list = []
|
797
|
+
bindings_list = []
|
798
|
+
joins_list.each_with_index do |joins, index|
|
799
|
+
# joins is an array of phrase arrays, each for one reading
|
800
|
+
@symbols.bind_roles_in_phrases_list(joins)
|
801
|
+
|
802
|
+
fact_roles_list << joins.map do |phrases|
|
803
|
+
ifr = invoked_fact_roles(phrases)
|
804
|
+
raise "Fact type reading not found for #{phrases.inspect}" unless ifr
|
805
|
+
ifr
|
806
|
+
end
|
807
|
+
bindings_list << joins.map do |phrases|
|
808
|
+
phrases.map{ |phrase| Hash === phrase ? phrase[:binding] : nil}.compact
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
# Each set of binding arrays in the list must share at least one common binding
|
813
|
+
bindings_by_join = bindings_list.map{|join| join.flatten}
|
814
|
+
common_bindings = bindings_by_join[1..-1].inject(bindings_by_join[0]) { |c, b| c & b }
|
815
|
+
# Was:
|
816
|
+
# common_bindings = bindings_list.inject(bindings_list[0]) { |common, bindings| common & bindings }
|
817
|
+
raise "Set constraints must have at least one common role between the sets" unless common_bindings.size > 0
|
818
|
+
|
819
|
+
# REVISIT: Do we need to constrain things such that each join path only includes *one* instance of each common binding?
|
820
|
+
|
821
|
+
# For each set of binding arrays, if there's more than one binding array in the set,
|
822
|
+
# it represents a join path. Here we check that each join path is complete, i.e. linked up.
|
823
|
+
# Each element of a join path is the array of bindings for a fact type invocation.
|
824
|
+
# Each invocation must share a binding (not one of the globally common ones) with
|
825
|
+
# another invocation in that join path.
|
826
|
+
bindings_list.each_with_index do |join, jpnum|
|
827
|
+
# Check that this bindings array creates a complete join path:
|
828
|
+
join.each_with_index do |bindings, i|
|
829
|
+
fact_type_roles = fact_roles_list[jpnum][i]
|
830
|
+
fact_type = fact_type_roles[0].fact_type
|
831
|
+
|
832
|
+
# The bindings are for one fact type invocation.
|
833
|
+
# These bindings must be joined to some later fact type by a common binding that isn't a globally-common one:
|
834
|
+
local_bindings = bindings-common_bindings
|
835
|
+
next if local_bindings.size == 0 # No join path is required, as only one fact type is invoked.
|
836
|
+
next if i == join.size-1 # We already checked that the last fact type invocation is joined
|
837
|
+
ok = local_bindings.detect do |local_binding|
|
838
|
+
j = i+1
|
839
|
+
join[j..-1].detect do |other_bindings|
|
840
|
+
other_fact_type_roles = fact_roles_list[jpnum][j]
|
841
|
+
other_fact_type = other_fact_type_roles[0].fact_type
|
842
|
+
j += 1
|
843
|
+
# These next two lines allow joining from/to an objectified fact type:
|
844
|
+
fact_type_roles.detect{|r| r.concept == other_fact_type.entity_type } ||
|
845
|
+
other_fact_type_roles.detect{|r| r.concept == fact_type.entity_type } ||
|
846
|
+
other_bindings.include?(local_binding)
|
847
|
+
end
|
848
|
+
end
|
849
|
+
raise "Incomplete join path; one of the bindings #{local_bindings.inspect} must re-occur to establish a join" unless ok
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
# Create the role sequences and their role references.
|
854
|
+
# Each role sequence contain one RoleRef for each common binding
|
855
|
+
# REVISIT: This results in ordering all RoleRefs according to the order of the common_bindings.
|
856
|
+
# This for example means that a set constraint having joins might have the join order changed so they all match.
|
857
|
+
# When you create e.g. a subset constraint in NORMA, make sure that the subset roles are created in the order of the preferred readings.
|
858
|
+
role_sequences = joins_list.map{|r| @constellation.RoleSequence(:new) }
|
859
|
+
common_bindings.each_with_index do |binding, index|
|
860
|
+
role_sequences.each_with_index do |rs, rsi|
|
861
|
+
join = bindings_list[rsi]
|
862
|
+
fact_pos = nil
|
863
|
+
join_pos = (0...join.size).detect do |i|
|
864
|
+
fact_pos = join[i].index(binding)
|
865
|
+
end
|
866
|
+
@constellation.RoleRef(rs, index).role = fact_roles_list[rsi][join_pos][fact_pos]
|
867
|
+
end
|
868
|
+
end
|
869
|
+
|
870
|
+
role_sequences
|
871
|
+
end
|
872
|
+
|
873
|
+
def presence_constraint(constrained_role_names, quantifier, phrases_list, context)
|
874
|
+
raise "REVISIT: Join presence constraints not supported yet" if phrases_list[0].size > 1
|
875
|
+
phrases_list = phrases_list.map{|r| r[0] }
|
876
|
+
#p phrases_list
|
877
|
+
|
878
|
+
@symbols = SymbolTable.new(@constellation, @vocabulary)
|
879
|
+
|
880
|
+
# Find players for all constrained_role_names. These may use leading or trailing adjective forms...
|
881
|
+
constrained_players = []
|
882
|
+
constrained_bindings = []
|
883
|
+
constrained_role_names.each do |role_name|
|
884
|
+
player, binding = @symbols.bind(role_name)
|
885
|
+
constrained_players << player
|
886
|
+
constrained_bindings << binding
|
887
|
+
end
|
888
|
+
#puts "Constrained bindings are #{constrained_bindings.inspect}"
|
889
|
+
#puts "Constrained bindings object_id's are #{constrained_bindings.map{|b|b.object_id.to_s}*","}"
|
890
|
+
|
891
|
+
# Find players for all the concepts in all phrases_list:
|
892
|
+
@symbols.bind_roles_in_phrases_list(phrases_list)
|
893
|
+
|
894
|
+
constrained_roles = []
|
895
|
+
unmatched_roles = constrained_role_names.clone
|
896
|
+
phrases_list.each do |phrases|
|
897
|
+
# puts phrases.inspect
|
898
|
+
|
899
|
+
# If this succeeds, the phrases found matches the roles in our phrases
|
900
|
+
fact_roles = invoked_fact_roles(phrases)
|
901
|
+
raise "Fact type reading not found for #{phrases.inspect}" unless fact_roles
|
902
|
+
|
903
|
+
# Look for the constrained role(s); the bindings will be the same
|
904
|
+
matched_bindings = phrases.select{|p| Hash === p}.map{|p| p[:binding]}
|
905
|
+
#puts "matched_bindings = #{matched_bindings.inspect}"
|
906
|
+
#puts "matched_bindings object_id's are #{matched_bindings.map{|b|b.object_id.to_s}*","}}"
|
907
|
+
matched_bindings.each_with_index{|b, pos|
|
908
|
+
i = constrained_bindings.index(b)
|
909
|
+
next unless i
|
910
|
+
unmatched_roles[i] = nil
|
911
|
+
#puts "found #{constrained_bindings[i].inspect} found as #{b.inspect} in position #{i.inspect}"
|
912
|
+
role = fact_roles[pos]
|
913
|
+
constrained_roles << role unless constrained_roles.include?(role)
|
914
|
+
}
|
915
|
+
end
|
916
|
+
|
917
|
+
# Check that all constrained roles were matched at least once:
|
918
|
+
unmatched_roles.compact!
|
919
|
+
raise "Constrained roles #{unmatched_roles.map{|ur| ur*"-"}*", "} not found in fact types" if unmatched_roles.size != 0
|
920
|
+
|
921
|
+
rs = @constellation.RoleSequence(:new)
|
922
|
+
#puts "constrained_roles: #{constrained_roles.map{|r| r.concept.name}.inspect}"
|
923
|
+
constrained_roles.each_with_index do |role, index|
|
924
|
+
raise "Constrained role #{constrained_role_names[index]} not found" unless role
|
925
|
+
rr = @constellation.RoleRef(rs, index)
|
926
|
+
rr.role = role
|
927
|
+
end
|
928
|
+
#puts "New external PresenceConstraint with quantifier = #{quantifier.inspect} over #{rs.describe}"
|
929
|
+
|
930
|
+
# REVISIT: Check that no existing PC spans the same roles (nor a superset nor subset?)
|
931
|
+
|
932
|
+
@constellation.PresenceConstraint(
|
933
|
+
:new,
|
934
|
+
:name => '',
|
935
|
+
:enforcement => '',
|
936
|
+
:vocabulary => @vocabulary,
|
937
|
+
:role_sequence => rs,
|
938
|
+
:min_frequency => quantifier[0],
|
939
|
+
:max_frequency => quantifier[1],
|
940
|
+
:is_preferred_identifier => false,
|
941
|
+
:is_mandatory => quantifier[0] && quantifier[0] > 0
|
942
|
+
)
|
943
|
+
end
|
944
|
+
|
945
|
+
def set_constraint(constrained_roles, quantifier, joins_list, context)
|
946
|
+
role_sequences = bind_joins_as_role_sequences(joins_list)
|
947
|
+
|
948
|
+
if quantifier[1] == nil
|
949
|
+
# create a presence constraint instead if we get quantifier = [N,nil] (at least N)
|
950
|
+
# We massage the bound role sequences to make this work.
|
951
|
+
raise "either/or constraint must have one common role" if role_sequences.size != 2 || role_sequences[0].all_role_ref.size != 1
|
952
|
+
second_role = role_sequences[1].all_role_ref.single.role
|
953
|
+
second_role_ref = @constellation.RoleRef(:role_sequence => role_sequences[0], :ordinal => 1, :role => second_role)
|
954
|
+
@constellation.deny(role_sequences[1].all_role_ref.single)
|
955
|
+
@constellation.deny(role_sequences[1])
|
956
|
+
@constellation.PresenceConstraint(
|
957
|
+
:new,
|
958
|
+
:name => '',
|
959
|
+
:enforcement => '',
|
960
|
+
:vocabulary => @vocabulary,
|
961
|
+
:role_sequence => role_sequences[0],
|
962
|
+
:min_frequency => quantifier[0],
|
963
|
+
:max_frequency => nil,
|
964
|
+
:is_preferred_identifier => false,
|
965
|
+
:is_mandatory => true
|
966
|
+
)
|
967
|
+
else
|
968
|
+
# Create a normal (mandatory) exclusion constraint:
|
969
|
+
constraint = @constellation.SetExclusionConstraint(:new)
|
970
|
+
constraint.vocabulary = @vocabulary
|
971
|
+
role_sequences.each_with_index do |rs, i|
|
972
|
+
@constellation.SetComparisonRoles(constraint, i, :role_sequence => rs)
|
973
|
+
end
|
974
|
+
constraint.is_mandatory = quantifier[0] == 1
|
975
|
+
end
|
976
|
+
end
|
977
|
+
|
978
|
+
def subset_constraint(joins_list, context)
|
979
|
+
role_sequences = bind_joins_as_role_sequences(joins_list)
|
980
|
+
|
981
|
+
#puts "subset_constraint:\n\t#{subset_readings.inspect}\n\t#{superset_readings.inspect}"
|
982
|
+
#puts "\t#{role_sequences.map{|rs| rs.describe}.inspect}"
|
983
|
+
#puts "subset_role_sequence = #{role_sequences[0].describe}"
|
984
|
+
#puts "superset_role_sequence = #{role_sequences[1].describe}"
|
985
|
+
|
986
|
+
# create the constraint:
|
987
|
+
constraint = @constellation.SubsetConstraint(:new)
|
988
|
+
constraint.vocabulary = @vocabulary
|
989
|
+
#constraint.name = nil
|
990
|
+
#constraint.enforcement =
|
991
|
+
constraint.subset_role_sequence = role_sequences[0]
|
992
|
+
constraint.superset_role_sequence = role_sequences[1]
|
993
|
+
end
|
994
|
+
|
995
|
+
def equality_constraint(joins_list, context)
|
996
|
+
#puts "REVISIT: equality\n\t#{joins_list.map{|rl| rl.inspect}*"\n\tif and only if\n\t"}"
|
997
|
+
|
998
|
+
role_sequences = bind_joins_as_role_sequences(joins_list)
|
999
|
+
|
1000
|
+
# Create the constraint:
|
1001
|
+
constraint = @constellation.SetEqualityConstraint(:new)
|
1002
|
+
constraint.vocabulary = @vocabulary
|
1003
|
+
role_sequences.each_with_index do |rs, i|
|
1004
|
+
@constellation.SetComparisonRoles(constraint, i, :role_sequence => rs)
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
# Search the supertypes of 'subtype' looking for an inheritance path to 'supertype',
|
1009
|
+
# and returning the array of TypeInheritance fact types from supertype to subtype.
|
1010
|
+
def inheritance_path(subtype, supertype)
|
1011
|
+
direct_inheritance = subtype.all_supertype_inheritance.select{|ti| ti.supertype == supertype}
|
1012
|
+
return direct_inheritance if (direct_inheritance[0])
|
1013
|
+
subtype.all_supertype_inheritance.each{|ti|
|
1014
|
+
ip = inheritance_path(ti.supertype, supertype)
|
1015
|
+
return ip+[ti] if (ip)
|
1016
|
+
}
|
1017
|
+
return nil
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
# For a given phrase array from the parser, find the matching declared reading, and return
|
1021
|
+
# the array of Role object in the same order as they occur in the reading.
|
1022
|
+
def invoked_fact_roles(phrases)
|
1023
|
+
# REVISIT: Possibly this special reading from the parser can be removed now?
|
1024
|
+
if (phrases[0] == "!SUBTYPE!")
|
1025
|
+
subtype = phrases[1][:binding].concept
|
1026
|
+
supertype = phrases[2][:binding].concept
|
1027
|
+
raise "#{subtype.name} is not a subtype of #{supertype.name}" unless subtype.supertypes_transitive.include?(supertype)
|
1028
|
+
ip = inheritance_path(subtype, supertype)
|
1029
|
+
return [
|
1030
|
+
ip[-1].all_role.detect{|r| r.concept == subtype},
|
1031
|
+
ip[0].all_role.detect{|r| r.concept == supertype}
|
1032
|
+
]
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
bindings = phrases.select{|p| Hash === p}
|
1036
|
+
players = bindings.map{|p| p[:binding].concept }
|
1037
|
+
invoked_fact_roles_by_players(phrases, players)
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def invoked_fact_roles_by_players(phrases, players)
|
1041
|
+
players[0].all_role.each do |role|
|
1042
|
+
# Does this fact type have the right number of roles?
|
1043
|
+
next if role.fact_type.all_role.size != players.size
|
1044
|
+
|
1045
|
+
# Does this fact type include the correct other players?
|
1046
|
+
# REVISIT: Might need subtype/supertype matching here, with an implied subtyping join invocation
|
1047
|
+
next if role.fact_type.all_role.detect{|r| !players.include?(r.concept)}
|
1048
|
+
|
1049
|
+
# Oooh, a real candidate. Check the reading words.
|
1050
|
+
debug "Considering "+role.fact_type.describe do
|
1051
|
+
next unless role.fact_type.all_reading.detect do |candidate_reading|
|
1052
|
+
debug "Considering reading"+candidate_reading.text do
|
1053
|
+
to_match = phrases.clone
|
1054
|
+
players_to_match = players.clone
|
1055
|
+
candidate_reading.words_and_role_refs.each do |wrr|
|
1056
|
+
if (wrr.is_a?(ActiveFacts::Metamodel::RoleRef))
|
1057
|
+
break unless Hash === to_match.first
|
1058
|
+
break unless binding = to_match[0][:binding]
|
1059
|
+
# REVISIT: May need to match super- or sub-types here too!
|
1060
|
+
break unless players_to_match[0] == wrr.role.concept
|
1061
|
+
break if wrr.leading_adjective && binding.leading_adjective != wrr.leading_adjective
|
1062
|
+
break if wrr.trailing_adjective && binding.trailing_adjective != wrr.trailing_adjective
|
1063
|
+
|
1064
|
+
# All matched.
|
1065
|
+
to_match.shift
|
1066
|
+
players_to_match.shift
|
1067
|
+
# elsif # REVISIT: Match "not" and "none" here as negating the fact type invocation
|
1068
|
+
else
|
1069
|
+
break unless String === to_match[0]
|
1070
|
+
break unless to_match[0] == wrr
|
1071
|
+
to_match.shift
|
1072
|
+
end
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
# This is the first matching candidate.
|
1076
|
+
# REVISIT: Since we do sub/supertype matching (and will do more!),
|
1077
|
+
# we need to accumulate all possible matches to be sure
|
1078
|
+
# there's only one, or the match is exact, or risk ambiguity.
|
1079
|
+
debug "Reading match was #{to_match.size == 0 ? "ok" : "bad"}"
|
1080
|
+
return candidate_reading.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.role} if to_match.size == 0
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
# Hmm, that didn't work, try the subtypes of the first player.
|
1087
|
+
# When a fact type matches like this, there is an implied join to the subtype.
|
1088
|
+
players[0].subtypes.each do |subtype|
|
1089
|
+
players[0] = subtype
|
1090
|
+
fr = invoked_fact_roles_by_players(phrases, players)
|
1091
|
+
return fr if fr
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
# REVISIT: Do we need to do this again for the supertypes of the first player?
|
1095
|
+
|
1096
|
+
nil
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
def bind_fact_reading(fact_type, qualifiers, phrases)
|
1100
|
+
reading = debug :reading, "Processing reading #{phrases.inspect}" do
|
1101
|
+
role_phrases = phrases.select do |phrase|
|
1102
|
+
Hash === phrase && phrase[:binding]
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
# All readings for a fact type must have the same number of roles.
|
1106
|
+
# This might be relaxed later for fact clauses, where readings might
|
1107
|
+
# be concatenated if the adjacent items are the same concept.
|
1108
|
+
if (fact_type && fact_type.all_reading.size > 0 && role_phrases.size != fact_type.all_role.size)
|
1109
|
+
raise "#{
|
1110
|
+
role_phrases.size > fact_type.all_role.size ? "Too many" : "Not all"
|
1111
|
+
} roles found for non-initial reading of #{fact_type.describe}"
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
# If the reading is the first and is an invocation of an existing fact type,
|
1115
|
+
# find and return the existing fact type and reading.
|
1116
|
+
if !fact_type
|
1117
|
+
bindings = role_phrases.map{|phrase| phrase[:binding]}
|
1118
|
+
bindings_by_name = bindings.sort_by{|b| [b.concept.name, b.leading_adjective||'', b.trailing_adjective||'']}
|
1119
|
+
bound_concepts_by_name = bindings_by_name.map{|b| b.concept}
|
1120
|
+
reading = nil
|
1121
|
+
first_role = nil
|
1122
|
+
debug :reading, "Looking for existing fact type to match #{phrases.inspect}" do
|
1123
|
+
first_role =
|
1124
|
+
bindings[0].concept.all_role.detect do |role|
|
1125
|
+
next if role.fact_type.all_role.size != bindings.size # Wrong arity
|
1126
|
+
concepts = role.fact_type.all_role.map{|r| r.concept }
|
1127
|
+
next unless bound_concepts_by_name == concepts.sort_by{|c| c.name} # Wrong players
|
1128
|
+
matching_reading =
|
1129
|
+
role.fact_type.all_reading.detect do |reading|
|
1130
|
+
debug :reading, "Considering #{reading.expand}"
|
1131
|
+
reading_role_refs = reading.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}
|
1132
|
+
reading_concepts = reading_role_refs.map{|rr| rr.role.concept}
|
1133
|
+
elements = reading.text.scan(/\{[0-9]+\}|\w+/)
|
1134
|
+
next false if elements.zip(phrases).detect do |element, phrase|
|
1135
|
+
if element =~ /\A\{([0-9]+)\}\Z/ # Must be a role player; need a matching binding
|
1136
|
+
!phrase.is_a?(Hash) or
|
1137
|
+
!(binding = phrase[:binding]) or
|
1138
|
+
!(role_ref = reading_role_refs[$1.to_i]) or # If we fail here, it's an error!
|
1139
|
+
role_ref.role.concept != binding.concept or
|
1140
|
+
=begin
|
1141
|
+
# REVISIT: This loose matching fails on the Metamodel with RingConstraints.
|
1142
|
+
# Need "best match" semantics, or some way to know that these adjectives are "extra" to the readings.
|
1143
|
+
(la = role_ref.leading_adjective) && binding[:leading_adjective] != la or
|
1144
|
+
(ta = role_ref.trailing_adjective) && binding[:trailing_adjective] != ta
|
1145
|
+
=end
|
1146
|
+
role_ref.leading_adjective != binding[:leading_adjective] or
|
1147
|
+
role_ref.trailing_adjective != binding[:trailing_adjective]
|
1148
|
+
else
|
1149
|
+
element != phrase
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
debug :reading, "'#{reading.expand}' matches!"
|
1153
|
+
true # There was no mismatch
|
1154
|
+
end
|
1155
|
+
matching_reading # This role was in a matching fact type!
|
1156
|
+
end
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
if first_role
|
1160
|
+
fact_type = first_role.fact_type
|
1161
|
+
|
1162
|
+
# Remember the roles for each binding, for subsequent readings:
|
1163
|
+
reading.role_sequence.all_role_ref.each_with_index do |rr, index|
|
1164
|
+
@symbols.roles_by_binding[bindings[index]] = rr.role
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
return [fact_type, reading]
|
1168
|
+
end
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
fact_type ||= @constellation.FactType(:new)
|
1172
|
+
|
1173
|
+
# Create the roles on the first reading, or look them up on subsequent readings.
|
1174
|
+
# If the player occurs twice, we must find one with matching adjectives.
|
1175
|
+
|
1176
|
+
role_sequence = @constellation.RoleSequence(:new) # RoleSequence for RoleRefs of this reading
|
1177
|
+
roles = []
|
1178
|
+
role_phrases.each_with_index do |role_phrase, index|
|
1179
|
+
binding = role_phrase[:binding]
|
1180
|
+
role_name = role_phrase[:role_name]
|
1181
|
+
player = binding.concept
|
1182
|
+
role = nil
|
1183
|
+
if (fact_type.all_reading.size == 0) # First reading
|
1184
|
+
# Assert this role of the fact type:
|
1185
|
+
role = @constellation.Role(fact_type, fact_type.all_role.size, :concept => player)
|
1186
|
+
role.role_name = role_name if role_name
|
1187
|
+
debug "Concept #{player.name} found, created role #{role.describe} by binding #{binding.inspect}"
|
1188
|
+
@symbols.roles_by_binding[binding] = role
|
1189
|
+
else # Subsequent readings
|
1190
|
+
#debug "Looking for role #{binding.inspect} in bindings #{@symbols.roles_by_binding.inspect}"
|
1191
|
+
role = @symbols.roles_by_binding[binding]
|
1192
|
+
raise "Role #{binding.inspect} not found in prior readings" if !role
|
1193
|
+
player = role.concept
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
# Save a role value restriction
|
1197
|
+
if (ranges = role_phrase[:restriction])
|
1198
|
+
role.role_value_restriction = value_restriction(ranges)
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
roles << role
|
1202
|
+
|
1203
|
+
# Create the RoleRefs for the RoleSequence
|
1204
|
+
|
1205
|
+
role_ref = @constellation.RoleRef(role_sequence, index, :role => roles[index])
|
1206
|
+
leading_adjective = role_phrase[:leading_adjective]
|
1207
|
+
role_ref.leading_adjective = leading_adjective if leading_adjective
|
1208
|
+
trailing_adjective = role_phrase[:trailing_adjective]
|
1209
|
+
role_ref.trailing_adjective = trailing_adjective if trailing_adjective
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
# Create any embedded constraints:
|
1213
|
+
debug "Creating embedded presence constraints for #{fact_type.describe}" do
|
1214
|
+
create_embedded_presence_constraints(fact_type, role_phrases, roles)
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
process_qualifiers(role_sequence, qualifiers)
|
1218
|
+
|
1219
|
+
# Save the first role sequence to be used for a default PresenceConstraint
|
1220
|
+
add_reading(fact_type, role_sequence, phrases)
|
1221
|
+
end
|
1222
|
+
[fact_type, reading]
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
def fact_type_identification(fact_type, name, prefer)
|
1226
|
+
if !@symbols.embedded_presence_constraints.detect{|pc| pc.max_frequency == 1}
|
1227
|
+
# Provide a default identifier for a fact type that's lacking one (over all roles):
|
1228
|
+
first_role_sequence = fact_type.preferred_reading.role_sequence
|
1229
|
+
#puts "Creating PC for #{name}: #{fact_type.describe}"
|
1230
|
+
identifier = @constellation.PresenceConstraint(
|
1231
|
+
:new,
|
1232
|
+
:vocabulary => @vocabulary,
|
1233
|
+
:name => "#{name}PK", # Is this a useful name?
|
1234
|
+
:role_sequence => first_role_sequence,
|
1235
|
+
:is_preferred_identifier => prefer,
|
1236
|
+
:max_frequency => 1 # Unique
|
1237
|
+
)
|
1238
|
+
# REVISIT: The UC might be provided later as an external constraint, relax this rule:
|
1239
|
+
#raise "'#{fact_type.default_reading}': non-unary fact types having no uniqueness constraints must be objectified (named)" unless fact_type.entity_type
|
1240
|
+
debug "Made default fact type identifier #{identifier.object_id} over #{first_role_sequence.describe} in #{fact_type.describe}"
|
1241
|
+
elsif prefer
|
1242
|
+
#debug "Made fact type identifier #{identifier.object_id} preferred over #{@symbols.embedded_presence_constraints[0].role_sequence.describe} in #{fact_type.describe}"
|
1243
|
+
@symbols.embedded_presence_constraints[0].is_preferred_identifier = true
|
1244
|
+
end
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
# Categorise the fact type clauses according to the set of role player names
|
1248
|
+
# Return an array where each element is an array of clauses, the clauses having
|
1249
|
+
# matching players, and otherwise preserving the order of definition.
|
1250
|
+
def clauses_by_fact_type(clauses)
|
1251
|
+
clause_group_by_role_players = {}
|
1252
|
+
clauses.inject([]) do |clause_groups, clause|
|
1253
|
+
type, qualifiers, phrases, context = *clause
|
1254
|
+
|
1255
|
+
debug "Clause: #{clause.inspect}"
|
1256
|
+
roles = phrases.map do |phrase|
|
1257
|
+
Hash === phrase ? phrase[:binding] : nil
|
1258
|
+
end.compact
|
1259
|
+
|
1260
|
+
# Look for an existing clause group involving these players, or make one:
|
1261
|
+
clause_group = clause_group_by_role_players[key = roles.sort]
|
1262
|
+
if clause_group # Another clause for an existing clause group
|
1263
|
+
clause_group << clause
|
1264
|
+
else # A new clause group
|
1265
|
+
clause_groups << (clause_group_by_role_players[key] = [clause])
|
1266
|
+
end
|
1267
|
+
clause_groups
|
1268
|
+
end
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
# For each fact reading there may be embedded mandatory, uniqueness or frequency constraints:
|
1272
|
+
def create_embedded_presence_constraints(fact_type, role_phrases, roles)
|
1273
|
+
embedded_presence_constraints = []
|
1274
|
+
role_phrases.zip(roles).each_with_index do |role_pair, index|
|
1275
|
+
role_phrase, role = *role_pair
|
1276
|
+
|
1277
|
+
next unless quantifier = role_phrase[:quantifier]
|
1278
|
+
|
1279
|
+
debug "Processing embedded constraint #{quantifier.inspect} on #{role.concept.name} in #{fact_type.describe}" do
|
1280
|
+
constrained_roles = roles.clone
|
1281
|
+
constrained_roles.delete_at(index)
|
1282
|
+
constraint = find_pc_over_roles(constrained_roles)
|
1283
|
+
if constraint
|
1284
|
+
debug "Setting max frequency to #{quantifier[1]} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}"
|
1285
|
+
raise "Conflicting maximum frequency for constraint" if constraint.max_frequency && constraint.max_frequency != quantifier[1]
|
1286
|
+
constraint.max_frequency = quantifier[1]
|
1287
|
+
else
|
1288
|
+
role_sequence = @constellation.RoleSequence(:new)
|
1289
|
+
constrained_roles.each_with_index do |constrained_role, i|
|
1290
|
+
role_ref = @constellation.RoleRef(role_sequence, i, :role => constrained_role)
|
1291
|
+
end
|
1292
|
+
constraint = @constellation.PresenceConstraint(
|
1293
|
+
:new,
|
1294
|
+
:vocabulary => @vocabulary,
|
1295
|
+
:role_sequence => role_sequence,
|
1296
|
+
:is_mandatory => quantifier[0] && quantifier[0] > 0, # REVISIT: Check "maybe" qualifier?
|
1297
|
+
:max_frequency => quantifier[1],
|
1298
|
+
:min_frequency => quantifier[0]
|
1299
|
+
)
|
1300
|
+
embedded_presence_constraints << constraint
|
1301
|
+
debug "Made new PC min=#{quantifier[0].inspect} max=#{quantifier[1].inspect} constraint #{constraint.object_id} over #{(e = fact_type.entity_type) ? e.name : role_sequence.describe} in #{fact_type.describe}"
|
1302
|
+
end
|
1303
|
+
end
|
1304
|
+
end
|
1305
|
+
@symbols.embedded_presence_constraints += embedded_presence_constraints
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
def process_qualifiers(role_sequence, qualifiers)
|
1309
|
+
return unless qualifiers.size > 0
|
1310
|
+
qualifiers.sort!
|
1311
|
+
|
1312
|
+
# Process the ring constraints:
|
1313
|
+
ring_constraints, qualifiers = qualifiers.partition{|q| RingTypes.include?(q) }
|
1314
|
+
unless ring_constraints.empty?
|
1315
|
+
# A Ring may be over a supertype/subtype pair, and this won't find that.
|
1316
|
+
role_refs = Array(role_sequence.all_role_ref)
|
1317
|
+
role_pairs = []
|
1318
|
+
player_supertypes_by_role = role_refs.map{|rr|
|
1319
|
+
concept = rr.role.concept
|
1320
|
+
concept.is_a?(ActiveFacts::Metamodel::EntityType) ? supertypes(concept) : [concept]
|
1321
|
+
}
|
1322
|
+
role_refs.each_with_index{|rr1, i|
|
1323
|
+
player1 = rr1.role.concept
|
1324
|
+
(i+1...role_refs.size).each{|j|
|
1325
|
+
rr2 = role_refs[j]
|
1326
|
+
player2 = rr2.role.concept
|
1327
|
+
if player_supertypes_by_role[i] - player_supertypes_by_role[j] != player_supertypes_by_role[i]
|
1328
|
+
role_pairs << [rr1.role, rr2.role]
|
1329
|
+
end
|
1330
|
+
}
|
1331
|
+
}
|
1332
|
+
raise "ring constraint (#{ring_constraints*" "}) role pair not found" if role_pairs.size == 0
|
1333
|
+
raise "ring constraint (#{ring_constraints*" "}) is ambiguous over roles of #{role_pairs.map{|rp| rp.map{|r| r.concept.name}}.inspect}" if role_pairs.size > 1
|
1334
|
+
roles = role_pairs[0]
|
1335
|
+
|
1336
|
+
# Ensure that the keys in RingPairs follow others:
|
1337
|
+
ring_constraints = ring_constraints.partition{|rc| !RingPairs.keys.include?(rc.downcase.to_sym) }.flatten
|
1338
|
+
|
1339
|
+
if ring_constraints.size > 1 and !RingPairs[ring_constraints[-1].to_sym].include?(ring_constraints[0].to_sym)
|
1340
|
+
raise "incompatible ring constraint types (#{ring_constraints*", "})"
|
1341
|
+
end
|
1342
|
+
ring_type = ring_constraints.map{|c| c.capitalize}*""
|
1343
|
+
|
1344
|
+
ring = @constellation.RingConstraint(
|
1345
|
+
:new,
|
1346
|
+
:vocabulary => @vocabulary,
|
1347
|
+
# :name => name, # REVISIT: Create a name for Ring Constraints?
|
1348
|
+
:role => roles[0],
|
1349
|
+
:other_role => roles[1],
|
1350
|
+
:ring_type => ring_type
|
1351
|
+
)
|
1352
|
+
|
1353
|
+
debug "Added #{ring.verbalise} #{ring.class.roles.keys.map{|k|"#{k} => "+ring.send(k).verbalise}*", "}"
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
return unless qualifiers.size > 0
|
1357
|
+
|
1358
|
+
# Process the remaining qualifiers:
|
1359
|
+
puts "REVISIT: Qualifiers #{qualifiers.inspect} over #{role_sequence.describe}"
|
1360
|
+
end
|
1361
|
+
|
1362
|
+
def find_pc_over_roles(roles)
|
1363
|
+
return nil if roles.size == 0 # Safeguard; this would chuck an exception otherwise
|
1364
|
+
roles[0].all_role_ref.each do |role_ref|
|
1365
|
+
next if role_ref.role_sequence.all_role_ref.map(&:role) != roles
|
1366
|
+
pc = role_ref.role_sequence.all_presence_constraint.single # Will return nil if there's more than one.
|
1367
|
+
#puts "Existing PresenceConstraint matches those roles!" if pc
|
1368
|
+
return pc if pc
|
1369
|
+
end
|
1370
|
+
nil
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
def add_reading(fact_type, role_sequence, phrases)
|
1374
|
+
ordinal = (fact_type.all_reading.map(&:ordinal).max||-1) + 1 # Use the next unused ordinal
|
1375
|
+
reading = @constellation.Reading(fact_type, ordinal, :role_sequence => role_sequence)
|
1376
|
+
role_num = -1
|
1377
|
+
reading.text = phrases.map {|phrase|
|
1378
|
+
Hash === phrase ? "{#{role_num += 1}}" : phrase
|
1379
|
+
}*" "
|
1380
|
+
raise "Wrong number of players (#{role_num+1}) found in reading #{reading.text} over #{fact_type.describe}" if role_num+1 != fact_type.all_role.size
|
1381
|
+
debug "Added reading #{reading.text}"
|
1382
|
+
reading
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
# Return an array of this entity type and all its supertypes, transitively:
|
1386
|
+
def supertypes(o)
|
1387
|
+
([o] + o.all_supertype_inheritance.map{|ti| supertypes(ti.supertype)}.flatten).uniq
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
def concept_by_name(name)
|
1391
|
+
player = @constellation.Concept[[@vocabulary.identifying_role_values, name]]
|
1392
|
+
|
1393
|
+
# REVISIT: Hack to allow facts to refer to standard types that will be imported from standard vocabulary:
|
1394
|
+
if !player && %w{Date DateAndTime Time}.include?(name)
|
1395
|
+
player = @constellation.ValueType(@vocabulary.identifying_role_values, name)
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
if (!player && @symbols.allowed_forward[name])
|
1399
|
+
player = @constellation.EntityType(@vocabulary, name)
|
1400
|
+
end
|
1401
|
+
player
|
1402
|
+
end
|
1403
|
+
|
1404
|
+
class SymbolTable #:nodoc:all
|
1405
|
+
# Externally built tables used in this binding context:
|
1406
|
+
attr_reader :roles_by_binding
|
1407
|
+
attr_accessor :embedded_presence_constraints
|
1408
|
+
attr_accessor :allowed_forward
|
1409
|
+
attr_reader :constellation
|
1410
|
+
attr_reader :vocabulary
|
1411
|
+
attr_reader :bindings_by_concept
|
1412
|
+
attr_reader :role_names
|
1413
|
+
|
1414
|
+
# A Binding here is a form of reference to a concept, being a name and optional adjectives, possibly designated by a role name:
|
1415
|
+
Binding = Struct.new("Binding", :concept, :name, :leading_adjective, :trailing_adjective, :role_name)
|
1416
|
+
class Binding
|
1417
|
+
def inspect
|
1418
|
+
"Binding(#{concept.class.basename} #{concept.name}, #{[leading_adjective, name, trailing_adjective].compact*"-"}#{role_name ? " (as #{role_name})" : ""})"
|
1419
|
+
end
|
1420
|
+
|
1421
|
+
# Any ordering works to allow a hash to be keyed by a set (unordered array) of Bindings:
|
1422
|
+
def <=>(other)
|
1423
|
+
object_id <=> other.object_id
|
1424
|
+
end
|
1425
|
+
end
|
1426
|
+
|
1427
|
+
def initialize(constellation, vocabulary)
|
1428
|
+
@constellation = constellation
|
1429
|
+
@vocabulary = vocabulary
|
1430
|
+
@bindings_by_concept = Hash.new {|h, k| h[k] = [] } # Indexed by Binding#name, maybe multiple entries for each name
|
1431
|
+
@role_names = {}
|
1432
|
+
|
1433
|
+
@embedded_presence_constraints = []
|
1434
|
+
@roles_by_binding = {} # Build a hash of allowed bindings on first reading (check against it on subsequent ones)
|
1435
|
+
@allowed_forward = {} # No roles may be forward-referenced
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
#
|
1439
|
+
# This method is the guts of role matching.
|
1440
|
+
# "words" may be a single word (and then the adjectives may also be used) or two words.
|
1441
|
+
# In either case a word is expected to be a defined concept or role name.
|
1442
|
+
# If a role_name is provided here, that's a *definition* and will only be accepted if legal
|
1443
|
+
# If allowed_forward is true, words is a single word and is not defined, create a forward Entity
|
1444
|
+
# If leading_speculative or trailing_speculative is true, the adjectives may not apply. If they do apply, use them.
|
1445
|
+
# If loose_binding_except is true, it's a hash containing names that may *not* be loose-bound... else none may.
|
1446
|
+
#
|
1447
|
+
# Loose binding is when a word without an adjective matches a role with, or vice verse.
|
1448
|
+
#
|
1449
|
+
def bind(words, leading_adjective = nil, trailing_adjective = nil, role_name = nil, allowed_forward = false, leading_speculative = false, trailing_speculative = false, loose_binding_except = nil)
|
1450
|
+
words = Array(words)
|
1451
|
+
if (words.size > 2 or words.size == 2 && (leading_adjective or trailing_adjective or allowed_forward))
|
1452
|
+
raise "role has too many adjectives '#{[leading_adjective, words, trailing_adjective].flatten.compact*" "}'"
|
1453
|
+
end
|
1454
|
+
|
1455
|
+
# Check for use of a role name, valid if they haven't used any adjectives or tried to define a role_name:
|
1456
|
+
binding = @role_names[words[0]]
|
1457
|
+
if binding && words.size == 1 # If ok, this is it.
|
1458
|
+
raise "May not use existing role name '#{words[0]}' to define a new role name" if role_name
|
1459
|
+
if (leading_adjective && !leading_speculative) || (trailing_adjective && !trailing_speculative)
|
1460
|
+
raise "May not use existing role name '#{words[0]}' with adjectives"
|
1461
|
+
end
|
1462
|
+
return binding.concept, binding
|
1463
|
+
end
|
1464
|
+
|
1465
|
+
# Look for an existing definition
|
1466
|
+
# If we have more than one word that might be the concept name, find which it is:
|
1467
|
+
words.each do |w|
|
1468
|
+
# Find the existing defined binding that matches this one:
|
1469
|
+
bindings = @bindings_by_concept[w]
|
1470
|
+
best_match = nil
|
1471
|
+
matched_adjectives = 0
|
1472
|
+
bindings.each do |binding|
|
1473
|
+
# Adjectives defined on the binding must be matched unless loose binding is allowed.
|
1474
|
+
loose_ok = loose_binding_except and !loose_binding_except[binding.concept.name]
|
1475
|
+
|
1476
|
+
# Don't allow binding a new role name to an existing one:
|
1477
|
+
next if role_name and role_name != binding.role_name
|
1478
|
+
|
1479
|
+
quality = 0
|
1480
|
+
if binding.leading_adjective != leading_adjective
|
1481
|
+
next if binding.leading_adjective && leading_adjective # Both set, but different
|
1482
|
+
next if !loose_ok && (!leading_speculative || !leading_adjective)
|
1483
|
+
quality += 1
|
1484
|
+
end
|
1485
|
+
|
1486
|
+
if binding.trailing_adjective != trailing_adjective
|
1487
|
+
next if binding.trailing_adjective && trailing_adjective # Both set, but different
|
1488
|
+
next if !loose_ok && (!trailing_speculative || !trailing_adjective)
|
1489
|
+
quality += 1
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
quality += 1 unless binding.role_name # A role name that was not matched... better if there wasn't one
|
1493
|
+
|
1494
|
+
if (quality > matched_adjectives || !best_match)
|
1495
|
+
best_match = binding # A better match than we had before
|
1496
|
+
matched_adjectives = quality
|
1497
|
+
break unless loose_ok || leading_speculative || trailing_speculative
|
1498
|
+
end
|
1499
|
+
end
|
1500
|
+
|
1501
|
+
if best_match
|
1502
|
+
# We've found the best existing definition
|
1503
|
+
|
1504
|
+
# Indicate which speculative adjectives were used so the clauses can be deleted:
|
1505
|
+
leading_adjective.replace("") if best_match.leading_adjective and leading_adjective and leading_speculative
|
1506
|
+
trailing_adjective.replace("") if best_match.trailing_adjective and trailing_adjective and trailing_speculative
|
1507
|
+
|
1508
|
+
return best_match.concept, best_match
|
1509
|
+
end
|
1510
|
+
|
1511
|
+
# No existing defined binding. Look up an existing concept of this name:
|
1512
|
+
player = concept(w, allowed_forward)
|
1513
|
+
next unless player
|
1514
|
+
|
1515
|
+
# Found a new binding for this player, save it.
|
1516
|
+
|
1517
|
+
# Check that a trailing adjective isn't an existing role name or concept:
|
1518
|
+
trailing_word = words[1] if w == words[0]
|
1519
|
+
if trailing_word
|
1520
|
+
raise "May not use existing role name '#{trailing_word}' with a new name or with adjectives" if @role_names[trailing_word]
|
1521
|
+
raise "ambiguous concept reference #{words*" '"}'" if concept(trailing_word)
|
1522
|
+
end
|
1523
|
+
leading_word = words[0] if w != words[0]
|
1524
|
+
|
1525
|
+
raise "may not redefine existing concept '#{role_name}' as a role name" if role_name and concept(role_name)
|
1526
|
+
|
1527
|
+
binding = Binding.new(
|
1528
|
+
player,
|
1529
|
+
w,
|
1530
|
+
(!leading_speculative && leading_adjective) || leading_word,
|
1531
|
+
(!trailing_speculative && trailing_adjective) || trailing_word,
|
1532
|
+
role_name
|
1533
|
+
)
|
1534
|
+
@bindings_by_concept[binding.name] << binding
|
1535
|
+
@role_names[binding.role_name] = binding if role_name
|
1536
|
+
return binding.concept, binding
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
# Not found.
|
1540
|
+
return nil
|
1541
|
+
end
|
1542
|
+
|
1543
|
+
# return the EntityType or ValueType this name refers to:
|
1544
|
+
def concept(name, allowed_forward = false)
|
1545
|
+
# See if the name is a defined concept in this vocabulary:
|
1546
|
+
player = @constellation.Concept[[virv = @vocabulary.identifying_role_values, name]]
|
1547
|
+
|
1548
|
+
# REVISIT: Hack to allow facts to refer to standard types that will be imported from standard vocabulary:
|
1549
|
+
if !player && %w{Date DateAndTime Time}.include?(name)
|
1550
|
+
player = @constellation.ValueType(virv, name)
|
1551
|
+
end
|
1552
|
+
|
1553
|
+
if !player && allowed_forward
|
1554
|
+
player = @constellation.EntityType(@vocabulary, name)
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
player
|
1558
|
+
end
|
1559
|
+
|
1560
|
+
def bind_roles_in_clauses(clauses, identification = [])
|
1561
|
+
identification ||= []
|
1562
|
+
bind_roles_in_phrases_list(
|
1563
|
+
clauses.map{|clause| clause[2]}, # Extract the phrases
|
1564
|
+
single_word_identifiers = identification.map{|i| i.size == 1 ? i[0] : nil}.compact.uniq
|
1565
|
+
)
|
1566
|
+
end
|
1567
|
+
|
1568
|
+
#
|
1569
|
+
# Walk through all phrases identifying role players.
|
1570
|
+
# Each role player phrase gets a :binding key added to it.
|
1571
|
+
#
|
1572
|
+
# Any adjectives that the parser didn't recognise are merged with their players here,
|
1573
|
+
# as long as they're indicated as adjectives of that player somewhere in the readings.
|
1574
|
+
#
|
1575
|
+
# Other words are turned from phrases (hashes) into simple strings.
|
1576
|
+
#
|
1577
|
+
def bind_roles_in_phrases_list(phrases_list, allowed_forwards = [])
|
1578
|
+
disallow_loose_binding = allowed_forwards.inject({}) { |h, v| h[v] = true; h }
|
1579
|
+
phrases_list.each do |phrases|
|
1580
|
+
debug :bind, "Binding phrases"
|
1581
|
+
|
1582
|
+
phrase_numbers_used_speculatively = []
|
1583
|
+
disallow_loose_binding_this_reading = disallow_loose_binding.clone
|
1584
|
+
phrases.each_with_index do |phrase, index|
|
1585
|
+
la = phrase[:leading_adjective]
|
1586
|
+
player_name = phrase[:word]
|
1587
|
+
ta = phrase[:trailing_adjective]
|
1588
|
+
role_name = phrase[:role_name]
|
1589
|
+
|
1590
|
+
# We use the preceeding phrase and/or following phrase speculatively if they're simple words:
|
1591
|
+
preceeding_phrase = nil
|
1592
|
+
following_phrase = nil
|
1593
|
+
if !la && index > 0 && (preceeding_phrase = phrases[index-1])
|
1594
|
+
preceeding_phrase = nil unless String === preceeding_phrase || preceeding_phrase.keys == [:word]
|
1595
|
+
la = preceeding_phrase[:word] if Hash === preceeding_phrase
|
1596
|
+
end
|
1597
|
+
if !ta && (following_phrase = phrases[index+1])
|
1598
|
+
following_phrase = nil unless following_phrase.keys == [:word]
|
1599
|
+
ta = following_phrase[:word] if following_phrase
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
# If the identification includes this player name as a single word, it's allowed to be forward referenced:
|
1603
|
+
allowed_forward = allowed_forwards.include?(player_name)
|
1604
|
+
|
1605
|
+
debug :bind, "Binding a role: #{[player_name, la, ta, role_name, allowed_forward, !!preceeding_phrase, !!following_phrase].inspect}"
|
1606
|
+
player, binding = bind(
|
1607
|
+
player_name,
|
1608
|
+
la, ta,
|
1609
|
+
role_name,
|
1610
|
+
allowed_forward,
|
1611
|
+
!!preceeding_phrase, !!following_phrase,
|
1612
|
+
phrases == phrases_list[0] ? nil : disallow_loose_binding_this_reading # Never allow loose binding on the first reading
|
1613
|
+
)
|
1614
|
+
disallow_loose_binding_this_reading[player.name] = true if player
|
1615
|
+
|
1616
|
+
# Arrange to delete the speculative adjectives that were used:
|
1617
|
+
if preceeding_phrase && preceeding_phrase[:word] == ""
|
1618
|
+
debug :bind, "binding consumed a speculative leading_adjective #{la}"
|
1619
|
+
# The numbers are adjusted to allow for prior deletions.
|
1620
|
+
phrase_numbers_used_speculatively << index-1-phrase_numbers_used_speculatively.size
|
1621
|
+
end
|
1622
|
+
if following_phrase && following_phrase[:word] == ""
|
1623
|
+
debug :bind, "binding consumed a speculative trailing_adjective #{ta}"
|
1624
|
+
phrase_numbers_used_speculatively << index+1-phrase_numbers_used_speculatively.size
|
1625
|
+
end
|
1626
|
+
|
1627
|
+
if player
|
1628
|
+
# Replace the words used to identify the role by a reference to the role itself,
|
1629
|
+
# leaving :quantifier, :function, :restriction and :literal intact
|
1630
|
+
phrase[:binding] = binding
|
1631
|
+
binding
|
1632
|
+
else
|
1633
|
+
raise "Internal error; role #{phrase.inspect} not matched" unless phrase.keys == [:word]
|
1634
|
+
# Just a linking word
|
1635
|
+
phrases[index] = phrase[:word]
|
1636
|
+
end
|
1637
|
+
debug :bind, "Bound phrase: #{phrase.inspect}" + " -> " + (player ? player.name+", "+binding.inspect : phrase[:word].inspect)
|
1638
|
+
|
1639
|
+
end
|
1640
|
+
phrase_numbers_used_speculatively.each do |index|
|
1641
|
+
phrases.delete_at(index)
|
1642
|
+
end
|
1643
|
+
debug :bind, "Bound phrases: #{phrases.inspect}"
|
1644
|
+
end
|
1645
|
+
end
|
1646
|
+
end # of SymbolTable class
|
1647
|
+
|
1648
|
+
# The Context manages some key information revealed or needed during parsing
|
1649
|
+
class Context
|
1650
|
+
# REVISIT; This class is "work in progress", supporting semantic predicates from Treetop.
|
1651
|
+
def initialize(compiler)
|
1652
|
+
@compiler = compiler
|
1653
|
+
@vocabularies = {}
|
1654
|
+
end
|
1655
|
+
|
1656
|
+
def vocabulary(v)
|
1657
|
+
# puts "Parser has started work on vocabulary #{v}"
|
1658
|
+
@vocabularies[v] = {}
|
1659
|
+
@terms = {}
|
1660
|
+
true
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
def entity_type(c)
|
1664
|
+
#puts "Parser has started work on entity_type '#{c}'"
|
1665
|
+
@terms[c] = true
|
1666
|
+
true
|
1667
|
+
end
|
1668
|
+
|
1669
|
+
def value_type(c)
|
1670
|
+
#puts "Parser has started work on value_type '#{c}'"
|
1671
|
+
@terms[c] = true
|
1672
|
+
true
|
1673
|
+
end
|
1674
|
+
|
1675
|
+
def objectified_fact_type(c)
|
1676
|
+
#puts "Parser has started work on objectified_fact_type '#{c}'"
|
1677
|
+
@terms[c] = true
|
1678
|
+
true
|
1679
|
+
end
|
1680
|
+
|
1681
|
+
def reset_role_names
|
1682
|
+
# puts "\tresetting role names #{@role_names.keys.sort*", "}" if @role_names && @role_names.size > 0
|
1683
|
+
@role_names = {}
|
1684
|
+
true
|
1685
|
+
end
|
1686
|
+
|
1687
|
+
def role_name(r)
|
1688
|
+
#puts "\tadding role name '#{r}'"
|
1689
|
+
@role_names[r] = true
|
1690
|
+
true
|
1691
|
+
end
|
1692
|
+
|
1693
|
+
def term?(t)
|
1694
|
+
#puts "term?(#{t})"
|
1695
|
+
false
|
1696
|
+
end
|
1697
|
+
|
1698
|
+
def global_term?(t)
|
1699
|
+
#puts "global_term?(#{t})"
|
1700
|
+
false
|
1701
|
+
end
|
1702
|
+
|
1703
|
+
def term_starts(s)
|
1704
|
+
#p s
|
1705
|
+
#debugger
|
1706
|
+
true
|
1707
|
+
end
|
1708
|
+
|
1709
|
+
def term_continues(s)
|
1710
|
+
#p s
|
1711
|
+
#debugger
|
1712
|
+
true
|
1713
|
+
end
|
1714
|
+
end
|
1715
|
+
|
1716
|
+
end
|
1717
|
+
end
|
1718
|
+
end
|