expressir 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.github/workflows/rust-ext.yml +41 -0
- data/.github/workflows/verify_remarks.yml +33 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +1 -0
- data/Cargo.toml +7 -0
- data/README.adoc +65 -0
- data/Rakefile +12 -0
- data/TODO.parity-ee/00-overview.md +58 -0
- data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
- data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
- data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
- data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
- data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
- data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
- data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
- data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
- data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
- data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
- data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
- data/TODO.parity-ee/parity-matrix.md +108 -0
- data/exe/expressir +6 -0
- data/ext/expressir_core/Cargo.toml +24 -0
- data/ext/expressir_core/extconf.rb +30 -0
- data/ext/expressir_core/src/lib.rs +446 -0
- data/lib/expressir/cli.rb +49 -4
- data/lib/expressir/commands/check.rb +56 -0
- data/lib/expressir/commands/expand.rb +26 -0
- data/lib/expressir/commands/fix.rb +33 -0
- data/lib/expressir/commands/flatten.rb +29 -0
- data/lib/expressir/commands/parity_inputs.rb +85 -0
- data/lib/expressir/commands/validate.rb +22 -0
- data/lib/expressir/commands.rb +5 -0
- data/lib/expressir/express/builder_registry.rb +11 -2
- data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
- data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
- data/lib/expressir/express/builders/type_builder.rb +8 -0
- data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
- data/lib/expressir/express/checker.rb +458 -0
- data/lib/expressir/express/concatenator.rb +79 -0
- data/lib/expressir/express/core.rb +66 -0
- data/lib/expressir/express/formatter.rb +25 -3
- data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
- data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
- data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
- data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
- data/lib/expressir/express/interface_dot.rb +105 -0
- data/lib/expressir/express/listing.rb +174 -0
- data/lib/expressir/express/model_traversal.rb +42 -0
- data/lib/expressir/express/node_position_index.rb +18 -4
- data/lib/expressir/express/parser.rb +242 -4
- data/lib/expressir/express/pretty_gate.rb +61 -0
- data/lib/expressir/express/refs_overlay.rb +47 -0
- data/lib/expressir/express/remark_attacher.rb +122 -10
- data/lib/expressir/express/remark_overlay.rb +86 -0
- data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
- data/lib/expressir/express/schema_source_formatter.rb +5 -5
- data/lib/expressir/express/self_schema_reference.rb +126 -0
- data/lib/expressir/express/shtolo.rb +522 -0
- data/lib/expressir/express.rb +13 -0
- data/lib/expressir/model/concerns.rb +15 -1
- data/lib/expressir/model/declarations/entity.rb +6 -1
- data/lib/expressir/model/declarations/interface.rb +13 -0
- data/lib/expressir/model/declarations/schema.rb +74 -23
- data/lib/expressir/model/indexes/item_graph.rb +135 -0
- data/lib/expressir/model/indexes.rb +1 -0
- data/lib/expressir/model/model_element.rb +30 -1
- data/lib/expressir/model/remark_info.rb +20 -1
- data/lib/expressir/model/remark_placement.rb +15 -2
- data/lib/expressir/model/repository.rb +8 -2
- data/lib/expressir/model.rb +2 -1
- data/lib/expressir/version.rb +1 -1
- data/lib/tasks/verify_remarks.rake +9 -0
- data/rakelib/verify_remarks.rake +16 -0
- metadata +42 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Self-schema-qualified references (#125, ISO 10303-11 scope rules).
|
|
6
|
+
#
|
|
7
|
+
# A schema may not reference its own declarations through itself:
|
|
8
|
+
# `'AIC_TOPOLOGICALLY_BOUNDED_SURFACE.B_SPLINE_SURFACE'` inside that
|
|
9
|
+
# schema's TYPEOF is technically incorrect — the prefix names the
|
|
10
|
+
# current schema while the item actually comes from another schema
|
|
11
|
+
# (implicit through the supertype chain of a USEd entity).
|
|
12
|
+
#
|
|
13
|
+
# {matches} finds string literals carrying such prefixes and resolves
|
|
14
|
+
# where the suffixed item truly lives (local declaration, or the
|
|
15
|
+
# defining schema within the closure). {fix!} rewrites each literal in
|
|
16
|
+
# place — dropping the prefix for local items, substituting the true
|
|
17
|
+
# defining schema otherwise.
|
|
18
|
+
module SelfSchemaReference
|
|
19
|
+
Match = Struct.new(:literal, :item, :source_schema, :schema,
|
|
20
|
+
keyword_init: true) do
|
|
21
|
+
# :local - the item is declared in this schema (drop the prefix)
|
|
22
|
+
# :foreign - the item's defining schema is known (substitute it)
|
|
23
|
+
# :unknown - the item could not be located; left untouched
|
|
24
|
+
def status
|
|
25
|
+
return :local if declared_locally
|
|
26
|
+
return :foreign if source_schema
|
|
27
|
+
|
|
28
|
+
:unknown
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_accessor :declared_locally
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# Every self-qualified string literal in +schema+.
|
|
37
|
+
def matches(schema)
|
|
38
|
+
prefix = /\A#{Regexp.escape(schema.id)}\.(.+)\z/i
|
|
39
|
+
literals(schema).filter_map do |literal|
|
|
40
|
+
suffix = literal.value[prefix, 1]
|
|
41
|
+
next unless suffix
|
|
42
|
+
|
|
43
|
+
item = suffix.strip
|
|
44
|
+
declared_locally = declared?(schema, item.safe_downcase)
|
|
45
|
+
source = declared_locally ? nil : defining_schema(schema, item)
|
|
46
|
+
match = Match.new(literal: literal, item: item,
|
|
47
|
+
source_schema: source, schema: schema)
|
|
48
|
+
match.declared_locally = declared_locally
|
|
49
|
+
match
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Rewrite every self-qualified literal in +schema+; returns the
|
|
54
|
+
# number of rewrites. Local items lose the prefix; foreign items get
|
|
55
|
+
# their true defining schema's id.
|
|
56
|
+
def fix!(schema)
|
|
57
|
+
matches(schema).count do |match|
|
|
58
|
+
replacement = case match.status
|
|
59
|
+
when :local then match.item
|
|
60
|
+
when :foreign
|
|
61
|
+
"#{match.source_schema.id}.#{match.item}"
|
|
62
|
+
else next false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
match.literal.value = replacement
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def literals(schema)
|
|
71
|
+
result = []
|
|
72
|
+
visitor = lambda do |node|
|
|
73
|
+
case node
|
|
74
|
+
when Model::Literals::String
|
|
75
|
+
result << node
|
|
76
|
+
when Model::ModelElement
|
|
77
|
+
node.class.attributes.each_key do |attr|
|
|
78
|
+
next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr)
|
|
79
|
+
|
|
80
|
+
value = node.public_send(attr)
|
|
81
|
+
case value
|
|
82
|
+
when Array then value.each { |item| visitor.call(item) }
|
|
83
|
+
when Model::ModelElement then visitor.call(value)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
visitor.call(schema)
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def defining_schema(schema, item)
|
|
93
|
+
key = item.safe_downcase
|
|
94
|
+
repository = repository_of(schema)
|
|
95
|
+
return nil unless repository
|
|
96
|
+
|
|
97
|
+
repository.schemas.each do |other|
|
|
98
|
+
next if other.equal?(schema)
|
|
99
|
+
next unless declared?(other, key)
|
|
100
|
+
|
|
101
|
+
return other
|
|
102
|
+
end
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def declared?(schema, key)
|
|
107
|
+
%i[types entities functions procedures constants rules
|
|
108
|
+
subtype_constraints].any? do |coll|
|
|
109
|
+
Array(schema.public_send(coll)).any? do |decl|
|
|
110
|
+
decl.respond_to?(:id) && decl.id&.safe_downcase == key
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def repository_of(schema)
|
|
116
|
+
current = schema.parent
|
|
117
|
+
while current
|
|
118
|
+
return current if current.is_a?(Model::Repository)
|
|
119
|
+
|
|
120
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
121
|
+
end
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# SHTOLO — short-to-long conversion per ISO 10303-11:2004 Annex G
|
|
6
|
+
# (expressir#32): flattens a multi-schema specification into a
|
|
7
|
+
# single ISO 10303-11:1994 longform schema.
|
|
8
|
+
#
|
|
9
|
+
# Two stages (annex-g-rules.md):
|
|
10
|
+
# G.1 multi-schema → intermediate `artifact` schema: interface
|
|
11
|
+
# closure copied as local declarations, renames resolved to
|
|
12
|
+
# original names, name clashes munged (G.NM.1), REFERENCE
|
|
13
|
+
# entities kept with the dependent-instantiability rule
|
|
14
|
+
# (G.1.6), select/rules/supertype expressions pruned (G.1.9).
|
|
15
|
+
# G.2 artifact → `longform`: extensible enums/selects resolved
|
|
16
|
+
# (G.2.3/G.2.4), subtype constraints eliminated (supertype
|
|
17
|
+
# expression → SUPERTYPE, total_over → rule, G.2.5), abstract
|
|
18
|
+
# entity → ABSTRACT SUPERTYPE, GENERIC_ENTITY → GENERIC,
|
|
19
|
+
# RENAMED → DERIVE (G.2.7), qualified strings rewritten.
|
|
20
|
+
#
|
|
21
|
+
# The output is a Model::Declarations::Schema named per the root
|
|
22
|
+
# schema; format it with the Formatter to obtain the longform file.
|
|
23
|
+
class Shtolo
|
|
24
|
+
DEPENDENT_RULE_NAME = "validate_dependently_instantiable_entity_data_types"
|
|
25
|
+
DEPENDENT_FN_NAME = "dependently_instantiated"
|
|
26
|
+
|
|
27
|
+
attr_reader :longform, :renames, :schema
|
|
28
|
+
|
|
29
|
+
# `root_schema` — the resolved root schema node.
|
|
30
|
+
# `repository` — must hold the full interface closure.
|
|
31
|
+
# `extenders` — :all (default, the WG12 requirement) folds every
|
|
32
|
+
# extensible SELECT/ENUMERATION extension found in the
|
|
33
|
+
# closure into its base type (Annex G.2.3/G.2.4);
|
|
34
|
+
# :none leaves extensible types exactly as declared —
|
|
35
|
+
# the shape eengine's default --flat emits.
|
|
36
|
+
def initialize(root_schema, repository, longform_name: nil,
|
|
37
|
+
extenders: :all)
|
|
38
|
+
@root = root_schema
|
|
39
|
+
@repository = repository
|
|
40
|
+
@longform_name = longform_name || @root.id
|
|
41
|
+
@extenders = extenders
|
|
42
|
+
@reference_entities = [] # G.1.6 entities
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def flatten
|
|
46
|
+
@schema = build_longform
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
IdPlan = Struct.new(:global, :own_by_schema, keyword_init: true) do
|
|
51
|
+
def own(schema)
|
|
52
|
+
own_by_schema[schema] || {}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# ---- interface closure (same semantics as Concatenator) ----
|
|
59
|
+
|
|
60
|
+
def closure
|
|
61
|
+
@closure ||= begin
|
|
62
|
+
by_name = @repository.schemas.to_h { |s| [s.id.safe_downcase, s] }
|
|
63
|
+
visited = {}
|
|
64
|
+
queue = [@root]
|
|
65
|
+
until queue.empty?
|
|
66
|
+
schema = queue.shift
|
|
67
|
+
key = schema.id.safe_downcase
|
|
68
|
+
next if visited.key?(key)
|
|
69
|
+
|
|
70
|
+
visited[key] = schema
|
|
71
|
+
Array(schema.interfaces).each do |iface|
|
|
72
|
+
name = iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
|
|
73
|
+
next unless name
|
|
74
|
+
|
|
75
|
+
target = by_name[name.safe_downcase]
|
|
76
|
+
queue << target if target && !visited.key?(name.safe_downcase)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
visited
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# ---- G.1 stage 1 ----
|
|
84
|
+
|
|
85
|
+
# Two-phase copy: plan every declaration's final id across the whole
|
|
86
|
+
# closure FIRST (G.NM.1 clashes are only known once every schema has
|
|
87
|
+
# been seen), then copy+rewrite each declaration against the complete
|
|
88
|
+
# map. Rewriting as we copy left stale references in declarations
|
|
89
|
+
# copied before a later clash was munged.
|
|
90
|
+
def build_artifact
|
|
91
|
+
schemas = [@root, *closure.values.reject { |s| s.equal?(@root) }]
|
|
92
|
+
plan = plan_ids(schemas)
|
|
93
|
+
|
|
94
|
+
declarations = []
|
|
95
|
+
schemas.each do |schema|
|
|
96
|
+
rename_map = renames_for(schema)
|
|
97
|
+
# Resolution order for a reference in this schema: its own local
|
|
98
|
+
# declarations (which may be munged), then interface aliases
|
|
99
|
+
# resolved through the referenced declaration's final id, then
|
|
100
|
+
# the global munge map for names the schema doesn't declare.
|
|
101
|
+
full = plan.global.dup
|
|
102
|
+
plan.own(schema).each { |k, final| full[k] = final }
|
|
103
|
+
rename_map.each do |alias_name, original|
|
|
104
|
+
resolved = plan.own(schema)[original.safe_downcase] ||
|
|
105
|
+
plan.global[original.safe_downcase] || original
|
|
106
|
+
full[alias_name.safe_downcase] = resolved
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
all_decls(schema).each do |decl|
|
|
110
|
+
copy = deep_copy(decl)
|
|
111
|
+
rewrite_references(copy, full)
|
|
112
|
+
if decl.respond_to?(:id) && decl.id &&
|
|
113
|
+
(final = plan.own(schema)[decl.id.safe_downcase])
|
|
114
|
+
copy.id = final
|
|
115
|
+
end
|
|
116
|
+
declarations << copy
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# G.1.2: interfaces themselves dissolve; REFERENCE entities
|
|
120
|
+
# keep dependent-instantiability semantics (G.1.6).
|
|
121
|
+
schema.interfaces.each do |iface|
|
|
122
|
+
next unless iface.kind == Model::Declarations::Interface::REFERENCE
|
|
123
|
+
|
|
124
|
+
iface.items.each do |item|
|
|
125
|
+
original = original_name(item, schema)
|
|
126
|
+
next unless closure[original]&.entities&.find { |e| e.id == original }
|
|
127
|
+
|
|
128
|
+
@reference_entities << original.safe_downcase
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
declarations << build_dependent_rule if @reference_entities.any?
|
|
134
|
+
|
|
135
|
+
Model::Declarations::Schema.new(
|
|
136
|
+
id: @longform_name,
|
|
137
|
+
version: nil,
|
|
138
|
+
interfaces: [],
|
|
139
|
+
constants: declarations.grep(Model::Declarations::Constant).compact,
|
|
140
|
+
types: declarations.grep(Model::Declarations::Type).compact,
|
|
141
|
+
entities: declarations.grep(Model::Declarations::Entity).compact,
|
|
142
|
+
subtype_constraints: declarations.grep(Model::Declarations::SubtypeConstraint).compact,
|
|
143
|
+
functions: declarations.grep(Model::Declarations::Function).compact,
|
|
144
|
+
procedures: declarations.grep(Model::Declarations::Procedure).compact,
|
|
145
|
+
rules: declarations.grep(Model::Declarations::Rule).compact,
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def all_decls(schema)
|
|
150
|
+
schema.constants + schema.types + schema.entities +
|
|
151
|
+
schema.subtype_constraints + schema.functions +
|
|
152
|
+
schema.rules + schema.procedures
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# G.NM.1: the first schema in closure order to declare a name keeps
|
|
156
|
+
# it; every later declaration of the same name is prefixed with ITS
|
|
157
|
+
# OWN schema's id, and the global map redirects the bare name so
|
|
158
|
+
# declarations from third schemas resolve to the first occurrence.
|
|
159
|
+
def plan_ids(schemas)
|
|
160
|
+
seen_first = {} # downcase id => owning schema (first occurrence)
|
|
161
|
+
global = {} # downcase id => munged final
|
|
162
|
+
own_by_schema = {} # schema => {downcase id => final id}
|
|
163
|
+
schemas.each do |schema|
|
|
164
|
+
own = {}
|
|
165
|
+
all_decls(schema).each do |decl|
|
|
166
|
+
next unless decl.respond_to?(:id) && decl.id
|
|
167
|
+
|
|
168
|
+
key = decl.id.safe_downcase
|
|
169
|
+
if seen_first.key?(key)
|
|
170
|
+
final = "#{schema.id.safe_downcase}_dot_#{decl.id}"
|
|
171
|
+
own[key] = final
|
|
172
|
+
global[key] = final
|
|
173
|
+
else
|
|
174
|
+
seen_first[key] = schema
|
|
175
|
+
own[key] = decl.id
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
own_by_schema[schema] = own
|
|
179
|
+
end
|
|
180
|
+
IdPlan.new(global: global, own_by_schema: own_by_schema)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Alias → original map applied when copying `schema`'s
|
|
184
|
+
# declarations: the aliases are declared by the schema's own
|
|
185
|
+
# interface items (`USE FROM s (orig AS alias)`, G.1.3).
|
|
186
|
+
def renames_for(schema)
|
|
187
|
+
map = {}
|
|
188
|
+
Array(schema.interfaces).each do |iface|
|
|
189
|
+
iface.items.each do |item|
|
|
190
|
+
original = item.ref.is_a?(String) ? item.ref : item.ref&.id
|
|
191
|
+
next unless original
|
|
192
|
+
|
|
193
|
+
map[item.id.safe_downcase] = original if item.id &&
|
|
194
|
+
item.id.safe_downcase != original.safe_downcase
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
map
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def original_name(item, schema)
|
|
201
|
+
original = item.ref.is_a?(String) ? item.ref : item.ref&.id
|
|
202
|
+
return schema.id unless original
|
|
203
|
+
|
|
204
|
+
original
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def deep_copy(obj)
|
|
208
|
+
sever = []
|
|
209
|
+
node = obj
|
|
210
|
+
while node.respond_to?(:parent) && node.parent
|
|
211
|
+
sever << node
|
|
212
|
+
node = node.parent
|
|
213
|
+
end
|
|
214
|
+
saved = sever.map(&:parent)
|
|
215
|
+
sever.each { |n| n.parent = nil }
|
|
216
|
+
Marshal.load(Marshal.dump(obj))
|
|
217
|
+
ensure
|
|
218
|
+
sever.each_with_index { |n, i| n.parent = saved[i] }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Rewrite SimpleReference ids against a rename map in ONE tree walk
|
|
222
|
+
# (each declaration is walked once, whatever the map's size — walking
|
|
223
|
+
# once per rename made flattening quadratic in the closure).
|
|
224
|
+
# Map keys are downcased old names, values the final names.
|
|
225
|
+
def rewrite_references(node, map)
|
|
226
|
+
return unless node.is_a?(Model::ModelElement)
|
|
227
|
+
|
|
228
|
+
if node.is_a?(Model::References::SimpleReference) && node.id &&
|
|
229
|
+
(to = map[node.id.safe_downcase])
|
|
230
|
+
node.id = preserve_case(node.id, to)
|
|
231
|
+
end
|
|
232
|
+
node.class.attributes.each_key do |attr|
|
|
233
|
+
next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr) || attr == :parent
|
|
234
|
+
|
|
235
|
+
value = node.public_send(attr)
|
|
236
|
+
case value
|
|
237
|
+
when Array then value.each { |item| rewrite_references(item, map) }
|
|
238
|
+
when Model::ModelElement then rewrite_references(value, map)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def preserve_case(current, new)
|
|
244
|
+
current == current.upcase ? new.upcase : new
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# G.1.6: parse-templates keep the annex rule/function verbatim.
|
|
248
|
+
def build_dependent_rule
|
|
249
|
+
list = @reference_entities.map(&:upcase).join(", ")
|
|
250
|
+
union = @reference_entities.join("+")
|
|
251
|
+
src = <<~EXP
|
|
252
|
+
SCHEMA __dependent__;
|
|
253
|
+
RULE #{DEPENDENT_RULE_NAME} FOR (#{list});
|
|
254
|
+
LOCAL
|
|
255
|
+
number_of_input_instances : INTEGER;
|
|
256
|
+
previous_in_chain : LIST OF GENERIC := [];
|
|
257
|
+
set_of_input_types : SET OF STRING := [];
|
|
258
|
+
all_instances : SET OF GENERIC := [];
|
|
259
|
+
END_LOCAL;
|
|
260
|
+
all_instances := #{union};
|
|
261
|
+
WHERE
|
|
262
|
+
wr1: dependently_instantiated(all_instances, set_of_input_types, previous_in_chain);
|
|
263
|
+
END_RULE;
|
|
264
|
+
END_SCHEMA;
|
|
265
|
+
EXP
|
|
266
|
+
parse_schema_snippet(src).schemas.first.rules.first
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def parse_schema_snippet(src)
|
|
270
|
+
Expressir::Express::Parser.from_exp(src)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ---- G.2 stage 2 ----
|
|
274
|
+
|
|
275
|
+
def build_longform
|
|
276
|
+
artifact = build_artifact
|
|
277
|
+
|
|
278
|
+
if @extenders == :all
|
|
279
|
+
resolve_extensible_enumerations!(artifact)
|
|
280
|
+
resolve_extensible_selects!(artifact)
|
|
281
|
+
end
|
|
282
|
+
eliminate_subtype_constraints!(artifact)
|
|
283
|
+
convert_abstract_entities!(artifact)
|
|
284
|
+
convert_generic_entities!(artifact)
|
|
285
|
+
convert_renamed_attributes!(artifact)
|
|
286
|
+
rewrite_qualified_strings!(artifact)
|
|
287
|
+
|
|
288
|
+
artifact
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def resolve_extensible_enumerations!(schema)
|
|
292
|
+
schema.types.select do |t|
|
|
293
|
+
t.underlying_type.is_a?(Model::DataTypes::Enumeration) && t.underlying_type.extensible
|
|
294
|
+
end.each do |base|
|
|
295
|
+
enum = base.underlying_type
|
|
296
|
+
extensions = schema.types.select do |t|
|
|
297
|
+
t.underlying_type.is_a?(Model::DataTypes::Enumeration) &&
|
|
298
|
+
!t.underlying_type.extensible &&
|
|
299
|
+
t.underlying_type.based_on&.id&.safe_downcase == base.id.safe_downcase
|
|
300
|
+
end
|
|
301
|
+
merged = enum.items.map(&:id) +
|
|
302
|
+
extensions.flat_map { |x| x.underlying_type.items.map(&:id) }
|
|
303
|
+
enum.items = merged.uniq.map { |id| Model::DataTypes::EnumerationItem.new(id: id) }
|
|
304
|
+
enum.extensible = false
|
|
305
|
+
enum.based_on = nil
|
|
306
|
+
|
|
307
|
+
extensions.each do |ext|
|
|
308
|
+
ext_items = ext.underlying_type.items.map(&:id)
|
|
309
|
+
exclusions = merged - ext_items
|
|
310
|
+
ext.underlying_type = Model::DataTypes::Enumeration.new(
|
|
311
|
+
items: enum.items,
|
|
312
|
+
)
|
|
313
|
+
ext.where_rules = exclusions.each_with_index.map do |item, i|
|
|
314
|
+
Model::Declarations::WhereRule.new(
|
|
315
|
+
id: "wr#{i + 1}",
|
|
316
|
+
expression: parse_expression("SELF <> #{item};"),
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def resolve_extensible_selects!(schema)
|
|
324
|
+
schema.types.select do |t|
|
|
325
|
+
t.underlying_type.is_a?(Model::DataTypes::Select) && t.underlying_type.extensible
|
|
326
|
+
end.each do |base|
|
|
327
|
+
sel = base.underlying_type
|
|
328
|
+
|
|
329
|
+
# all-extenders for GENERIC_ENTITY selects (WG12 requirement,
|
|
330
|
+
# #32): the extenders are the entity types themselves — fold in
|
|
331
|
+
# every entity the artifact carries, so the longform select is
|
|
332
|
+
# complete against the closure. Flags stay as declared: the
|
|
333
|
+
# extensible/generic-entity markers are part of the shortform's
|
|
334
|
+
# semantics and are not expressir's to drop.
|
|
335
|
+
if sel.generic_entity
|
|
336
|
+
sel.items = schema.entities.map do |e|
|
|
337
|
+
Model::References::SimpleReference.new(id: e.id)
|
|
338
|
+
end
|
|
339
|
+
next
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
extensions = schema.types.select do |t|
|
|
343
|
+
t.underlying_type.is_a?(Model::DataTypes::Select) &&
|
|
344
|
+
!t.underlying_type.extensible &&
|
|
345
|
+
t.underlying_type.based_on&.id&.safe_downcase == base.id.safe_downcase
|
|
346
|
+
end
|
|
347
|
+
merged_items = sel.items +
|
|
348
|
+
extensions.flat_map { |x| x.underlying_type.items }
|
|
349
|
+
seen_ids = {}
|
|
350
|
+
uniq = merged_items.select do |item|
|
|
351
|
+
key = item.id.safe_downcase
|
|
352
|
+
next false if seen_ids.key?(key) || key.empty?
|
|
353
|
+
|
|
354
|
+
seen_ids[key] = true
|
|
355
|
+
end
|
|
356
|
+
sel.items = uniq
|
|
357
|
+
sel.extensible = false
|
|
358
|
+
sel.based_on = nil
|
|
359
|
+
|
|
360
|
+
extensions.each do |ext|
|
|
361
|
+
keep = ext.underlying_type.items.map { |i| i.id.upcase }
|
|
362
|
+
exclusions = uniq.reject { |i| keep.include?(i.id.upcase) }
|
|
363
|
+
base_ref = "#{@longform_name.upcase}.#{base.id.upcase}"
|
|
364
|
+
ext.underlying_type = Model::DataTypes::Select.new(
|
|
365
|
+
items: sel.items.map { |i| Model::References::SimpleReference.new(id: i.id) },
|
|
366
|
+
)
|
|
367
|
+
ext.where_rules = exclusions.each_with_index.map do |item, i|
|
|
368
|
+
Model::Declarations::WhereRule.new(
|
|
369
|
+
id: "wr#{i + 1}",
|
|
370
|
+
expression: parse_expression("NOT ('#{base_ref}.#{item.id.upcase}' IN TYPEOF(SELF));"),
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def eliminate_subtype_constraints!(schema)
|
|
378
|
+
schema.subtype_constraints.each do |sc|
|
|
379
|
+
entity = schema.entities.find do |e|
|
|
380
|
+
e.id.safe_downcase == sc.applies_to&.id&.safe_downcase
|
|
381
|
+
end
|
|
382
|
+
next unless entity
|
|
383
|
+
|
|
384
|
+
if sc.supertype_expression
|
|
385
|
+
expr = sc.supertype_expression
|
|
386
|
+
entity.supertype_expression =
|
|
387
|
+
entity.supertype_expression ? combine_andor(entity.supertype_expression, expr) : expr
|
|
388
|
+
end
|
|
389
|
+
if entity.abstract
|
|
390
|
+
# G.2.6: abstract entity ⇒ ABSTRACT SUPERTYPE (formatter
|
|
391
|
+
# renders from abstract + supertype status).
|
|
392
|
+
end
|
|
393
|
+
next if Array(sc.total_over).empty?
|
|
394
|
+
|
|
395
|
+
rule_name = "total_over_#{sc.id}"
|
|
396
|
+
targets = Array(sc.total_over).map { |r| r.id.upcase }
|
|
397
|
+
supertype = sc.applies_to&.id
|
|
398
|
+
rule = parse_schema_snippet(<<~EXP).schemas.first.rules.first
|
|
399
|
+
SCHEMA __to__;
|
|
400
|
+
RULE #{rule_name} FOR (#{supertype.upcase});
|
|
401
|
+
WHERE
|
|
402
|
+
wr1: SIZEOF(QUERY(#{supertype.downcase}_i <* #{supertype} |
|
|
403
|
+
SIZEOF(['#{@longform_name.upcase}.#{targets.first}'] * TYPEOF(#{supertype.downcase}_i)) = 0)) = 0;
|
|
404
|
+
END_RULE;
|
|
405
|
+
END_SCHEMA;
|
|
406
|
+
EXP
|
|
407
|
+
rule.id = rule_name
|
|
408
|
+
schema.rules = Array(schema.rules) + [rule]
|
|
409
|
+
end
|
|
410
|
+
schema.subtype_constraints = []
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def combine_andor(left, right)
|
|
414
|
+
Model::SupertypeExpressions::BinarySupertypeExpression.new(
|
|
415
|
+
operator: Model::SupertypeExpressions::BinarySupertypeExpression::ANDOR,
|
|
416
|
+
operand1: left, operand2: right
|
|
417
|
+
)
|
|
418
|
+
rescue StandardError
|
|
419
|
+
left
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def convert_abstract_entities!(schema)
|
|
423
|
+
schema.entities.each do |entity|
|
|
424
|
+
next unless entity.abstract
|
|
425
|
+
|
|
426
|
+
entity.abstract = true # formatter renders ABSTRACT SUPERTYPE when supertype present
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def convert_generic_entities!(schema)
|
|
431
|
+
each_node(schema) do |node|
|
|
432
|
+
replace_in_parent(node, Model::DataTypes::Generic.new) if node.is_a?(Model::DataTypes::GenericEntity)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def replace_in_parent(node, replacement)
|
|
437
|
+
parent = node.parent
|
|
438
|
+
return unless parent
|
|
439
|
+
|
|
440
|
+
parent.class.attributes.each_key do |attr|
|
|
441
|
+
next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr)
|
|
442
|
+
|
|
443
|
+
value = parent.public_send(attr)
|
|
444
|
+
case value
|
|
445
|
+
when Array
|
|
446
|
+
value.each_with_index do |item, index|
|
|
447
|
+
parent.public_send(attr)[index] = replacement if item.equal?(node)
|
|
448
|
+
end
|
|
449
|
+
else
|
|
450
|
+
parent.public_send(:"#{attr}=", replacement) if value.equal?(node) && parent.respond_to?(:"#{attr}=")
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def convert_renamed_attributes!(schema)
|
|
456
|
+
schema.entities.each do |entity|
|
|
457
|
+
entity.attributes.each_with_index do |attr, i|
|
|
458
|
+
next unless attr.supertype_attribute
|
|
459
|
+
|
|
460
|
+
old_name, supertype = supertype_target(attr.supertype_attribute)
|
|
461
|
+
entity.attributes[i] = Model::Declarations::DerivedAttribute.new(
|
|
462
|
+
id: attr.id || old_name,
|
|
463
|
+
type: attr.type,
|
|
464
|
+
expression: parse_expression("SELF\\#{supertype}.#{old_name}"),
|
|
465
|
+
)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# `SELF\ENTITY.attr` parses as AttributeReference(ref: GroupReference
|
|
471
|
+
# (SELF, entity), attribute: attr); a bare qualified attribute has
|
|
472
|
+
# only the attribute name.
|
|
473
|
+
def supertype_target(ref)
|
|
474
|
+
return [ref.id, nil] unless ref.is_a?(Model::References::AttributeReference)
|
|
475
|
+
|
|
476
|
+
group = ref.ref
|
|
477
|
+
[ref.attribute&.id,
|
|
478
|
+
group.is_a?(Model::References::GroupReference) ? group.entity&.id : nil]
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# G.NM.2: string literals carrying schema-qualified names (e.g.
|
|
482
|
+
# TYPEOF arguments 'SCHEMA.TYPE') move to the longform name.
|
|
483
|
+
# Structurally these appear as DataTypes::String; path references
|
|
484
|
+
# in expressions are already renamed by the copy pass.
|
|
485
|
+
def rewrite_qualified_strings!(schema)
|
|
486
|
+
prefixes = closure.keys.map(&:upcase)
|
|
487
|
+
each_node(schema) do |node|
|
|
488
|
+
next unless node.is_a?(Model::Literals::String) && node.value
|
|
489
|
+
|
|
490
|
+
node.value = node.value.sub(/\A(#{prefixes.join('|')})\./i,
|
|
491
|
+
"#{@longform_name.upcase}.")
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Attribute-driven structural traversal over the whole subtree.
|
|
496
|
+
def each_node(root, &)
|
|
497
|
+
stack = [root]
|
|
498
|
+
until stack.empty?
|
|
499
|
+
node = stack.pop
|
|
500
|
+
yield node
|
|
501
|
+
next unless node.is_a?(Model::ModelElement)
|
|
502
|
+
|
|
503
|
+
node.class.attributes.each_key do |attr|
|
|
504
|
+
next if Model::ModelElement::SKIP_ATTRIBUTES.include?(attr)
|
|
505
|
+
|
|
506
|
+
value = node.public_send(attr)
|
|
507
|
+
case value
|
|
508
|
+
when Array then value.each { |item| stack << item if item.is_a?(Model::ModelElement) }
|
|
509
|
+
when Model::ModelElement then stack << value
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def parse_expression(src)
|
|
516
|
+
snippet = "SCHEMA __x__; ENTITY __e__; WHERE w1 : #{src.chomp(';')}; END_ENTITY; END_SCHEMA;"
|
|
517
|
+
parsed = parse_schema_snippet(snippet)
|
|
518
|
+
parsed.schemas.first.entities.first.where_rules.first.expression
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
data/lib/expressir/express.rb
CHANGED
|
@@ -10,23 +10,36 @@ module Expressir
|
|
|
10
10
|
|
|
11
11
|
# Core classes (autoloaded for lazy loading)
|
|
12
12
|
autoload :Cache, "#{__dir__}/express/cache"
|
|
13
|
+
autoload :Checker, "#{__dir__}/express/checker"
|
|
14
|
+
autoload :Concatenator, "#{__dir__}/express/concatenator"
|
|
15
|
+
autoload :Core, "#{__dir__}/express/core"
|
|
16
|
+
autoload :RemarkOverlay, "#{__dir__}/express/remark_overlay"
|
|
17
|
+
autoload :ModelTraversal, "#{__dir__}/express/model_traversal"
|
|
18
|
+
autoload :RefsOverlay, "#{__dir__}/express/refs_overlay"
|
|
13
19
|
autoload :Error, "#{__dir__}/express/error"
|
|
14
20
|
autoload :Formatter, "#{__dir__}/express/formatter"
|
|
15
21
|
autoload :Formatters, "#{__dir__}/express/formatters"
|
|
16
22
|
autoload :Grammar, "#{__dir__}/express/grammar"
|
|
17
23
|
autoload :HyperlinkFormatter, "#{__dir__}/express/hyperlink_formatter"
|
|
24
|
+
autoload :InterfaceDot, "#{__dir__}/express/interface_dot"
|
|
18
25
|
autoload :LineMap, "#{__dir__}/express/line_map"
|
|
26
|
+
autoload :Listing, "#{__dir__}/express/listing"
|
|
19
27
|
autoload :ModelVisitor, "#{__dir__}/express/model_visitor"
|
|
20
28
|
autoload :NodePositionIndex, "#{__dir__}/express/node_position_index"
|
|
21
29
|
autoload :ParallelFiles, "#{__dir__}/express/parallel_files"
|
|
22
30
|
autoload :Parser, "#{__dir__}/express/parser"
|
|
23
31
|
autoload :PrettyFormatter, "#{__dir__}/express/pretty_formatter"
|
|
32
|
+
autoload :PrettyGate, "#{__dir__}/express/pretty_gate"
|
|
24
33
|
autoload :RemarkAttacher, "#{__dir__}/express/remark_attacher"
|
|
25
34
|
autoload :RemarkScanner, "#{__dir__}/express/remark_scanner"
|
|
26
35
|
autoload :ResolveReferencesModelVisitor,
|
|
27
36
|
"#{__dir__}/express/resolve_references_model_visitor"
|
|
37
|
+
autoload :SelfSchemaReference, "#{__dir__}/express/self_schema_reference"
|
|
38
|
+
autoload :Shtolo, "#{__dir__}/express/shtolo"
|
|
28
39
|
autoload :SchemaBlockScanner, "#{__dir__}/express/schema_block_scanner"
|
|
29
40
|
autoload :SchemaHeadFormatter, "#{__dir__}/express/schema_head_formatter"
|
|
41
|
+
autoload :SchemaPlainSourceFormatter,
|
|
42
|
+
"#{__dir__}/express/schema_plain_source_formatter"
|
|
30
43
|
autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
|
|
31
44
|
autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
|
|
32
45
|
autoload :SourceFormatter, "#{__dir__}/express/source_formatter"
|