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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.github/workflows/rust-ext.yml +41 -0
  4. data/.github/workflows/verify_remarks.yml +33 -0
  5. data/.gitignore +6 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_todo.yml +1 -0
  8. data/Cargo.toml +7 -0
  9. data/README.adoc +65 -0
  10. data/Rakefile +12 -0
  11. data/TODO.parity-ee/00-overview.md +58 -0
  12. data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
  13. data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
  14. data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
  15. data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
  16. data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
  17. data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
  18. data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
  19. data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
  20. data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
  21. data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
  22. data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
  23. data/TODO.parity-ee/parity-matrix.md +108 -0
  24. data/exe/expressir +6 -0
  25. data/ext/expressir_core/Cargo.toml +24 -0
  26. data/ext/expressir_core/extconf.rb +30 -0
  27. data/ext/expressir_core/src/lib.rs +446 -0
  28. data/lib/expressir/cli.rb +49 -4
  29. data/lib/expressir/commands/check.rb +56 -0
  30. data/lib/expressir/commands/expand.rb +26 -0
  31. data/lib/expressir/commands/fix.rb +33 -0
  32. data/lib/expressir/commands/flatten.rb +29 -0
  33. data/lib/expressir/commands/parity_inputs.rb +85 -0
  34. data/lib/expressir/commands/validate.rb +22 -0
  35. data/lib/expressir/commands.rb +5 -0
  36. data/lib/expressir/express/builder_registry.rb +11 -2
  37. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
  38. data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
  39. data/lib/expressir/express/builders/type_builder.rb +8 -0
  40. data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
  41. data/lib/expressir/express/checker.rb +458 -0
  42. data/lib/expressir/express/concatenator.rb +79 -0
  43. data/lib/expressir/express/core.rb +66 -0
  44. data/lib/expressir/express/formatter.rb +25 -3
  45. data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
  46. data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
  47. data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
  48. data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
  49. data/lib/expressir/express/interface_dot.rb +105 -0
  50. data/lib/expressir/express/listing.rb +174 -0
  51. data/lib/expressir/express/model_traversal.rb +42 -0
  52. data/lib/expressir/express/node_position_index.rb +18 -4
  53. data/lib/expressir/express/parser.rb +242 -4
  54. data/lib/expressir/express/pretty_gate.rb +61 -0
  55. data/lib/expressir/express/refs_overlay.rb +47 -0
  56. data/lib/expressir/express/remark_attacher.rb +122 -10
  57. data/lib/expressir/express/remark_overlay.rb +86 -0
  58. data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
  59. data/lib/expressir/express/schema_source_formatter.rb +5 -5
  60. data/lib/expressir/express/self_schema_reference.rb +126 -0
  61. data/lib/expressir/express/shtolo.rb +522 -0
  62. data/lib/expressir/express.rb +13 -0
  63. data/lib/expressir/model/concerns.rb +15 -1
  64. data/lib/expressir/model/declarations/entity.rb +6 -1
  65. data/lib/expressir/model/declarations/interface.rb +13 -0
  66. data/lib/expressir/model/declarations/schema.rb +74 -23
  67. data/lib/expressir/model/indexes/item_graph.rb +135 -0
  68. data/lib/expressir/model/indexes.rb +1 -0
  69. data/lib/expressir/model/model_element.rb +30 -1
  70. data/lib/expressir/model/remark_info.rb +20 -1
  71. data/lib/expressir/model/remark_placement.rb +15 -2
  72. data/lib/expressir/model/repository.rb +8 -2
  73. data/lib/expressir/model.rb +2 -1
  74. data/lib/expressir/version.rb +1 -1
  75. data/lib/tasks/verify_remarks.rake +9 -0
  76. data/rakelib/verify_remarks.rake +16 -0
  77. 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
@@ -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"