expressir 2.4.3 → 2.4.4

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.github/workflows/rust-ext.yml +41 -0
  4. data/.github/workflows/verify_remarks.yml +33 -0
  5. data/.gitignore +6 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_todo.yml +1 -0
  8. data/Cargo.toml +7 -0
  9. data/README.adoc +65 -0
  10. data/Rakefile +12 -0
  11. data/TODO.parity-ee/00-overview.md +58 -0
  12. data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
  13. data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
  14. data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
  15. data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
  16. data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
  17. data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
  18. data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
  19. data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
  20. data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
  21. data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
  22. data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
  23. data/TODO.parity-ee/parity-matrix.md +108 -0
  24. data/exe/expressir +6 -0
  25. data/ext/expressir_core/Cargo.toml +24 -0
  26. data/ext/expressir_core/extconf.rb +30 -0
  27. data/ext/expressir_core/src/lib.rs +446 -0
  28. data/lib/expressir/cli.rb +49 -4
  29. data/lib/expressir/commands/check.rb +56 -0
  30. data/lib/expressir/commands/expand.rb +26 -0
  31. data/lib/expressir/commands/fix.rb +33 -0
  32. data/lib/expressir/commands/flatten.rb +29 -0
  33. data/lib/expressir/commands/parity_inputs.rb +85 -0
  34. data/lib/expressir/commands/validate.rb +22 -0
  35. data/lib/expressir/commands.rb +5 -0
  36. data/lib/expressir/express/builder_registry.rb +11 -2
  37. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -7
  38. data/lib/expressir/express/builders/subtype_constraint_builder.rb +3 -1
  39. data/lib/expressir/express/builders/type_builder.rb +8 -0
  40. data/lib/expressir/express/builders/unique_rule_builder.rb +8 -1
  41. data/lib/expressir/express/checker.rb +458 -0
  42. data/lib/expressir/express/concatenator.rb +79 -0
  43. data/lib/expressir/express/core.rb +66 -0
  44. data/lib/expressir/express/formatter.rb +25 -3
  45. data/lib/expressir/express/formatters/data_types_formatter.rb +2 -2
  46. data/lib/expressir/express/formatters/declarations_formatter.rb +1 -0
  47. data/lib/expressir/express/formatters/remark_formatter.rb +30 -6
  48. data/lib/expressir/express/formatters/statements_formatter.rb +7 -1
  49. data/lib/expressir/express/interface_dot.rb +105 -0
  50. data/lib/expressir/express/listing.rb +174 -0
  51. data/lib/expressir/express/model_traversal.rb +42 -0
  52. data/lib/expressir/express/node_position_index.rb +18 -4
  53. data/lib/expressir/express/parser.rb +242 -4
  54. data/lib/expressir/express/pretty_gate.rb +61 -0
  55. data/lib/expressir/express/refs_overlay.rb +47 -0
  56. data/lib/expressir/express/remark_attacher.rb +122 -10
  57. data/lib/expressir/express/remark_overlay.rb +86 -0
  58. data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
  59. data/lib/expressir/express/schema_source_formatter.rb +5 -5
  60. data/lib/expressir/express/self_schema_reference.rb +126 -0
  61. data/lib/expressir/express/shtolo.rb +522 -0
  62. data/lib/expressir/express.rb +13 -0
  63. data/lib/expressir/model/concerns.rb +15 -1
  64. data/lib/expressir/model/declarations/entity.rb +6 -1
  65. data/lib/expressir/model/declarations/interface.rb +13 -0
  66. data/lib/expressir/model/declarations/schema.rb +74 -23
  67. data/lib/expressir/model/indexes/item_graph.rb +135 -0
  68. data/lib/expressir/model/indexes.rb +1 -0
  69. data/lib/expressir/model/model_element.rb +30 -1
  70. data/lib/expressir/model/remark_info.rb +20 -1
  71. data/lib/expressir/model/remark_placement.rb +15 -2
  72. data/lib/expressir/model/repository.rb +8 -2
  73. data/lib/expressir/model.rb +2 -1
  74. data/lib/expressir/version.rb +1 -1
  75. data/lib/tasks/verify_remarks.rake +9 -0
  76. data/rakelib/verify_remarks.rake +16 -0
  77. metadata +42 -2
