expressir 2.4.31-arm-linux → 2.4.36-arm-linux

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +34 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +1 -1
  5. data/TODO.eengine/00-overview.md +39 -0
  6. data/TODO.eengine/01-resolution-diagnostics.md +30 -0
  7. data/TODO.eengine/02-check-catalog-completion.md +53 -0
  8. data/TODO.eengine/03-entity-computations.md +22 -0
  9. data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
  10. data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
  11. data/TODO.eengine/06-population-p21.md +34 -0
  12. data/TODO.eengine/07-schema-compare-patch.md +25 -0
  13. data/TODO.eengine/08-official-corpus-gate.md +7 -0
  14. data/TODO.eengine/09-mim-mapping-parser.md +9 -0
  15. data/TODO.parity-ee/00-overview.md +1 -1
  16. data/TODO.sota/00-overview.md +57 -0
  17. data/TODO.sota/01-type-checking.md +115 -0
  18. data/TODO.sota/02-evaluator.md +86 -0
  19. data/TODO.sota/03-conformance.md +53 -0
  20. data/TODO.sota/04-lsp.md +70 -0
  21. data/TODO.sota/05-interop.md +55 -0
  22. data/TODO.sota/06-dialects.md +57 -0
  23. data/TODO.sota/07-reference-artifacts.md +55 -0
  24. data/TODO.suma-improvements/07-part28-xml.md +10 -13
  25. data/docs/_guides/interop/index.adoc +25 -0
  26. data/docs/_guides/interop/json-schema.adoc +88 -0
  27. data/docs/_guides/interop/owl-rdf.adoc +87 -0
  28. data/docs/_guides/reference/checker-notes.adoc +55 -0
  29. data/docs/_guides/reference/checker.adoc +36 -0
  30. data/docs/_guides/reference/evaluator.adoc +40 -0
  31. data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
  32. data/docs/_guides/reference/index.adoc +22 -0
  33. data/docs/_guides/reference/lsp.adoc +41 -0
  34. data/docs/_guides/reference/p21.adoc +30 -0
  35. data/docs/_guides/reference/shtolo.adoc +36 -0
  36. data/exe/expressir-lsp +6 -0
  37. data/expressir.gemspec +2 -2
  38. data/lib/expressir/3.2/expressir_core.so +0 -0
  39. data/lib/expressir/3.3/expressir_core.so +0 -0
  40. data/lib/expressir/3.4/expressir_core.so +0 -0
  41. data/lib/expressir/4.0/expressir_core.so +0 -0
  42. data/lib/expressir/cli.rb +25 -0
  43. data/lib/expressir/commands/check.rb +3 -1
  44. data/lib/expressir/commands/export_json_schema.rb +29 -0
  45. data/lib/expressir/commands/export_owl.rb +26 -0
  46. data/lib/expressir/commands/smrl_xml_index.rb +25 -0
  47. data/lib/expressir/commands/validate.rb +3 -0
  48. data/lib/expressir/commands.rb +3 -0
  49. data/lib/expressir/conformance/trend.rb +100 -0
  50. data/lib/expressir/conformance.rb +8 -0
  51. data/lib/expressir/eval/environment.rb +83 -0
  52. data/lib/expressir/eval/expression.rb +610 -0
  53. data/lib/expressir/eval/interpreter.rb +316 -0
  54. data/lib/expressir/eval/rules.rb +88 -0
  55. data/lib/expressir/eval/values.rb +74 -0
  56. data/lib/expressir/eval.rb +17 -0
  57. data/lib/expressir/express/builder.rb +17 -5
  58. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
  59. data/lib/expressir/express/builders/expression_builder.rb +9 -1
  60. data/lib/expressir/express/checker.rb +970 -5
  61. data/lib/expressir/express/dialect.rb +44 -0
  62. data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
  63. data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
  64. data/lib/expressir/express/parser.rb +9 -4
  65. data/lib/expressir/express/pretty_formatter.rb +9 -14
  66. data/lib/expressir/express/schema_compare.rb +194 -0
  67. data/lib/expressir/express/shtolo.rb +194 -63
  68. data/lib/expressir/express.rb +3 -0
  69. data/lib/expressir/interop/json_schema.rb +195 -0
  70. data/lib/expressir/interop/owl.rb +216 -0
  71. data/lib/expressir/interop.rb +13 -0
  72. data/lib/expressir/lsp/document.rb +226 -0
  73. data/lib/expressir/lsp/server.rb +184 -0
  74. data/lib/expressir/lsp/workspace.rb +402 -0
  75. data/lib/expressir/lsp.rb +11 -0
  76. data/lib/expressir/model/declarations/entity.rb +132 -0
  77. data/lib/expressir/model/declarations/schema.rb +37 -6
  78. data/lib/expressir/p21/builder.rb +92 -0
  79. data/lib/expressir/p21/grammar.rb +107 -0
  80. data/lib/expressir/p21/parser.rb +16 -0
  81. data/lib/expressir/p21/population.rb +35 -0
  82. data/lib/expressir/p21/validator.rb +213 -0
  83. data/lib/expressir/p21.rb +17 -0
  84. data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
  85. data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
  86. data/lib/expressir/reference/note_catalog.rb +54 -0
  87. data/lib/expressir/reference.rb +10 -0
  88. data/lib/expressir/version.rb +1 -1
  89. data/lib/expressir.rb +5 -0
  90. data/lib/tasks/conformance.rake +27 -0
  91. data/lib/tasks/reference.rake +25 -0
  92. metadata +68 -4
@@ -32,8 +32,9 @@ module Expressir
32
32
 
33
33
  attr_reader :notes
34
34
 
35
- def initialize(repository)
35
+ def initialize(repository, dialect: nil)
36
36
  @repository = repository
37
+ @dialect = dialect.nil? ? nil : Dialect.normalize(dialect)
37
38
  @notes = []
38
39
  # Case-folded index to LISTS of schemas: EXPRESS schema ids are
39
40
  # case-insensitive, and two schemas differing only in case are
@@ -82,6 +83,37 @@ module Expressir
82
83
  blength insert remove
83
84
  ].freeze
84
85
 
