expressir 2.2.1 → 2.3.1

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +681 -78
  4. data/Gemfile +4 -1
  5. data/README.adoc +63 -26
  6. data/benchmark/srl_benchmark.rb +399 -0
  7. data/benchmark/srl_native_benchmark.rb +146 -0
  8. data/benchmark/srl_ruby_benchmark.rb +132 -0
  9. data/expressir.gemspec +3 -2
  10. data/lib/expressir/benchmark.rb +1 -1
  11. data/lib/expressir/changes/item_change.rb +0 -2
  12. data/lib/expressir/changes/mapping_change.rb +0 -2
  13. data/lib/expressir/changes/schema_change.rb +0 -3
  14. data/lib/expressir/changes/version_change.rb +0 -4
  15. data/lib/expressir/changes.rb +5 -6
  16. data/lib/expressir/cli.rb +10 -24
  17. data/lib/expressir/commands/changes.rb +0 -2
  18. data/lib/expressir/commands/changes_import_eengine.rb +2 -5
  19. data/lib/expressir/commands/changes_validate.rb +0 -2
  20. data/lib/expressir/commands/format.rb +1 -1
  21. data/lib/expressir/commands/manifest.rb +0 -7
  22. data/lib/expressir/commands/package.rb +16 -29
  23. data/lib/expressir/commands/validate.rb +0 -2
  24. data/lib/expressir/commands/validate_ascii.rb +0 -1
  25. data/lib/expressir/commands/validate_load.rb +1 -1
  26. data/lib/expressir/commands.rb +20 -0
  27. data/lib/expressir/config.rb +0 -2
  28. data/lib/expressir/coverage.rb +11 -4
  29. data/lib/expressir/eengine/arm_compare_report.rb +1 -5
  30. data/lib/expressir/eengine/changes_section.rb +1 -4
  31. data/lib/expressir/eengine/compare_report.rb +1 -13
  32. data/lib/expressir/eengine/mim_compare_report.rb +1 -5
  33. data/lib/expressir/eengine/modified_object.rb +1 -3
  34. data/lib/expressir/eengine.rb +9 -0
  35. data/lib/expressir/errors.rb +3 -5
  36. data/lib/expressir/express/builder.rb +82 -24
  37. data/lib/expressir/express/builder_registry.rb +411 -0
  38. data/lib/expressir/express/builders/attribute_decl_builder.rb +0 -6
  39. data/lib/expressir/express/builders/built_in_builder.rb +5 -18
  40. data/lib/expressir/express/builders/constant_builder.rb +4 -19
  41. data/lib/expressir/express/builders/declaration_builder.rb +0 -4
  42. data/lib/expressir/express/builders/derive_clause_builder.rb +0 -2
  43. data/lib/expressir/express/builders/derived_attr_builder.rb +0 -2
  44. data/lib/expressir/express/builders/domain_rule_builder.rb +0 -2
  45. data/lib/expressir/express/builders/entity_decl_builder.rb +11 -13
  46. data/lib/expressir/express/builders/explicit_attr_builder.rb +5 -8
  47. data/lib/expressir/express/builders/expression_builder.rb +25 -67
  48. data/lib/expressir/express/builders/function_decl_builder.rb +20 -18
  49. data/lib/expressir/express/builders/interface_builder.rb +0 -20
  50. data/lib/expressir/express/builders/inverse_attr_builder.rb +0 -2
  51. data/lib/expressir/express/builders/inverse_attr_type_builder.rb +0 -6
  52. data/lib/expressir/express/builders/inverse_clause_builder.rb +0 -2
  53. data/lib/expressir/express/builders/literal_builder.rb +1 -15
  54. data/lib/expressir/express/builders/procedure_decl_builder.rb +20 -19
  55. data/lib/expressir/express/builders/qualifier_builder.rb +0 -27
  56. data/lib/expressir/express/builders/reference_builder.rb +1 -10
  57. data/lib/expressir/express/builders/rule_decl_builder.rb +21 -19
  58. data/lib/expressir/express/builders/schema_body_decl_builder.rb +0 -4
  59. data/lib/expressir/express/builders/schema_decl_builder.rb +7 -13
  60. data/lib/expressir/express/builders/schema_version_builder.rb +0 -6
  61. data/lib/expressir/express/builders/simple_id_builder.rb +1 -10
  62. data/lib/expressir/express/builders/statement_builder.rb +4 -32
  63. data/lib/expressir/express/builders/subtype_constraint_builder.rb +6 -30
  64. data/lib/expressir/express/builders/syntax_builder.rb +18 -7
  65. data/lib/expressir/express/builders/type_builder.rb +3 -45
  66. data/lib/expressir/express/builders/type_decl_builder.rb +1 -7
  67. data/lib/expressir/express/builders/unique_clause_builder.rb +1 -3
  68. data/lib/expressir/express/builders/unique_rule_builder.rb +0 -2
  69. data/lib/expressir/express/builders/where_clause_builder.rb +1 -3
  70. data/lib/expressir/express/builders.rb +47 -35
  71. data/lib/expressir/express/error.rb +0 -3
  72. data/lib/expressir/express/formatter.rb +17 -19
  73. data/lib/expressir/express/formatters/data_types_formatter.rb +295 -293
  74. data/lib/expressir/express/formatters/declarations_formatter.rb +617 -615
  75. data/lib/expressir/express/formatters/expressions_formatter.rb +146 -144
  76. data/lib/expressir/express/formatters/literals_formatter.rb +35 -33
  77. data/lib/expressir/express/formatters/references_formatter.rb +34 -32
  78. data/lib/expressir/express/formatters/remark_formatter.rb +174 -209
  79. data/lib/expressir/express/formatters/remark_item_formatter.rb +18 -16
  80. data/lib/expressir/express/formatters/statements_formatter.rb +190 -188
  81. data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +41 -39
  82. data/lib/expressir/express/formatters.rb +22 -0
  83. data/lib/expressir/express/parser.rb +266 -47
  84. data/lib/expressir/express/pretty_formatter.rb +68 -47
  85. data/lib/expressir/express/remark_attacher.rb +254 -162
  86. data/lib/expressir/express/streaming_builder.rb +0 -3
  87. data/lib/expressir/express/transformer/remark_handling.rb +1 -3
  88. data/lib/expressir/express.rb +29 -0
  89. data/lib/expressir/manifest/resolver.rb +0 -3
  90. data/lib/expressir/manifest/validator.rb +0 -3
  91. data/lib/expressir/manifest.rb +6 -0
  92. data/lib/expressir/model/cache.rb +1 -1
  93. data/lib/expressir/model/concerns.rb +19 -0
  94. data/lib/expressir/model/data_types/aggregate.rb +1 -1
  95. data/lib/expressir/model/data_types/array.rb +1 -1
  96. data/lib/expressir/model/data_types/bag.rb +1 -1
  97. data/lib/expressir/model/data_types/binary.rb +1 -1
  98. data/lib/expressir/model/data_types/boolean.rb +1 -1
  99. data/lib/expressir/model/data_types/enumeration.rb +1 -1
  100. data/lib/expressir/model/data_types/enumeration_item.rb +1 -1
  101. data/lib/expressir/model/data_types/generic.rb +1 -1
  102. data/lib/expressir/model/data_types/generic_entity.rb +1 -1
  103. data/lib/expressir/model/data_types/integer.rb +1 -1
  104. data/lib/expressir/model/data_types/list.rb +1 -1
  105. data/lib/expressir/model/data_types/logical.rb +1 -1
  106. data/lib/expressir/model/data_types/number.rb +1 -1
  107. data/lib/expressir/model/data_types/real.rb +1 -1
  108. data/lib/expressir/model/data_types/select.rb +1 -1
  109. data/lib/expressir/model/data_types/set.rb +1 -1
  110. data/lib/expressir/model/data_types/string.rb +1 -1
  111. data/lib/expressir/model/data_types.rb +25 -0
  112. data/lib/expressir/model/declarations/attribute.rb +1 -1
  113. data/lib/expressir/model/declarations/constant.rb +1 -1
  114. data/lib/expressir/model/declarations/derived_attribute.rb +1 -1
  115. data/lib/expressir/model/declarations/entity.rb +4 -1
  116. data/lib/expressir/model/declarations/function.rb +3 -1
  117. data/lib/expressir/model/declarations/informal_proposition_rule.rb +2 -1
  118. data/lib/expressir/model/declarations/interface.rb +1 -1
  119. data/lib/expressir/model/declarations/interface_item.rb +1 -1
  120. data/lib/expressir/model/declarations/interfaced_item.rb +1 -1
  121. data/lib/expressir/model/declarations/inverse_attribute.rb +1 -1
  122. data/lib/expressir/model/declarations/parameter.rb +1 -1
  123. data/lib/expressir/model/declarations/procedure.rb +3 -1
  124. data/lib/expressir/model/declarations/remark_item.rb +1 -1
  125. data/lib/expressir/model/declarations/rule.rb +4 -1
  126. data/lib/expressir/model/declarations/schema.rb +2 -1
  127. data/lib/expressir/model/declarations/schema_version.rb +1 -1
  128. data/lib/expressir/model/declarations/schema_version_item.rb +1 -1
  129. data/lib/expressir/model/declarations/subtype_constraint.rb +1 -1
  130. data/lib/expressir/model/declarations/type.rb +4 -1
  131. data/lib/expressir/model/declarations/unique_rule.rb +1 -1
  132. data/lib/expressir/model/declarations/variable.rb +1 -1
  133. data/lib/expressir/model/declarations/where_rule.rb +1 -1
  134. data/lib/expressir/model/declarations.rb +31 -0
  135. data/lib/expressir/model/dependency_resolver.rb +0 -2
  136. data/lib/expressir/model/exp_file.rb +39 -0
  137. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -1
  138. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -1
  139. data/lib/expressir/model/expressions/binary_expression.rb +1 -1
  140. data/lib/expressir/model/expressions/entity_constructor.rb +1 -1
  141. data/lib/expressir/model/expressions/function_call.rb +1 -1
  142. data/lib/expressir/model/expressions/interval.rb +1 -1
  143. data/lib/expressir/model/expressions/query_expression.rb +1 -1
  144. data/lib/expressir/model/expressions/unary_expression.rb +1 -1
  145. data/lib/expressir/model/expressions.rb +18 -0
  146. data/lib/expressir/model/identifier.rb +5 -1
  147. data/lib/expressir/model/indexes.rb +11 -0
  148. data/lib/expressir/model/literals/binary.rb +1 -1
  149. data/lib/expressir/model/literals/integer.rb +1 -1
  150. data/lib/expressir/model/literals/logical.rb +1 -1
  151. data/lib/expressir/model/literals/real.rb +1 -1
  152. data/lib/expressir/model/literals/string.rb +1 -1
  153. data/lib/expressir/model/literals.rb +13 -0
  154. data/lib/expressir/model/model_element.rb +7 -15
  155. data/lib/expressir/model/references/attribute_reference.rb +1 -1
  156. data/lib/expressir/model/references/group_reference.rb +1 -1
  157. data/lib/expressir/model/references/index_reference.rb +1 -1
  158. data/lib/expressir/model/references/simple_reference.rb +1 -1
  159. data/lib/expressir/model/references.rb +12 -0
  160. data/lib/expressir/model/remark_info.rb +1 -7
  161. data/lib/expressir/model/repository.rb +76 -41
  162. data/lib/expressir/model/repository_validator.rb +0 -2
  163. data/lib/expressir/model/search_engine.rb +12 -35
  164. data/lib/expressir/model/statements/alias.rb +1 -1
  165. data/lib/expressir/model/statements/assignment.rb +1 -1
  166. data/lib/expressir/model/statements/case.rb +1 -1
  167. data/lib/expressir/model/statements/case_action.rb +1 -1
  168. data/lib/expressir/model/statements/compound.rb +1 -1
  169. data/lib/expressir/model/statements/escape.rb +1 -1
  170. data/lib/expressir/model/statements/if.rb +1 -1
  171. data/lib/expressir/model/statements/null.rb +1 -1
  172. data/lib/expressir/model/statements/procedure_call.rb +1 -1
  173. data/lib/expressir/model/statements/repeat.rb +1 -1
  174. data/lib/expressir/model/statements/return.rb +1 -1
  175. data/lib/expressir/model/statements/skip.rb +1 -1
  176. data/lib/expressir/model/statements.rb +20 -0
  177. data/lib/expressir/model/supertype_expressions/binary_supertype_expression.rb +1 -1
  178. data/lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb +1 -1
  179. data/lib/expressir/model/supertype_expressions.rb +12 -0
  180. data/lib/expressir/model.rb +28 -4
  181. data/lib/expressir/package/builder.rb +35 -4
  182. data/lib/expressir/package/metadata.rb +0 -2
  183. data/lib/expressir/package/reader.rb +0 -1
  184. data/lib/expressir/package.rb +8 -0
  185. data/lib/expressir/schema_manifest.rb +5 -7
  186. data/lib/expressir/schema_manifest_entry.rb +3 -5
  187. data/lib/expressir/transformer.rb +7 -0
  188. data/lib/expressir/version.rb +1 -1
  189. data/lib/expressir.rb +23 -171
  190. metadata +46 -6
  191. data/lib/expressir/express/builders/token_builder.rb +0 -15
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Expressir
6
4
  module Express
