expressir 2.2.0 → 2.3.0

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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +254 -77
  4. data/Gemfile +4 -1
  5. data/README.adoc +63 -26
  6. data/benchmark/srl_benchmark.rb +386 -0
  7. data/benchmark/srl_native_benchmark.rb +142 -0
  8. data/benchmark/srl_ruby_benchmark.rb +130 -0
  9. data/expressir.gemspec +4 -2
  10. data/lib/expressir/benchmark.rb +1 -1
  11. data/lib/expressir/changes/item_change.rb +0 -1
  12. data/lib/expressir/changes/mapping_change.rb +0 -1
  13. data/lib/expressir/changes/schema_change.rb +0 -2
  14. data/lib/expressir/changes/version_change.rb +0 -3
  15. data/lib/expressir/changes.rb +5 -6
  16. data/lib/expressir/cli.rb +10 -24
  17. data/lib/expressir/commands/base.rb +2 -9
  18. data/lib/expressir/commands/changes.rb +0 -2
  19. data/lib/expressir/commands/changes_import_eengine.rb +0 -3
  20. data/lib/expressir/commands/changes_validate.rb +0 -2
  21. data/lib/expressir/commands/format.rb +5 -3
  22. data/lib/expressir/commands/manifest.rb +0 -7
  23. data/lib/expressir/commands/package.rb +93 -101
  24. data/lib/expressir/commands/validate.rb +0 -2
  25. data/lib/expressir/commands/validate_ascii.rb +2 -4
  26. data/lib/expressir/commands/validate_load.rb +8 -5
  27. data/lib/expressir/commands.rb +20 -0
  28. data/lib/expressir/config.rb +0 -2
  29. data/lib/expressir/coverage.rb +11 -4
  30. data/lib/expressir/eengine/arm_compare_report.rb +1 -4
  31. data/lib/expressir/eengine/changes_section.rb +1 -3
  32. data/lib/expressir/eengine/compare_report.rb +1 -13
  33. data/lib/expressir/eengine/mim_compare_report.rb +1 -4
  34. data/lib/expressir/eengine/modified_object.rb +1 -2
  35. data/lib/expressir/eengine.rb +9 -0
  36. data/lib/expressir/errors.rb +113 -0
  37. data/lib/expressir/express/builder.rb +22 -7
  38. data/lib/expressir/express/builder_registry.rb +411 -0
  39. data/lib/expressir/express/builders/attribute_decl_builder.rb +0 -6
  40. data/lib/expressir/express/builders/built_in_builder.rb +1 -16
  41. data/lib/expressir/express/builders/constant_builder.rb +4 -19
  42. data/lib/expressir/express/builders/declaration_builder.rb +0 -4
  43. data/lib/expressir/express/builders/derive_clause_builder.rb +0 -2
  44. data/lib/expressir/express/builders/derived_attr_builder.rb +0 -2
  45. data/lib/expressir/express/builders/domain_rule_builder.rb +0 -2
  46. data/lib/expressir/express/builders/entity_decl_builder.rb +7 -13
  47. data/lib/expressir/express/builders/explicit_attr_builder.rb +5 -8
  48. data/lib/expressir/express/builders/expression_builder.rb +31 -67
  49. data/lib/expressir/express/builders/function_decl_builder.rb +20 -18
  50. data/lib/expressir/express/builders/interface_builder.rb +0 -20
  51. data/lib/expressir/express/builders/inverse_attr_builder.rb +0 -2
  52. data/lib/expressir/express/builders/inverse_attr_type_builder.rb +0 -6
  53. data/lib/expressir/express/builders/inverse_clause_builder.rb +0 -2
  54. data/lib/expressir/express/builders/literal_builder.rb +1 -15
  55. data/lib/expressir/express/builders/procedure_decl_builder.rb +20 -19
  56. data/lib/expressir/express/builders/qualifier_builder.rb +0 -27
  57. data/lib/expressir/express/builders/reference_builder.rb +1 -10
  58. data/lib/expressir/express/builders/rule_decl_builder.rb +21 -19
  59. data/lib/expressir/express/builders/schema_body_decl_builder.rb +0 -4
  60. data/lib/expressir/express/builders/schema_decl_builder.rb +7 -13
  61. data/lib/expressir/express/builders/schema_version_builder.rb +0 -6
  62. data/lib/expressir/express/builders/simple_id_builder.rb +1 -10
  63. data/lib/expressir/express/builders/statement_builder.rb +4 -32
  64. data/lib/expressir/express/builders/subtype_constraint_builder.rb +6 -30
  65. data/lib/expressir/express/builders/syntax_builder.rb +60 -7
  66. data/lib/expressir/express/builders/type_builder.rb +3 -45
  67. data/lib/expressir/express/builders/type_decl_builder.rb +1 -7
  68. data/lib/expressir/express/builders/unique_clause_builder.rb +1 -3
  69. data/lib/expressir/express/builders/unique_rule_builder.rb +0 -2
  70. data/lib/expressir/express/builders/where_clause_builder.rb +1 -3
  71. data/lib/expressir/express/builders.rb +47 -35
  72. data/lib/expressir/express/error.rb +0 -3
  73. data/lib/expressir/express/formatter.rb +17 -19
  74. data/lib/expressir/express/formatters/data_types_formatter.rb +295 -293
  75. data/lib/expressir/express/formatters/declarations_formatter.rb +617 -615
  76. data/lib/expressir/express/formatters/expressions_formatter.rb +146 -144
  77. data/lib/expressir/express/formatters/literals_formatter.rb +35 -33
  78. data/lib/expressir/express/formatters/references_formatter.rb +34 -32
  79. data/lib/expressir/express/formatters/remark_formatter.rb +176 -209
  80. data/lib/expressir/express/formatters/remark_item_formatter.rb +18 -16
  81. data/lib/expressir/express/formatters/statements_formatter.rb +190 -188
  82. data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +41 -39
  83. data/lib/expressir/express/formatters.rb +22 -0
  84. data/lib/expressir/express/parser.rb +40 -41
  85. data/lib/expressir/express/pretty_formatter.rb +68 -47
  86. data/lib/expressir/express/remark_attacher.rb +210 -147
  87. data/lib/expressir/express/streaming_builder.rb +0 -3
  88. data/lib/expressir/express/transformer/remark_handling.rb +1 -2
  89. data/lib/expressir/express.rb +29 -0
  90. data/lib/expressir/manifest/resolver.rb +0 -3
  91. data/lib/expressir/manifest/validator.rb +0 -3
  92. data/lib/expressir/manifest.rb +6 -0
  93. data/lib/expressir/model/cache.rb +1 -1
  94. data/lib/expressir/model/concerns.rb +19 -0
  95. data/lib/expressir/model/data_types/aggregate.rb +1 -1
  96. data/lib/expressir/model/data_types/array.rb +1 -1
  97. data/lib/expressir/model/data_types/bag.rb +1 -1
  98. data/lib/expressir/model/data_types/binary.rb +1 -1
  99. data/lib/expressir/model/data_types/boolean.rb +1 -1
  100. data/lib/expressir/model/data_types/enumeration.rb +1 -1
  101. data/lib/expressir/model/data_types/enumeration_item.rb +1 -1
  102. data/lib/expressir/model/data_types/generic.rb +1 -1
  103. data/lib/expressir/model/data_types/generic_entity.rb +1 -1
  104. data/lib/expressir/model/data_types/integer.rb +1 -1
  105. data/lib/expressir/model/data_types/list.rb +1 -1
  106. data/lib/expressir/model/data_types/logical.rb +1 -1
  107. data/lib/expressir/model/data_types/number.rb +1 -1
  108. data/lib/expressir/model/data_types/real.rb +1 -1
  109. data/lib/expressir/model/data_types/select.rb +1 -1
  110. data/lib/expressir/model/data_types/set.rb +1 -1
  111. data/lib/expressir/model/data_types/string.rb +1 -1
  112. data/lib/expressir/model/data_types.rb +25 -0
  113. data/lib/expressir/model/declarations/attribute.rb +1 -1
  114. data/lib/expressir/model/declarations/constant.rb +1 -1
  115. data/lib/expressir/model/declarations/derived_attribute.rb +1 -1
  116. data/lib/expressir/model/declarations/entity.rb +4 -1
  117. data/lib/expressir/model/declarations/function.rb +3 -1
  118. data/lib/expressir/model/declarations/informal_proposition_rule.rb +2 -1
  119. data/lib/expressir/model/declarations/interface.rb +1 -1
  120. data/lib/expressir/model/declarations/interface_item.rb +1 -1
  121. data/lib/expressir/model/declarations/interfaced_item.rb +1 -1
  122. data/lib/expressir/model/declarations/inverse_attribute.rb +1 -1
  123. data/lib/expressir/model/declarations/parameter.rb +1 -1
  124. data/lib/expressir/model/declarations/procedure.rb +3 -1
  125. data/lib/expressir/model/declarations/remark_item.rb +1 -1
  126. data/lib/expressir/model/declarations/rule.rb +4 -1
  127. data/lib/expressir/model/declarations/schema.rb +2 -1
  128. data/lib/expressir/model/declarations/schema_version.rb +1 -1
  129. data/lib/expressir/model/declarations/schema_version_item.rb +1 -1
  130. data/lib/expressir/model/declarations/subtype_constraint.rb +1 -1
  131. data/lib/expressir/model/declarations/type.rb +4 -1
  132. data/lib/expressir/model/declarations/unique_rule.rb +1 -1
  133. data/lib/expressir/model/declarations/variable.rb +1 -1
  134. data/lib/expressir/model/declarations/where_rule.rb +1 -1
  135. data/lib/expressir/model/declarations.rb +31 -0
  136. data/lib/expressir/model/dependency_resolver.rb +0 -2
  137. data/lib/expressir/model/exp_file.rb +38 -0
  138. data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -1
  139. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -1
  140. data/lib/expressir/model/expressions/binary_expression.rb +1 -1
  141. data/lib/expressir/model/expressions/entity_constructor.rb +1 -1
  142. data/lib/expressir/model/expressions/function_call.rb +1 -1
  143. data/lib/expressir/model/expressions/interval.rb +1 -1
  144. data/lib/expressir/model/expressions/query_expression.rb +1 -1
  145. data/lib/expressir/model/expressions/unary_expression.rb +1 -1
  146. data/lib/expressir/model/expressions.rb +18 -0
  147. data/lib/expressir/model/identifier.rb +5 -1
  148. data/lib/expressir/model/indexes.rb +11 -0
  149. data/lib/expressir/model/literals/binary.rb +1 -1
  150. data/lib/expressir/model/literals/integer.rb +1 -1
  151. data/lib/expressir/model/literals/logical.rb +1 -1
  152. data/lib/expressir/model/literals/real.rb +1 -1
  153. data/lib/expressir/model/literals/string.rb +1 -1
  154. data/lib/expressir/model/literals.rb +13 -0
  155. data/lib/expressir/model/model_element.rb +7 -15
  156. data/lib/expressir/model/references/attribute_reference.rb +1 -1
  157. data/lib/expressir/model/references/group_reference.rb +1 -1
  158. data/lib/expressir/model/references/index_reference.rb +1 -1
  159. data/lib/expressir/model/references/simple_reference.rb +1 -1
  160. data/lib/expressir/model/references.rb +12 -0
  161. data/lib/expressir/model/remark_info.rb +1 -7
  162. data/lib/expressir/model/repository.rb +72 -36
  163. data/lib/expressir/model/repository_validator.rb +0 -2
  164. data/lib/expressir/model/search_engine.rb +6 -30
  165. data/lib/expressir/model/statements/alias.rb +1 -1
  166. data/lib/expressir/model/statements/assignment.rb +1 -1
  167. data/lib/expressir/model/statements/case.rb +1 -1
  168. data/lib/expressir/model/statements/case_action.rb +1 -1
  169. data/lib/expressir/model/statements/compound.rb +1 -1
  170. data/lib/expressir/model/statements/escape.rb +1 -1
  171. data/lib/expressir/model/statements/if.rb +1 -1
  172. data/lib/expressir/model/statements/null.rb +1 -1
  173. data/lib/expressir/model/statements/procedure_call.rb +1 -1
  174. data/lib/expressir/model/statements/repeat.rb +1 -1
  175. data/lib/expressir/model/statements/return.rb +1 -1
  176. data/lib/expressir/model/statements/skip.rb +1 -1
  177. data/lib/expressir/model/statements.rb +20 -0
  178. data/lib/expressir/model/supertype_expressions/binary_supertype_expression.rb +1 -1
  179. data/lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb +1 -1
  180. data/lib/expressir/model/supertype_expressions.rb +12 -0
  181. data/lib/expressir/model.rb +28 -4
  182. data/lib/expressir/package/builder.rb +33 -4
  183. data/lib/expressir/package/metadata.rb +0 -1
  184. data/lib/expressir/package/reader.rb +0 -1
  185. data/lib/expressir/package.rb +8 -0
  186. data/lib/expressir/schema_manifest.rb +5 -6
  187. data/lib/expressir/schema_manifest_entry.rb +3 -4
  188. data/lib/expressir/transformer.rb +7 -0
  189. data/lib/expressir/version.rb +1 -1
  190. data/lib/expressir.rb +23 -173
  191. metadata +64 -9
  192. data/lib/expressir/express/builders/token_builder.rb +0 -15