86
+ USE_ITEM_KINDS = [Model::Declarations::Entity, Model::Declarations::Type].freeze
87
+ REFERENCE_ITEM_KINDS = [
88
+ Model::Declarations::Constant, Model::Declarations::Entity,
89
+ Model::Declarations::Function, Model::Declarations::Procedure,
90
+ Model::Declarations::Type
91
+ ].freeze
92
+
93
+ NUMERIC_TYPES = %i[integer real number].freeze
94
+ LOGICAL_TYPES = %i[logical boolean].freeze
95
+ ORDERING_FAMILIES = {
96
+ numeric: NUMERIC_TYPES,
97
+ logical: LOGICAL_TYPES,
98
+ string: %i[string],
99
+ binary: %i[binary],
100
+ enum: %i[enum],
101
+ }.freeze
102
+
103
+ TYPE_CLASS_SYMBOLS = {
104
+ Model::DataTypes::Integer => :integer,
105
+ Model::DataTypes::Real => :real,
106
+ Model::DataTypes::Number => :number,
107
+ Model::DataTypes::String => :string,
108
+ Model::DataTypes::Binary => :binary,
109
+ Model::DataTypes::Boolean => :boolean,
110
+ Model::DataTypes::Logical => :logical,
111
+ Model::DataTypes::List => :aggregate,
112
+ Model::DataTypes::Set => :aggregate,
113
+ Model::DataTypes::Bag => :aggregate,
114
+ Model::DataTypes::Array => :aggregate,
115
+ }.freeze
116
+
85
117
  private
86
118
 
87
119
  def note!(id, severity, schema, message, node = nil)
@@ -92,6 +124,7 @@ module Expressir
92
124
  def check_schema(schema)
93
125
  check_duplicates(schema)
94
126
  check_interfaces(schema)
127
+ check_type_resolutions(schema)
95
128
  check_self_schema_reference(schema)
96
129
  schema.entities.each { |e| check_entity(schema, e) }
97
130
  schema.types.each do |t|
@@ -100,12 +133,632 @@ module Expressir
100
133
  check_where_rules(schema, t, t.where_rules)
101
134
  end
102
135
  schema.rules.each { |r| check_where_rules(schema, r, r.where_rules) }
136
+ check_subtype_constraints(schema)
103
137
  schema.entities.each do |entity|
104
138
  check_aggregate_bounds(schema, entity)
105
139
  check_attrib_name_fun(schema, entity)
140
+ check_attribute_redeclarations(schema, entity)
106
141
  end
107
142
  check_string_references(schema)
108
143
  check_unresolved_refs(schema)