7
5
  # Handles attaching remarks (comments) to model elements after parsing.
@@ -13,89 +11,13 @@ module Expressir
13
11
  # 2. Proximity-based matching for simple tags
14
12
  # 3. NOT creating spurious schema-level items for ambiguous tags
15
13
  class RemarkAttacher
16
- # Types that support informal propositions
17
- INFORMAL_PROPOSITION_TYPES = [
18
- Model::Declarations::Entity,
19
- Model::Declarations::Rule,
20
- Model::Declarations::Type,
21
- Model::Declarations::InformalPropositionRule,
22
- ].freeze
23
-
24
- # Types that support remark items (have remark_items attribute)
25
- # These are types where we can create RemarkItem children
26
- REMARK_ITEM_TYPES = [
27
- Model::Declarations::Schema,
28
- Model::Declarations::Entity,
29
- Model::Declarations::Type,
30
- Model::Declarations::Rule,
31
- Model::Declarations::Function,
32
- Model::Declarations::Procedure,
33
- Model::Declarations::InformalPropositionRule,
34
- Model::Declarations::WhereRule,
35
- Model::Declarations::UniqueRule,
36
- # Attribute types (all include Identifier which provides remark_items)
37
- Model::Declarations::Attribute,
38
- Model::Declarations::DerivedAttribute,
39
- Model::Declarations::InverseAttribute,
40
- ].freeze
41
-
42
- # Types that support where rules
43
- WHERE_RULE_TYPES = [
44
- Model::Declarations::Entity,
45
- Model::Declarations::Type,
46
- Model::Declarations::Rule,
47
- Model::Declarations::Function,
48
- Model::Declarations::Procedure,
49
- ].freeze
50
-
51
- # Scope container types (can contain other declarations)
52
- SCOPE_CONTAINER_TYPES = [
53
- Model::Declarations::Schema,
54
- Model::Declarations::Function,
55
- Model::Declarations::Procedure,
56
- Model::Declarations::Rule,
57
- Model::Declarations::Entity,
58
- Model::Declarations::Type,
59
- ].freeze
60
-
61
- # Types that support remarks (have Identifier module or define remarks directly)
62
- REMARKS_SUPPORT_TYPES = [
63
- # Declaration types with Identifier module
64
- Model::Declarations::Schema,
65
- Model::Declarations::Entity,
66
- Model::Declarations::Type,
67
- Model::Declarations::Function,
68
- Model::Declarations::Procedure,
69
- Model::Declarations::Rule,
70
- Model::Declarations::Constant,
71
- Model::Declarations::Attribute,
72
- Model::Declarations::InverseAttribute,
73
- Model::Declarations::DerivedAttribute,
74
- Model::Declarations::WhereRule,
75
- Model::Declarations::UniqueRule,
76
- Model::Declarations::InformalPropositionRule,
77
- Model::Declarations::SubtypeConstraint,
78
- Model::Declarations::Parameter,
79
- Model::Declarations::Variable,
80
- # Statement types with Identifier module
81
- Model::Statements::Alias,
82
- Model::Statements::Repeat,
83
- # Expression types with Identifier module
84
- Model::Expressions::QueryExpression,
85
- # Data types with Identifier module
86
- Model::DataTypes::Aggregate,
87
- Model::DataTypes::EnumerationItem,
88
- Model::DataTypes::Generic,
89
- Model::DataTypes::GenericEntity,
90
- # Types with remarks attribute defined directly (not via Identifier)
91
- Model::Declarations::RemarkItem,
92
- ].freeze
93
-
94
14
  def initialize(source)