@@ -22,7 +22,21 @@ module Expressir
22
22
  # Marker for types supporting where rules
23
23
  module HasWhereRules; end
24
24
 
25
+ # Marker for types that can own a remark written after them on the same
26
+ # line, as in `x := 1; -- why`. Includers are ModelElements, so they all
27
+ # carry the `untagged_remarks` collection the remark is stored in.
28
+ #
29
+ # Attachment and formatting must agree on this exactly: a type the
30
+ # attacher marks INLINE but the formatter declines to emit loses the
31
+ # remark with nothing to show for it. Both ask this one question.
32
+ #
33
+ # Not the same question as NodePositionIndex#remark_scope?, which decides
34
+ # who owns a remark on a line of its OWN.
35
+ module TakesInlineRemark; end
36
+
25
37
  # Marker for executable statements (ISO 10303-11 section 13)
26
- module Statement; end
38
+ module Statement
39
+ include TakesInlineRemark
40
+ end
27
41
  end
28
42
  end
@@ -17,7 +17,12 @@ module Expressir
17
17
  attribute :abstract, :boolean
18
18
  attribute :supertype_expression, ModelElement
19
19
  attribute :subtype_of, ModelElement, collection: true
20
- attribute :attributes, Attribute, collection: true
20
+ # Heterogeneous by design: Attribute, DerivedAttribute, and
21
+ # InverseAttribute all live here (the builders concatenate the
22
+ # three clauses in declaration order). The ModelElement base
23
+ # keeps from_hash polymorphic via the _class marker, exactly
24
+ # like the Ruby-built model.
25
+ attribute :attributes, ModelElement, collection: true
21
26
  attribute :derived_attributes, DerivedAttribute, collection: true
22
27
  attribute :inverse_attributes, InverseAttribute, collection: true
23
28
  attribute :unique_rules, UniqueRule, collection: true
@@ -4,6 +4,9 @@ module Expressir
4
4
  # Specified in ISO 10303-11:2004
5
5
  # - section 11 Interface specification
6
6
  class Interface < ModelElement
7
+ # A remark may trail the clause: `REFERENCE FROM x; -- why`.
8
+ include TakesInlineRemark
9
+
7
10
  USE = "USE".freeze
8
11
  REFERENCE = "REFERENCE".freeze
9
12
 
@@ -12,11 +15,21 @@ module Expressir
12
15
  attribute :items, InterfaceItem, collection: true
13
16
  attribute :_class, :string, default: -> { self.class.name }
14
17
 
18
+ # Registers :items with the tree-walker registry: the position index
19
+ # needs the item list to size the clause's span, so an opener-line
20
+ # remark gets the OPENER_REGION and renders back on the clause's
21
+ # first line instead of after the closing semicolon.
22
+ collection_attributes :items
23
+
15
24
  key_value do
16
25
  map "_class", to: :_class, render_default: true
17
26
  map "kind", to: :kind
18
27
  map "schema", to: :schema
19
28
  map "items", to: :items
29
+ # Without this the clause's remark survives a format but not a
30
+ # cache round trip: it would be dropped on serialization and never
31
+ # come back.
32
+ map "untagged_remarks", to: :untagged_remarks
20
33
  end
21
34
  end
22
35
  end
@@ -9,7 +9,7 @@ module Expressir
9
9
 
10
10
  # Tree-walker collection attributes (TODO.bugs/12 — single source
11
11
  # of truth; NodePositionIndex derives COLLECTION_REGISTRY from this).
12
- collection_attributes :constants, :types, :entities,
12
+ collection_attributes :interfaces, :constants, :types, :entities,
13
13
  :subtype_constraints, :functions, :rules,
14
14
  :procedures, :remark_items
15
15
 
@@ -60,6 +60,7 @@ module Expressir
60
60
  map :header, to: :header
61
61
  map :formatted, to: :formatted