@@ -13,89 +13,13 @@ module Expressir
13
13
  # 2. Proximity-based matching for simple tags
14
14
  # 3. NOT creating spurious schema-level items for ambiguous tags
15
15
  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
16
  def initialize(source)
95
17
  @source = source
96
18
  @attached_spans = Set.new
97
19
  @line_cache = {}
98
20
  @model = nil
21
+ @source_lines = nil # cached @source.lines
22
+ @scope_map = nil # cached scope at each line number
99
23
  end
100
24
 
101
25
  def attach(model)
@@ -190,51 +114,63 @@ module Expressir
190
114
  end
191
115
  end
192
116
 
117
+ def source_lines
118
+ @source_lines ||= @source.lines
119
+ end
120
+
193
121
  def get_line_number(position)
194
122
  return 1 if position.nil? || position.zero?
195
123
 
196
124
  @line_cache[position] ||= @source.byteslice(0...position).count("\n") + 1
197
125
  end
198
126
 
127
+ alias get_line_number_from_offset get_line_number
128
+
199
129
  def attach_tagged_remarks(model, remarks)
200
- schema_ids = repository?(model) ? model.schemas.filter_map(&:id) : []
130
+ tagged = remarks.select { |r| r[:tag] }
131
+ return if tagged.empty?
201
132
 
