expressir 2.4.35-arm-linux → 2.4.37-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 (66) 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.sota/00-overview.md +57 -0
  6. data/TODO.sota/01-type-checking.md +115 -0
  7. data/TODO.sota/02-evaluator.md +86 -0
  8. data/TODO.sota/03-conformance.md +53 -0
  9. data/TODO.sota/04-lsp.md +70 -0
  10. data/TODO.sota/05-interop.md +55 -0
  11. data/TODO.sota/06-dialects.md +57 -0
  12. data/TODO.sota/07-reference-artifacts.md +55 -0
  13. data/docs/_guides/interop/index.adoc +25 -0
  14. data/docs/_guides/interop/json-schema.adoc +88 -0
  15. data/docs/_guides/interop/owl-rdf.adoc +87 -0
  16. data/docs/_guides/reference/checker-notes.adoc +55 -0
  17. data/docs/_guides/reference/checker.adoc +36 -0
  18. data/docs/_guides/reference/evaluator.adoc +40 -0
  19. data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
  20. data/docs/_guides/reference/index.adoc +22 -0
  21. data/docs/_guides/reference/lsp.adoc +41 -0
  22. data/docs/_guides/reference/p21.adoc +30 -0
  23. data/docs/_guides/reference/shtolo.adoc +36 -0
  24. data/exe/expressir-lsp +6 -0
  25. data/expressir.gemspec +2 -2
  26. data/lib/expressir/3.2/expressir_core.so +0 -0
  27. data/lib/expressir/3.3/expressir_core.so +0 -0
  28. data/lib/expressir/3.4/expressir_core.so +0 -0
  29. data/lib/expressir/4.0/expressir_core.so +0 -0
  30. data/lib/expressir/cli.rb +18 -0
  31. data/lib/expressir/commands/check.rb +3 -1
  32. data/lib/expressir/commands/export_json_schema.rb +29 -0
  33. data/lib/expressir/commands/export_owl.rb +26 -0
  34. data/lib/expressir/commands/validate.rb +3 -0
  35. data/lib/expressir/commands.rb +2 -0
  36. data/lib/expressir/conformance/trend.rb +100 -0
  37. data/lib/expressir/conformance.rb +8 -0
  38. data/lib/expressir/eval/environment.rb +83 -0
  39. data/lib/expressir/eval/expression.rb +610 -0
  40. data/lib/expressir/eval/interpreter.rb +316 -0
  41. data/lib/expressir/eval/rules.rb +88 -0
  42. data/lib/expressir/eval/values.rb +74 -0
  43. data/lib/expressir/eval.rb +17 -0
  44. data/lib/expressir/express/builders/expression_builder.rb +9 -1
  45. data/lib/expressir/express/checker.rb +779 -2
  46. data/lib/expressir/express/dialect.rb +44 -0
  47. data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
  48. data/lib/expressir/express/parser.rb +9 -4
  49. data/lib/expressir/express.rb +1 -0
  50. data/lib/expressir/interop/json_schema.rb +195 -0
  51. data/lib/expressir/interop/owl.rb +216 -0
  52. data/lib/expressir/interop.rb +13 -0
  53. data/lib/expressir/lsp/document.rb +226 -0
  54. data/lib/expressir/lsp/server.rb +184 -0
  55. data/lib/expressir/lsp/workspace.rb +402 -0
  56. data/lib/expressir/lsp.rb +11 -0
  57. data/lib/expressir/mapping/refpath.rb +65 -4
  58. data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
  59. data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
  60. data/lib/expressir/reference/note_catalog.rb +54 -0
  61. data/lib/expressir/reference.rb +10 -0
  62. data/lib/expressir/version.rb +1 -1
  63. data/lib/expressir.rb +4 -0
  64. data/lib/tasks/conformance.rake +27 -0
  65. data/lib/tasks/reference.rake +25 -0
  66. metadata +50 -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
@@ -89,6 +90,30 @@ module Expressir
89
90
  Model::Declarations::Type
90
91
  ].freeze
91
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
+
92
117
  private
93
118
 
94
119
  def note!(id, severity, schema, message, node = nil)
@@ -116,6 +141,624 @@ module Expressir
116
141
  end
117
142
  check_string_references(schema)
118
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)
119
762
  end
120
763
 
121
764
  # Self-qualified literals naming the CURRENT schema (#125): local
@@ -409,6 +1052,137 @@ module Expressir
409
1052
  .select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
410
1053
  end
411
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
+
412
1186
  def check_subtype_constraints(schema)
413
1187
  schema.subtype_constraints.each do |constraint|
414
1188
  expr = constraint.supertype_expression
@@ -583,7 +1357,10 @@ module Expressir
583
1357
 
584
1358
  Array(current.subtype_of).each do |ref|
585
1359
  id = ref_id(ref)
586
- queue << find_entity(schema, id) if id
1360
+ next unless id
1361
+
1362
+ target = find_entity(schema, id)
1363
+ queue << target if target
587
1364
  end
588
1365
  end
589
1366
  nil