62
62
  map :source, to: :source
63
+ map :source_hyperlinked, to: :source_hyperlinked
63
64
  map :full_source, to: :full_source
64
65
  map :children, to: :children
65
66
  map :safe_children, to: :safe_children
@@ -96,8 +97,18 @@ module Expressir
96
97
  @formatted ||= format(no_remarks: false)
97
98
  end
98
99
 
100
+ # Schema head text without hyperlinks (#255) — the `source` face
101
+ # templates call for the verbatim schema declaration block.
102
+ # Deliberately NOT @source: that ivar holds the raw source span
103
+ # lutaml-model's `source=` stores when include_source is on.
99
104
  def source
100
- @source ||= Expressir::Express::SchemaSourceFormatter.format(self)
105
+ @plain_source ||= Expressir::Express::SchemaPlainSourceFormatter.format(self) # rubocop:disable Naming/MemoizedInstanceVariableName
106
+ end
107
+
108
+ # Schema head text with cross-references rendered as hyperlinks
109
+ # (#255) — the hyperlinked counterpart of #source.
110
+ def source_hyperlinked
111
+ Expressir::Express::SchemaSourceFormatter.format(self)
101
112
  end
102
113
 
103
114
  private
@@ -118,30 +129,70 @@ module Expressir
118
129
  def interfaced_items
119
130
  return [] unless parent
120
131
 
132
+ visited_schemas = {}
121
133
  interfaces.flat_map do |interface|
122
- schema = parent.children_by_id[interface.schema.id.safe_downcase]
123
- next [] unless schema
124
-
125
- safe_children = schema.safe_children
126
- children_by_id = safe_children.each_with_object({}) do |child, hash|
127
- hash[child.id.safe_downcase] = child if child.id
128
- end
129
-
130
- if interface.items.empty?
131
- safe_children.map do |base_item|
132
- create_interfaced_item(base_item.id, base_item)
133
- end
134
- else
135
- interface.items.filter_map do |item|
136
- base_item = children_by_id[item.ref.id.safe_downcase]
137
- if base_item
138
- create_interfaced_item(item.id || base_item.id,
139
- base_item)
140
- end
141
- end
142
- end
134
+ items_for_interface(interface, visited_schemas)
143
135
  end.compact
144
136
  end
137
+
138
+ # Direct items of one interface plus the items visible through
139
+ # the foreign schema's own USE interfaces (transitive USE
140
+ # visibility, eeng :use-only → :use-from recursion).
141
+ def items_for_interface(interface, visited_schemas)
142
+ schema = foreign_schema(interface.schema.id.safe_downcase)
143
+ return [] unless schema
144
+ # Visited means this schema's contribution is already in the
145
+ # caller's result: re-entering — mutual USE FROM included —
146
+ # recursed forever when the interface carried an item list
147
+ # (the old guard only fired for the list-less form).
148
+ return [] if visited_schemas.key?(schema.id.safe_downcase)
149
+
150
+ visited_schemas[schema.id.safe_downcase] = true
151
+
152
+ safe_children = schema.safe_children
153
+ children_by_id = safe_children.each_with_object({}) do |child, hash|
154
+ hash[child.id.safe_downcase] = child if child.id
155
+ end
156
+
157
+ own = if interface.items.empty?
158
+ safe_children.map do |base_item|
159
+ create_interfaced_item(base_item.id, base_item)
160
+ end
161
+ else
162
+ interface.items.filter_map do |item|
163
+ base_item = children_by_id[item.ref.id.safe_downcase]
164
+ if base_item
165
+ create_interfaced_item(item.id || base_item.id,
166
+ base_item)
167
+ end
168
+ end
169
+ end
170
+
171
+ # Transitive items are not restricted by this interface's
172
+ # item list: implicit interfacing through USE is unfiltered
173
+ # (eeng :use-only → :use-from recursion finds any id in the
174
+ # foreign schema's use scope).
175
+ own + Array(schema.interfaces).filter_map do |foreign_iface|
176
+ next nil unless foreign_iface.kind == Interface::USE
177
+
178
+ foreign_items = items_for_interface(foreign_iface,
179
+ visited_schemas)
180
+ foreign_items.empty? ? nil : foreign_items
181
+ end.flatten
182
+ end
183
+
184
+ # Find a foreign schema by name: same file first, then the
185
+ # enclosing repository (expressir's compiled/batch repository).
186
+ def foreign_schema(name)
187
+ current = parent
188
+ while current
189
+ return current.children_by_id[name] if current.respond_to?(:children_by_id) &&
190
+ current.children_by_id[name]
191
+
192
+ current = current.is_a?(ModelElement) ? current.parent : nil
193
+ end
194
+ nil
195
+ end
145
196
  end