202
- # Collect nodes with positions for finding containing scopes
133
+ @model = model
134
+
135
+ # Build scope map ONCE: O(file_lines) scan instead of O(n*file_lines) for n remarks
136
+ # This is the key optimization that makes scope lookup O(1) per remark
137
+ @scope_map ||= build_scope_map
138
+
139
+ # Collect nodes with positions for position-based fallback
203
140
  nodes_with_positions = []
204
141
  collect_nodes_with_positions(model, nodes_with_positions)
205
142
  # Use stable sort to ensure deterministic ordering across Ruby versions
206
- # When positions are equal, preserve original order using index as tie-breaker
207
143
  nodes_with_positions.sort_by!.with_index { |n, i| [n[:position] || Float::INFINITY, i] }
208
144
 
209
- remarks.select do |r|
210
- r[:tag]
211
- end.sort_by { |r| r[:position] }.each do |remark|
145
+ tagged.sort_by { |r| r[:position] }.each do |remark|
212
146
  next if @attached_spans.include?(remark[:position])
213
147
 
214
148
  tag = remark[:tag]
215
149
  target = nil
216
150
 
151
+ # Find containing scope using pre-computed scope map (O(1))
152
+ # Falls back to position-based lookup if scope map doesn't have the line
153
+ containing_scope = find_containing_scope_by_name(remark[:line])
154
+ containing_scope ||= find_containing_scope_position(remark[:line],
155
+ nodes_with_positions)
156
+
217
157
  # Check if this is an informal proposition tag (IP\d+)
