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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +34 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +1 -1
  5. data/TODO.eengine/00-overview.md +39 -0
  6. data/TODO.eengine/01-resolution-diagnostics.md +30 -0
  7. data/TODO.eengine/02-check-catalog-completion.md +53 -0
  8. data/TODO.eengine/03-entity-computations.md +22 -0
  9. data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
  10. data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
  11. data/TODO.eengine/06-population-p21.md +34 -0
  12. data/TODO.eengine/07-schema-compare-patch.md +25 -0
  13. data/TODO.eengine/08-official-corpus-gate.md +7 -0
  14. data/TODO.eengine/09-mim-mapping-parser.md +9 -0
  15. data/TODO.parity-ee/00-overview.md +1 -1
  16. data/TODO.sota/00-overview.md +57 -0
  17. data/TODO.sota/01-type-checking.md +115 -0
  18. data/TODO.sota/02-evaluator.md +86 -0
  19. data/TODO.sota/03-conformance.md +53 -0
  20. data/TODO.sota/04-lsp.md +70 -0
  21. data/TODO.sota/05-interop.md +55 -0
  22. data/TODO.sota/06-dialects.md +57 -0
  23. data/TODO.sota/07-reference-artifacts.md +55 -0
  24. data/TODO.suma-improvements/07-part28-xml.md +10 -13
  25. data/docs/_guides/interop/index.adoc +25 -0
  26. data/docs/_guides/interop/json-schema.adoc +88 -0
  27. data/docs/_guides/interop/owl-rdf.adoc +87 -0
  28. data/docs/_guides/reference/checker-notes.adoc +55 -0
  29. data/docs/_guides/reference/checker.adoc +36 -0
  30. data/docs/_guides/reference/evaluator.adoc +40 -0
  31. data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
  32. data/docs/_guides/reference/index.adoc +22 -0
  33. data/docs/_guides/reference/lsp.adoc +41 -0
  34. data/docs/_guides/reference/p21.adoc +30 -0
  35. data/docs/_guides/reference/shtolo.adoc +36 -0
  36. data/exe/expressir-lsp +6 -0
  37. data/expressir.gemspec +2 -2
  38. data/lib/expressir/3.2/expressir_core.so +0 -0
  39. data/lib/expressir/3.3/expressir_core.so +0 -0
  40. data/lib/expressir/3.4/expressir_core.so +0 -0
  41. data/lib/expressir/4.0/expressir_core.so +0 -0
  42. data/lib/expressir/cli.rb +25 -0
  43. data/lib/expressir/commands/check.rb +3 -1
  44. data/lib/expressir/commands/export_json_schema.rb +29 -0
  45. data/lib/expressir/commands/export_owl.rb +26 -0
  46. data/lib/expressir/commands/smrl_xml_index.rb +25 -0
  47. data/lib/expressir/commands/validate.rb +3 -0
  48. data/lib/expressir/commands.rb +3 -0
  49. data/lib/expressir/conformance/trend.rb +100 -0
  50. data/lib/expressir/conformance.rb +8 -0
  51. data/lib/expressir/eval/environment.rb +83 -0
  52. data/lib/expressir/eval/expression.rb +610 -0
  53. data/lib/expressir/eval/interpreter.rb +316 -0
  54. data/lib/expressir/eval/rules.rb +88 -0
  55. data/lib/expressir/eval/values.rb +74 -0
  56. data/lib/expressir/eval.rb +17 -0
  57. data/lib/expressir/express/builder.rb +17 -5
  58. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
  59. data/lib/expressir/express/builders/expression_builder.rb +9 -1
  60. data/lib/expressir/express/checker.rb +970 -5
  61. data/lib/expressir/express/dialect.rb +44 -0
  62. data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
  63. data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
  64. data/lib/expressir/express/parser.rb +9 -4
  65. data/lib/expressir/express/pretty_formatter.rb +9 -14
  66. data/lib/expressir/express/schema_compare.rb +194 -0
  67. data/lib/expressir/express/shtolo.rb +194 -63
  68. data/lib/expressir/express.rb +3 -0
  69. data/lib/expressir/interop/json_schema.rb +195 -0
  70. data/lib/expressir/interop/owl.rb +216 -0
  71. data/lib/expressir/interop.rb +13 -0
  72. data/lib/expressir/lsp/document.rb +226 -0
  73. data/lib/expressir/lsp/server.rb +184 -0
  74. data/lib/expressir/lsp/workspace.rb +402 -0
  75. data/lib/expressir/lsp.rb +11 -0
  76. data/lib/expressir/model/declarations/entity.rb +132 -0
  77. data/lib/expressir/model/declarations/schema.rb +37 -6
  78. data/lib/expressir/p21/builder.rb +92 -0
  79. data/lib/expressir/p21/grammar.rb +107 -0
  80. data/lib/expressir/p21/parser.rb +16 -0
  81. data/lib/expressir/p21/population.rb +35 -0
  82. data/lib/expressir/p21/validator.rb +213 -0
  83. data/lib/expressir/p21.rb +17 -0
  84. data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
  85. data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
  86. data/lib/expressir/reference/note_catalog.rb +54 -0
  87. data/lib/expressir/reference.rb +10 -0
  88. data/lib/expressir/version.rb +1 -1
  89. data/lib/expressir.rb +5 -0
  90. data/lib/tasks/conformance.rake +27 -0
  91. data/lib/tasks/reference.rake +25 -0
  92. metadata +68 -4
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # Turns a {Grammar} capture tree into a {Population}.
6
+ module Builder
7
+ module_function
8
+
9
+ def build(tree)
10
+ header = build_header(tree[:header_section])
11
+ instances = build_instances(tree[:data_section])
12
+ Population.new(header: header, instances: instances)
13
+ end
14
+
15
+ def build_header(section)
16
+ Array(section[:header_entity]).each_with_object({}) do |entry, hash|
17
+ record = entry[:simple_record]
18
+ hash[record_keyword(record)] = record_params(record)
19
+ end
20
+ end
21
+
22
+ def build_instances(section)
23
+ list = section[:instance_list]
24
+ entries = list.is_a?(Array) ? list : [list]
25
+ entries.each_with_object({}) do |entry, hash|
26
+ if (complex = entry[:complex_entity_instance])
27
+ hash[instance_id(complex)] = build_complex(complex)
28
+ elsif (simple = entry[:simple_entity_instance])
29
+ record = simple[:simple_record]
30
+ hash[instance_id(simple)] = Record.new(record_keyword(record),
31
+ record_params(record))
32
+ end
33
+ end
34
+ end
35
+
36
+ def instance_id(body)
37
+ "##{body[:entity_instance_name].to_s.delete_prefix('#')}"
38
+ end
39
+
40
+ def build_complex(body)
41
+ subs = body[:subsuper_record]
42
+ subs = [subs] unless subs.is_a?(Array)
43
+ records = subs.map do |sub|
44
+ record = sub[:simple_record]
45
+ Record.new(record_keyword(record), record_params(record))
46
+ end
47
+ ComplexRecord.new(records)
48
+ end
49
+
50
+ def record_keyword(record)
51
+ record[:keyword].to_s.delete_prefix("!")
52
+ end
53
+
54
+ # A parameter_list capture is a hash when there is a single parameter
55
+ # and an array of {parameter: ...} entries otherwise.
56
+ def parameter_list_values(capture)
57
+ return [] if capture.nil?
58
+
59
+ entries = capture.is_a?(Array) ? capture : [capture]
60
+ entries.map { |entry| build_value(entry[:parameter]) }
61
+ end
62
+
63
+ def record_params(record)
64
+ parameter_list_values(record[:parameter_list])
65
+ end
66
+
67
+ def build_value(param)
68
+ if (typed = param[:typed_parameter])
69
+ TypedValue.new(typed[:keyword].to_s.delete_prefix("!"),
70
+ parameter_list_values(typed[:parameter_list]))
71
+ elsif param[:parameter_list]
72
+ parameter_list_values(param[:parameter_list])
73
+ elsif param[:entity_instance_name]
74
+ "##{param[:entity_instance_name].to_s.delete_prefix('#')}"
75
+ elsif param[:number_literal]
76
+ text = param[:number_literal].to_s
77
+ text.match?(/[.Ee]/) ? text.to_f : text.to_i
78
+ elsif param[:enumeration]
79
+ Enumeration.new(param[:enumeration].to_s.delete_prefix(".").delete_suffix("."))
80
+ elsif param[:binary_literal]
81
+ Binary.new(param[:binary_literal].to_s.delete_prefix('"').delete_suffix('"'))
82
+ elsif param[:string_literal]
83
+ param[:string_literal].to_s[1..-2].gsub("''", "'")
84
+ elsif param[:unset]
85
+ :unset
86
+ elsif param[:derived]
87
+ :derived
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Expressir
6
+ module P21
7
+ # ISO 10303-21 exchange-structure grammar (TODO.eengine/06 step 1).
8
+ # Parses the HEADER section and the DATA section's instance list
9
+ # into a capture tree; {Expressir::P21::PopulationBuilder} turns
10
+ # the tree into a {Expressir::P21::Population}.
11
+ #
12
+ # Reference: ISO 10303-21 exchange structure grammar
13
+ # (`~/src/mn/iso-10303-detached-docs/sources/iso-10303-21*/`);
14
+ # scope ladder in TODO.eengine/06 — the supported subset is the
15
+ # header FILE_* entities plus simple/complex data instances with
16
+ # lists, typed parameters, references, enumerations, binaries,
17
+ # strings, unset ($) and derived (*).
18
+ class Grammar < Parsanol::Parser
19
+ root(:exchange_structure)
20
+
21
+ rule(:comment) { str("/*") >> (str("*/").absent? >> any).repeat >> str("*/") }
22
+ rule(:ws?) { (match[" \t\r\n"] | comment).repeat }
23
+ rule(:ws1) { (match[" \t\r\n"] | comment).repeat(1) }
24
+ rule(:keyword) { match["!"].maybe >> match["A-Za-z0-9_"].repeat(1) }
25
+ rule(:entity_instance_name) { str("#") >> match["0-9"].repeat(1) }
26
+
27
+ rule(:string_literal) do
28
+ # ISO 10303-21: doubled quotes are escaped quotes
29
+ str("'") >> ((str("'").absent? >> any) | str("''")).repeat >> str("'")
30
+ end
31
+ rule(:binary_literal) { str('"') >> match["0-9A-Fa-f"].repeat >> str('"') }
32
+ rule(:enumeration) { str(".") >> match["A-Za-z0-9_"].repeat(1) >> str(".") }
33
+ rule(:unset) { str("$") }
34
+ rule(:derived) { str("*") }
35
+
36
+ rule(:parameter_list) do
37
+ str("(") >> ws? >>
38
+ (parameter.as(:parameter) >>
39
+ (str(",") >> ws? >> parameter.as(:parameter)).repeat).maybe >>
40
+ str(")") >> ws?
41
+ end
42
+
43
+ rule(:typed_parameter) do
44
+ keyword.as(:keyword) >> ws? >> parameter_list.as(:parameter_list)
45
+ end
46
+ rule(:list_parameter) { parameter_list }
47
+
48
+ rule(:number_literal) do
49
+ match["+-"].maybe >> match["0-9"].repeat(1) >>
50
+ (str(".") >> match["0-9"].repeat(1)).maybe >>
51
+ (match["Ee"] >> match["+-"].maybe >> match["0-9"].repeat(1)).maybe
52
+ end
53
+
54
+ rule(:parameter) do
55
+ typed_parameter.as(:typed_parameter) |
56
+ list_parameter.as(:parameter_list) |
57
+ entity_instance_name.as(:entity_instance_name) |
58
+ number_literal.as(:number_literal) |
59
+ enumeration.as(:enumeration) |
60
+ binary_literal.as(:binary_literal) |
61
+ string_literal.as(:string_literal) |
62
+ unset.as(:unset) | derived.as(:derived)
63
+ end
64
+
65
+ rule(:simple_record) do
66
+ keyword.as(:keyword) >> ws? >> parameter_list.as(:parameter_list)
67
+ end
68
+
69
+ rule(:subsuper_record) do
70
+ str("(") >> ws? >> simple_record.as(:simple_record).repeat(1) >>
71
+ str(")") >> ws?
72
+ end
73
+
74
+ rule(:simple_entity_instance) do
75
+ (entity_instance_name.as(:entity_instance_name) >> ws? >>
76
+ str("=") >> ws? >> simple_record.as(:simple_record) >>
77
+ str(";") >> ws?).as(:simple_entity_instance)
78
+ end
79
+ rule(:complex_entity_instance) do
80
+ (entity_instance_name.as(:entity_instance_name) >> ws? >>
81
+ str("=") >> ws? >> subsuper_record.as(:subsuper_record) >>
82
+ str(";") >> ws?).as(:complex_entity_instance)
83
+ end
84
+ rule(:entity_instance) { complex_entity_instance | simple_entity_instance }
85
+
86
+ rule(:header_entity) do
87
+ simple_record.as(:simple_record) >> str(";") >> ws?
88
+ end
89
+ rule(:header_section) do
90
+ str("HEADER;") >> ws? >> header_entity.repeat(1).as(:header_entity) >>
91
+ str("ENDSEC;") >> ws?
92
+ end
93
+ rule(:data_section) do
94
+ str("DATA;") >> ws? >>
95
+ (str("(") >> ws? >> parameter >> str(");") >> ws?).maybe >>
96
+ entity_instance.repeat.as(:instance_list) >>
97
+ str("ENDSEC;") >> ws?
98
+ end
99
+ rule(:exchange_structure) do
100
+ ws? >> str("ISO-10303-21;") >> ws? >>
101
+ header_section.as(:header_section) >>
102
+ data_section.as(:data_section) >>
103
+ str("END-ISO-10303-21;") >> ws?
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # Facade: parse an ISO 10303-21 exchange structure into a
6
+ # {Population} (TODO.eengine/06 step 1).
7
+ module Parser
8
+ module_function
9
+
10
+ def parse(source)
11
+ tree = Grammar.new.parse(source)
12
+ Builder.build(tree)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # A parsed ISO 10303-21 exchange structure (TODO.eengine/06).
6
+ # `header` — Hash: keyword => parameters (Array of values).
7
+ # `instances`— Hash: "#id" => Record.
8
+ #
9
+ # Values: String (string literal), Integer (reference id, keyed as
10
+ # "#id" in instances), Expressir::P21::Enumeration,
11
+ # Expressir::P21::Binary, Expressir::P21::TypedValue,
12
+ # Expressir::P21::ComplexRecord, :unset, :derived, or nested
13
+ # Arrays for lists.
14
+ class Population
15
+ attr_reader :header, :instances
16
+
17
+ def initialize(header: {}, instances: {})
18
+ @header = header
19
+ @instances = instances
20
+ end
21
+ end
22
+
23
+ # A data-section instance: `#12 = KEYWORD(param, ...) ;`
24
+ Record = Struct.new(:keyword, :params)
25
+
26
+ # `#21 = (A(...) B(...))` — complex entity instance.
27
+ ComplexRecord = Struct.new(:records)
28
+
29
+ TypedValue = Struct.new(:keyword, :value)
30
+
31
+ Enumeration = Struct.new(:name)
32
+
33
+ Binary = Struct.new(:hex)
34
+ end
35
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # Validates a {Population} against a loaded schema
6
+ # (TODO.eengine/06 step 2). Single-schema scope: instance keywords
7
+ # must be declared in the given schema; USE/REFERENCE visibility is
8
+ # a later ladder step.
9
+ #
10
+ # Mapping follows ISO 10303-21 clause 12: derived attributes are
11
+ # not mapped (a supertype attribute redeclared as DERIVED keeps its
12
+ # slot, valued `*`), inverse attributes are not mapped, and the
13
+ # value order is supertype attributes first.
14
+ module Validator
15
+ Note = Struct.new(:id, :severity, :instance, :message, keyword_init: true)
16
+
17
+ module_function
18
+
19
+ # `short_names` maps exchange-structure short names (ISO 10303-21,
20
+ # clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
21
+ def validate(population, schema, short_names: nil)
22
+ short_names = short_names&.transform_keys(&:downcase)
23
+ notes = []
24
+ check_file_schema(notes, population, schema)
25
+ population.instances.each do |id, instance|
26
+ if instance.is_a?(ComplexRecord)
27
+ check_complex(notes, schema, population, id, instance, short_names)
28
+ else
29
+ check_simple(notes, schema, population, id, instance, short_names)
30
+ end
31
+ end
32
+ notes
33
+ end
34
+
35
+ def errors(notes)
36
+ notes.select { |note| note.severity == :error }
37
+ end
38
+
39
+ def warnings(notes)
40
+ notes.select { |note| note.severity == :warning }
41
+ end
42
+
43
+ def valid?(notes)
44
+ errors(notes).empty?
45
+ end
46
+
47
+ def note!(notes, id, severity, instance, message)
48
+ notes << Note.new(id: id, severity: severity, instance: instance,
49
+ message: message)
50
+ end
51
+
52
+ def check_file_schema(notes, population, schema)
53
+ declared = Array(population.header["FILE_SCHEMA"]).flatten.first
54
+ return unless declared && schema.id
55
+ return if declared.downcase == schema.id.downcase
56
+
57
+ note!(notes, :p21_schema_mismatch, :warning, nil,
58
+ "FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
59
+ end
60
+
61
+ def check_simple(notes, schema, population, id, record, short_names)
62
+ entity = find_entity(schema, record.keyword, short_names)
63
+ unless entity
64
+ note!(notes, :p21_unknown_entity, :error, id,
65
+ "#{record.keyword} is not declared in schema '#{schema.id}'")
66
+ return
67
+ end
68
+
69
+ check_params(notes, schema, population, id, record.params,
70
+ mapped_slots(entity))
71
+ end
72
+
73
+ def check_complex(notes, schema, population, id, complex, short_names)
74
+ pairs = complex.records.filter_map do |record|
75
+ entity = find_entity(schema, record.keyword, short_names)
76
+ unless entity
77
+ note!(notes, :p21_unknown_entity, :error, id,
78
+ "#{record.keyword} is not declared in schema '#{schema.id}'")
79
+ next nil
80
+ end
81
+
82
+ [record, entity]
83
+ end
84
+ return if pairs.empty?
85
+
86
+ check_clique(notes, id, pairs.map { |_, entity| entity })
87
+ pairs.each do |record, entity|
88
+ slots = explicit_attributes(entity).map { |attr| [attr, false] }
89
+ check_params(notes, schema, population, id, record.params, slots)
90
+ end
91
+ end
92
+
93
+ # A complex instance's records must form a valid supertype graph
94
+ # path: they share at least one common supertype.
95
+ def check_clique(notes, id, entities)
96
+ common = closure_keys(entities.first)
97
+ entities.drop(1).each { |entity| common &= closure_keys(entity) }
98
+ return unless common.empty?
99
+
100
+ note!(notes, :p21_complex_clique, :error, id,
101
+ "#{id}: complex records share no common supertype")
102
+ end
103
+
104
+ def closure_keys(entity)
105
+ keys = entity.supertype_closure.filter_map { |e| e.id&.downcase }
106
+ keys << entity.id&.downcase
107
+ end
108
+
109
+ # Part 21 value order: supertype attributes first; an attribute
110
+ # redeclared as DERIVED in the entity keeps its inherited slot,
111
+ # valued `*` (12.2.6).
112
+ def mapped_slots(entity)
113
+ own_derived_keys = own_derived_attributes(entity)
114
+ .filter_map { |attr| attr.id&.downcase }
115
+ inherited = entity.supertype_closure.flat_map do |supertype|
116
+ explicit_attributes(supertype)
117
+ end
118
+ inherited.map do |attr|
119
+ [attr, own_derived_keys.include?(attr.id&.downcase)]
120
+ end + explicit_attributes(entity).map { |attr| [attr, false] }
121
+ end
122
+
123
+ def check_params(notes, schema, population, id, params, slots)
124
+ if params.size != slots.size
125
+ note!(notes, :p21_arity_mismatch, :error, id,
126
+ "#{id}: #{params.size} values for #{slots.size} mapped attributes")
127
+ end
128
+
129
+ slots.zip(params).each do |(attr, derived_slot), value|
130
+ next if value.nil?
131
+
132
+ if derived_slot
133
+ next if value == :derived
134
+
135
+ note!(notes, :p21_derived_slot, :error, id,
136
+ "#{id}: attribute '#{attr.id}' redeclared DERIVED must be '*'")
137
+ next
138
+ end
139
+
140
+ check_value(notes, schema, population, id, attr, value)
141
+ end
142
+ end
143
+
144
+ def check_value(notes, schema, population, id, attr, value)
145
+ case value
146
+ when String
147
+ return unless value.match?(/\A#\d+\z/)
148
+
149
+ return if population.instances.key?(value)
150
+
151
+ note!(notes, :p21_unknown_reference, :error, id,
152
+ "#{id}: reference #{value} has no instance")
153
+ when :derived
154
+ note!(notes, :p21_derived_value, :error, id,
155
+ "#{id}: '*' outside a derived slot")
156
+ when TypedValue
157
+ return if find_entity(schema, value.keyword) || find_type(schema, value.keyword)
158
+
159
+ note!(notes, :p21_unknown_typed_keyword, :error, id,
160
+ "#{id}: typed parameter '#{value.keyword}' does not resolve")
161
+ when Enumeration
162
+ items = enumeration_items(schema, attr)
163
+ return unless items
164
+
165
+ names = items.filter_map { |item| item.id&.downcase }
166
+ return if names.include?(value.name.downcase)
167
+
168
+ note!(notes, :p21_enum_value, :error, id,
169
+ "#{id}: .#{value.name}. is not in attribute '#{attr.id}' enumeration")
170
+ when Array
171
+ value.each { |item| check_value(notes, schema, population, id, attr, item) }
172
+ end
173
+ end
174
+
175
+ def explicit_attributes(entity)
176
+ Array(entity.attributes)
177
+ .select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
178
+ end
179
+
180
+ def own_derived_attributes(entity)
181
+ Array(entity.attributes)
182
+ .select { |attr| attr.instance_of?(Model::Declarations::DerivedAttribute) }
183
+ end
184
+
185
+ def find_entity(schema, keyword, short_names = nil)
186
+ key = keyword.downcase
187
+ Array(schema.entities).find { |entity| entity.id&.downcase == key } ||
188
+ resolve_short_name(schema, key, short_names)
189
+ end
190
+
191
+ def resolve_short_name(schema, key, short_names)
192
+ return nil unless short_names.is_a?(Hash) && short_names[key]
193
+
194
+ find_entity(schema, short_names[key], nil)
195
+ end
196
+
197
+ def find_type(schema, keyword)
198
+ Array(schema.types).find { |type| type.id&.downcase == keyword.downcase }
199
+ end
200
+
201
+ def enumeration_items(schema, attr)
202
+ type = attr&.type
203
+ return Array(type.items) if type.is_a?(Model::DataTypes::Enumeration)
204
+
205
+ return nil unless type.is_a?(Model::References::SimpleReference)
206
+
207
+ declaration = find_type(schema, type.id.to_s)
208
+ underlying = declaration&.underlying_type
209
+ underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ # ISO 10303-21 exchange-structure support (TODO.eengine/06).
5
+ module P21
6
+ autoload :Population, "#{__dir__}/p21/population"
7
+ autoload :Record, "#{__dir__}/p21/population"
8
+ autoload :ComplexRecord, "#{__dir__}/p21/population"
9
+ autoload :TypedValue, "#{__dir__}/p21/population"
10
+ autoload :Enumeration, "#{__dir__}/p21/population"
11
+ autoload :Binary, "#{__dir__}/p21/population"
12
+ autoload :Grammar, "#{__dir__}/p21/grammar"
13
+ autoload :Builder, "#{__dir__}/p21/builder"
14
+ autoload :Validator, "#{__dir__}/p21/validator"
15
+ autoload :Parser, "#{__dir__}/p21/parser"
16
+ end
17
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Expressir
6
+ module Reference
7
+ # Rule-by-rule cross-walk between the parsanol grammar and the
8
+ # ISO 10303-11 Annex A/B productions. The mapping data is generated
9
+ # by `rake reference:crosswalk`; the spec fails when a grammar rule
10
+ # has no row or the rendered guide is stale.
11
+ class GrammarCrosswalk
12
+ DATA_PATH = File.expand_path("grammar_crosswalk.yml", __dir__)
13
+ GRAMMAR_RULE = /rule\(:(\w+)/
14
+ PRODUCTION_STATUSES = %w[aligned decomposed].freeze
15
+
16
+ class << self
17
+ def data
18
+ @data ||= YAML.load_file(DATA_PATH)
19
+ end
20
+
21
+ def grammar_path
22
+ Expressir.root_path.join(data.dig("source", "grammar"))
23
+ end
24
+
25
+ def grammar_rules
26
+ File.read(grammar_path).scan(GRAMMAR_RULE).flatten.uniq.sort
27
+ end
28
+
29
+ def rows
30
+ data["rules"]
31
+ end
32
+
33
+ def annex_without_rule
34
+ data["annex_without_rule"]
35
+ end
36
+
37
+ def statuses
38
+ data["statuses"]
39
+ end
40
+
41
+ # Returns the list of inconsistencies; empty means in sync.
42
+ def validate
43
+ errors = []
44
+ declared = rows.map { |row| row["rule"] }
45
+ missing = grammar_rules - declared
46
+ unknown = declared - grammar_rules
47
+ duplicates = declared.tally.select { |_, count| count > 1 }.keys
48
+ errors << "grammar rules without a cross-walk row: #{missing.join(', ')}" if missing.any?
49
+ errors << "cross-walk rows without a grammar rule: #{unknown.join(', ')}" if unknown.any?
50
+ errors << "duplicate cross-walk rows: #{duplicates.join(', ')}" if duplicates.any?
51
+
52
+ rows.each do |row|
53
+ status = row["status"]
54
+ unless statuses.key?(status)
55
+ errors << "#{row['rule']}: unknown status '#{status}'"
56
+ end
57
+ if PRODUCTION_STATUSES.include?(status) && !row["production"]
58
+ errors << "#{row['rule']}: #{status} row needs a production"
59
+ end
60
+ end
61
+ errors
62
+ end
63
+
64
+ def render_adoc
65
+ lines = [
66
+ "= Grammar cross-walk",
67
+ "",
68
+ "// Generated by `rake reference:crosswalk` from",
69
+ "// #{data.dig('source', 'grammar')} and the Annex A snapshot.",
70
+ "// Do not edit by hand; the reference spec fails when this file is stale.",
71
+ "",
72
+ "== Purpose",
73
+ "",
74
+ "Every parsanol grammar rule is mapped to the ISO 10303-11:2004",
75
+ "Annex A production it implements. Divergences from the standard",
76
+ "grammar are classified with one of these statuses:",
77
+ "",
78
+ *statuses.map { |name, description| "* `#{name}` — #{description}" },
79
+ "",
80
+ "== Rules",
81
+ "",
82
+ '[cols="2,3,1,4", options="header"]',
83
+ "|===",
84
+ "| Parsanol rule | Annex A production | Status | Note",
85
+ "",
86
+ *rows.map { |row| rule_row(row) },
87
+ "|===",
88
+ "",
89
+ "== Annex A productions without a dedicated rule",
90
+ "",
91
+ "These productions are handled by supporting code rather than a",
92
+ "grammar rule:",
93
+ "",
94
+ '[cols="3,1,5", options="header"]',
95
+ "|===",
96
+ "| Annex A production | Status | Handled by",
97
+ "",
98
+ *annex_without_rule.map { |row| annex_row(row) },
99
+ "|===",
100
+ "",
101
+ ]
102
+ lines.join("\n")
103
+ end
104
+
105
+ private
106
+
107
+ def rule_row(row)
108
+ note = row["literal"] ? "literal `#{row['literal']}`" : row["note"]
109
+ "| `#{row['rule']}` | #{cell(row['production'])} | #{row['status']} | #{cell(note)}"
110
+ end
111
+
112
+ def annex_row(row)
113
+ "| `#{row['production']}` | #{row['status']} | #{cell(row['note'])}"
114
+ end
115
+
116
+ def cell(text)
117
+ text.to_s.gsub("|", "\\|")
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end