146
197
  end
147
198
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Model
5
+ module Indexes
6
+ # Item-level graph over a resolved repository: nodes are schema
7
+ # items (canonical "schema.item" paths), edges are inheritance,
8
+ # interface imports, and attribute-type references.
9
+ #
10
+ # {Model::ModelElement#path} is file-qualified and empty for
11
+ # id-less nodes, so node identity is the last two path segments —
12
+ # unique because schema names are unique within a repository.
13
+ class ItemGraph
14
+ attr_reader :nodes, :subtype_of, :dependencies
15
+
16
+ # @param repository [Repository] a resolved repository
17
+ def initialize(repository)
18
+ @nodes = {}
19
+ @subtype_of = Hash.new { |h, k| h[k] = [] }
20
+ @dependencies = Hash.new { |h, k| h[k] = [] }
21
+ build(repository)
22
+ end
23
+
24
+ # Plain tables for artifact persistence — node keys only, no
25
+ # model references. `ItemGraph.from_tables` is the zero-model
26
+ # counterpart used on warm loads.
27
+ def tables
28
+ {
29
+ "nodes" => @nodes.keys,
30
+ "subtype_of" => @subtype_of.reject { |_, parents| parents.empty? },
31
+ "dependencies" => @dependencies.reject { |_, deps| deps.empty? },
32
+ }
33
+ end
34
+
35
+ def self.from_tables(tables)
36
+ graph = allocate
37
+ graph.instance_variable_set(:@nodes, tables["nodes"].to_h { |k| [k, nil] })
38
+ graph.instance_variable_set(
39
+ :@subtype_of,
40
+ Hash.new { |h, k| h[k] = [] }.update(tables["subtype_of"] || {}),
41
+ )
42
+ graph.instance_variable_set(
43
+ :@dependencies,
44
+ Hash.new { |h, k| h[k] = [] }.update(tables["dependencies"] || {}),
45
+ )
46
+ graph
47
+ end
48
+
49
+ def include?(path)
50
+ @nodes.key?(canonical(path))
51
+ end
52
+
53
+ # Transitive supertype closure of an entity, breadth-first.
54
+ def supertypes(path)
55
+ key = canonical(path)
56
+ return [] unless @nodes.key?(key)
57
+
58
+ seen = {}
59
+ result = []
60
+ queue = @subtype_of[key].dup
61
+ until queue.empty?
62
+ current = queue.shift
63
+ next if seen[current]
64
+
65
+ seen[current] = true
66
+ result << current
67
+ queue.concat(@subtype_of[current])
68
+ end
69
+ result
70
+ end
71
+
72
+ # Schemas whose items `schema` imports (USE FROM / REFERENCE
73
+ # FROM), direct only. Accepts a schema object or its id.
74
+ def dependencies_of(schema)
75
+ schema_id = schema.respond_to?(:id) ? schema.id : schema.to_s
76
+ @dependencies[schema_id.safe_downcase].dup
77
+ end
78
+
79
+ def subtype_edges
80
+ @subtype_of.flat_map { |child, parents| parents.map { |p| [child, p] } }
81
+ end
82
+
83
+ def cross_schema_subtype_edges
84
+ subtype_edges.count { |child, parent| child.split(".")[0] != parent.split(".")[0] }
85
+ end
86
+
87
+ private
88
+
89
+ def canonical(path)
90
+ return nil unless path
91
+
92
+ parts = path.to_s.split(".")
93
+ return nil if parts.size < 2
94
+
95
+ parts[-2, 2].join(".").safe_downcase
96
+ end
97
+
98
+ def build(repository)
99
+ # Nodes register before their subtype edges: subtype references
100
+ # may be unresolved (the declaring schema is not in the
101
+ # repository), and an edge to a node that does not exist would
102
+ # surface as a phantom path in supertype closures.
103
+ edges = []
104
+ repository.schemas.each do |schema|
105
+ schema_id = schema.id.safe_downcase
106
+ schema.entities.to_a.each do |entity|
107
+ key = node_key(schema_id, entity)
108
+ @nodes[key] = entity
109
+ edges << [key, schema_id, entity]
110
+ end
111
+ schema.types.to_a.each do |type|
112
+ @nodes[node_key(schema_id, type)] = type
113
+ end
114
+ schema.interfaces.to_a.each do |interface|
115
+ target = interface.schema.is_a?(String) ? interface.schema : interface.schema&.id
116
+ next unless target
117
+
118
+ @dependencies[schema_id] << target.safe_downcase
119
+ end
120
+ end
121
+ edges.each do |key, schema_id, entity|
122
+ entity.subtype_of.to_a.each do |ref|
123
+ parent = ref.base_path ? canonical(ref.base_path) : "#{schema_id}.#{ref.id.safe_downcase}"
124
+ @subtype_of[key] << parent if parent && @nodes.key?(parent)
125
+ end
126
+ end
127
+ end
128
+
129
+ def node_key(schema_id, element)
130
+ canonical(element.path) || "#{schema_id}.#{element.id.safe_downcase}"
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -6,6 +6,7 @@ module Expressir
6
6
  autoload :EntityIndex, "#{__dir__}/indexes/entity_index"