95
15
  @source = source
96
16
  @attached_spans = Set.new
97
17
  @line_cache = {}
98
18
  @model = nil
19
+ @source_lines = nil # cached @source.lines
20
+ @scope_map = nil # cached scope at each line number
99
21
  end
100
22
 
101
23
  def attach(model)
@@ -103,6 +25,15 @@ module Expressir
103
25
  remarks = extract_all_remarks
104
26
  attach_tagged_remarks(model, remarks)
105
27
  attach_untagged_remarks(model, remarks)
28
+
29
+ # Free expensive data structures after attachment is complete.
30
+ # These are only needed during the attach process.
31
+ @source = nil
32
+ @source_lines = nil
33
+ @scope_map = nil
34
+ @line_cache = nil
35
+ @remarks_cache = nil
36
+
106
37
  model
107
38
  end
108
39
 
@@ -190,51 +121,63 @@ module Expressir
190
121
  end
191
122
  end
192
123
 
124
+ def source_lines
125
+ @source_lines ||= @source.lines
126
+ end
127
+
193
128
  def get_line_number(position)
194
129
  return 1 if position.nil? || position.zero?
195
130
 
196
131
  @line_cache[position] ||= @source.byteslice(0...position).count("\n") + 1
197
132
  end
198
133
 
134
+ alias get_line_number_from_offset get_line_number
135
+
199
136
  def attach_tagged_remarks(model, remarks)