144
+ check_expression_types(schema)
145
+ check_statement_types(schema)
146
+ check_rule_structure(schema)
147
+ check_dialect(schema) if @dialect
148
+ end
149
+
150
+ # ---- TODO.sota/06: edition awareness ----
151
+ # The grammar accepts both editions; the selected dialect decides
152
+ # which declarations are out of edition. The audited change list
153
+ # is spec/fixtures/dialects/iso-10303-11-2004-changes.yaml.
154
+ def check_dialect(schema)
155
+ modern = dialect_2004_constructs(schema)
156
+ deprecated = Array(schema.entities).select do |entity|
157
+ deprecated_inline_supertype?(entity)
158
+ end
159
+
160
+ if @dialect == Dialect::EDITION_1994
161
+ modern.each do |node, message|
162
+ note!(:check_dialect_2004_only, :error, schema,
163
+ "dialect 10303-11:1994: #{message}", node)
164
+ end
165
+ else
166
+ deprecated.each do |entity|
167
+ note!(:check_dialect_deprecated, :warning, schema,
168
+ "dialect 10303-11:2004: #{entity.id}: inline subtype " \
169
+ "constraint syntax is deprecated; use SUBTYPE_CONSTRAINT " \
170
+ "(Annex K)", entity)
171
+ end
172
+ end
173
+
174
+ return unless modern.any? && deprecated.any?
175
+
176
+ note!(:check_dialect_mixed, :warning, schema,
177
+ "dialect: schema mixes 2004-only declarations with " \
178
+ "edition-1 inline subtype constraint syntax", deprecated.first)
179
+ end
180
+
181
+ def dialect_2004_constructs(schema)
182
+ constructs = []
183
+ Array(schema.types).each do |type|
184
+ underlying = type.underlying_type
185
+ next unless underlying
186
+
187
+ if underlying.respond_to?(:extensible) && underlying.extensible
188
+ constructs << [type,
189
+ "#{type.id}: EXTENSIBLE is not part of " \
190
+ "ISO 10303-11:1994 (2004 edition, 8.4.1/8.4.2)"]
191
+ end
192
+ if underlying.respond_to?(:based_on) && underlying.based_on
193
+ constructs << [type,
194
+ "#{type.id}: BASED_ON is not part of " \
195
+ "ISO 10303-11:1994 (2004 edition, 8.4.1/8.4.2)"]
196
+ end
197
+ if underlying.respond_to?(:generic_entity) && underlying.generic_entity
198
+ constructs << [type,
199
+ "#{type.id}: GENERIC_ENTITY is not part of " \
200
+ "ISO 10303-11:1994 (2004 edition, 8.4.2)"]
201
+ end
202
+ end
203
+ Array(schema.subtype_constraints).each do |constraint|
204
+ constructs << [constraint,
205
+ "#{constraint.id}: SUBTYPE_CONSTRAINT is not part " \
206
+ "of ISO 10303-11:1994 (2004 edition, 9; Annex G)"]
207
+ end
208
+ constructs
209
+ end
210
+
211
+ def deprecated_inline_supertype?(entity)
212
+ expression = entity.supertype_expression
213
+ expression.is_a?(Model::SupertypeExpressions::OneofSupertypeExpression) ||
214
+ expression.is_a?(Model::SupertypeExpressions::BinarySupertypeExpression)
215
+ end
216
+
217
+ # ---- TODO.sota/01 tier 2: operator domain typing ----
218
+ # ISO 10303-11:2004 ch.12. eengine's expression checks traverse
219
+ # structurally only (kernel/check.lisp), so these notes carry the
220
+ # new :check_expr_operand_domain id with the clause in the
221
+ # message. A violation is flagged only when BOTH operand types
222
+ # are statically known.
223
+
224
+ def check_expression_types(schema)
225
+ check_initializer_types(schema)
226
+ expression_scopes(schema).each do |scope|
227
+ @current_expression_env = expression_env(schema, scope)
228
+ each_node(scope) do |node|
229
+ case node
230
+ when Model::Expressions::BinaryExpression
231
+ check_binary_expression_types(schema, node)
232
+ when Model::Expressions::UnaryExpression
233
+ check_unary_expression_types(schema, node)
234
+ when Model::Expressions::Interval
235
+ check_interval_types(schema, node)
236
+ when Model::References::IndexReference
237
+ check_index_reference_types(schema, node)
238
+ end
239
+ end
240
+ end
241
+ @current_expression_env = nil
242
+ end
243
+
244
+ def expression_scopes(schema)
245
+ Array(schema.constants) + Array(schema.types) +
246
+ Array(schema.entities) + Array(schema.functions) +
247
+ Array(schema.procedures) + Array(schema.rules)
248
+ end
249
+
250
+ # name → declared type node, for identifiers visible to the
251
+ # scope's expressions: the schema's constants, plus the scope's
252
+ # own parameters/variables/attributes.
253
+ def expression_env(schema, scope)
254
+ env = {}
255
+ Array(schema.constants).each do |constant|
256
+ env[constant.id&.safe_downcase] = constant.type if constant.id
257
+ end
258
+ case scope
259
+ when Model::Declarations::Entity
260
+ Array(scope.attributes).each do |attr|
261
+ env[attr.id&.safe_downcase] = attr.type if attr.id
262
+ end
263
+ when Model::Declarations::Function, Model::Declarations::Procedure
264
+ Array(scope.parameters).each do |param|
265
+ env[param.id&.safe_downcase] = param.type if param.id
266
+ end
267
+ Array(scope.variables).each do |variable|
268
+ env[variable.id&.safe_downcase] = variable.type if variable.id
269
+ end
270
+ when Model::Declarations::Rule
271
+ Array(scope.variables).each do |variable|
272
+ env[variable.id&.safe_downcase] = variable.type if variable.id
273
+ end
274
+ end
275
+ env
276
+ end
277
+
278
+ def check_binary_expression_types(schema, node)
279
+ left = static_type(schema, node.operand1)
280
+ right = static_type(schema, node.operand2)
281
+ return if undecidable?(left, right)
282
+
283
+ domain = binary_domain_violation(node.operator, left, right)
284
+ return unless domain
285
+
286
+ note!(:check_expr_operand_domain, :error, schema,
287
+ "operator '#{binary_operator_spelling(node.operator)}': " \
288
+ "#{domain} (#{left} vs #{right})", node)
289
+ end
290
+
291
+ def undecidable?(*types)
292
+ types.any? { |t| t.nil? || t == :unknown || t == :generic }
293
+ end
294
+
295
+ # nil = ok; String = the violation message body. Clauses per the
296
+ # TODO.sota/01 clause map.
297
+ def binary_domain_violation(operator, left, right)
298
+ case operator
299
+ when "ADDITION"
300
+ return nil if numeric_pair?(left, right) ||
301
+ same_pair?(left, right, :string) ||
302
+ same_pair?(left, right, :binary) ||
303
+ left == :aggregate || right == :aggregate
304
+
305
+ # 12.6 table 17: union admits an element singleton on either
306
+ # side of an aggregate; element↔base-type compatibility is a
307
+ # later tier.
308
+ "addition requires numeric, string, binary or aggregate " \
309
+ "operands (12.1, 12.3.2, 12.5.2, 12.6)"
310
+ when "SUBTRACTION"
311
+ return nil if numeric_pair?(left, right) ||
312
+ (left == :aggregate && right != :unknown) ||
313
+ same_pair?(left, right, :aggregate)
314
+
315
+ # 12.6 table 18: the difference's right operand may be an
316
+ # element singleton; the left must be an aggregate.
317
+ "difference requires an aggregate left operand (12.1, 12.6)"
318
+ when "MULTIPLICATION"
319
+ return nil if numeric_pair?(left, right) ||
320
+ same_pair?(left, right, :aggregate)
321
+
322
+ "intersection requires aggregate operands (12.1, 12.6)"
323
+ when "REAL_DIVISION", "EXPONENTIATION", "INTEGER_DIVISION", "MODULO"
324
+ "operator requires numeric operands (12.1)" unless numeric_pair?(left, right)
325
+ when "AND", "OR", "XOR"
326
+ "logical operator requires logical operands (12.4)" unless
327
+ LOGICAL_TYPES.include?(left) && LOGICAL_TYPES.include?(right)
328
+ when "EQUAL", "NOT_EQUAL"
329
+ "value comparison requires comparable operands (12.2.1)" unless
330
+ comparable_equality?(left, right)
331
+ when "LESS_THAN", "LESS_THAN_OR_EQUAL", "GREATER_THAN",
332
+ "GREATER_THAN_OR_EQUAL"
333
+ if left == :aggregate || right == :aggregate
334
+ unless left == :aggregate && right == :aggregate
335
+ return "subset/superset requires aggregate operands (12.6)"
336
+ end
337
+
338
+ return nil
339
+ end
340
+
341
+ "ordering comparison requires same-family operands (12.2.1)" unless
342
+ same_ordering_family?(left, right)
343
+ when "INSTANCE_EQUAL", "INSTANCE_NOT_EQUAL"
344
+ unless %i[entity aggregate select].include?(left) &&
345
+ %i[entity aggregate select].include?(right)
346
+ "instance comparison requires entity or aggregate " \
347
+ "operands (12.2.2)"
348
+ end
349
+ when "IN"
350
+ "membership requires an aggregate right operand (12.2.3)" unless
351
+ right == :aggregate
352
+ when "LIKE"
353
+ "like requires string operands (12.2.5)" unless
354
+ left == :string && right == :string
355
+ when "COMBINE"
356
+ unless left == :entity && right == :entity
357
+ "complex entity construction requires entity operands " \
358
+ "(12, precedence table note)"
359
+ end
360
+ end
361
+ end
362
+
363
+ def numeric_pair?(left, right)
364
+ NUMERIC_TYPES.include?(left) && NUMERIC_TYPES.include?(right)
365
+ end
366
+
367
+ def same_pair?(left, right, kind)
368
+ left == kind && right == kind
369
+ end
370
+
371
+ # 12.2.1: both operands of the same comparison family, with the
372
+ # numeric family spanning integer/real/number; aggregate/entity/
373
+ # select operands compare for (in)equality only.
374
+ def comparable_equality?(left, right)
375
+ return true if %i[aggregate entity select].include?(left) &&
376
+ %i[aggregate entity select].include?(right)
377
+
378
+ same_ordering_family?(left, right)
379
+ end
380
+
381
+ def same_ordering_family?(left, right)
382
+ ORDERING_FAMILIES.values.any? do |family|
383
+ family.include?(left) && family.include?(right)
384
+ end
385
+ end
386
+
387
+ def binary_operator_spelling(symbol)
388
+ {
389
+ "ADDITION" => "+", "SUBTRACTION" => "-", "REAL_DIVISION" => "/",
390
+ "INTEGER_DIVISION" => "div", "MODULO" => "mod",
391
+ "INSTANCE_EQUAL" => ":=:", "INSTANCE_NOT_EQUAL" => ":<>:"
392
+ }.fetch(symbol, symbol.downcase)
393
+ end
394
+
395
+ def check_unary_expression_types(schema, node)
396
+ t = static_type(schema, node.operand)
397
+ return if undecidable?(t)
398
+
399
+ violation =
400
+ if node.operator == "NOT"
401
+ "not requires a logical operand (12.4)" unless
402
+ LOGICAL_TYPES.include?(t)
403
+ else
404
+ unless NUMERIC_TYPES.include?(t)
405
+ "unary '#{node.operator.downcase}' requires a numeric " \
406
+ "operand (12.1)"
407
+ end
408
+ end
409
+ return unless violation
410
+
411
+ note!(:check_expr_operand_domain, :error, schema, violation, node)
412
+ end
413
+
414
+ def check_interval_types(schema, node)
415
+ types = [node.low, node.item, node.high].map do |part|
416
+ static_type(schema, part)
417
+ end
418
+ return if types.any? { |t| undecidable?(t) }
419
+ return if types.all? { |t| same_ordering_family?(types.first, t) } ||
420
+ numeric_family?(*types)
421
+
422
+ note!(:check_expr_operand_domain, :error, schema,
423
+ "interval bounds must be mutually comparable (12.2.4) " \
424
+ "(#{types.join(' vs ')})", node)
425
+ end
426
+
427
+ def numeric_family?(*types)
428
+ types.all? { |t| NUMERIC_TYPES.include?(t) }
429
+ end
430
+
431
+ # Static type of an expression node: a symbol from the lattice
432
+ # in TODO.sota/01 (:unknown when not statically decidable).
433
+ def static_type(schema, node, depth = 0)
434
+ return :unknown if node.nil? || depth > 8
435
+ return TYPE_CLASS_SYMBOLS[node.class] if TYPE_CLASS_SYMBOLS.key?(node.class)
436
+
437
+ case node
438
+ when Model::Literals::Integer then :integer
439
+ when Model::Literals::Real then :real
440
+ when Model::Literals::String then :string
441
+ when Model::Literals::Binary then :binary
442
+ when Model::Literals::Logical then :logical
443
+ when Model::Expressions::AggregateInitializer,
444
+ Model::Expressions::QueryExpression then :aggregate
445
+ when Model::Expressions::EntityConstructor,
446
+ Model::References::GroupReference then :entity
447
+ when Model::Expressions::BinaryExpression
448
+ binary_result_type(schema, node, depth)
449
+ when Model::Expressions::FunctionCall
450
+ function = schema_function(schema, node)
451
+ return :unknown unless function
452
+
453
+ static_type(schema, function.return_type, depth + 1)
454
+ when Model::DataTypes::Enumeration then :enum
455
+ when Model::DataTypes::Select then :select
456
+ when Model::DataTypes::Generic, Model::DataTypes::GenericEntity
457
+ :generic
458
+ when Model::References::SimpleReference
459
+ reference_type(schema, node, depth)
460
+ when Model::References::AttributeReference
461
+ attribute_reference_type(schema, node, depth)
462
+ when Model::References::IndexReference
463
+ index_element_type(schema, node, depth)
464
+ else :unknown
465
+ end
466
+ end
467
+
468
+ def binary_result_type(schema, node, depth)
469
+ case node.operator
470
+ when "EQUAL", "NOT_EQUAL", "LESS_THAN", "LESS_THAN_OR_EQUAL",
471
+ "GREATER_THAN", "GREATER_THAN_OR_EQUAL", "IN", "LIKE",
472
+ "INSTANCE_EQUAL", "INSTANCE_NOT_EQUAL", "AND", "OR", "XOR"
473
+ :logical
474
+ when "ADDITION"
475
+ left = static_type(schema, node.operand1, depth + 1)
476
+ NUMERIC_TYPES.include?(left) ? left : :unknown
477
+ when "REAL_DIVISION" then :real
478
+ when "INTEGER_DIVISION", "MODULO" then :integer
479
+ when "EXPONENTIATION"
480
+ both = [node.operand1, node.operand2].map do |operand|
481
+ static_type(schema, operand, depth + 1)
482
+ end
483
+ both.all?(:integer) ? :integer : :unknown
484
+ else :unknown
485
+ end
486
+ end
487
+
488
+ def builtin_symbol(name)
489
+ {
490
+ "integer" => :integer, "real" => :real, "number" => :number,
491
+ "string" => :string, "binary" => :binary, "boolean" => :boolean,
492
+ "logical" => :logical
493
+ }.fetch(name, :unknown)
494
+ end
495
+
496
+ def schema_function(schema, node)
497
+ callee = node.function
498
+ return nil unless callee.is_a?(Model::References::SimpleReference)
499
+
500
+ Array(schema.functions).find do |function|
501
+ function.id&.safe_downcase == callee.id&.safe_downcase
502
+ end
503
+ end
504
+
505
+ def reference_type(schema, node, depth)
506
+ key = node.id&.safe_downcase
507
+ return :unknown unless key
508
+
509
+ # builtin type names may arrive as unresolved references
510
+ # depending on parse layout
511
+ return builtin_symbol(key) if builtin?(node.id)
512
+
513
+ if (bound = @current_expression_env&.[](key))
514
+ return static_type(schema, bound, depth + 1)
515
+ end
516
+
517
+ declaration = find_type(schema, node.id.to_s)
518
+ return static_type(schema, declaration.underlying_type, depth + 1) if declaration
519
+
520
+ find_entity(schema, node.id) ? :entity : :unknown
521
+ end
522
+
523
+ # `color.red` parses as an AttributeReference over a reference to
524
+ # the enumeration TYPE (base infers :enum). An entity-typed base
525
+ # resolves through the entity's own attributes.
526
+ def attribute_reference_type(schema, node, depth)
527
+ base_node = node.ref
528
+ base = static_type(schema, base_node, depth + 1)
529
+ return :enum if base == :enum
530
+
531
+ entity = entity_for_reference(schema, base_node)
532
+ return :unknown unless entity
533
+
534
+ attr = Array(entity.attributes).find do |candidate|
535
+ candidate.id&.safe_downcase == node.attribute&.id&.safe_downcase
536
+ end
537
+ return :unknown unless attr
538
+
539
+ static_type(schema, attr.type, depth + 1)
540
+ end
541
+
542
+ def entity_for_reference(schema, node)
543
+ return nil unless node.is_a?(Model::References::SimpleReference)
544
+
545
+ find_entity(schema, node.id.to_s)
546
+ end
547
+
548
+ # ---- tier 3: aggregates through indexing ----
549
+
550
+ # 12.6.1 / 12.3.1 / 12.5.1: indexing yields the aggregate's base
551
+ # element; string indexing yields a string; binary indexing a
552
+ # binary.
553
+ def index_element_type(schema, node, depth)
554
+ element = index_element_type_node(schema, node.ref, depth + 1)
555
+ return :unknown if element.nil?
556
+
557
+ symbol = static_type(schema, element, depth + 1)
558
+ return symbol unless symbol == :unknown
559
+
560
+ case element
561
+ when Model::DataTypes::String then :string
562
+ when Model::DataTypes::Binary then :binary
563
+ else :unknown
564
+ end
565
+ end
566
+
567
+ def index_element_type_node(schema, node, depth)
568
+ return nil if node.nil? || depth > 8
569
+
570
+ type_node = declared_type_node(schema, node, depth)
571
+ case type_node
572
+ when Model::DataTypes::List, Model::DataTypes::Set,
573
+ Model::DataTypes::Bag, Model::DataTypes::Array
574
+ type_node.base_type
575
+ when Model::DataTypes::String, Model::DataTypes::Binary
576
+ type_node
577
+ when Model::DataTypes::Select
578
+ nil
579
+ end
580
+ end
581
+
582
+ # The declared type NODE an expression position names (as opposed
583
+ # to the static_type symbol): scope environment, schema types and
584
+ # entities, function returns.
585
+ def declared_type_node(schema, node, depth)
586
+ case node
587
+ when Model::References::SimpleReference
588
+ key = node.id&.safe_downcase
589
+ return nil unless key
590
+
591
+ @current_expression_env&.[](key) ||
592
+ find_type(schema, node.id.to_s)&.underlying_type ||
593
+ (find_entity(schema, node.id) && Model::DataTypes::GenericEntity.new)
594
+ when Model::References::IndexReference
595
+ index_element_type_node(schema, node.ref, depth + 1)
596
+ when Model::Expressions::FunctionCall
597
+ function = schema_function(schema, node)
598
+ function&.return_type
599
+ end
600
+ end
601
+
602
+ # 12.6.1/12.3.1/12.5.1: the indexed value shall be an aggregate,
603
+ # string or binary; 12.6.1/12.3.1/12.5.1 + 12.1: index positions
604
+ # shall be numeric.
605
+ def check_index_reference_types(schema, node)
606
+ base = static_type(schema, node.ref)
607
+ unless undecidable?(base) || base == :aggregate ||
608
+ base == :string || base == :binary || base == :select
609
+ note!(:check_expr_operand_domain, :error, schema,
610
+ "indexing requires an aggregate, string or binary " \
611
+ "operand (12.6.1, 12.3.1, 12.5.1) (#{base})", node)
612
+ end
613
+
614
+ [node.index1, node.index2].compact.each do |position|
615
+ position_type = static_type(schema, position)
616
+ next if undecidable?(position_type)
617
+ next if NUMERIC_TYPES.include?(position_type)
618
+
619
+ note!(:check_expr_operand_domain, :error, schema,
620
+ "index position shall be numeric (12.6.1) " \
621
+ "(#{position_type})", position)
622
+ end
623
+ end
624
+
625
+ # ---- tier 4: initializer compatibility (12.11) ----
626
+
627
+ # `x : T := expr` in CONSTANT, DERIVE and LOCAL: the initializer
628
+ # shall be compatible with the declared type (same type or a
629
+ # specialization, 12.11); numerics widen integer→real→number.
630
+ def check_initializer_types(schema)
631
+ initializers(schema).each do |owner, declaration|
632
+ @current_expression_env = if owner
633
+ expression_env(schema, owner)
634
+ else
635
+ expression_env(schema, schema)
636
+ end
637
+ declared = static_type(schema, declaration.type)
638
+ expression = declaration.expression
639
+ next if expression.nil? || undecidable?(declared)
640
+
641
+ expressed = static_type(schema, expression)
642
+ next if undecidable?(expressed)
643
+ next if initializer_compatible?(schema, declaration.type,
644
+ declared, expression, expressed)
645
+
646
+ note!(:check_expr_initializer_type, :error, schema,
647
+ "initializer #{expressed} is not compatible with the " \
648
+ "declared #{declared} (12.11)", declaration)
649
+ end
650
+ @current_expression_env = nil
651
+ end
652
+
653
+ def initializers(schema)
654
+ pairs = []
655
+ Array(schema.constants).each do |constant|
656
+ pairs << [nil, constant] if constant.expression
657
+ end
658
+ schema.entities.each do |entity|
659
+ Array(entity.attributes).each do |attr|
660
+ next unless attr.is_a?(Model::Declarations::Attribute) &&
661
+ !attr.is_a?(Model::Declarations::InverseAttribute) &&
662
+ attr.expression
663
+
664
+ pairs << [entity, attr]
665
+ end
666
+ end
667
+ schema.functions.each do |function|
668
+ Array(function.variables).each do |variable|
669
+ pairs << [function, variable] if variable.expression
670
+ end
671
+ end
672
+ pairs
673
+ end
674
+
675
+ def initializer_compatible?(schema, declared_node, declared, expression, expressed)
676
+ # enum and entity symbols are families, not identities — their
677
+ # compatibility is decided by declaration comparison below.
678
+ return true if declared == expressed &&
679
+ !%i[enum entity].include?(declared)
680
+ return true if declared == :number && NUMERIC_TYPES.include?(expressed)
681
+ return true if declared == :real && expressed == :integer
682
+ return true if LOGICAL_TYPES.include?(declared) &&
683
+ LOGICAL_TYPES.include?(expressed)
684
+ return true if declared == :select
685
+ return true if expressed == :select
686
+ return true if declared == :aggregate && expressed == :aggregate
687
+
688
+ # enumerations: same declared TYPE (expression enum members are
689
+ # AttributeReferences over the enumeration type)
690
+ return same_enum_declaration?(schema, declared_node, expression) if
691
+ declared == :enum && expressed == :enum
692
+
693
+ # entities: same entity or a specialization (12.11)
694
+ return entities_compatible?(schema, declared_node, expression) if
695
+ declared == :entity && expressed == :entity
696
+
697
+ false
698
+ end
699
+
700
+ # Resolve a type reference chain to its Type or Entity
701
+ # declaration: schema types and entities first, then the scope
702
+ # environment, then attribute/index member resolution.
703
+ def resolved_declaration(schema, node, depth = 0)
704
+ return nil if node.nil? || depth > 6
705
+
706
+ case node
707
+ when Model::Declarations::Type, Model::Declarations::Entity
708
+ node
709
+ when Model::References::SimpleReference
710
+ key = node.id&.safe_downcase
711
+ return nil unless key
712
+
713
+ bound = @current_expression_env&.[](key)
714
+ resolved_declaration(schema,
715
+ bound || find_type(schema, node.id.to_s) ||
716
+ find_entity(schema, node.id), depth + 1)
717
+ when Model::References::AttributeReference
718
+ owner = resolved_declaration(schema, node.ref, depth + 1)
719
+ # an enumeration member (`color.red`): the declaration is the
720
+ # enumeration TYPE itself
721
+ return owner if owner.is_a?(Model::Declarations::Type) &&
722
+ owner.underlying_type.is_a?(Model::DataTypes::Enumeration)
723
+
724
+ return nil unless owner.is_a?(Model::Declarations::Entity)
725
+
726
+ attr = Array(owner.attributes).find do |candidate|
727
+ candidate.id&.safe_downcase == node.attribute&.id&.safe_downcase
728
+ end
729
+ resolved_declaration(schema, attr&.type, depth + 1)
730
+ when Model::References::IndexReference
731
+ element = index_element_type_node(schema, node.ref, depth + 1)
732
+ resolved_declaration(schema, element, depth + 1)
733
+ when Model::References::GroupReference
734
+ resolved_declaration(schema, node.entity, depth + 1)
735
+ when Model::Expressions::FunctionCall
736
+ function = schema_function(schema, node)
737
+ resolved_declaration(schema, function&.return_type, depth + 1)
738
+ end
739
+ end
740
+
741
+ def same_enum_declaration?(schema, declared_node, expression)
742
+ slot_enum = resolved_declaration(schema, declared_node)
743
+ expr_enum = resolved_declaration(schema, expression)
744
+ return false unless slot_enum.is_a?(Model::Declarations::Type) &&
745
+ expr_enum.is_a?(Model::Declarations::Type)
746
+
747
+ slot_enum.underlying_type.is_a?(Model::DataTypes::Enumeration) &&
748
+ expr_enum.underlying_type.is_a?(Model::DataTypes::Enumeration) &&
749
+ slot_enum.id&.safe_downcase == expr_enum.id&.safe_downcase
750
+ end
751
+
752
+ def entities_compatible?(schema, declared_node, expression)
753
+ slot_entity = resolved_declaration(schema, declared_node)
754
+ expr_entity = resolved_declaration(schema, expression)
755
+ return false unless slot_entity.is_a?(Model::Declarations::Entity) &&
756
+ expr_entity.is_a?(Model::Declarations::Entity)
757
+
758
+ slot_entity.id&.safe_downcase == expr_entity.id&.safe_downcase ||
759
+ expr_entity.supertype_closure
760
+ .map { |e| e.id&.safe_downcase }
761
+ .include?(slot_entity.id&.safe_downcase)
109
762
  end