7
7
  autoload :TypeIndex, "#{__dir__}/indexes/type_index"
8
8
  autoload :ReferenceIndex, "#{__dir__}/indexes/reference_index"
9
+ autoload :ItemGraph, "#{__dir__}/indexes/item_graph"
9
10
  end
10
11
  end
11
12
  end
@@ -61,7 +61,11 @@ module Expressir
61
61
 
62
62
  # ---- End child-attributes macro ----
63
63
 
64
- SKIP_ATTRIBUTES = %i[parent _class].freeze
64
+ # :source is skipped deliberately: its reader formats the node,
65
+ # and reading it during construction (attach_parent_to_children on
66
+ # a from_hash-built object whose attributes are not yet applied)
67
+ # would memoize an incomplete rendering forever.
68
+ SKIP_ATTRIBUTES = %i[parent _class source].freeze
65
69
  # :parent is a special attribute that is used to store the parent of the current element
66
70
  # It is not a real attribute
67
71
  # attribute :parent, ModelElement
@@ -101,6 +105,7 @@ module Expressir
101
105
  "Expressir::Model::Declarations::Entity" => "Expressir::Model::Declarations::Entity",
102
106
  "Expressir::Model::Declarations::Function" => "Expressir::Model::Declarations::Function",
103
107
  "Expressir::Model::Declarations::InterfaceItem" => "Expressir::Model::Declarations::InterfaceItem",
108
+ "Expressir::Model::Declarations::InverseAttribute" => "Expressir::Model::Declarations::InverseAttribute",
104
109
  "Expressir::Model::Declarations::Interface" => "Expressir::Model::Declarations::Interface",
105
110
  "Expressir::Model::Declarations::InterfacedItem" => "Expressir::Model::Declarations::InterfacedItem",
106
111
  "Expressir::Model::Declarations::Parameter" => "Expressir::Model::Declarations::Parameter",
@@ -152,6 +157,10 @@ module Expressir
152
157
  key_value do
153
158
  map "_class", to: :_class, render_default: true,
154
159
  polymorphic_map: POLYMORPHIC_CLASS_MAP
160
+ # Hydrate-only: byte positions ride the wire (Rust core path)
161
+ # without ever appearing in to_hash — the serialized shape stays
162
+ # identical to the Ruby-built model.
163
+ map "source_offset", to: :source_offset, serialize: false
155
164
  end