200
- schema_ids = repository?(model) ? model.schemas.filter_map(&:id) : []
137
+ tagged = remarks.select { |r| r[:tag] }
138
+ return if tagged.empty?
139
+
140
+ @model = model
141
+
142
+ # Build scope map ONCE: O(file_lines) scan instead of O(n*file_lines) for n remarks
143
+ # This is the key optimization that makes scope lookup O(1) per remark
144
+ @scope_map ||= build_scope_map
201
145
 
202
- # Collect nodes with positions for finding containing scopes
146
+ # Collect nodes with positions for position-based fallback
203
147
  nodes_with_positions = []
204
148
  collect_nodes_with_positions(model, nodes_with_positions)
205
149
  # Use stable sort to ensure deterministic ordering across Ruby versions
206
- # When positions are equal, preserve original order using index as tie-breaker
207
150
  nodes_with_positions.sort_by!.with_index { |n, i| [n[:position] || Float::INFINITY, i] }
208
151
 
209
- remarks.select do |r|
210
- r[:tag]
211
- end.sort_by { |r| r[:position] }.each do |remark|
152
+ tagged.sort_by { |r| r[:position] }.each do |remark|
212
153
  next if @attached_spans.include?(remark[:position])
213
154
 
214
155
  tag = remark[:tag]
215
156
  target = nil
216
157
 
158
+ # Find containing scope using pre-computed scope map (O(1))
159
+ # Falls back to position-based lookup if scope map doesn't have the line
160
+ containing_scope = find_containing_scope_by_name(remark[:line])
161
+ containing_scope ||= find_containing_scope_position(remark[:line],
162
+ nodes_with_positions)
163
+
217
164
  # Check if this is an informal proposition tag (IP\d+)
218
165
  if tag.match?(/^IP\d+$/)
219
- # Find the containing scope (entity, type, rule) that supports informal propositions
220
- target = find_containing_scope_for_ip(remark[:line],
221
- nodes_with_positions)
222
- if target
223
- # Create or find the informal proposition
224
- target = create_or_find_informal_proposition(target, tag)
166
+ scope = containing_scope
167
+ if scope.nil?
168
+ scope = find_scope_by_source_text(remark[:line])
169
+ end
170
+ if scope && supports_informal_propositions?(scope)
171
+ target = create_or_find_informal_proposition(scope, tag)
225
172
  end