218
158
  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)
159
+ scope = containing_scope
160
+ if scope.nil?
161
+ scope = find_scope_by_source_text(remark[:line])
162
+ end
163
+ if scope && supports_informal_propositions?(scope)
164
+ target = create_or_find_informal_proposition(scope, tag)
225
165
  end
226
166
  end
227
167
 
228
168
  # Standard path-based lookup
229
169
  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
170
  # Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
235
171
  if tag.include?(":") && !tag.include?(".")
236
172
  target = handle_prefixed_tag(tag, containing_scope, model,
237
- schema_ids)
173
+ get_schema_ids(model))
238
174
  end
239
175
 
240
176
  # Strategy 1: Try exact path lookup
@@ -255,6 +191,7 @@ module Expressir
255
191
 
256
192
  # Then try schema prefix
257
193
  if target.nil?
194
+ schema_ids = get_schema_ids(model)
258
195
  schema_ids.each do |schema_id|
259
196
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
260
197
  break if target
@@ -275,8 +212,8 @@ module Expressir
275
212
  end
276
213
 
277
214
  # 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
215
  if target.nil? && !function_rule_procedure?(containing_scope)
216
+ schema_ids = get_schema_ids(model)
280
217
  schema_ids.each do |schema_id|
281
218
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
282
219
  break if target
@@ -284,6 +221,7 @@ module Expressir
284
221
  end
285
222
  else
286
223
  # No containing scope, try with schema prefix
224
+ schema_ids = get_schema_ids(model)
287
225
  schema_ids.each do |schema_id|