110
763
 
111
764
  # Self-qualified literals naming the CURRENT schema (#125): local
@@ -184,12 +837,42 @@ module Expressir
184
837
  end
185
838
 
186
839
  resource_names(iface).each do |name|
187
- next if foreign_decl?(foreign, name)
840
+ decl = foreign_declaration(foreign, name)
841
+ unless decl
842
+ note!(:check_unresolved_ref, :error, schema,
843
+ "interface resource '#{name}' not in schema '#{target}'", iface)
844
+ next
845
+ end
188
846
 
189
- note!(:check_unresolved_ref, :error, schema,
190
- "interface resource '#{name}' not in schema '#{target}'", iface)
847
+ # eeng resolve-interface: use-from resolves via
848
+ # find-type/entity, reference-from via the 5-kind or-chain;
849
+ # anything else is "Resource '~A' not found".
850
+ next if item_kind_interfacable?(iface.kind, decl)
851
+
852
+ note!(:check_interface_kind_restriction, :error, schema,
853
+ "interface resource '#{name}' is a #{decl.class.name.split('::').last}: " + "#{iface.kind} resolves #{allowed_kinds(iface.kind)} only", iface)
854
+ end
855
+ end
856
+ end
857
+
858
+ def item_kind_interfacable?(kind, decl)
859
+ (kind == Model::Declarations::Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS)
860
+ .any? { |k| decl.is_a?(k) }
861
+ end
862
+
863
+ def allowed_kinds(kind)
864
+ (kind == Model::Declarations::Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS)
865
+ .map { |k| k.name.split("::").last }.join(", ")
866
+ end
867
+
868
+ def foreign_declaration(schema, name)
869
+ key = name.safe_downcase
870
+ DECL_COLLECTIONS.each do |coll|
871
+ Array(schema.public_send(coll)).each do |d|
872
+ return d if d.id&.safe_downcase == key
191
873
  end