226
173
  end
227
174
 
228
175
  # Standard path-based lookup
229
176
  if target.nil?
230
- # Find containing scope for scope-aware path resolution
231
- containing_scope = find_containing_scope(remark[:line],
232
- nodes_with_positions)
233
-
234
177
  # Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
235
178
  if tag.include?(":") && !tag.include?(".")
236
179
  target = handle_prefixed_tag(tag, containing_scope, model,
237
- schema_ids)
180
+ get_schema_ids(model))
238
181
  end
239
182
 
240
183
  # Strategy 1: Try exact path lookup
@@ -255,6 +198,7 @@ module Expressir
255
198
 
256
199
  # Then try schema prefix
257
200
  if target.nil?
201
+ schema_ids = get_schema_ids(model)
258
202
  schema_ids.each do |schema_id|
259
203
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
260
204
  break if target
@@ -275,8 +219,8 @@ module Expressir
275
219
  end
276
220
 
277
221
  # Only fall back to schema prefix if NOT inside a function/rule/procedure
278
- # This prevents remarks inside scopes from attaching to schema-level items
279
222
  if target.nil? && !function_rule_procedure?(containing_scope)
223
+ schema_ids = get_schema_ids(model)
280
224
  schema_ids.each do |schema_id|
281
225
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
282
226
  break if target
@@ -284,6 +228,7 @@ module Expressir
284
228
  end
285
229
  else
286
230
  # No containing scope, try with schema prefix
231
+ schema_ids = get_schema_ids(model)
287
232
  schema_ids.each do |schema_id|
288
233
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
289
234
  break if target
@@ -299,19 +244,23 @@ module Expressir
299
244
  if scope_path
300
245
  full_path = "#{scope_path}.#{tag}"
301
246
  target = create_implicit_remark_item(model, full_path,
302
- schema_ids)
247
+ get_schema_ids(model))
303
248
  end
304
249
  end
305
250
  # Fall back to schema prefix
306
251
  if target.nil?
307
- target = create_implicit_remark_item(model, tag, schema_ids)
252
+ target = create_implicit_remark_item(model, tag,
253
+ get_schema_ids(model))
308
254
  end
309
255
  end
310
256
 
311
257
  # Strategy 4: For simple tags at schema level, create implicit item
312
- if target.nil? && !tag.include?(".") && schema_ids.any?
313
- target = create_implicit_remark_item_at_schema(model, tag,
314
- schema_ids.first)
258
+ if target.nil? && !tag.include?(".")
259
+ schema_ids = get_schema_ids(model)
260
+ if schema_ids.any?
261
+ target = create_implicit_remark_item_at_schema(model, tag,
262
+ schema_ids.first)
263
+ end
315
264
  end
316
265
  end
317
266
 
@@ -323,6 +272,113 @@ module Expressir
323
272
  end
324
273
  end
325
274
 
