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.
- checksums.yaml +4 -4
- data/.github/workflows/conformance.yml +34 -0
- data/.gitignore +3 -0
- data/Gemfile +1 -1
- data/TODO.sota/00-overview.md +57 -0
- data/TODO.sota/01-type-checking.md +115 -0
- data/TODO.sota/02-evaluator.md +86 -0
- data/TODO.sota/03-conformance.md +53 -0
- data/TODO.sota/04-lsp.md +70 -0
- data/TODO.sota/05-interop.md +55 -0
- data/TODO.sota/06-dialects.md +57 -0
- data/TODO.sota/07-reference-artifacts.md +55 -0
- data/docs/_guides/interop/index.adoc +25 -0
- data/docs/_guides/interop/json-schema.adoc +88 -0
- data/docs/_guides/interop/owl-rdf.adoc +87 -0
- data/docs/_guides/reference/checker-notes.adoc +55 -0
- data/docs/_guides/reference/checker.adoc +36 -0
- data/docs/_guides/reference/evaluator.adoc +40 -0
- data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
- data/docs/_guides/reference/index.adoc +22 -0
- data/docs/_guides/reference/lsp.adoc +41 -0
- data/docs/_guides/reference/p21.adoc +30 -0
- data/docs/_guides/reference/shtolo.adoc +36 -0
- data/exe/expressir-lsp +6 -0
- data/expressir.gemspec +2 -2
- data/lib/expressir/3.2/expressir_core.so +0 -0
- data/lib/expressir/3.3/expressir_core.so +0 -0
- data/lib/expressir/3.4/expressir_core.so +0 -0
- data/lib/expressir/4.0/expressir_core.so +0 -0
- data/lib/expressir/cli.rb +18 -0
- data/lib/expressir/commands/check.rb +3 -1
- data/lib/expressir/commands/export_json_schema.rb +29 -0
- data/lib/expressir/commands/export_owl.rb +26 -0
- data/lib/expressir/commands/validate.rb +3 -0
- data/lib/expressir/commands.rb +2 -0
- data/lib/expressir/conformance/trend.rb +100 -0
- data/lib/expressir/conformance.rb +8 -0
- data/lib/expressir/eval/environment.rb +83 -0
- data/lib/expressir/eval/expression.rb +610 -0
- data/lib/expressir/eval/interpreter.rb +316 -0
- data/lib/expressir/eval/rules.rb +88 -0
- data/lib/expressir/eval/values.rb +74 -0
- data/lib/expressir/eval.rb +17 -0
- data/lib/expressir/express/builders/expression_builder.rb +9 -1
- data/lib/expressir/express/checker.rb +779 -2
- data/lib/expressir/express/dialect.rb +44 -0
- data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
- data/lib/expressir/express/parser.rb +9 -4
- data/lib/expressir/express.rb +1 -0
- data/lib/expressir/interop/json_schema.rb +195 -0
- data/lib/expressir/interop/owl.rb +216 -0
- data/lib/expressir/interop.rb +13 -0
- data/lib/expressir/lsp/document.rb +226 -0
- data/lib/expressir/lsp/server.rb +184 -0
- data/lib/expressir/lsp/workspace.rb +402 -0
- data/lib/expressir/lsp.rb +11 -0
- data/lib/expressir/mapping/refpath.rb +65 -4
- data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
- data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
- data/lib/expressir/reference/note_catalog.rb +54 -0
- data/lib/expressir/reference.rb +10 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +4 -0
- data/lib/tasks/conformance.rake +27 -0
- data/lib/tasks/reference.rake +25 -0
- 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
|
-
|
|
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
|