192
874
  end
875
+ nil
193
876
  end
194
877
 
195
878
  def iface_schema_name(iface)
@@ -210,6 +893,85 @@ module Expressir
210
893
  end
211
894
  end
212
895
 
896
+ # eeng check-p11 context checks (kernel/check.lisp:160-201,
897
+ # 299-336, 337-395): constant/local/parameter/return types must
898
+ # resolve to a TYPE or ENTITY visible in the schema (find-type/
899
+ # entity semantics - constants, functions, procedures do NOT
900
+ # satisfy them). Note ids are the eeng ones.
901
+ def check_type_resolutions(schema)
902
+ schema.constants.each do |constant|
903
+ check_type_ref(schema, Array(constant.type), :check_constant_type, constant.id, constant)
904
+ end
905
+
906
+ schema.functions.each do |function|
907
+ Array(function.parameters).each do |param|
908
+ check_type_ref(schema, Array(param.type), :check_parameter_type, param.id, param)
909
+ end
910
+ check_type_ref(schema, Array(function.return_type),
911
+ :check_function_return_type, function.id, function)
912
+ Array(function.variables).each do |variable|
913
+ check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
914
+ end
915
+ end
916
+
917
+ schema.procedures.each do |procedure|
918
+ Array(procedure.parameters).each do |param|
919
+ check_type_ref(schema, Array(param.type), :check_parameter_type, param.id, param)
920
+ end
921
+ Array(procedure.variables).each do |variable|
922
+ check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
923
+ end
924
+ end
925
+
926
+ schema.rules.each do |rule|
927
+ Array(rule.applies_to).each do |entity_ref|
928
+ # parameter-rule: the FOR entities resolve via
929
+ # find-type/entity too (kernel/check.lisp:311-322)
930
+ check_entity_ref(schema, Array(entity_ref), :check_parameter_type, rule.id, rule)
931
+ end
932
+ Array(rule.variables).each do |variable|
933
+ check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
934
+ end
935
+ end
936
+ end
937
+
938
+ def check_type_ref(schema, nodes, note_id, name, node)
939
+ nodes.each do |type|
940
+ next unless type.is_a?(Model::References::SimpleReference)
941
+ next if type.id.nil? || builtin?(type.id)
942
+ next if resolves_to_type_or_entity?(schema, type.id)
943
+
944
+ note!(note_id, :error, schema,
945
+ "type '#{type.id}' of '#{name}' does not resolve to a type or entity", node || type)
946
+ end
947
+ end
948
+
949
+ def check_entity_ref(schema, nodes, note_id, name, node)
950
+ nodes.each do |ref|
951
+ next unless ref.is_a?(Model::References::SimpleReference)
952
+ next if ref.id.nil?
953
+ next if resolves_to_type_or_entity?(schema, ref.id)
954
+
955
+ note!(note_id, :error, schema,
956
+ "entity '#{ref.id}' of '#{name}' does not resolve to a type or entity", node || ref)
957
+ end
958
+ end
959
+
960
+ def resolves_to_type_or_entity?(schema, name)
961
+ key = name.safe_downcase
962
+ schema.children.any? do |child|
963
+ next false unless child.id&.safe_downcase == key
964
+
965
+ child.is_a?(Model::Declarations::Type) || child.is_a?(Model::Declarations::Entity) ||
966
+ (child.is_a?(Model::Declarations::InterfacedItem) && interface_item_type_or_entity?(child))
967
+ end
968
+ end
969
+
970
+ def interface_item_type_or_entity?(item)
971
+ base = item.base_item
972
+ base.is_a?(Model::Declarations::Type) || base.is_a?(Model::Declarations::Entity)
973
+ end
974
+
213
975
  def check_entity(schema, entity)