288
226
  target = find_by_exact_path(model, "#{schema_id}.#{tag}")
289
227
  break if target
@@ -299,19 +237,22 @@ module Expressir
299
237
  if scope_path
300
238
  full_path = "#{scope_path}.#{tag}"
301
239
  target = create_implicit_remark_item(model, full_path,
302
- schema_ids)
240
+ get_schema_ids(model))
303
241
  end
304
242
  end
305
243
  # Fall back to schema prefix
306
244
  if target.nil?
307
- target = create_implicit_remark_item(model, tag, schema_ids)
245
+ target = create_implicit_remark_item(model, tag, get_schema_ids(model))
308
246
  end
309
247
  end
310
248
 
311
249
  # 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)
250
+ if target.nil? && !tag.include?(".")
251
+ schema_ids = get_schema_ids(model)
252
+ if schema_ids.any?
253
+ target = create_implicit_remark_item_at_schema(model, tag,
254
+ schema_ids.first)
255
+ end
315
256
  end
316
257
  end
317
258
 
@@ -323,6 +264,112 @@ module Expressir
323
264
  end
324
265
  end
325
266
 
267
+ # Position-based fallback for finding containing scope.
268
+ # Used when scope map lookup returns nil (e.g., for remarks at lines
269
+ # outside any declared scope's end_line, or for non-scope-containers).
270
+ def find_containing_scope_position(remark_line, nodes_with_positions)
271
+ containing_nodes = nodes_with_positions.select do |n|
272
+ n[:line] && n[:end_line] && remark_line >= n[:line] && remark_line <= n[:end_line] &&
273
+ !repository?(n[:node]) && !cache?(n[:node])
274
+ end
275
+
276
+ containing_nodes.reverse_each do |n|
277
+ node = n[:node]
278
+ return node if node.is_a?(Model::ScopeContainer)
279
+ end
280
+
281
+ nil
282
+ end
283
+ # Done once per RemarkAttacher instance (O(file_lines)).
284
+ # Each find_containing_scope call then becomes O(1).
285
+ def build_scope_map
286
+ lines = source_lines
287
+ scope_map = {}
288
+ return scope_map if lines.empty?
289
+
290
+ # Track nested scopes by scanning all lines once
291
+ scope_stack = [] # array of {type:, name:, line:}
292
+
293
+ lines.each_with_index do |line, idx|
294
+ line_num = idx + 1
295
+
296
+ # Check for START keywords first
297
+ if line =~ /^\s*SCHEMA\s+(\w+)/i
298
+ scope_stack << { type: :schema, name: $1, line: line_num }
299
+ end
300
+
301
+ if line =~ /^\s*FUNCTION\s+(\w+)/i
302
+ scope_stack << { type: :function, name: $1, line: line_num }
303
+ end
304
+
305
+ if line =~ /^\s*PROCEDURE\s+(\w+)/i
306
+ scope_stack << { type: :procedure, name: $1, line: line_num }
307
+ end
308
+
309
+ if line =~ /^\s*RULE\s+(\w+)/i
310
+ scope_stack << { type: :rule, name: $1, line: line_num }
311
+ end
312
+
313
+ if line =~ /^\s*ENTITY\s+(\w+)/i
314
+ scope_stack << { type: :entity, name: $1, line: line_num }
315
+ end
316
+
317
+ if line =~ /^\s*TYPE\s+(\w+)/i
318
+ scope_stack << { type: :type, name: $1, line: line_num }
319
+ end
320
+
321
+ # Check for END keywords (inline closures on same line handled here)
322
+ if (line =~ /END_TYPE/i) && (scope_stack.last&.dig(:type) == :type)
323
+ scope_stack.pop
324
+ end
325
+ if (line =~ /END_FUNCTION/i) && (scope_stack.last&.dig(:type) == :function)
326
+ scope_stack.pop
327
+ end
328
+ if (line =~ /END_PROCEDURE/i) && (scope_stack.last&.dig(:type) == :procedure)
329
+ scope_stack.pop
330
+ end
331
+ if (line =~ /END_RULE/i) && (scope_stack.last&.dig(:type) == :rule)
332
+ scope_stack.pop
333
+ end
334
+ if (line =~ /END_ENTITY/i) && (scope_stack.last&.dig(:type) == :entity)
335
+ scope_stack.pop
336
+ end
337
+ if (line =~ /END_SCHEMA/i) && (scope_stack.last&.dig(:type) == :schema)
338
+ scope_stack.pop
339
+ end
340
+
341
+ # Record the innermost scope for this line
342
+ scope_map[line_num] = scope_stack.last&.dig(:name)
343
+ end
344
+
345
+ scope_map
346
+ end
347
+
348
+ # O(1) scope lookup using pre-computed scope map
349
+ def find_containing_scope_by_name(remark_line)
350
+ return nil unless @scope_map
351
+
352
+ scope_name = @scope_map[remark_line]
353
+ return nil unless scope_name
354
+
355
+ # Find the model node for this scope
356
+ return nil unless @model
357
+
358
+ @model.schemas.each do |schema|
359
+ return schema if schema.id == scope_name
360
+
361
+ %i[functions procedures rules entities types].each do |decl_type|
362
+ collection = schema.send(decl_type)
363
+ next unless collection.is_a?(Array)
364
+
365
+ found = collection.find { |n| n.id == scope_name }
366
+ return found if found
367
+ end
368
+ end
369
+
370
+ nil
371
+ end
372
+
326
373
  def find_node_in_scope(scope, tag)