275
+ # Position-based fallback for finding containing scope.
276
+ # Used when scope map lookup returns nil (e.g., for remarks at lines
277
+ # outside any declared scope's end_line, or for non-scope-containers).
278
+ def find_containing_scope_position(remark_line, nodes_with_positions)
279
+ containing_nodes = nodes_with_positions.select do |n|
280
+ n[:line] && n[:end_line] && remark_line >= n[:line] && remark_line <= n[:end_line] &&
281
+ !repository?(n[:node]) && !cache?(n[:node])
282
+ end
283
+
284
+ containing_nodes.reverse_each do |n|
285
+ node = n[:node]
286
+ return node if node.is_a?(Model::ScopeContainer)
287
+ end
288
+
289
+ nil
290
+ end
291
+
292
+ # Done once per RemarkAttacher instance (O(file_lines)).
293
+ # Each find_containing_scope call then becomes O(1).
294
+ def build_scope_map
295
+ lines = source_lines
296
+ scope_map = {}
297
+ return scope_map if lines.empty?
298
+
299
+ # Track nested scopes by scanning all lines once
300
+ scope_stack = [] # array of {type:, name:, line:}
301
+
302
+ lines.each_with_index do |line, idx|
303
+ line_num = idx + 1
304
+
305
+ # Check for START keywords first
306
+ if line =~ /^\s*SCHEMA\s+(\w+)/i
307
+ scope_stack << { type: :schema, name: $1, line: line_num }
308
+ end
309
+
310
+ if line =~ /^\s*FUNCTION\s+(\w+)/i
311
+ scope_stack << { type: :function, name: $1, line: line_num }
312
+ end
313
+
314
+ if line =~ /^\s*PROCEDURE\s+(\w+)/i
315
+ scope_stack << { type: :procedure, name: $1, line: line_num }
316
+ end
317
+
318
+ if line =~ /^\s*RULE\s+(\w+)/i
319
+ scope_stack << { type: :rule, name: $1, line: line_num }
320
+ end
321
+
322
+ if line =~ /^\s*ENTITY\s+(\w+)/i
323
+ scope_stack << { type: :entity, name: $1, line: line_num }
324
+ end
325
+
326
+ if line =~ /^\s*TYPE\s+(\w+)/i
327
+ scope_stack << { type: :type, name: $1, line: line_num }
328
+ end
329
+
330
+ # Check for END keywords (inline closures on same line handled here)
331
+ if (line =~ /END_TYPE/i) && (scope_stack.last&.dig(:type) == :type)
332
+ scope_stack.pop
333
+ end
334
+ if (line =~ /END_FUNCTION/i) && (scope_stack.last&.dig(:type) == :function)
335
+ scope_stack.pop
336
+ end
337
+ if (line =~ /END_PROCEDURE/i) && (scope_stack.last&.dig(:type) == :procedure)
338
+ scope_stack.pop
339
+ end
340
+ if (line =~ /END_RULE/i) && (scope_stack.last&.dig(:type) == :rule)
341
+ scope_stack.pop
342
+ end
343
+ if (line =~ /END_ENTITY/i) && (scope_stack.last&.dig(:type) == :entity)
344
+ scope_stack.pop
345
+ end
346
+ if (line =~ /END_SCHEMA/i) && (scope_stack.last&.dig(:type) == :schema)
347
+ scope_stack.pop
348
+ end
349
+
350
+ # Record the innermost scope for this line
351
+ scope_map[line_num] = scope_stack.last&.dig(:name)
352
+ end
353
+
354
+ scope_map
355
+ end
356
+
357
+ # O(1) scope lookup using pre-computed scope map
358
+ def find_containing_scope_by_name(remark_line)
359
+ return nil unless @scope_map
360
+
361
+ scope_name = @scope_map[remark_line]
362
+ return nil unless scope_name
363
+
364
+ # Find the model node for this scope
365
+ return nil unless @model
366
+
367
+ @model.schemas.each do |schema|
368
+ return schema if schema.id == scope_name
369
+
370
+ %i[functions procedures rules entities types].each do |decl_type|
371
+ collection = schema.send(decl_type)
372
+ next unless collection.is_a?(Array)
373
+
374
+ found = collection.find { |n| n.id == scope_name }
375
+ return found if found
376
+ end
377
+ end
378
+
379
+ nil
380
+ end
381
+
326
382
  def find_node_in_scope(scope, tag)
327
383
  return nil unless scope
328
384
 
@@ -465,7 +521,7 @@ module Expressir
465
521
  return nil unless where_rules&.any?
466
522
 
467
523
  # Search source text for WHERE clause containing this remark
468
- lines = @source.lines
524
+ lines = source_lines
469
525
 
470
526
  where_rules.each do |wr|
471
527
  next unless wr.id
@@ -502,30 +558,16 @@ module Expressir
502
558
  end
503
559
 
504
560
  def find_containing_scope(remark_line, nodes_with_positions)
505
- # First try text-based detection (more reliable when source tracking is broken)
506
- text_based_scope = find_scope_by_text_search(remark_line)
507
- return text_based_scope if text_based_scope
561
+ # First try scope map (O(1) once built)
562
+ scope = find_containing_scope_by_name(remark_line)
563
+ return scope if scope
508
564
 
509
565
  # Fallback to position-based detection
510
- # Exclude Repository and Cache as they are not semantic scopes
511
- containing_nodes = nodes_with_positions.select do |n|
512
- n[:line] && n[:end_line] && remark_line >= n[:line] && remark_line <= n[:end_line] &&
513
- !repository?(n[:node]) && !cache?(n[:node])
514
- end
515
-
516
- # Return the innermost scope container (function, procedure, rule, entity, type)
517
- containing_nodes.reverse_each do |n|
518
- node = n[:node]
519
- SCOPE_CONTAINER_TYPES.each do |scope_class|
520
- return node if node.is_a?(scope_class)
521
- end
522
- end
523
-
524
- nil
566
+ find_containing_scope_position(remark_line, nodes_with_positions)
525
567
  end
526
568
 
527
569
  def find_scope_by_text_search(remark_line)
528
- lines = @source.lines
570
+ lines = source_lines
529
571
  return nil if remark_line < 1 || remark_line > lines.length
530
572
 
531
573
  # Track nested scopes by searching backwards from remark_line
@@ -641,23 +683,19 @@ module Expressir
641
683
  end
642
684
 
643
685
  def find_containing_scope_for_ip(remark_line, nodes_with_positions)
644
- # First try text-based detection (more reliable when source tracking is broken)
645
- # This handles cases where node end_line doesn't include trailing remarks
646
- text_based_scope = find_scope_by_text_search(remark_line)
647
- if text_based_scope && supports_informal_propositions?(text_based_scope)
648
- return text_based_scope
686
+ # First try scope map (O(1))
687
+ scope = find_containing_scope_by_name(remark_line)
688
+ if scope && supports_informal_propositions?(scope)
689
+ return scope
649
690
  end
650
691
 
651
692
  # Fallback to position-based detection
652
- # Find nodes that contain this remark line
653
- # Exclude Repository and Cache as they are not semantic scopes
654
693
  containing_nodes = nodes_with_positions.select do |n|
655
694
  n[:line] && n[:end_line] && remark_line >= n[:line] && remark_line <= n[:end_line] &&