214
976
  Array(entity.subtype_of).each do |ref|
215
977
  id = ref.is_a?(String) ? ref : ref.id
@@ -282,6 +1044,206 @@ module Expressir
282
1044
 
283
1045
  # eeng check-supertype-ref: every entity named in the SUPERTYPE
284
1046
  # expression must resolve to an entity of this schema.
1047
+ # eeng check-p11 subtype_constraint (kernel/check.lisp:426) only
1048
+ # checks the supertype expression; the note id matches the entity
1049
+ # supertype path (kernel/check.lisp:523).
1050
+ def explicit_attributes(entity)
1051
+ Array(entity.attributes)
1052
+ .select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
1053
+ end
1054
+
1055
+ # ---- tier 5: statement flow (13.2.1, 13.5, 13.6, 13.7) ----
1056
+
1057
+ def statement_scopes(schema)
1058
+ Array(schema.functions) + Array(schema.procedures) +
1059
+ Array(schema.rules)
1060
+ end
1061
+
1062
+ def check_statement_types(schema)
1063
+ statement_scopes(schema).each do |owner|
1064
+ @current_expression_env = expression_env(schema, owner)
1065
+ each_node(owner) do |node|
1066
+ case node
1067
+ when Model::Statements::Assignment
1068
+ check_assignment_type(schema, node)
1069
+ when Model::Statements::If
1070
+ check_logical_expression(schema, node.expression,
1071
+ "if condition (13.5)", node)
1072
+ when Model::Statements::Repeat
1073
+ check_repeat_types(schema, node)
1074
+ when Model::Statements::Return
1075
+ check_return_type(schema, owner, node)
1076
+ end
1077
+ end
1078
+ end
1079
+ @current_expression_env = nil
1080
+ end
1081
+
1082
+ def check_assignment_type(schema, node)
1083
+ declared = static_type(schema, node.ref)
1084
+ expressed = static_type(schema, node.expression)
1085
+ return if undecidable?(declared, expressed)
1086
+ return if initializer_compatible?(schema, node.ref, declared,
1087
+ node.expression, expressed)
1088
+
1089
+ note!(:check_expr_assignment_type, :error, schema,
1090
+ "assignment of #{expressed} to a #{declared} variable is " \
1091
+ "not compatible (13.2.1, 12.11)", node)
1092
+ end
1093
+
1094
+ def check_logical_expression(schema, expression, label, node)
1095
+ t = static_type(schema, expression)
1096
+ return if undecidable?(t)
1097
+ return if LOGICAL_TYPES.include?(t)
1098
+
1099
+ note!(:check_expr_operand_domain, :error, schema,
1100
+ "#{label} shall be a logical expression", node)
1101
+ end
1102
+
1103
+ def check_repeat_types(schema, node)
1104
+ [node.while_expression, node.until_expression].compact.each do |condition|
1105
+ check_logical_expression(schema, condition,
1106
+ "repeat control condition (13.6)", node)
1107
+ end
1108
+ [node.bound1, node.bound2, node.increment].compact.each do |bound|
1109
+ t = static_type(schema, bound)
1110
+ next if undecidable?(t)
1111
+ next if NUMERIC_TYPES.include?(t)
1112
+
1113
+ note!(:check_expr_operand_domain, :error, schema,
1114
+ "repeat increment control bounds shall be numeric " \
1115
+ "(13.6.1) (#{t})", bound)
1116
+ end
1117
+ end
1118
+
1119
+ def check_return_type(schema, owner, node)
1120
+ unless owner.is_a?(Model::Declarations::Function)
1121
+ return unless node.expression
1122
+
1123
+ note!(:check_expr_return, :error, schema,
1124
+ "a procedure return shall not specify an expression " \
1125
+ "(13.7)", node)
1126
+ end
1127
+
1128
+ expression = node.expression
1129
+ return if expression.nil?
1130
+ return if owner.is_a?(Model::Declarations::Function) &&
1131
+ !owner.return_type
1132
+
1133
+ declared = static_type(schema, owner.return_type)
1134
+ expressed = static_type(schema, expression)
1135
+ return if undecidable?(declared, expressed)
1136
+ return if initializer_compatible?(schema, owner.return_type,
1137
+ declared, expression, expressed)
1138
+
1139
+ note!(:check_expr_return, :error, schema,
1140
+ "return expression #{expressed} is not compatible with " \
1141
+ "the declared return #{declared} (13.7, 12.11)", node)
1142
+ end
1143
+
1144
+ # ---- tier 6: WHERE/UNIQUE structure (9.4) ----
1145
+
1146
+ def check_rule_structure(schema)
1147
+ schema.entities.each do |entity|
1148
+ check_where_booleans(schema, entity, Array(entity.where_rules))
1149
+ check_unique_labels(schema, entity)
1150
+ end
1151
+ schema.types.each do |type|
1152
+ check_where_booleans(schema, type, Array(type.where_rules))
1153
+ end
1154
+ schema.rules.each do |rule|
1155
+ check_where_booleans(schema, rule, Array(rule.where_rules))
1156
+ end
1157
+ end
1158
+
1159
+ def check_where_booleans(schema, owner, where_rules)
1160
+ @current_expression_env = expression_env(schema, owner)
1161
+ where_rules.each do |rule|
1162
+ t = static_type(schema, rule.expression)
1163
+ next if undecidable?(t)
1164
+ next if LOGICAL_TYPES.include?(t)
1165
+
1166
+ note!(:check_expr_where_boolean, :error, schema,
1167
+ "WHERE '#{rule.id}' shall be a logical expression (9.4)", rule)
1168
+ end
1169
+ @current_expression_env = nil
1170
+ end
1171
+
1172
+ def check_unique_labels(schema, entity)
1173
+ seen = {}
1174
+ Array(entity.unique_rules).each do |rule|
1175
+ key = rule.id&.safe_downcase
1176
+ next unless key
1177
+
1178
+ if seen[key]
1179
+ note!(:check_expr_unique_duplicate, :error, schema,
1180
+ "duplicate UNIQUE label '#{rule.id}' (9.4)", rule)
1181
+ end
1182
+ seen[key] = true
1183
+ end
1184
+ end
1185
+
1186
+ def check_subtype_constraints(schema)
1187
+ schema.subtype_constraints.each do |constraint|
1188
+ expr = constraint.supertype_expression
1189
+ next unless expr
1190
+
1191
+ each_reference(expr) do |ref|
1192
+ id = ref.id
1193
+ next if id.nil?
1194
+ # Names outside this schema's scope (foreign entities of a
1195
+ # single-file parse) cannot be verified here.
1196
+ next unless resolves_to_type_or_entity?(schema, id)
1197
+ next if find_entity(schema, id)
1198
+
1199
+ note!(:check_supertype_ref, :error, schema,
1200
+ "SUBTYPE_CONSTRAINT #{constraint.id}: supertype " \
1201
+ "'#{id}' is not an entity", constraint)
1202
+ end
1203
+ end
1204
+ end
1205
+
1206
+ # A `SELF\<supertype>.<attr>` redeclaration must name a supertype
1207
+ # in the inheritance closure carrying that attribute (ISO
1208
+ # 10303-11:2004, 9.2.2.3). eeng kernel/check.lisp:255 leaves this
1209
+ # as a TODO comment, so this check has no eeng note id.
1210
+ def check_attribute_redeclarations(schema, entity)
1211
+ explicit_attributes(entity).each do |attr|
1212
+ tag = attr.supertype_attribute
1213
+ next unless tag.is_a?(Model::References::AttributeReference)
1214
+
1215
+ sup_id = tag.ref&.entity&.id
1216
+ attr_id = tag.attribute&.id
1217
+ if sup_id.nil?
1218
+ note!(:check_attribute_redeclaration, :error, schema,
1219
+ "ENTITY #{entity.id}: redeclaration of '#{attr.id}' " \
1220
+ "names no supertype", attr)
1221
+ next
1222
+ end
1223
+
1224
+ supertype = entity.supertype_closure
1225
+ .find { |st| st.id&.safe_downcase == sup_id.safe_downcase }
1226
+ if supertype.nil?
1227
+ # The named supertype may be foreign (USE FROM) and thus
1228
+ # outside the closure of a single-file parse; only flag it
1229
+ # when it resolves in this scope yet is not a supertype.
1230
+ if find_entity(schema, sup_id)
1231
+ note!(:check_attribute_redeclaration, :error, schema,
1232
+ "ENTITY #{entity.id}: redeclaration '#{attr.id}' names " \
1233
+ "'#{sup_id}', which is not a supertype", attr)
1234
+ end
1235
+ next
1236
+ end
1237
+
1238
+ next if explicit_attributes(supertype)
1239
+ .any? { |candidate| candidate.id&.safe_downcase == attr_id&.safe_downcase }
1240
+
1241
+ note!(:check_attribute_redeclaration, :error, schema,
1242
+ "ENTITY #{entity.id}: redeclaration '#{attr.id}': supertype " \
1243
+ "'#{sup_id}' has no attribute '#{attr_id}'", attr)
1244
+ end
1245
+ end
1246
+
285
1247
  def check_supertype_refs(schema, entity)
286
1248
  expr = entity.supertype_expression
287
1249
  return unless expr
@@ -395,7 +1357,10 @@ module Expressir
395
1357
 
396
1358
  Array(current.subtype_of).each do |ref|
397
1359
  id = ref_id(ref)
398
- queue << find_entity(schema, id) if id
1360
+ next unless id
1361
+
1362
+ target = find_entity(schema, id)
1363
+ queue << target if target
399
1364
  end
400
1365
  end
401
1366
  nil