327
374
  return nil unless scope
328
375
 
@@ -465,7 +512,7 @@ module Expressir
465
512
  return nil unless where_rules&.any?
466
513
 
467
514
  # Search source text for WHERE clause containing this remark
468
- lines = @source.lines
515
+ lines = source_lines
469
516
 
470
517
  where_rules.each do |wr|
471
518
  next unless wr.id
@@ -502,30 +549,16 @@ module Expressir
502
549
  end
503
550
 
504
551
  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
552
+ # First try scope map (O(1) once built)
553
+ scope = find_containing_scope_by_name(remark_line)
554
+ return scope if scope
508
555
 
509
556
  # 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
557
+ find_containing_scope_position(remark_line, nodes_with_positions)
525
558
  end
526
559
 
527
560
  def find_scope_by_text_search(remark_line)
528
- lines = @source.lines
561
+ lines = source_lines
529
562
  return nil if remark_line < 1 || remark_line > lines.length
530
563
 
531
564
  # Track nested scopes by searching backwards from remark_line
@@ -641,23 +674,19 @@ module Expressir
641
674
  end
642
675
 
643
676
  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
677
+ # First try scope map (O(1))
678
+ scope = find_containing_scope_by_name(remark_line)
679
+ if scope && supports_informal_propositions?(scope)
680
+ return scope
649
681
  end
650
682
 
651
683
  # 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
684
  containing_nodes = nodes_with_positions.select do |n|
655
685
  n[:line] && n[:end_line] && remark_line >= n[:line] && remark_line <= n[:end_line] &&
656
686
  !repository?(n[:node]) && !cache?(n[:node])
657
687
  end
658
688
 
659
689
  # Find the innermost node that supports informal propositions
660
- # Priority: Entity, Rule, Type, Schema
661
690
  if containing_nodes.any?
662
691
  containing_nodes.reverse_each do |n|
663
692
  node = n[:node]
@@ -676,7 +705,7 @@ module Expressir
676
705
 
677
706
  def find_scope_by_source_text(remark_line)
678
707
  # Search backwards from remark_line for containing scope
679
- lines = @source.lines
708
+ lines = source_lines
680
709
 
681
710
  # Find the entity/type/rule that contains this line
682
711
  entity_start = nil
@@ -756,7 +785,10 @@ module Expressir
756
785
  end
757
786
 
758
787
  def find_by_exact_path(model, path)
759
- return nil unless path && repository?(model)
788
+ return nil unless path
789
+
790
+ # Only Repository and ExpFile support path-based find
791
+ return nil unless repository?(model) || exp_file?(model)
760
792
 
761
793
  # Try original path
762
794
  result = safe_find(model, path)
@@ -768,7 +800,8 @@ module Expressir
768
800
  end
769
801
 
770
802
  def create_implicit_remark_item_at_schema(model, item_id, schema_id)
771
- return nil unless repository?(model)
803
+ # Only Repository and ExpFile support schema lookup
804
+ return nil unless repository?(model) || exp_file?(model)
772
805
 
773
806
  schema = safe_find(model, schema_id)
774
807
  return nil unless schema.is_a?(Model::Declarations::Schema)
@@ -789,7 +822,7 @@ module Expressir
789
822
  end
790
823
 
791
824
  def create_implicit_remark_item(model, path, schema_ids = [])
792
- return nil unless repository?(model)
825
+ return nil unless repository?(model) || exp_file?(model)
793
826
 
794
827
  # Normalize path (handle "ip:IP1" format)
795
828
  clean_path = normalize_path(path)
