expressir 2.4.37-aarch64-linux → 2.4.39-aarch64-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.
@@ -20,13 +20,17 @@ module Expressir
20
20
  # clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
21
21
  def validate(population, schema, short_names: nil)
22
22
  short_names = short_names&.transform_keys(&:downcase)
23
+ typing = Typing.new(schema, short_names: short_names)
23
24
  notes = []
24
25
  check_file_schema(notes, population, schema)
26
+ check_sections(notes, population, schema)
25
27
  population.instances.each do |id, instance|
26
28
  if instance.is_a?(ComplexRecord)
27
- check_complex(notes, schema, population, id, instance, short_names)
29
+ check_complex(notes, schema, population, id, instance,
30
+ short_names, typing)
28
31
  else
29
- check_simple(notes, schema, population, id, instance, short_names)
32
+ check_simple(notes, schema, population, id, instance,
33
+ short_names, typing)
30
34
  end
31
35
  end
32
36
  notes
@@ -49,6 +53,24 @@ module Expressir
49
53
  message: message)
50
54
  end
51
55
 
56
+ # Annex E (ISO 10303-21 clause 11.2): each DATA section's schema
57
+ # contexts must intersect the declared schema list.
58
+ def check_sections(notes, population, schema)
59
+ known = ([schema.id] +
60
+ Array(population.header["FILE_SCHEMA"]).flatten)
61
+ .filter_map { |name| name.to_s.downcase unless name.to_s.empty? }
62
+ Array(population.sections).each do |section|
63
+ next if section.contexts.empty?
64
+ next if section.contexts.any? do |context|
65
+ known.include?(context.to_s.downcase)
66
+ end
67
+
68
+ note!(notes, :p21_section_context, :warning, nil,
69
+ "DATA section '#{section.name}' context(s) " \
70
+ "#{section.contexts.join(', ')} are not in the schema list")
71
+ end
72
+ end
73
+
52
74
  def check_file_schema(notes, population, schema)
53
75
  declared = Array(population.header["FILE_SCHEMA"]).flatten.first
54
76
  return unless declared && schema.id
@@ -58,7 +80,8 @@ module Expressir
58
80
  "FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
59
81
  end
60
82
 
61
- def check_simple(notes, schema, population, id, record, short_names)
83
+ def check_simple(notes, schema, population, id, record, short_names,
84
+ typing)
62
85
  entity = find_entity(schema, record.keyword, short_names)
63
86
  unless entity
64
87
  note!(notes, :p21_unknown_entity, :error, id,
@@ -67,10 +90,11 @@ module Expressir
67
90
  end
68
91
 
69
92
  check_params(notes, schema, population, id, record.params,
70
- mapped_slots(entity))
93
+ mapped_slots(entity), typing)
71
94
  end
72
95
 
73
- def check_complex(notes, schema, population, id, complex, short_names)
96
+ def check_complex(notes, schema, population, id, complex, short_names,
97
+ typing)
74
98
  pairs = complex.records.filter_map do |record|
75
99
  entity = find_entity(schema, record.keyword, short_names)
76
100
  unless entity
@@ -84,9 +108,14 @@ module Expressir
84
108
  return if pairs.empty?
85
109
 
86
110
  check_clique(notes, id, pairs.map { |_, entity| entity })
111
+ typing.issues(complex).each do |message|
112
+ note!(notes, :check_p21_complex_instantiation, :error, id,
113
+ "#{id}: #{message}")
114
+ end
87
115
  pairs.each do |record, entity|
88
116
  slots = explicit_attributes(entity).map { |attr| [attr, false] }
89
- check_params(notes, schema, population, id, record.params, slots)
117
+ check_params(notes, schema, population, id, record.params, slots,
118
+ typing)
90
119
  end
91
120
  end
92
121
 
@@ -120,14 +149,14 @@ module Expressir
120
149
  end + explicit_attributes(entity).map { |attr| [attr, false] }
121
150
  end
122
151
 
123
- def check_params(notes, schema, population, id, params, slots)
152
+ def check_params(notes, schema, population, id, params, slots, typing)
124
153
  if params.size != slots.size
125
154
  note!(notes, :p21_arity_mismatch, :error, id,
126
155
  "#{id}: #{params.size} values for #{slots.size} mapped attributes")
127
156
  end
128
157
 
129
158
  slots.zip(params).each do |(attr, derived_slot), value|