156
165
 
157
166
  def source
@@ -278,6 +287,24 @@ module Expressir
278
287
  end
279
288
  end
280
289
 
290
+ # Wire the parent pointers of this node's entire subtree. Needed
291
+ # for models materialized outside the builders (e.g. hydrated
292
+ # from the Rust core path), whose parents start unset.
293
+ def wire_parents(parent = nil)
294
+ self.parent = parent
295
+ self.class.attributes.each_key do |attr|
296
+ next if SKIP_ATTRIBUTES.include?(attr) || attr == :parent
297
+
298
+ value = public_send(attr)
299
+ case value
300
+ when Array
301
+ value.each { |v| v.wire_parents(self) if v.is_a?(ModelElement) }
302
+ when ModelElement
303
+ value.wire_parents(self)
304
+ end
305
+ end
306
+ end
307
+
281
308
  # @return [Array<Declaration>]
282
309
  def children
283
310
  []
@@ -327,6 +354,8 @@ module Expressir
327
354
  # @return [nil]
328
355
  def attach_parent_to_children
329
356
  self.class.attributes.each_pair do |symbol, _lutaml_attr|
357
+ next if SKIP_ATTRIBUTES.include?(symbol)
358
+
330
359
  value = public_send(symbol)
331
360
 
332
361
  case value
@@ -9,7 +9,10 @@ module Expressir
9
9
  attribute :tag, :string # optional remark tag like "entity.attr"
10
10
  # See RemarkPlacement. nil = legacy placement.
11
11
  attribute :placement, :string
12
- # For TRAILING remarks, which body of the owner they close.
12
+ # Qualifies the placement. For TRAILING, which body of the owner the
13
+ # remark closes, named by that body's attribute. For INLINE,
14
+ # RemarkPlacement::OPENER_REGION when it trailed a compound statement's
15
+ # opening line rather than a whole statement.
13
16
  attribute :region, :string
14
17
  attribute :source_offset, :integer # byte offset in source text
15
18
 
@@ -43,6 +46,22 @@ module Expressir
43
46
  placement == RemarkPlacement::INLINE
44
47
  end
45
48
 
49
+ # Check if this remark trailed the opening line of a statement spanning
50
+ # several lines, as in `IF x THEN -- why`
51
+ # @return [Boolean]
52
+ def opener?
53
+ inline? && region == RemarkPlacement::OPENER_REGION
54
+ end
55
+
56
+ # Check if this INLINE remark belongs after a mid-construct keyword of
57
+ # its owning construct — `ELSE -- why`, `OTHERWISE : -- why` — rather
58
+ # than after the statement's closing keyword.
59
+ # @param name [Symbol, String] a RemarkPlacement region constant
60
+ # @return [Boolean]
61
+ def inline_region?(name)
62
+ inline? && region == name.to_s
63
+ end
64
+
46
65
  # Check if this remark closes the given body of its owning node
47
66
  # @param name [Symbol, String] region attribute name
48
67
  # @return [Boolean]
@@ -14,8 +14,11 @@ module Expressir
14
14
  # keyword. The owner is the body's owner, and RemarkInfo#region names
15
15
  # which body: an IF owns two, and they close at different keywords.
16
16
  # - INLINE — on the same line as the owning statement, after its text.
17
- # Only single-line statements take these: appending to a multi-line
18
- # statement would move the remark down to its closing keyword.
17
+ # Emitted after the whole statement, which for a single-line one is the
18
+ # line it was written on. RemarkInfo#region separates the other case:
19
+ # OPENER_REGION means the remark trailed the opening line of a statement
20
+ # spanning several lines, as in `IF x THEN -- why`, and belongs back on
21
+ # that opener rather than after the closing keyword.
19
22
  #
20
23
  # `nil` means the remark was attached by a legacy path, or predates
21
24
  # placement tracking. Those keep their historical emission and must not
@@ -24,6 +27,16 @@ module Expressir
24
27
  LEADING = "leading"