@@ -913,7 +946,7 @@ module Expressir
913
946
  end
914
947
 
915
948
  def get_line_content(line_num)
916
- lines = @source.lines
949
+ lines = source_lines
917
950
  return "" if line_num < 1 || line_num > lines.length
918
951
 
919
952
  lines[line_num - 1]
@@ -952,20 +985,29 @@ module Expressir
952
985
 
953
986
  # Only add remarks to nodes that support them
954
987
  if supports_remarks?(node)
955
- node.remarks ||= []
956
- node.remarks << text
988
+ # Always add to remarks attribute (for types that have it)
989
+ if node.respond_to?(:remarks) && node.respond_to?(:remarks=)
990
+ node.remarks ||= []
991
+ node.remarks << text
992
+ end
957
993
 
958
994
  if tag.nil?
959
- remark_info = Model::RemarkInfo.new(text: text, format: format,
960
- tag: tag)
995
+ # Untagged remark: store in untagged_remarks
996
+ remark_info = Model::RemarkInfo.new(text: text, format: format)
961
997
  node.untagged_remarks ||= []
962
998
  node.untagged_remarks << remark_info
963
999
  end
964
1000
  end
965
1001
  end
966
1002
 
1003
+ # All ModelElement subclasses have untagged_remarks from ModelElement
967
1004
  def supports_remarks?(obj)
968
- REMARKS_SUPPORT_TYPES.any? { |t| obj.is_a?(t) }
1005
+ obj.is_a?(Model::ModelElement)
1006
+ end
1007
+
1008
+ # Types that include HasRemarkItems can have remark_items
1009
+ def supports_remark_items?(obj)
1010
+ obj.is_a?(Model::HasRemarkItems)
969
1011
  end
970
1012
 
971
1013
  def collect_nodes_with_positions(node, result, visited = Set.new)
@@ -985,7 +1027,8 @@ module Expressir
985
1027
  # For container nodes, use the maximum end_line from children
986
1028
  # This is needed because source.length only covers the declaration, not the body
987
1029
  children_end_line = calculate_children_end_line(node)
988
- end_line = [source_end_line, children_end_line].compact.max || source_end_line
1030
+ end_line = [source_end_line,
1031
+ children_end_line].compact.max || source_end_line
989
1032
 
990
1033
  result << {
991
1034
  node: node,
@@ -1075,13 +1118,23 @@ module Expressir
1075
1118
  # Find the node that CONTAINS this remark line
1076
1119
  # This handles preamble remarks and embedded remarks
1077
1120
  # Exclude Repository and Cache as they are not semantic scopes
1121
+ # But include ExpFile for file-level preamble remarks
1078
1122
  containing = nodes.select do |n|
1079
1123
  n[:line] && n[:end_line] && n[:line] <= remark_line && n[:end_line] >= remark_line &&
1080
1124
  !repository?(n[:node]) && !cache?(n[:node])
1081
1125
  end
1082
1126
 
1083
1127
  if containing.any?
1084
- # Return the most specific (smallest) containing node
1128
+ # Prefer ExpFile for preamble remarks (before first schema)
1129
+ # Otherwise return the most specific (smallest) containing node
1130
+ exp_file_node = containing.find { |n| exp_file?(n[:node]) }
1131
+ # If this is a preamble remark (before first schema line), use ExpFile
1132
+ if exp_file_node
1133
+ first_schema_line = exp_file_node[:node].schemas&.first&.source_offset
1134
+ if first_schema_line && remark_line < get_line_number(first_schema_line)
1135
+ return exp_file_node[:node]
1136
+ end
1137
+ end
1085
1138
  # Sort by span size and return the smallest
1086
1139
  containing.min_by { |n| n[:end_line] - n[:line] }[:node]
1087
1140
  else
@@ -1096,24 +1149,34 @@ module Expressir
1096
1149
 
1097
1150
  # Type checking helper methods
1098
1151
 
1152
+ def get_schema_ids(model)
1153
+ if repository?(model)
1154
+ model.schemas.filter_map(&:id)
1155
+ elsif exp_file?(model)
1156
+ model.schemas.filter_map(&:id)
1157
+ else
1158
+ []
1159
+ end
1160
+ end
1161
+
1099
1162
  def repository?(obj)
1100
1163
  obj.is_a?(Model::Repository)
1101
1164
  end
1102
1165
 
1166
+ def exp_file?(obj)
1167
+ obj.is_a?(Model::ExpFile)
1168
+ end
1169
+
1103
1170
  def cache?(obj)
1104
1171
  obj.is_a?(Model::Cache)
1105
1172
  end
1106
1173
 
1107
1174
  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) }