656
695
  !repository?(n[:node]) && !cache?(n[:node])
657
696
  end
658
697
 
659
698
  # Find the innermost node that supports informal propositions
660
- # Priority: Entity, Rule, Type, Schema
661
699
  if containing_nodes.any?
662
700
  containing_nodes.reverse_each do |n|
663
701
  node = n[:node]
@@ -676,7 +714,7 @@ module Expressir
676
714
 
677
715
  def find_scope_by_source_text(remark_line)
678
716
  # Search backwards from remark_line for containing scope
679
- lines = @source.lines
717
+ lines = source_lines
680
718
 
681
719
  # Find the entity/type/rule that contains this line
682
720
  entity_start = nil
@@ -756,7 +794,10 @@ module Expressir
756
794
  end
757
795
 
758
796
  def find_by_exact_path(model, path)
759
- return nil unless path && repository?(model)
797
+ return nil unless path
798
+
799
+ # Only Repository and ExpFile support path-based find
800
+ return nil unless repository?(model) || exp_file?(model)
760
801
 
761
802
  # Try original path
762
803
  result = safe_find(model, path)
@@ -768,7 +809,8 @@ module Expressir
768
809
  end
769
810
 
770
811
  def create_implicit_remark_item_at_schema(model, item_id, schema_id)
771
- return nil unless repository?(model)
812
+ # Only Repository and ExpFile support schema lookup
813
+ return nil unless repository?(model) || exp_file?(model)
772
814
 
773
815
  schema = safe_find(model, schema_id)
774
816
  return nil unless schema.is_a?(Model::Declarations::Schema)
@@ -789,7 +831,7 @@ module Expressir
789
831
  end
790
832
 
791
833
  def create_implicit_remark_item(model, path, schema_ids = [])
792
- return nil unless repository?(model)
834
+ return nil unless repository?(model) || exp_file?(model)
793
835
 
794
836
  # Normalize path (handle "ip:IP1" format)
795
837
  clean_path = normalize_path(path)
@@ -913,7 +955,7 @@ module Expressir
913
955
  end
914
956
 
915
957
  def get_line_content(line_num)
916
- lines = @source.lines
958
+ lines = source_lines
917
959
  return "" if line_num < 1 || line_num > lines.length
918
960
 
919
961
  lines[line_num - 1]
@@ -952,20 +994,29 @@ module Expressir
952
994
 
953
995
  # Only add remarks to nodes that support them
954
996
  if supports_remarks?(node)
955
- node.remarks ||= []
956
- node.remarks << text
997
+ # Always add to remarks attribute (for types that have it)
998
+ if node.respond_to?(:remarks) && node.respond_to?(:remarks=)
999
+ node.remarks ||= []
1000
+ node.remarks << text
1001
+ end
957
1002
 
958
1003
  if tag.nil?
959
- remark_info = Model::RemarkInfo.new(text: text, format: format,
960
- tag: tag)
1004
+ # Untagged remark: store in untagged_remarks
1005
+ remark_info = Model::RemarkInfo.new(text: text, format: format)
961
1006
  node.untagged_remarks ||= []
962
1007
  node.untagged_remarks << remark_info
963
1008
  end
964
1009
  end
965
1010
  end
966
1011
 
1012
+ # All ModelElement subclasses have untagged_remarks from ModelElement
967
1013
  def supports_remarks?(obj)
968
- REMARKS_SUPPORT_TYPES.any? { |t| obj.is_a?(t) }
1014
+ obj.is_a?(Model::ModelElement)
1015
+ end
1016
+
1017
+ # Types that include HasRemarkItems can have remark_items
1018
+ def supports_remark_items?(obj)
1019
+ obj.is_a?(Model::HasRemarkItems)
969
1020
  end
970
1021
 
971
1022
  def collect_nodes_with_positions(node, result, visited = Set.new)
@@ -979,20 +1030,41 @@ module Expressir
979
1030
  # The parser always provides this via Slice#offset
980
1031
  if node.source_offset
981
1032
  pos = node.source_offset
982
- line = get_line_number(pos)
983
- source_end_line = get_line_number(pos + node.source.length)
984
-
985
- # For container nodes, use the maximum end_line from children
986
- # This is needed because source.length only covers the declaration, not the body
987
- children_end_line = calculate_children_end_line(node)
988
- end_line = [source_end_line, children_end_line].compact.max || source_end_line
989
-
990
- result << {
991
- node: node,
992
- position: pos,
993
- line: line,
994
- end_line: end_line,
995
- }
1033
+ # Validate offset: native parser returns 0 for leaf nodes (WhereRule)
1034
+ # where it can't determine the actual position. These have short
1035
+ # expression-like source ("TRUE;") that doesn't appear at file start.
1036
+ # Container nodes (Schema, Entity, Type) have declaration-like source
1037
+ # that either starts at position 0 legitimately or is clearly valid.
1038
+ valid = pos.positive?
1039
+ if !valid && pos.zero? && node.source
1040
+ src = node.source.to_s
1041
+ # Accept position=0 if source is a declaration keyword line
1042
+ valid = src.start_with?("SCHEMA", "ENTITY", "TYPE", "FUNCTION",
1043
+ "PROCEDURE", "RULE", "CONSTANT", "VARIABLE",
1044
+ "USE", "REFERENCE", "END_SCHEMA", "END_ENTITY",
1045
+ "END_TYPE", "END_FUNCTION", "END_PROCEDURE",
1046
+ "END_RULE", "END_CONSTANT", "END_VARIABLE")
1047
+ end
1048
+ if valid
1049
+ line = get_line_number(pos)
1050
+ source_end_line = get_line_number(pos + node.source.length)
1051
+
1052
+ # For container nodes, use the maximum end_line from children
1053
+ # This is needed because source.length only covers the declaration, not the body
1054
+ children_end_line = calculate_children_end_line(node)
1055
+ end_line = [source_end_line,
1056
+ children_end_line].compact.max || source_end_line
1057
+
1058
+ result << {
1059
+ node: node,
1060
+ position: pos,
1061
+ line: line,
1062
+ end_line: end_line,
1063
+ }
1064
+ else
1065
+ # Invalid offset — treat as unknown position
1066
+ result << { node: node, position: nil, line: nil, end_line: nil }
1067
+ end
996
1068
  else