25
28
  TRAILING = "trailing"
26
29
  INLINE = "inline"
30
+
31
+ # Region of an INLINE remark that trailed a compound statement's opener.
32
+ OPENER_REGION = "opener"
33
+
34
+ # Regions of INLINE remarks that trailed a mid-construct keyword line —
35
+ # `ELSE -- why` or `OTHERWISE : -- why`. The owning construct is the
36
+ # enclosing IF / CASE, and the remark belongs back after that keyword,
37
+ # not after the construct's closing END_.
38
+ ELSE_REGION = "else"
39
+ OTHERWISE_REGION = "otherwise"
27
40
  end
28
41
  end
29
42
  end
@@ -24,6 +24,10 @@ module Expressir
24
24
  # Index instances (lazy-loaded)
25
25
  attr_reader :entity_index, :type_index, :reference_index
26
26
 
27
+ # Item-level graph; loaded from the compiled-set artifact on
28
+ # warm starts, built on demand otherwise.
29
+ attr_accessor :item_graph
30
+
27
31
  # Restore deserialized indexes (used by Package::Reader)
28
32
  def restore_indexes(entity_index: nil, type_index: nil,
29
33
  reference_index: nil)
@@ -56,10 +60,12 @@ reference_index: nil)
56
60
  schemas&.each { |schema| @_schemas << schema }
57
61
  end
58
62
 
59
- # Get all schemas (from both files and direct storage)
63
+ # Get all schemas (from both files and direct storage).
64
+ # Files that failed to parse are nil in `files` — a parse failure must
65
+ # not crash schema enumeration with a NoMethodError from in here.
60
66
  # @return [Array<Declarations::Schema>]
61
67
  def schemas
62
- file_schemas = files&.flat_map(&:schemas)&.compact || []
68
+ file_schemas = files.compact.flat_map(&:schemas).compact
63
69
  file_schemas + @_schemas
64
70
  end
65
71
 
@@ -3,13 +3,14 @@
3
3
  module Expressir
4
4
  module Model
5
5
  # Marker modules — autoload the same concerns.rb file from each name.
6
- # The first reference triggers the load; the file defines all seven markers.
6
+ # The first reference triggers the load; the file defines all eight markers.
7
7
  autoload :HasId, "#{__dir__}/model/concerns"
8
8
  autoload :HasRemarkItems, "#{__dir__}/model/concerns"
9
9
  autoload :HasRemarks, "#{__dir__}/model/concerns"
10
10
  autoload :ScopeContainer, "#{__dir__}/model/concerns"
11
11
  autoload :HasInformalPropositions, "#{__dir__}/model/concerns"
12
12
  autoload :HasWhereRules, "#{__dir__}/model/concerns"
13
+ autoload :TakesInlineRemark, "#{__dir__}/model/concerns"
13
14
  autoload :Statement, "#{__dir__}/model/concerns"
14
15
 
15
16
  autoload :ModelElement, "#{__dir__}/model/model_element"
@@ -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.3".freeze
6
+ VERSION = "2.4.4".freeze
7
7
  end
8
8
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `rake verify:remarks` — the remark conservation gate. Runs only the
4
+ # production-scale checks (`:production_scale` tag; see spec_helper's
5
+ # filter), which the default suite excludes: they parse a full-size
6
+ # schema and cost about a minute.
7
+ RSpec::Core::RakeTask.new(:"verify:remarks") do |task|
8
+ task.rspec_opts = "--tag production_scale"
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :verify do
4
+ desc "Check remarks against the recorded round-trip gaps, production included"
5
+ task :remarks do
6
+ specs = %w[
7
+ spec/expressir/remark_conservation_spec.rb
8
+ spec/expressir/remark_ownership_spec.rb
9
+ spec/expressir/express/formatter_remark_conservation_spec.rb
10
+ spec/expressir/express/formatter_remark_ownership_spec.rb
11
+ ]
12
+
13
+ sh({ "EXPRESSIR_PRODUCTION_SCALE" => "1" },
14
+ "bundle", "exec", "rspec", *specs)
15
+ end
16
+ end