expressir 2.4.35-x86_64-linux → 2.4.37-x86_64-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
@@ -0,0 +1,610 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Eval
5
+ # Pure expression evaluation (TODO.sota/02 step 1): ISO 10303-11
6
+ # ch.12 over the value model. Instance-aware operations (attribute
7
+ # access on entities, TYPEOF/USEDIN/EXISTS) arrive with step 3 and
8
+ # raise Error here.
9
+ module Expression
10
+ V = Expressir::Eval::Values
11
+
12
+ module_function
13
+
14
+ def evaluate(node, env)
15
+ case node
16
+ when Model::Literals::Integer then node.value.to_i
17
+ when Model::Literals::Real then node.value.to_f
18
+ when Model::Literals::String then node.value.to_s
19
+ when Model::Literals::Binary then V::Binary.new(bits: node.value.to_s)
20
+ when Model::Literals::Logical then logical_literal(node)
21
+ when Model::Expressions::BinaryExpression then evaluate_binary(node, env)
22
+ when Model::Expressions::UnaryExpression then evaluate_unary(node, env)
23
+ when Model::Expressions::Interval then evaluate_interval(node, env)
24
+ when Model::Expressions::AggregateInitializer
25
+ evaluate_aggregate_initializer(node, env)
26
+ when Model::Expressions::QueryExpression then evaluate_query(node, env)
27
+ when Model::Expressions::FunctionCall then evaluate_call(node, env)
28
+ when Model::References::SimpleReference then evaluate_reference(node, env)
29
+ when Model::References::AttributeReference
30
+ evaluate_attribute_reference(node, env)
31
+ when Model::References::IndexReference then evaluate_index(node, env)
32
+ when Model::References::GroupReference then evaluate_group_reference(node, env)
33
+ else
34
+ raise Error, "unsupported expression #{node.class.name}"
35
+ end
36
+ end
37
+
38
+ def logical_literal(node)
39
+ node.value == "TRUE" ? V::Logical.true_value : V::Logical.false_value
40
+ end
41
+
42
+ def evaluate_reference(node, env)
43
+ value = env.lookup(node.id)
44
+ return value unless value.nil?
45
+
46
+ raise Error, "unbound identifier '#{node.id}'"
47
+ end
48
+
49
+ def evaluate_group_reference(node, env)
50
+ base = node.ref
51
+ if base.is_a?(Model::References::SimpleReference) &&
52
+ base.id&.safe_downcase == "self"
53
+ value = env.lookup("self")
54
+ return value if value
55
+
56
+ raise Error, "SELF requires an instance context (step 4)"
57
+ end
58
+
59
+ raise Error, "instance access requires a population"
60
+ end
61
+
62
+ # `color.red` (enumeration members) or `instance.attr`.
63
+ def evaluate_attribute_reference(node, env)
64
+ base = node.ref
65
+ if base.is_a?(Model::References::SimpleReference)
66
+ declaration = find_declared_type(env, base.id)
67
+ if declaration
68
+ return V::Enumeration.new(type: declaration.id,
69
+ item: node.attribute.id)
70
+ end
71
+ end
72
+
73
+ owner = evaluate(base, env)
74
+ if owner.is_a?(V::Instance)
75
+ return instance_attribute(owner, node.attribute&.id, env)
76
+ end
77
+
78
+ raise Error, "instance attribute access requires a population"
79
+ end
80
+
81
+ def instance_attribute(instance, attribute_name, env)
82
+ record = instance.record
83
+ record = simple_record_of(record, attribute_name, env) if
84
+ record.is_a?(Expressir::P21::ComplexRecord)
85
+ unless record
86
+ raise Error, "attribute '#{attribute_name}' is not mapped on " \
87
+ "#{record.keyword}"
88
+ end
89
+
90
+ entity = env.entity_for_keyword(record.keyword)
91
+ raise Error, "unknown entity '#{record.keyword}'" unless entity
92
+
93
+ index = env.slots_for(entity)[attribute_name.safe_downcase]
94
+ unless index
95
+ raise Error, "attribute '#{attribute_name}' is not mapped on " \
96
+ "'#{record.keyword}'"
97
+ end
98
+
99
+ instance_value(record.params[index], env)
100
+ end
101
+
102
+ # P21 record parameters are plain Ruby data; convert them to the
103
+ # evaluator's value model at the instance boundary.
104
+ def instance_value(raw, env)
105
+ case raw
106
+ when :unset, :derived then V::INDETERMINATE
107
+ when Array
108
+ V::Aggregate.of(:list, raw.map { |item| instance_value(item, env) })
109
+ when String
110
+ if raw.start_with?("#")
111
+ env.instance_for(raw) || raw
112
+ else
113
+ raw
114
+ end
115
+ when Expressir::P21::Enumeration
116
+ V::Enumeration.new(type: nil, item: raw.name)
117
+ when Expressir::P21::Binary
118
+ V::Binary.new(bits: raw.hex)
119
+ when Expressir::P21::TypedValue
120
+ instance_value(raw.value, env)
121
+ else raw
122
+ end
123
+ end
124
+
125
+ def simple_record_of(complex_record, attribute_name, env)
126
+ Array(complex_record.records).find do |record|
127
+ entity = env.entity_for_keyword(record.keyword)
128
+ entity &&
129
+ env.slots_for(entity).key?(attribute_name.safe_downcase)
130
+ end
131
+ end
132
+
133
+ def find_declared_type(env, name)
134
+ schema = env.schema
135
+ return nil unless schema
136
+
137
+ Array(schema.types).find do |type|
138
+ type.id&.safe_downcase == name.safe_downcase &&
139
+ type.underlying_type.is_a?(Model::DataTypes::Enumeration)
140
+ end
141
+ end
142
+
143
+ def evaluate_aggregate_initializer(node, env)
144
+ V::Aggregate.of(:list, Array(node.items).map do |item|
145
+ evaluate(item.respond_to?(:expression) ? item.expression : item, env)
146
+ end)
147
+ end
148
+
149
+ def evaluate_index(node, env)
150
+ base = evaluate(node.ref, env)
151
+ index1 = evaluate(node.index1, env)
152
+ index2 = node.index2 ? evaluate(node.index2, env) : nil
153
+ case base
154
+ when String then string_slice(base, index1, index2)
155
+ when V::Binary then V::Binary.new(bits: string_slice(base.bits, index1, index2))
156
+ when V::Aggregate
157
+ raise Error, "aggregate indexing selects one element (12.6.1)" if index2
158
+
159
+ base.elements.fetch(index1 - 1)
160
+ else
161
+ raise Error, "cannot index #{describe(base)}"
162
+ end
163
+ end
164
+
165
+ def string_slice(value, index1, index2)
166
+ return value[index1 - 1] unless index2
167
+
168
+ value[index1 - 1, index2 - index1 + 1] || ""
169
+ end
170
+
171
+ def evaluate_query(node, env)
172
+ source = evaluate(node.aggregate_source, env)
173
+ unless source.is_a?(V::Aggregate)
174
+ raise Error, "QUERY source shall be an aggregate (12.6.7)"
175
+ end
176
+
177
+ kept = source.elements.select do |element|
178
+ inner = env.push({ node.id.safe_downcase => element })
179
+ logical?(evaluate(node.expression, inner))
180
+ end
181
+ V::Aggregate.of(source.kind, kept)
182
+ end
183
+
184
+ def evaluate_call(node, env)
185
+ callee = node.function
186
+ name = callee.is_a?(Model::References::SimpleReference) ? callee.id : nil
187
+ raise Error, "unsupported call target #{callee.class.name}" unless name
188
+
189
+ arguments = Array(node.parameters).map { |arg| evaluate(arg, env) }
190
+ env.interpreter.call(name, arguments)
191
+ end
192
+
193
+ def evaluate_unary(node, env)
194
+ operand = evaluate(node.operand, env)
195
+ return V::INDETERMINATE if indeterminate?(operand)
196
+
197
+ case node.operator
198
+ when "PLUS" then to_number!(operand)
199
+ when "MINUS" then -to_number!(operand)
200
+ when "NOT"
201
+ logical = as_logical(operand)
202
+ case logical.value
203
+ when V::LOGICAL_TRUE then V::Logical.false_value
204
+ when V::LOGICAL_FALSE then V::Logical.true_value
205
+ else V::Logical.unknown
206
+ end
207
+ end
208
+ end
209
+
210
+ def evaluate_interval(node, env)
211
+ left = relational(evaluate(node.low, env), node.operator1,
212
+ evaluate(node.item, env))
213
+ right = relational(evaluate(node.item, env), node.operator2,
214
+ evaluate(node.high, env))
215
+ evaluate_logical("AND", left, right)
216
+ end
217
+
218
+ LOGICAL_OPS = %w[AND OR XOR].freeze
219
+
220
+ def evaluate_binary(node, env)
221
+ operator = node.operator
222
+ left = evaluate(node.operand1, env)
223
+ right = evaluate(node.operand2, env)
224
+ if LOGICAL_OPS.include?(operator)
225
+ return evaluate_logical(operator, left, right)
226
+ end
227
+
228
+ return V::Logical.unknown if indeterminate?(left, right)
229
+
230
+ case operator
231
+ when "ADDITION" then evaluate_addition(left, right)
232
+ when "SUBTRACTION" then evaluate_difference(left, right)
233
+ when "MULTIPLICATION" then evaluate_intersection(left, right)
234
+ when "REAL_DIVISION" then to_number!(left).to_f / to_number!(right)
235
+ when "INTEGER_DIVISION", "MODULO" then evaluate_div_mod(operator, left, right)
236
+ when "EXPONENTIATION" then to_number!(left)**to_number!(right)
237
+ when "COMBINE"
238
+ raise Error, "complex entity construction requires instances (step 3)"
239
+ else
240
+ evaluate_relational(operator, left, right)
241
+ end
242
+ end
243
+
244
+ def evaluate_div_mod(operator, left, right)
245
+ first = truncate(left)
246
+ second = truncate(right)
247
+ operator == "INTEGER_DIVISION" ? first / second : first % second
248
+ end
249
+
250
+ # ---- arithmetic (12.1) ----
251
+
252
+ def numeric_pair?(left, right)
253
+ left.is_a?(Numeric) && right.is_a?(Numeric)
254
+ end
255
+
256
+ def string_pair?(left, right)
257
+ left.is_a?(String) && right.is_a?(String)
258
+ end
259
+
260
+ def binary_pair?(left, right)
261
+ left.is_a?(V::Binary) && right.is_a?(V::Binary)
262
+ end
263
+
264
+ def aggregate_pair?(left, right)
265
+ left.is_a?(V::Aggregate) && right.is_a?(V::Aggregate)
266
+ end
267
+
268
+ def arithmetic(left, right, operator)
269
+ result = to_number!(left).send(operator, to_number!(right))
270
+ both_integer?(left, right) ? result : result.to_f
271
+ end
272
+
273
+ def truncate(value)
274
+ value.is_a?(Integer) ? value : to_number!(value).to_f.truncate
275
+ end
276
+
277
+ def to_number!(value)
278
+ return value if value.is_a?(Numeric)
279
+
280
+ raise Error, "expected a numeric operand, got #{describe(value)}"
281
+ end
282
+
283
+ def integer?(value)
284
+ value.is_a?(Integer)
285
+ end
286
+
287
+ def both_integer?(left, right)
288
+ integer?(left) && integer?(right)
289
+ end
290
+
291
+ def evaluate_addition(left, right)
292
+ return arithmetic(left, right, :+) if numeric_pair?(left, right)
293
+ return left + right if string_pair?(left, right)
294
+ return V::Binary.new(bits: left.bits + right.bits) if binary_pair?(left, right)
295
+ return union(left, right) if left.is_a?(V::Aggregate) || right.is_a?(V::Aggregate)
296
+
297
+ raise Error,
298
+ "addition requires numeric, string, binary or aggregate " \
299
+ "operands (12.1, 12.3.2, 12.5.2, 12.6)"
300
+ end
301
+
302
+ def evaluate_difference(left, right)
303
+ return arithmetic(left, right, :-) if numeric_pair?(left, right)
304
+ if left.is_a?(V::Aggregate) || right.is_a?(V::Aggregate)
305
+ return aggregate_difference(left, right)
306
+ end
307
+
308
+ raise Error,
309
+ "difference requires numeric or aggregate operands (12.1, 12.6)"
310
+ end
311
+
312
+ def evaluate_intersection(left, right)
313
+ return arithmetic(left, right, :*) if numeric_pair?(left, right)
314
+ return aggregate_intersection(left, right) if aggregate_pair?(left, right)
315
+
316
+ raise Error,
317
+ "intersection requires numeric or aggregate operands (12.1, 12.6)"
318
+ end
319
+
320
+ # ---- logical operators (12.4) ----
321
+
322
+ def evaluate_logical(operator, left, right)
323
+ left = as_logical(left)
324
+ right = as_logical(right)
325
+ return V::Logical.unknown if [left, right].any? { |l| l.value == :unknown }
326
+
327
+ case operator
328
+ when "AND"
329
+ left.value == V::LOGICAL_TRUE && right.value == V::LOGICAL_TRUE ? V::Logical.true_value : V::Logical.false_value
330
+ when "OR"
331
+ left.value == V::LOGICAL_TRUE || right.value == V::LOGICAL_TRUE ? V::Logical.true_value : V::Logical.false_value
332
+ else # XOR
333
+ left.value == right.value ? V::Logical.false_value : V::Logical.true_value
334
+ end
335
+ end
336
+
337
+ def as_logical(value)
338
+ return value if value.is_a?(V::Logical)
339
+
340
+ raise Error, "logical operator requires logical operands (12.4)"
341
+ end
342
+
343
+ def logical?(value)
344
+ value.is_a?(V::Logical) && value.value == V::LOGICAL_TRUE
345
+ end
346
+
347
+ # ---- relational (12.2) ----
348
+
349
+ def evaluate_relational(operator, left, right)
350
+ case operator
351
+ when "IN" then evaluate_in(left, right)
352
+ when "LIKE" then evaluate_like(left, right)
353
+ when "INSTANCE_EQUAL", "INSTANCE_NOT_EQUAL"
354
+ raise Error, "instance comparison requires instances (step 3)"
355
+ else
356
+ result = relational(left, relational_operator(operator), right)
357
+ operator == "NOT_EQUAL" ? negate(result) : result
358
+ end
359
+ end
360
+
361
+ def evaluate_like(left, right)
362
+ hit = like_match?(left, right)
363
+ V::Logical.new(value: hit ? V::LOGICAL_TRUE : V::LOGICAL_FALSE)
364
+ end
365
+
366
+ def evaluate_in(element, aggregate)
367
+ return V::Logical.unknown if indeterminate?(element, aggregate)
368
+ unless aggregate.is_a?(V::Aggregate)
369
+ raise Error, "membership requires an aggregate right operand (12.2.3)"
370
+ end
371
+
372
+ return V::Logical.true_value if aggregate.elements.any? { |e| values_equal?(element, e) }
373
+
374
+ hole = aggregate.elements.any? { |e| indeterminate?(e) }
375
+ V::Logical.new(value: hole ? V::LOGICAL_UNKNOWN : V::LOGICAL_FALSE)
376
+ end
377
+
378
+ def relational_operator(operator)
379
+ { "EQUAL" => :"=", "NOT_EQUAL" => :"=", "LESS_THAN" => :<,
380
+ "LESS_THAN_OR_EQUAL" => :"<=", "GREATER_THAN" => :>,
381
+ "GREATER_THAN_OR_EQUAL" => :">=" }.fetch(operator)
382
+ end
383
+
384
+ def relational(left, operator, right)
385
+ return V::Logical.unknown if indeterminate?(left, right)
386
+
387
+ ordering = order(left, right)
388
+ result = case operator
389
+ when :"=" then ordering.zero?
390
+ when :< then ordering.negative?
391
+ when :"<=" then ordering <= 0
392
+ when :> then ordering.positive?
393
+ when :">=" then ordering >= 0
394
+ end
395
+ V::Logical.new(value: result ? V::LOGICAL_TRUE : V::LOGICAL_FALSE)
396
+ end
397
+
398
+ def negate(logical)
399
+ case logical.value
400
+ when V::LOGICAL_TRUE then V::Logical.false_value
401
+ when V::LOGICAL_FALSE then V::Logical.true_value
402
+ else V::Logical.unknown
403
+ end
404
+ end
405
+
406
+ # 12.2.1 orderings: numeric, string (code point), binary (bit),
407
+ # logical (false < unknown < true), enumeration (item name;
408
+ # declaration position is the caller's refinement).
409
+ def order(left, right)
410
+ return left <=> right if left.is_a?(Numeric) && right.is_a?(Numeric)
411
+ return left <=> right if left.is_a?(String) && right.is_a?(String)
412
+ return left.bits <=> right.bits if left.is_a?(V::Binary) && right.is_a?(V::Binary)
413
+ return logical_order(left) <=> logical_order(right) if
414
+ left.is_a?(V::Logical) && right.is_a?(V::Logical)
415
+ return left.item <=> right.item if
416
+ left.is_a?(V::Enumeration) && right.is_a?(V::Enumeration)
417
+
418
+ raise Error, "cannot compare #{describe(left)} with #{describe(right)}"
419
+ end
420
+
421
+ LOGICAL_ORDER = {
422
+ V::LOGICAL_FALSE => 0,
423
+ V::LOGICAL_UNKNOWN => 1,
424
+ V::LOGICAL_TRUE => 2,
425
+ }.freeze
426
+
427
+ def logical_order(logical)
428
+ LOGICAL_ORDER.fetch(logical.value)
429
+ end
430
+
431
+ # ---- LIKE (12.2.5, table 11) ----
432
+
433
+ def like_match?(target, pattern)
434
+ unless target.is_a?(String) && pattern.is_a?(String)
435
+ raise Error, "like requires string operands (12.2.5)"
436
+ end
437
+
438
+ match_sequence?(target, pattern.chars, 0)
439
+ end
440
+
441
+ def match_sequence?(target, pattern, offset)
442
+ return offset == target.length if pattern.empty?
443
+
444
+ char = pattern[0]
445
+ rest = pattern[1..]
446
+ case char
447
+ when "\\" then match_escaped?(target, rest, offset)
448
+ when "*" then match_star?(target, rest, offset)
449
+ when "&" then match_sequence?(target, rest, target.length)
450
+ when "$" then match_dollar?(target, rest, offset)
451
+ when "!" then match_negation?(target, rest, offset)
452
+ else
453
+ match_single?(target, rest, offset) { |tc| char_matches?(char, tc) }
454
+ end
455
+ end
456
+
457
+ def match_escaped?(target, rest, offset)
458
+ char = rest[0]
459
+ return false if char.nil?
460
+
461
+ match_single?(target, rest[1..], offset) { |tc| tc == char }
462
+ end
463
+
464
+ def match_star?(target, rest, offset)
465
+ (offset..target.length).any? do |split|
466
+ match_sequence?(target, rest, split)
467
+ end
468
+ end
469
+
470
+ def match_dollar?(target, rest, offset)
471
+ index = target.index(" ", offset) || target.length
472
+ prefix = target[offset...index] || ""
473
+ remaining = target[index..] || ""
474
+ match_sequence?(prefix, rest, 0) && match_sequence?(remaining, [], 0)
475
+ end
476
+
477
+ def match_negation?(target, rest, offset)
478
+ negated = rest[0]
479
+ return false if negated.nil? || offset >= target.length
480
+
481
+ target_char = target[offset]
482
+ matches = case negated
483
+ when "@" then target_char.match?(/[[:alpha:]]/)
484
+ when "^" then target_char.match?(/[[:upper:]]/)
485
+ when "#" then target_char.match?(/[[:digit:]]/)
486
+ when "?" then true
487
+ else target_char == negated
488
+ end
489
+ return false if matches
490
+
491
+ match_sequence?(target, rest[1..], offset + 1)
492
+ end
493
+
494
+ def match_single?(target, rest, offset)
495
+ return false if offset >= target.length
496
+
497
+ yield(target[offset]) && match_sequence?(target, rest, offset + 1)
498
+ end
499
+
500
+ def char_matches?(pattern_char, target_char)
501
+ case pattern_char
502
+ when "@" then target_char.match?(/[[:alpha:]]/)
503
+ when "^" then target_char.match?(/[[:upper:]]/)
504
+ when "#" then target_char.match?(/[[:digit:]]/)
505
+ when "?" then true
506
+ else pattern_char == target_char
507
+ end
508
+ end
509
+
510
+ # ---- aggregates (12.6, tables 16-18) ----
511
+
512
+ # Table 17: union results take the promoted kind (bag absorbs
513
+ # everything; set over set/element; list over list/element).
514
+ def union(left, right)
515
+ left_agg = left.is_a?(V::Aggregate) ? left : nil
516
+ right_agg = right.is_a?(V::Aggregate) ? right : nil
517
+ kind = union_kind(left_agg&.kind, right_agg&.kind)
518
+ elements = (left_agg&.elements || [left]) +
519
+ (right_agg&.elements || [right])
520
+ V::Aggregate.of(kind, dedup_if_set(kind, elements))
521
+ end
522
+
523
+ def union_kind(left_kind, right_kind)
524
+ return :bag if [left_kind, right_kind].include?(:bag)
525
+ return :set if [left_kind, right_kind].include?(:set)
526
+
527
+ :list
528
+ end
529
+
530
+ def dedup_if_set(kind, elements)
531
+ return elements unless kind == :set
532
+
533
+ seen = []
534
+ elements.reject do |element|
535
+ duplicate = seen.any? { |s| values_equal?(s, element) }
536
+ seen << element
537
+ duplicate
538
+ end
539
+ end
540
+
541
+ # Tables 18: difference covers bag and set operands; the right
542
+ # operand may be an element singleton. The result keeps the left
543
+ # operand's kind.
544
+ def aggregate_difference(left, right)
545
+ kind = left.is_a?(V::Aggregate) ? left.kind : right.kind
546
+ unless %i[bag set].include?(kind)
547
+ raise Error,
548
+ "difference requires bag or set operands (12.6, table 18)"
549
+ end
550
+
551
+ left_elements = left.is_a?(V::Aggregate) ? left.elements : [left]
552
+ right_elements = right.is_a?(V::Aggregate) ? right.elements : [right]
553
+ remaining = left_elements.reject do |element|
554
+ right_elements.any? { |r| values_equal?(r, element) }
555
+ end
556
+ V::Aggregate.of(kind, remaining)
557
+ end
558
+
559
+ # Table 16: intersection covers bag and set operands only;
560
+ # bag∩bag → bag, anything else → set.
561
+ def aggregate_intersection(left, right)
562
+ unless aggregate_pair?(left, right) &&
563
+ %i[bag set].include?(left.kind) &&
564
+ %i[bag set].include?(right.kind)
565
+ raise Error,
566
+ "intersection requires bag or set operands (12.6, table 16)"
567
+ end
568
+
569
+ kind = left.kind == :bag && right.kind == :bag ? :bag : :set
570
+ kept = left.elements.select do |element|
571
+ right.elements.any? { |r| values_equal?(r, element) }
572
+ end
573
+ V::Aggregate.of(kind, dedup_if_set(kind, kept))
574
+ end
575
+
576
+ def values_equal?(one, other)
577
+ return true if one.equal?(other)
578
+ return false if one.class != other.class
579
+
580
+ case one
581
+ when V::Enumeration then one.item == other.item
582
+ when V::Logical then one.value == other.value
583
+ when V::Binary then one.bits == other.bits
584
+ when V::Aggregate
585
+ one.elements.size == other.elements.size &&
586
+ one.elements.zip(other.elements).all? { |x, y| values_equal?(x, y) }
587
+ else one == other
588
+ end
589
+ end
590
+
591
+ # ---- indeterminate propagation ----
592
+
593
+ def indeterminate?(*values)
594
+ values.any? { |value| value.equal?(V::INDETERMINATE) }
595
+ end
596
+
597
+ def describe(value)
598
+ case value
599
+ when V::Logical then "LOGICAL"
600
+ when V::Enumeration then "ENUMERATION"
601
+ when V::Binary then "BINARY"
602
+ when V::Aggregate then "aggregate"
603
+ when String then "string"
604
+ when Numeric then "number"
605
+ else value.equal?(V::INDETERMINATE) ? "?" : value.class.name
606
+ end
607
+ end
608
+ end
609
+ end
610
+ end