997
1069
  # No source_offset available - should not happen if parser provides Slice
998
1070
  result << { node: node, position: nil, line: nil, end_line: nil }
@@ -1075,13 +1147,23 @@ module Expressir
1075
1147
  # Find the node that CONTAINS this remark line
1076
1148
  # This handles preamble remarks and embedded remarks
1077
1149
  # Exclude Repository and Cache as they are not semantic scopes
1150
+ # But include ExpFile for file-level preamble remarks
1078
1151
  containing = nodes.select do |n|
1079
1152
  n[:line] && n[:end_line] && n[:line] <= remark_line && n[:end_line] >= remark_line &&
1080
1153
  !repository?(n[:node]) && !cache?(n[:node])
1081
1154
  end
1082
1155
 
1083
1156
  if containing.any?
1084
- # Return the most specific (smallest) containing node
1157
+ # Prefer ExpFile for preamble remarks (before first schema)
1158
+ # Otherwise return the most specific (smallest) containing node
1159
+ exp_file_node = containing.find { |n| exp_file?(n[:node]) }
1160
+ # If this is a preamble remark (before first schema line), use ExpFile
1161
+ if exp_file_node
1162
+ first_schema_line = exp_file_node[:node].schemas&.first&.source_offset
1163
+ if first_schema_line && remark_line < get_line_number(first_schema_line)
1164
+ return exp_file_node[:node]
1165
+ end
1166
+ end
1085
1167
  # Sort by span size and return the smallest
1086
1168
  containing.min_by { |n| n[:end_line] - n[:line] }[:node]
1087
1169
  else
@@ -1096,24 +1178,34 @@ module Expressir
1096
1178
 
1097
1179
  # Type checking helper methods
1098
1180
 
1181
+ def get_schema_ids(model)
1182
+ if repository?(model)
1183
+ model.schemas.filter_map(&:id)
1184
+ elsif exp_file?(model)
1185
+ model.schemas.filter_map(&:id)
1186
+ else
1187
+ []
1188
+ end
1189
+ end
1190
+
1099
1191
  def repository?(obj)
1100
1192
  obj.is_a?(Model::Repository)
1101
1193
  end
1102
1194
 
1195
+ def exp_file?(obj)
1196
+ obj.is_a?(Model::ExpFile)
1197
+ end
1198
+
1103
1199
  def cache?(obj)
1104
1200
  obj.is_a?(Model::Cache)
1105
1201
  end
1106
1202
 
1107
1203
  def supports_informal_propositions?(obj)
1108
- INFORMAL_PROPOSITION_TYPES.any? { |t| obj.is_a?(t) }
1109
- end
1110
-
1111
- def supports_remark_items?(obj)
1112
- REMARK_ITEM_TYPES.any? { |t| obj.is_a?(t) }
1204
+ obj.is_a?(Model::HasInformalPropositions)
1113
1205
  end
1114
1206
 
1115
1207
  def supports_where_rules?(obj)
1116
- WHERE_RULE_TYPES.any? { |t| obj.is_a?(t) }
1208
+ obj.is_a?(Model::HasWhereRules)
1117
1209
  end
1118
1210
 
1119
1211
  # Safe accessor methods that return nil instead of NoMethodError
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "error"
4
- require_relative "builder"
5
-
6
3
  module Expressir
7
4
  module Express
8
5
  # Streaming builder that receives parse events from Parsanol native parser.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "parsanol"
4
- require "set"
5
4
 
6
5
  module Expressir
7
6
  module Express
@@ -31,8 +30,7 @@ module Expressir
31
30
  # @param remarks [Array<Hash>] The extracted remarks
32
31
  def attach_remarks(model, remarks)
33
32
  # Group remarks by their tag
34
- tagged_remarks = remarks.select { |r| r[:tag] }
35
- untagged_remarks = remarks.reject { |r| r[:tag] }
33
+ tagged_remarks, untagged_remarks = remarks.partition { |r| r[:tag] }
36
34
 
37
35
  # Process tagged remarks
38
36
  tagged_remarks.each do |remark|