130
- next if value.nil?
159
+ next if value.nil? || value == :unset
131
160
 
132
161
  if derived_slot
133
162
  next if value == :derived
@@ -138,6 +167,11 @@ module Expressir
138
167
  end
139
168
 
140
169
  check_value(notes, schema, population, id, attr, value)
170
+ slot_type_issues(schema, population, attr&.type, value, typing)
171
+ .each do |(category, message)|
172
+ note!(notes, FINDING_IDS.fetch(category), :error, id,
173
+ "#{id}: #{message}")
174
+ end
141
175
  end
142
176
  end
143
177
 
@@ -147,6 +181,9 @@ module Expressir
147
181
  return unless value.match?(/\A#\d+\z/)
148
182
 
149
183
  return if population.instances.key?(value)
184
+ # An id bound in the REFERENCE section names an external
185
+ # instance (ISO 10303-21 clause 10); resolution is remote.
186
+ return if population.references.key?(value)
150
187
 
151
188
  note!(notes, :p21_unknown_reference, :error, id,
152
189
  "#{id}: reference #{value} has no instance")
@@ -208,6 +245,380 @@ module Expressir
208
245
  underlying = declaration&.underlying_type
209
246
  underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
210
247
  end
248
+
249
+ # ---- TODO.instance/01: value/type conformance ----
250
+ # ISO 10303-11 ch.8 domains over the clause-12 slots. Every
251
+ # finding carries its clause: simple types 8.1.x, aggregation
252
+ # types 8.2.x, named types 8.4.x, attribute domains 9.2.1,
253
+ # complex instantiation 9.2.5/9.2.5.2, domain rules 9.2.2.2.
254
+
255
+ FINDING_IDS = {
256
+ domain: :check_p21_value_domain,
257
+ aggregate: :check_p21_aggregate,
258
+ reference: :check_p21_reference_type,
259
+ select: :check_p21_select_arm,
260
+ complex: :check_p21_complex_instantiation,
261
+ }.freeze
262
+ MAX_TYPE_DEPTH = 16
263
+ PRIMITIVE_DOMAINS = {
264
+ Model::DataTypes::Integer => ["INTEGER", "an unquoted integer (8.1.3)"],
265
+ Model::DataTypes::Real => ["REAL", "a number (8.1.2)"],
266
+ Model::DataTypes::Number => ["NUMBER", "a number (8.1.1)"],
267
+ Model::DataTypes::String => ["STRING", "a single-quoted string (8.1.6)"],
268
+ Model::DataTypes::Binary => ["BINARY", "a binary literal (8.1.7)"],
269
+ Model::DataTypes::Boolean => ["BOOLEAN", ".T. or .F. (8.1.5)"],
270
+ Model::DataTypes::Logical => ["LOGICAL", ".T., .F. or .U. (8.1.4)"],
271
+ }.freeze
272
+ private_constant :FINDING_IDS, :MAX_TYPE_DEPTH, :PRIMITIVE_DOMAINS
273
+
274
+ def slot_type_issues(schema, population, type, value, typing, depth = 0,
275
+ seen = {}.compare_by_identity)
276
+ return [] unless type.is_a?(Model::ModelElement)
277
+ return [] if depth >= MAX_TYPE_DEPTH
278
+ if value.is_a?(TypedValue)
279
+ return typed_value_issues(schema, population, type, value, typing,
280
+ depth, seen)
281
+ end
282
+
283
+ case type
284
+ when Model::Declarations::Type
285
+ defined_type_issues(schema, population, type, value, typing,
286
+ depth, seen)
287
+ when Model::Declarations::Entity
288
+ reference_issues(schema, population, value, type, typing)
289
+ when Model::References::SimpleReference
290
+ reference_slot_issues(schema, population, type, value, typing,
291
+ depth, seen)
292
+ when Model::DataTypes::List, Model::DataTypes::Array,
293
+ Model::DataTypes::Set, Model::DataTypes::Bag
294
+ aggregate_issues(schema, population, type, value, typing, depth, seen)
295
+ when Model::DataTypes::Select
296
+ select_issues(schema, population, type, value, typing, depth, seen)
297
+ when Model::DataTypes::Enumeration
298
+ [] # 8.4.1 membership is checked by check_value
299
+ else
300
+ primitive_issues(type, value)
301
+ end
302
+ end
303
+
304
+ def reference_slot_issues(schema, population, type, value, typing,
305
+ depth, seen)
306
+ declaration = find_type(schema, type.id.to_s)
307
+ if declaration.is_a?(Model::Declarations::Type)
308
+ return defined_type_issues(schema, population, declaration, value,
309
+ typing, depth, seen)
310
+ end
311
+
312
+ entity = find_entity(schema, type.id.to_s)
313
+ return [] unless entity
314
+
315
+ reference_issues(schema, population, value, entity, typing)
316
+ end
317
+
318
+ def defined_type_issues(schema, population, declaration, value, typing,
319
+ depth, seen)
320
+ return [] if seen.include?(declaration)
321
+
322
+ seen[declaration] = true
323
+ domain_rule_issues(schema, value, declaration) +
324
+ slot_type_issues(schema, population, declaration.underlying_type,
325
+ value, typing, depth + 1, seen)
326
+ end
327
+
328
+ def domain_rule_issues(schema, value, declaration)
329
+ self_value = domain_value(value, declaration)
330
+ return [] if self_value.nil?
331
+
332
+ Array(declaration.where_rules).filter_map do |rule|
333
+ next unless rule.expression
334
+
335
+ env = Eval::Environment.new(schema: schema).define("self", self_value)
336
+ result = Eval::Expression.evaluate(rule.expression, env)
337
+ next unless logical_false?(result)
338
+
339
+ [:domain,
340
+ "value violates domain rule '#{rule.id}' of " \
341
+ "TYPE '#{declaration.id}' (9.2.2.2)"]
342
+ rescue StandardError
343
+ next # an unevaluable domain rule is not a finding here
344
+ end
345
+ end
346
+
347
+ def domain_value(value, declaration)
348
+ case value
349
+ when Integer, Float then value
350
+ when String then reference?(value) ? nil : value
351
+ when Enumeration
352
+ Eval::Values::Enumeration.new(type: declaration.id, item: value.name)
353
+ when Binary then Eval::Values::Binary.new(bits: value.hex)
354
+ end
355
+ end
356
+
357
+ def logical_false?(result)
358
+ return true if [false, Eval::Values::LOGICAL_FALSE].include?(result)
359
+ return result.value == Eval::Values::LOGICAL_FALSE if result.is_a?(Eval::Values::Logical)
360
+
361
+ false
362
+ end
363
+
364
+ def primitive_issues(type, value)
365
+ domain, expectation = PRIMITIVE_DOMAINS[type.class]
366
+ return [] unless domain
367
+
368
+ ok = case type
369
+ when Model::DataTypes::Integer then value.is_a?(Integer)
370
+ when Model::DataTypes::Real, Model::DataTypes::Number
371
+ value.is_a?(Numeric)
372
+ when Model::DataTypes::String
373
+ value.is_a?(String) && !reference?(value)
374
+ when Model::DataTypes::Binary then value.is_a?(Binary)
375
+ when Model::DataTypes::Boolean, Model::DataTypes::Logical
376
+ logical_literal?(value)
377
+ end
378
+ return [] if ok
379
+
380
+ [[:domain, "#{domain} slot requires #{expectation}"]]
381
+ end
382
+
383
+ def logical_literal?(value)
384
+ return true if [true, false].include?(value)
385
+
386
+ value.is_a?(Enumeration) && %w[T F U].include?(value.name.to_s.upcase)
387
+ end
388
+
389
+ def reference_issues(_schema, population, value, entity, typing)
390
+ return [] unless reference?(value)
391
+
392
+ target = population.instances[value]
393
+ return [] unless target
394
+
395
+ conformed = typing.of(target)
396
+ return [] if conformed.any? do |candidate|
397
+ candidate.id&.safe_downcase == entity.id&.safe_downcase
398
+ end
399
+
400
+ [[:reference,
401
+ "reference #{value} conforms to " \
402
+ "#{conformed.filter_map(&:id).join(', ')} but '#{entity.id}' is " \
403
+ "required (9.2.1)"]]
404
+ end
405
+
406
+ def typed_value_issues(schema, population, _type, value, typing, depth,
407
+ seen)
408
+ entity = find_entity(schema, value.keyword)
409
+ if entity
410
+ return reference_issues(schema, population, value.value, entity,
411
+ typing)
412
+ end
413
+
414
+ declaration = find_type(schema, value.keyword)
415
+ return [] unless declaration.is_a?(Model::Declarations::Type)
416
+
417
+ underlying = declaration.underlying_type
418
+ inner = value.value
419
+ if inner.is_a?(Array) && inner.size == 1 && !aggregate?(underlying)
420
+ inner = inner.first
421
+ end
422
+ slot_type_issues(schema, population, underlying, inner, typing,
423
+ depth + 1, seen)
424
+ end
425
+
426
+ def aggregate?(type)
427
+ type.is_a?(Model::DataTypes::List) || type.is_a?(Model::DataTypes::Array) ||
428
+ type.is_a?(Model::DataTypes::Set) || type.is_a?(Model::DataTypes::Bag)
429
+ end
430
+
431
+ def aggregate_issues(schema, population, type, value, typing, depth,
432
+ seen)
433
+ unless value.is_a?(Array)
434
+ return [[:domain, "#{aggregate_name(type)} attribute requires an " + "aggregate value (#{aggregate_section(type)})"]]
435
+ end
436
+
437
+ aggregate_bound_issues(type, value) +
438
+ aggregate_unique_issues(type, value) +
439
+ aggregate_element_issues(schema, population, type, value, typing,
440
+ depth, seen)
441
+ end
442
+
443
+ def aggregate_bound_issues(type, value)
444
+ lower, upper = aggregate_bounds(type)
445
+ issues = []
446
+ if lower && value.size < lower
447
+ issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), below the bound #{lower} " + "(#{aggregate_section(type)})"]
448
+ end
449
+ if upper && value.size > upper
450
+ issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), above the bound #{upper} " + "(#{aggregate_section(type)})"]
451
+ end
452
+ issues
453
+ end
454
+
455
+ def aggregate_bounds(type)
456
+ [bound_value(type.bound1), bound_value(type.bound2)]
457
+ end
458
+
459
+ def bound_value(bound)
460
+ return nil unless bound.is_a?(Model::Literals::Integer)
461
+
462
+ bound.value.to_i
463
+ end
464
+
465
+ def aggregate_unique_issues(type, value)
466
+ return [] unless type.is_a?(Model::DataTypes::Set)
467
+
468
+ keys = value.map { |element| p21_value_key(element) }
469
+ return [] if keys.uniq.size == keys.size
470
+
471
+ [[:aggregate, "SET has duplicate elements (8.2.4)"]]
472
+ end
473
+
474
+ def aggregate_element_issues(schema, population, type, value, typing,
475
+ depth, _seen)
476
+ issues = []
477
+ value.each_with_index do |element, index|
478
+ unset = element.nil? || element == :unset
479
+ if unset
480
+ next unless type.is_a?(Model::DataTypes::Array) && !type.optional
481
+
482
+ issues << [:aggregate, "ARRAY element #{index} is unset but the array is not OPTIONAL (8.2.1)"]
483
+ next
484
+ end
485
+
486
+ # Sibling elements re-walk the same types with different
487
+ # values: only the walk along one nesting path shares `seen`.
488
+ slot_type_issues(schema, population, type.base_type, element,
489
+ typing, depth + 1, {}.compare_by_identity)
490
+ .each do |(category, message)|
491
+ issues << [category, "element #{index}: #{message}"]
492
+ end
493
+ end
494
+ issues
495
+ end
496
+
497
+ def select_issues(schema, population, type, value, typing, depth, seen)
498
+ arms = select_arms(schema, type, seen)
499
+ return [] if arms.empty?
500
+
501
+ select_arm_issues(schema, population, value, arms, typing, depth, seen)
502
+ end
503
+
504
+ # Leaf domains of a SELECT: item entities, item defined types, and
505
+ # nested selects, plus the BASED_ON chain (8.4.2).
506
+ def select_arms(schema, type, seen)
507
+ return [] if seen.include?(type)
508
+
509
+ seen[type] = true
510
+ arms = []
511
+ Array(type.items).each do |item|
512
+ next unless item.respond_to?(:id) && item.id
513
+
514
+ declaration = find_type(schema, item.id.to_s)
515
+ if declaration.is_a?(Model::Declarations::Type)
516
+ underlying = declaration.underlying_type
517
+ if underlying.is_a?(Model::DataTypes::Select)
518
+ arms.concat(select_arms(schema, underlying, seen))
519
+ else
520
+ arms << [:type, declaration]
521
+ end
522
+ next
523
+ end
524
+
525
+ entity = find_entity(schema, item.id.to_s)
526
+ arms << [:entity, entity] if entity
527
+ end
528
+ base = based_on_select(schema, type)
529
+ arms.concat(select_arms(schema, base, seen)) if base
530
+ arms
531
+ end
532
+
533
+ def based_on_select(schema, type)
534
+ based = type.based_on
535
+ return nil unless based.respond_to?(:id) && based.id
536
+
537
+ declaration = find_type(schema, based.id.to_s)
538
+ return nil unless declaration.is_a?(Model::Declarations::Type)
539
+
540
+ underlying = declaration.underlying_type
541
+ underlying.is_a?(Model::DataTypes::Select) ? underlying : nil
542
+ end
543
+
544
+ def select_arm_issues(schema, population, value, arms, typing, depth,
545
+ seen)
546
+ if reference?(value)
547
+ target = population.instances[value]
548
+ return [] unless target
549
+
550
+ conformed = typing.of(target)
551
+ ok = arms.any? do |(kind, node)|
552
+ kind == :entity &&
553
+ conformed.any? do |candidate|
554
+ candidate.id&.safe_downcase == node.id&.safe_downcase
555
+ end
556
+ end
557
+ return [] if ok
558
+
559
+ return [[:select, "reference #{value} fits no arm of the SELECT (8.4.2)"]]
560
+ end
561
+
562
+ if value.is_a?(TypedValue)
563
+ ok = arms.any? do |(_kind, node)|
564
+ node.id&.safe_downcase == value.keyword&.downcase
565
+ end
566
+ return [] if ok
567
+
568
+ return [[:select, "typed value '#{value.keyword}' fits no arm of the SELECT (8.4.2)"]]
569
+ end
570
+
571
+ return [] if value.is_a?(Enumeration) # 8.4.1 membership via check_value
572
+
573
+ literal_ok = arms.any? do |(kind, node)|
574
+ kind == :type && slot_type_issues(
575
+ schema, population, node.underlying_type, value, typing,
576
+ depth + 1, seen
577
+ ).empty?
578
+ end
579
+ return [] if literal_ok
580
+
581
+ [[:select, "value #{p21_display(value)} fits no arm of the SELECT (8.4.2)"]]
582
+ end
583
+
584
+ def reference?(value)
585
+ value.is_a?(String) && value.match?(/\A#\d+\z/)
586
+ end
587
+
588
+ def p21_value_key(value)
589
+ case value
590
+ when Enumeration then [:enum, value.name&.downcase]
591
+ when Binary then [:binary, value.hex]
592
+ when Array then [:list, *value.map { |element| p21_value_key(element) }]
593
+ else [:value, value]
594
+ end
595
+ end
596
+
597
+ def p21_display(value)
598
+ case value
599
+ when Enumeration then ".#{value.name}."
600
+ when Binary then "%#{value.hex}"
601
+ else value.to_s
602
+ end
603
+ end
604
+
605
+ def aggregate_name(type)
606
+ case type
607
+ when Model::DataTypes::List then "LIST"
608
+ when Model::DataTypes::Array then "ARRAY"
609
+ when Model::DataTypes::Set then "SET"
610
+ else "BAG"
611
+ end
612
+ end
613
+
614
+ def aggregate_section(type)
615
+ case type
616
+ when Model::DataTypes::List then "8.2.2"
617
+ when Model::DataTypes::Array then "8.2.1"
618
+ when Model::DataTypes::Set then "8.2.4"
619
+ else "8.2.3"
620
+ end
621
+ end
211
622
  end
212
623
  end
213
624
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # Emits a {Population} back to ISO 10303-21 text (TODO.instance/03).
6
+ # Deterministic: the same population always serializes to the same
7
+ # bytes — HEADER entities in insertion order, DATA instances in id
8
+ # order, anchors/references/named sections re-emitted when present.
9
+ module Writer
10
+ module_function
11
+
12
+ def emit(population)
13
+ lines = ["ISO-10303-21;", "HEADER;"]
14
+ population.header.each do |keyword, params|
15
+ lines << "#{keyword}(#{emit_params(params)});"
16
+ end
17
+ lines << "ENDSEC;"
18
+ unless population.anchors.empty?
19
+ lines << "ANCHOR;"
20
+ population.anchors.each do |label, value|
21
+ lines << "<#{label}> = #{emit_value(value)};"
22
+ end
23
+ lines << "ENDSEC;"
24
+ end
25
+ unless population.references.empty?
26
+ lines << "REFERENCE;"
27
+ population.references.each do |id, anchor|
28
+ lines << "#{id} = #{anchor};"
29
+ end
30
+ lines << "ENDSEC;"
31
+ end
32
+ emit_data_sections(population, lines)
33
+ lines << "END-ISO-10303-21;"
34
+ "#{lines.join("\n")}\n"
35
+ end
36
+
37
+ def emit_data_sections(population, lines)
38
+ if population.sections.empty?
39
+ lines << "DATA;"
40
+ emit_instances(population.instances, lines)
41
+ lines << "ENDSEC;"
42
+ return
43
+ end
44
+
45
+ population.sections.each do |section|
46
+ lines << named_data_line(section)
47
+ section.instance_ids.each do |id|
48
+ lines << "#{id}=#{emit_record(population.instances[id])};"
49
+ end
50
+ lines << "ENDSEC;"
51
+ end
52
+ end
53
+
54
+ def named_data_line(section)
55
+ return "DATA;" if section.name.nil?
56
+
57
+ contexts = section.contexts
58
+ .map { |context| "'#{context}'" }.join(", ")
59
+ "DATA ('#{section.name}', (#{contexts}));"
60
+ end
61
+
62
+ def emit_instances(instances, lines)
63
+ instances.each do |id, record|
64
+ lines << "#{id}=#{emit_record(record)};"
65
+ end
66
+ end
67
+
68
+ def emit_record(record)
69
+ if record.is_a?(ComplexRecord)
70
+ "(#{record.records.map { |part| emit_simple(part) }.join})"
71
+ else
72
+ emit_simple(record)
73
+ end
74
+ end
75
+
76
+ def emit_simple(record)
77
+ "#{record.keyword}(#{emit_params(record.params)})"
78
+ end
79
+
80
+ def emit_params(params)
81
+ params.map { |value| emit_value(value) }.join(",")
82
+ end
83
+
84
+ def emit_value(value)
85
+ case value
86
+ when Array then "(#{value.map { |item| emit_value(item) }.join(',')})"
87
+ when TypedValue
88
+ "#{value.keyword}(#{emit_params(value.value)})"
89
+ when Enumeration then ".#{value.name}."
90
+ when Binary then "\"#{value.hex}\""
91
+ when :unset then "$"
92
+ when :derived then "*"
93
+ when true then ".T."
94
+ when false then ".F."
95
+ when String then string_or_reference(value)
96
+ else value.to_s
97
+ end
98
+ end
99
+
100
+ def string_or_reference(value)
101
+ return value if value.match?(/\A#\d+\z/)
102
+
103
+ "'#{value.gsub("'", "''")}'"
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/expressir/p21.rb CHANGED
@@ -12,6 +12,12 @@ module Expressir
12
12
  autoload :Grammar, "#{__dir__}/p21/grammar"
13
13
  autoload :Builder, "#{__dir__}/p21/builder"
14
14
  autoload :Validator, "#{__dir__}/p21/validator"
15
+ autoload :Typing, "#{__dir__}/p21/typing"
16
+ autoload :DatasetManifest, "#{__dir__}/p21/dataset_manifest"
17
+ autoload :Receipt, "#{__dir__}/p21/receipt"
18
+ autoload :Diff, "#{__dir__}/p21/diff"
15
19
  autoload :Parser, "#{__dir__}/p21/parser"
20
+ autoload :Writer, "#{__dir__}/p21/writer"
21
+ autoload :Json, "#{__dir__}/p21/json"
16
22
  end
17
23
  end
@@ -3,6 +3,6 @@ module Expressir
3
3
  # autoload it (Ruby autoload only works for module/class constants, not
4
4
  # for string constants like `Expressir::VERSION`).
5
5
  module Version
6
- VERSION = "2.4.37".freeze
6
+ VERSION = "2.4.39".freeze
7
7
  end
8
8
  end
data/lib/expressir.rb CHANGED
@@ -76,6 +76,7 @@ module Expressir
76
76
  autoload :Model, "#{__dir__}/expressir/model"
77
77
  autoload :Commands, "#{__dir__}/expressir/commands"
78
78
  autoload :Changes, "#{__dir__}/expressir/changes"
79
+ autoload :Eval, "#{__dir__}/expressir/eval"
79
80
  autoload :Conformance, "#{__dir__}/expressir/conformance"
80
81
  autoload :Package, "#{__dir__}/expressir/package"
81
82
  autoload :Manifest, "#{__dir__}/expressir/manifest"