1175
+ obj.is_a?(Model::HasInformalPropositions)
1113
1176
  end
1114
1177
 
1115
1178
  def supports_where_rules?(obj)
1116
- WHERE_RULE_TYPES.any? { |t| obj.is_a?(t) }
1179
+ obj.is_a?(Model::HasWhereRules)
1117
1180
  end
1118
1181
 
1119
1182
  # 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.
@@ -31,8 +31,7 @@ module Expressir
31
31
  # @param remarks [Array<Hash>] The extracted remarks
32
32
  def attach_remarks(model, remarks)
33
33
  # Group remarks by their tag
34
- tagged_remarks = remarks.select { |r| r[:tag] }
35
- untagged_remarks = remarks.reject { |r| r[:tag] }
34
+ tagged_remarks, untagged_remarks = remarks.partition { |r| r[:tag] }
36
35
 
37
36
  # Process tagged remarks
38
37
  tagged_remarks.each do |remark|
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ autoload :Builder, "#{__dir__}/express/builder"
6
+ autoload :Builders, "#{__dir__}/express/builders"
7
+ # autoload :BuilderRegistry, "#{__dir__}/express/builder_registry"
8
+
9
+ # Core classes (autoloaded for lazy loading)
10
+ autoload :Cache, "#{__dir__}/express/cache"
11
+ autoload :Error, "#{__dir__}/express/error"
12
+ autoload :Formatter, "#{__dir__}/express/formatter"
13
+ autoload :Formatters, "#{__dir__}/express/formatters"
14
+ autoload :HyperlinkFormatter, "#{__dir__}/express/hyperlink_formatter"
15
+ autoload :ModelVisitor, "#{__dir__}/express/model_visitor"
16
+ autoload :Parser, "#{__dir__}/express/parser"
17
+ autoload :PrettyFormatter, "#{__dir__}/express/pretty_formatter"
18
+ autoload :RemarkAttacher, "#{__dir__}/express/remark_attacher"
19
+ autoload :ResolveReferencesModelVisitor,
20
+ "#{__dir__}/express/resolve_references_model_visitor"
21
+ autoload :SchemaHeadFormatter, "#{__dir__}/express/schema_head_formatter"
22
+ autoload :StreamingBuilder, "#{__dir__}/express/streaming_builder"
23
+
24
+ autoload :Transformer, "#{__dir__}/express/transformer"
25
+ end
26
+ end
27
+
28
+ # Eagerly load builders (they register themselves at load time)
29
+ require_relative "express/builder_registry"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../model/dependency_resolver"
4
-
5
3
  module Expressir
6
4
  module Manifest
7
5
  # Resolves schema paths in manifests using pattern-based path discovery
@@ -131,7 +129,6 @@ module Expressir
131
129
  # @param schema_path [String] Path to the schema file
132
130
  # @return [String] The schema name declared in the file
133
131
  def extract_schema_name(schema_path)
134
- require_relative "../express/parser"
135
132
  repo = Expressir::Express::Parser.from_file(schema_path)
136
133
  schema = repo.schemas.first
137
134
  schema&.id || File.basename(schema_path, ".*")
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../model/dependency_resolver"
4
-
5
3
  module Expressir
6
4
  module Manifest
7
5
  # Validates schema manifests for completeness and integrity
@@ -159,7 +157,6 @@ module Expressir
159
157
  # @param schema_path [String] Path to the schema file
160
158
  # @return [String] The schema name declared in the file
161
159
  def extract_schema_name(schema_path)
162
- require_relative "../express/parser"
163
160
  repo = Expressir::Express::Parser.from_file(schema_path)
164
161
  schema = repo.schemas.first
165
162
  schema&.id || File.basename(schema_path, ".*")
@@ -0,0 +1,6 @@
1
+ module Expressir
2
+ module Manifest
3
+ autoload :Validator, "#{__dir__}/manifest/validator"
4
+ autoload :Resolver, "#{__dir__}/manifest/resolver"
5
+ end
6
+ end
@@ -5,7 +5,7 @@ module Expressir
5
5
  attribute :version, :string
6
6
  attribute :root_path, :string
7
7
  attribute :content, ModelElement
8
- attribute :_class, :string, default: -> { send(:name) }
8
+ attribute :_class, :string, default: -> { self.class.name }
9
9
 
10
10
  key_value do
11
11
  map "_class", to: :_class, render_default: true