expressir 2.3.0 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +480 -49
  3. data/benchmark/srl_benchmark.rb +47 -34
  4. data/benchmark/srl_native_benchmark.rb +20 -16
  5. data/benchmark/srl_ruby_benchmark.rb +14 -12
  6. data/expressir.gemspec +2 -2
  7. data/lib/expressir/changes/item_change.rb +0 -1
  8. data/lib/expressir/changes/mapping_change.rb +0 -1
  9. data/lib/expressir/changes/schema_change.rb +0 -1
  10. data/lib/expressir/changes/version_change.rb +0 -1
  11. data/lib/expressir/commands/changes_import_eengine.rb +2 -2
  12. data/lib/expressir/commands/validate_ascii.rb +0 -1
  13. data/lib/expressir/eengine/arm_compare_report.rb +0 -1
  14. data/lib/expressir/eengine/changes_section.rb +0 -1
  15. data/lib/expressir/eengine/mim_compare_report.rb +0 -1
  16. data/lib/expressir/eengine/modified_object.rb +0 -1
  17. data/lib/expressir/express/builder.rb +64 -21
  18. data/lib/expressir/express/builders/built_in_builder.rb +4 -2
  19. data/lib/expressir/express/builders/entity_decl_builder.rb +8 -4
  20. data/lib/expressir/express/builders/expression_builder.rb +0 -6
  21. data/lib/expressir/express/builders/function_decl_builder.rb +8 -8
  22. data/lib/expressir/express/builders/procedure_decl_builder.rb +8 -8
  23. data/lib/expressir/express/builders/rule_decl_builder.rb +8 -8
  24. data/lib/expressir/express/builders/syntax_builder.rb +2 -44
  25. data/lib/expressir/express/formatters/remark_formatter.rb +1 -3
  26. data/lib/expressir/express/parser.rb +234 -14
  27. data/lib/expressir/express/remark_attacher.rb +47 -18
  28. data/lib/expressir/express/transformer/remark_handling.rb +0 -1
  29. data/lib/expressir/model/exp_file.rb +2 -1
  30. data/lib/expressir/model/model_element.rb +1 -1
  31. data/lib/expressir/model/repository.rb +8 -9
  32. data/lib/expressir/model/search_engine.rb +7 -6
  33. data/lib/expressir/package/builder.rb +3 -1
  34. data/lib/expressir/package/metadata.rb +0 -1
  35. data/lib/expressir/schema_manifest.rb +0 -1
  36. data/lib/expressir/schema_manifest_entry.rb +0 -1
  37. data/lib/expressir/version.rb +1 -1
  38. metadata +15 -15
@@ -5,21 +5,21 @@
5
5
  # Compares Parsanol Ruby vs Native performance on full STEPmod Resource Library
6
6
  # Features: Live progress, emojis, colors, per-schema stats
7
7
 
8
- require 'bundler/setup'
9
- require 'benchmark'
10
- require 'fileutils'
8
+ require "bundler/setup"
9
+ require "benchmark"
10
+ require "fileutils"
11
11
 
12
12
  # Force loading of native extension
13
- require 'parsanol'
14
- require 'parsanol/native'
13
+ require "parsanol"
14
+ require "parsanol/native"
15
15
 
16
16
  # Now require expressir
17
- require 'expressir'
17
+ require "expressir"
18
18
 
19
19
  # Configuration
20
- SRL_PATH = '/Users/mulgogi/src/mn/iso-10303/schemas/resources'
21
- ITERATIONS = (ENV['ITERATIONS'] || 1).to_i
22
- TIMEOUT_SECONDS = (ENV['TIMEOUT'] || 30).to_i # Timeout per file
20
+ SRL_PATH = "/Users/mulgogi/src/mn/iso-10303/schemas/resources"
21
+ ITERATIONS = (ENV["ITERATIONS"] || 1).to_i
22
+ TIMEOUT_SECONDS = (ENV["TIMEOUT"] || 30).to_i # Timeout per file
23
23
 
24
24
  # Check if we're running in an interactive terminal
25
25
  INTERACTIVE = $stdout.tty?
@@ -64,7 +64,9 @@ include Colors
64
64
  module Terminal
65
65
  class << self
66
66
  def width
67
- IO.console.winsize[1] rescue 80
67
+ IO.console.winsize[1]
68
+ rescue StandardError
69
+ 80
68
70
  end
69
71
 
70
72
  def clear_line
@@ -85,7 +87,7 @@ class ProgressBar
85
87
  @current = 0
86
88
  end
87
89
 
88
- def render(current, label = '')
90
+ def render(current, label = "")
89
91
  @current = current
90
92
  percentage = (@current.to_f / @total * 100).round(1)
91
93
  filled = (@width * @current / @total.to_f).round
@@ -97,7 +99,9 @@ class ProgressBar
97
99
  end
98
100
 
99
101
  def find_exp_files
100
- Dir.glob("#{SRL_PATH}/*/*.exp").sort.reject { |f| f.include?('quantities_and_units') }
102
+ Dir.glob("#{SRL_PATH}/*/*.exp").reject do |f|
103
+ f.include?("quantities_and_units")
104
+ end
101
105
  end
102
106
 
103
107
  def count_lines(files)
@@ -158,50 +162,56 @@ class ParserBenchmark
158
162
  @results = []
159
163
  end
160
164
 
161
- def run(files, total_lines)
165
+ def run(files, _total_lines)
162
166
  puts "#{@color}#{@emoji} #{@name}#{@RESET}"
163
167
  puts "#{DIM}┌──────────────────────────────────────────────────────────┐#{RESET}"
164
168
 
165
169
  progress_bar = ProgressBar.new(files.size, 25)
166
- iteration_results = { success: 0, failed: 0, errors: [], time: 0, schema_times: [] }
170
+ iteration_results = { success: 0, failed: 0, errors: [], time: 0,
171
+ schema_times: [] }
167
172
  start_time = Time.now
168
173
 
169
174
  files.each_with_index do |file, idx|
170
- schema_name = File.basename(file, '.exp')
175
+ schema_name = File.basename(file, ".exp")
171
176
  file_start = Time.now
172
177
  schema_lines = File.read(file).lines.count
173
178
 
174
179
  begin
175
- require 'timeout'
180
+ require "timeout"
176
181
  Timeout.timeout(TIMEOUT_SECONDS) do
177
182
  if @use_native
178
183
  content = File.read(file)
179
- Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
184
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
185
+ use_native: true)
180
186
  else
181
187
  Expressir::Express::Parser.from_file(file, skip_references: true)
182
188
  end
183
189
  end
184
190
  iteration_results[:success] += 1
185
191
  status = "#{BRIGHT_GREEN}✓#{RESET}"
186
- rescue Timeout::Error => e
192
+ rescue Timeout::Error
187
193
  iteration_results[:failed] += 1
188
- iteration_results[:errors] << { file: File.basename(file), error: "Timeout after #{TIMEOUT_SECONDS}s" }
194
+ iteration_results[:errors] << { file: File.basename(file),
195
+ error: "Timeout after #{TIMEOUT_SECONDS}s" }
189
196
  status = "#{BRIGHT_YELLOW}⏱#{RESET}"
190
- rescue => e
197
+ rescue StandardError => e
191
198
  iteration_results[:failed] += 1
192
- iteration_results[:errors] << { file: File.basename(file), error: e.message[0..60] }
199
+ iteration_results[:errors] << { file: File.basename(file),
200
+ error: e.message[0..60] }
193
201
  status = "#{BRIGHT_RED}✗#{RESET}"
194
202
  end
195
203
 
196
204
  elapsed = Time.now - file_start
197
- iteration_results[:schema_times] << { name: schema_name, time: elapsed, lines: schema_lines }
205
+ iteration_results[:schema_times] << { name: schema_name, time: elapsed,
206
+ lines: schema_lines }
198
207
 
199
208
  # Live progress update
200
209
  if INTERACTIVE
201
210
  progress_label = "#{status} #{schema_name[0..20].ljust(21)}"
202
- print "#{Terminal.clear_line} #{progress_bar.render(idx + 1, progress_label)}"
211
+ print "#{Terminal.clear_line} #{progress_bar.render(idx + 1,
212
+ progress_label)}"
203
213
  $stdout.flush
204
- elsif (idx + 1) % 10 == 0 || idx == 0
214
+ elsif ((idx + 1) % 10).zero? || idx.zero?
205
215
  # Print progress every 10 files when not interactive
206
216
  pct = ((idx + 1).to_f / files.size * 100).round(1)
207
217
  puts " #{progress_bar.render(idx + 1, "#{pct}% complete")}"
@@ -230,7 +240,7 @@ class ParserBenchmark
230
240
  puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}⏱️ Time:#{RESET} #{BRIGHT_WHITE}#{format_time(avg_time).rjust(5)}#{RESET}"
231
241
  puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speed:#{RESET} #{BRIGHT_WHITE}#{format_number(lines_per_sec).rjust(5)}#{RESET} lines/sec"
232
242
 
233
- if result[:failed] > 0 && result[:failed] <= 5
243
+ if result[:failed].positive? && result[:failed] <= 5
234
244
  puts "#{DIM}├──────────────────────────────────────────────────────────┤#{RESET}"
235
245
  puts "#{DIM}│#{RESET} #{BRIGHT_RED}⚠️ Errors:#{RESET}"
236
246
  result[:errors].first(3).each do |err|
@@ -241,7 +251,8 @@ class ParserBenchmark
241
251
  puts "#{DIM}└──────────────────────────────────────────────────────────┘#{RESET}"
242
252
  puts
243
253
 
244
- { avg_time: avg_time, lines_per_sec: lines_per_sec, files_per_sec: files_per_sec }
254
+ { avg_time: avg_time, lines_per_sec: lines_per_sec,
255
+ files_per_sec: files_per_sec }
245
256
  end
246
257
 
247
258
  def print_slowest_schemas(result, count = 5)
@@ -290,7 +301,7 @@ def print_comparison(ruby_stats, native_stats, files, total_lines)
290
301
  puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⏱️ Time Saved:#{RESET} #{BRIGHT_GREEN}#{format_time(time_saved)} per run#{RESET} #{DIM}│#{RESET}"
291
302
  else
292
303
  puts "#{DIM}│#{RESET} #{BOLD}#{BRIGHT_BLUE}🏆 WINNER: Ruby Parser#{RESET} #{DIM}│#{RESET}"
293
- puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speedup:#{RESET} #{BOLD}#{BRIGHT_BLUE}#{(1/speedup).round(1)}x FASTER#{RESET} #{DIM}│#{RESET}"
304
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speedup:#{RESET} #{BOLD}#{BRIGHT_BLUE}#{(1 / speedup).round(1)}x FASTER#{RESET} #{DIM}│#{RESET}"
294
305
  end
295
306
 
296
307
  puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
@@ -336,15 +347,16 @@ warmup_file = files.first
336
347
 
337
348
  begin
338
349
  Expressir::Express::Parser.from_file(warmup_file, skip_references: true)
339
- rescue => e
350
+ rescue StandardError => e
340
351
  puts "#{BRIGHT_YELLOW}⚠️ Ruby warmup warning: #{e.message[0..40]}#{RESET}"
341
352
  end
342
353
 
343
354
  if Parsanol::Native.available?
344
355
  begin
345
356
  content = File.read(warmup_file)
346
- Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
347
- rescue => e
357
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
358
+ use_native: true)
359
+ rescue StandardError => e
348
360
  puts "#{BRIGHT_YELLOW}⚠️ Native warmup warning: #{e.message[0..40]}#{RESET}"
349
361
  end
350
362
  end
@@ -356,7 +368,7 @@ ruby_benchmark = ParserBenchmark.new(
356
368
  name: "Ruby Parser",
357
369
  emoji: "💎",
358
370
  color: BRIGHT_BLUE,
359
- use_native: false
371
+ use_native: false,
360
372
  )
361
373
 
362
374
  ruby_result = ruby_benchmark.run(files, total_lines)
@@ -369,17 +381,18 @@ if Parsanol::Native.available?
369
381
  name: "Native Parser (Rust)",
370
382
  emoji: "🦀",
371
383
  color: BRIGHT_CYAN,
372
- use_native: true
384
+ use_native: true,
373
385
  )
374
386
 
375
387
  native_result = native_benchmark.run(files, total_lines)
376
- native_stats = native_benchmark.print_summary(files, total_lines, native_result)
388
+ native_stats = native_benchmark.print_summary(files, total_lines,
389
+ native_result)
377
390
  native_benchmark.print_slowest_schemas(native_result)
378
391
 
379
392
  # Print comparison
380
393
  print_comparison(ruby_stats, native_stats, files, total_lines)
381
394
  else
382
- puts "#{BRIGHT_YELLOW}⚠️ Native parser not available. Run \`rake compile\` in parsanol-ruby to build.#{RESET}"
395
+ puts "#{BRIGHT_YELLOW}⚠️ Native parser not available. Run `rake compile` in parsanol-ruby to build.#{RESET}"
383
396
  puts
384
397
  end
385
398
 
@@ -4,22 +4,22 @@
4
4
  # SRL Benchmark Script for Expressir - Native Parser Only
5
5
  # Tests Rust native parser performance on full STEPmod Resource Library
6
6
 
7
- require 'bundler/setup'
8
- require 'benchmark'
9
- require 'fileutils'
7
+ require "bundler/setup"
8
+ require "benchmark"
9
+ require "fileutils"
10
10
 
11
11
  # Force loading of native extension first
12
- require 'parsanol'
13
- require 'parsanol/native'
12
+ require "parsanol"
13
+ require "parsanol/native"
14
14
 
15
15
  # Now require expressir
16
- require 'expressir'
16
+ require "expressir"
17
17
 
18
18
  # Configuration
19
- SRL_PATH = '/Users/mulgogi/src/mn/iso-10303/schemas/resources'
19
+ SRL_PATH = "/Users/mulgogi/src/mn/iso-10303/schemas/resources"
20
20
 
21
21
  def find_exp_files
22
- Dir.glob("#{SRL_PATH}/*/*.exp").sort
22
+ Dir.glob("#{SRL_PATH}/*/*.exp")
23
23
  end
24
24
 
25
25
  def count_lines(files)
@@ -59,9 +59,10 @@ puts "Warming up..."
59
59
  warmup_file = files.first
60
60
  begin
61
61
  content = File.read(warmup_file)
62
- Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
62
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
63
+ use_native: true)
63
64
  puts "Warmup complete."
64
- rescue => e
65
+ rescue StandardError => e
65
66
  puts "Warning: Warmup failed: #{e.message}"
66
67
  puts e.backtrace.first(5)
67
68
  end
@@ -80,19 +81,22 @@ time = Benchmark.realtime do
80
81
  file_start = Time.now
81
82
  begin
82
83
  content = File.read(file)
83
- Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
84
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
85
+ use_native: true)
84
86
  results[:success] += 1
85
87
  file_time = Time.now - file_start
86
- file_times << { file: File.basename(file), time: file_time, lines: content.lines.count }
88
+ file_times << { file: File.basename(file), time: file_time,
89
+ lines: content.lines.count }
87
90
 
88
91
  # Progress every 10 files
89
- if (idx + 1) % 10 == 0
92
+ if ((idx + 1) % 10).zero?
90
93
  print "."
91
94
  $stdout.flush
92
95
  end
93
- rescue => e
96
+ rescue StandardError => e
94
97
  results[:failed] += 1
95
- results[:errors] << { file: File.basename(file), error: e.message[0..100] }
98
+ results[:errors] << { file: File.basename(file),
99
+ error: e.message[0..100] }
96
100
  end
97
101
  end
98
102
  end
@@ -112,7 +116,7 @@ puts " Time: #{results[:total_time].round(2)}s"
112
116
  puts " Speed: #{lines_per_sec} lines/sec"
113
117
  puts " Speed: #{files_per_sec} files/sec"
114
118
 
115
- if results[:failed] > 0
119
+ if results[:failed].positive?
116
120
  puts
117
121
  puts "Errors (first 10):"
118
122
  results[:errors].first(10).each do |err|
@@ -4,18 +4,18 @@
4
4
  # SRL Benchmark Script for Expressir - Ruby Parser Only
5
5
  # Tests pure Ruby parser performance on full STEPmod Resource Library
6
6
 
7
- require 'bundler/setup'
8
- require 'benchmark'
9
- require 'fileutils'
7
+ require "bundler/setup"
8
+ require "benchmark"
9
+ require "fileutils"
10
10
 
11
11
  # Load expressir WITHOUT native extension
12
- require 'expressir'
12
+ require "expressir"
13
13
 
14
14
  # Configuration
15
- SRL_PATH = '/Users/mulgogi/src/mn/iso-10303/schemas/resources'
15
+ SRL_PATH = "/Users/mulgogi/src/mn/iso-10303/schemas/resources"
16
16
 
17
17
  def find_exp_files
18
- Dir.glob("#{SRL_PATH}/*/*.exp").sort
18
+ Dir.glob("#{SRL_PATH}/*/*.exp")
19
19
  end
20
20
 
21
21
  def count_lines(files)
@@ -46,7 +46,7 @@ warmup_file = files.first
46
46
  begin
47
47
  Expressir::Express::Parser.from_file(warmup_file, skip_references: true)
48
48
  puts "Warmup complete."
49
- rescue => e
49
+ rescue StandardError => e
50
50
  puts "Warning: Warmup failed: #{e.message}"
51
51
  end
52
52
  puts
@@ -67,16 +67,18 @@ time = Benchmark.realtime do
67
67
  results[:success] += 1
68
68
  file_time = Time.now - file_start
69
69
  content = File.read(file)
70
- file_times << { file: File.basename(file), time: file_time, lines: content.lines.count }
70
+ file_times << { file: File.basename(file), time: file_time,
71
+ lines: content.lines.count }
71
72
 
72
73
  # Progress every 10 files
73
- if (idx + 1) % 10 == 0
74
+ if ((idx + 1) % 10).zero?
74
75
  print "."
75
76
  $stdout.flush
76
77
  end
77
- rescue => e
78
+ rescue StandardError => e
78
79
  results[:failed] += 1
79
- results[:errors] << { file: File.basename(file), error: e.message[0..100] }
80
+ results[:errors] << { file: File.basename(file),
81
+ error: e.message[0..100] }
80
82
  end
81
83
  end
82
84
  end
@@ -96,7 +98,7 @@ puts " Time: #{results[:total_time].round(2)}s"
96
98
  puts " Speed: #{lines_per_sec} lines/sec"
97
99
  puts " Speed: #{files_per_sec} files/sec"
98
100
 
99
- if results[:failed] > 0
101
+ if results[:failed].positive?
100
102
  puts
101
103
  puts "Errors (first 10):"
102
104
  results[:errors].first(10).each do |err|
data/expressir.gemspec CHANGED
@@ -38,9 +38,9 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "liquid"
39
39
  spec.add_dependency "lutaml-model", "~> 0.8.0"
40
40
  spec.add_dependency "moxml"
41
- spec.add_dependency "paint"
42
- spec.add_dependency "parsanol", "~> 1.3.7", ">= 1.3.7"
43
41
  spec.add_dependency "nokogiri"
42
+ spec.add_dependency "paint"
43
+ spec.add_dependency "parsanol", "~> 1.3.9", ">= 1.3.9"
44
44
  spec.add_dependency "ruby-progressbar", "~> 1.11"
45
45
  spec.add_dependency "rubyzip", "~> 2.3"
46
46
  spec.add_dependency "table_tennis"
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Changes
6
5
  # Represents a single change to an EXPRESS construct
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Changes
6
5
  # Represents a mapping change entry
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Changes
6
5
  # Represents changes to an EXPRESS schema across multiple versions
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Changes
6
5
  # Represents a version version of schema changes
@@ -11,7 +11,7 @@ module Expressir
11
11
  # @param version [String] Version identifier
12
12
  # @param options [Hash] Additional options
13
13
  # @return [Expressir::Changes::SchemaChange]
14
- def self.from_xml(xml_content, schema_name, version, **options)
14
+ def self.from_xml(xml_content, schema_name, version, **)
15
15
  require "expressir/changes"
16
16
 
17
17
  # Parse into CompareReport using Lutaml::Model
@@ -19,7 +19,7 @@ module Expressir
19
19
 
20
20
  # Convert to SchemaChange
21
21
  convert_to_schema_change(compare_report, schema_name, version,
22
- xml_content: xml_content, **options)
22
+ xml_content: xml_content, **)
23
23
  end
24
24
 
25
25
  # File-based workflow (backward compatible)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "paint"
5
4
  require "table_tennis"
6
5
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Eengine
6
5
  # Represents an Eengine ARM comparison XML report
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Eengine
6
5
  # Represents a section of changes (modifications, additions, or deletions)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Eengine
6
5
  # Represents an Eengine MIM comparison XML report
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module Expressir
5
4
  module Eengine
6
5
  # Represents a modified EXPRESS object in an Eengine comparison report
@@ -26,6 +26,21 @@ module Expressir
26
26
  # @param source [String, nil] The original source code
27
27
  # @param include_source [Boolean, nil] Whether to include source
28
28
  # @return [Model::ModelElement] The built model object
29
+ # Operator tokens that return nil (separators, punctuation)
30
+ # When these appear as the first key in a multi-key hash, they should be
31
+ # skipped in favor of the content key. This handles grammar patterns like
32
+ # `element >> (op_comma >> element).repeat` which produce
33
+ # {:op_comma => ..., :element => {...}}.
34
+ OPERATOR_TOKENS = Set.new(%i[
35
+ op_comma op_colon op_decl op_delim op_leftparen op_rightparen
36
+ op_leftbracket op_rightbracket op_left_curly_brace op_right_curly_brace
37
+ op_period op_pipe op_double_backslash op_double_pipe op_double_asterisk
38
+ op_asterisk op_slash op_plus op_minus op_less_equal op_greater_equal
39
+ op_less_greater op_less_than op_greater_than op_equals
40
+ op_colon_less_greater_colon op_colon_equals_colon
41
+ op_query_begin op_query_end op_question_mark
42
+ ]).freeze
43
+
29
44
  def build(ast, source: nil, include_source: nil)
30
45
  return nil unless ast
31
46
 
@@ -44,25 +59,41 @@ module Expressir
44
59
  snake_data = fast_convert_keys(node_data)
45
60
 
46
61
  builder = @register[handler_key]
47
- raise Error::UnknownNodeTypeError, node_type unless builder
48
-
49
- result = builder.call(snake_data)
50
-
51
- # Always store source_offset for remark attachment (if source is available)
52
- # Only store source text when include_source is requested
53
- if @source && result.is_a?(Model::ModelElement)
54
- source_info = extract_source_info(node_data)
55
- if source_info
56
- # Store offset for remark attachment (always needed)
57
- result.source_offset = source_info[:offset]
58
- # Store source text only when explicitly requested
59
- if @include_source
60
- result.source = source_info[:text]
62
+ if builder
63
+ result = builder.call(snake_data)
64
+
65
+ # Fast path: single-key hash or non-nil result
66
+ if !result.nil? || ast.keys.length <= 1
67
+ attach_source_info(result, node_data)
68
+ return result
69
+ end
70
+
71
+ # Slow path: operator token returned nil in multi-key hash.
72
+ # Try other keys for actual content. This handles
73
+ # {:op_comma => ..., :element => {...}} where the first key
74
+ # is an operator separator rather than a content key.
75
+ if OPERATOR_TOKENS.include?(handler_key)
76
+ ast.each_key do |key|
77
+ next if key == node_type
78
+
79
+ h_key = cached_snake_case(key)
80
+ h_builder = @register[h_key]
81
+ next unless h_builder
82
+
83
+ n_data = ast[key]
84
+ s_data = fast_convert_keys(n_data)
85
+ result = h_builder.call(s_data)
86
+
87
+ unless result.nil?
88
+ attach_source_info(result, n_data)
89
+ return result
90
+ end
61
91
  end
62
92
  end
93
+ else
94
+ raise Error::UnknownNodeTypeError, node_type
63
95
  end
64
-
65
- result
96
+ nil
66
97
  when Array
67
98
  ast.map do |item|
68
99
  build(item)
@@ -83,7 +114,9 @@ module Expressir
83
114
 
84
115
  result = build(ast)
85
116
 
86
- if source && result
117
+ # Only attach remarks if include_source is explicitly true
118
+ # (nil means use default behavior - attach remarks)
119
+ if source && result && include_source != false
87
120
  attacher = RemarkAttacher.new(source)
88
121
  attacher.attach(result)
89
122
  end
@@ -113,6 +146,7 @@ module Expressir
113
146
  def ensure_array(value)
114
147
  return [] if value.nil?
115
148
  return [] if value.is_a?(Parsanol::Slice)
149
+
116
150
  value.is_a?(Array) ? value : [value]
117
151
  end
118
152
 
@@ -121,9 +155,8 @@ module Expressir
121
155
  def build_children(ast_array)
122
156
  return [] if ast_array.nil?
123
157
 
124
- # Handle Parsanol::Slice (including empty Slices from native parser)
125
- # Convert to empty Array to match Ruby parser behavior
126
- # Native parser returns empty slices where Ruby parser returns empty arrays
158
+ # Handle Parsanol::Slice (empty Slices from optional rules)
159
+ # Convert to empty Array
127
160
  if ast_array.is_a?(Parsanol::Slice)
128
161
  return []
129
162
  end
@@ -138,7 +171,7 @@ module Expressir
138
171
  ast_array.each do |item|
139
172
  next if item.nil?
140
173
 
141
- # Empty Slices from native parser should be treated as empty arrays
174
+ # Empty Slices from optional rules should be treated as empty arrays
142
175
  if item.is_a?(Parsanol::Slice)
143
176
  next
144
177
  end
@@ -328,6 +361,16 @@ module Expressir
328
361
  }
329
362
  end
330
363
 
364
+ def attach_source_info(result, data)
365
+ return unless @source && result.is_a?(Model::ModelElement)
366
+
367
+ source_info = extract_source_info(data)
368
+ return unless source_info
369
+
370
+ result.source_offset = source_info[:offset]
371
+ result.source = source_info[:text] if @include_source
372
+ end
373
+
331
374
  def find_slice(data, depth = 0)
332
375
  return nil if depth > 10
333
376
 
@@ -13,12 +13,14 @@ module Expressir
13
13
  end
14
14
 
15
15
  def build_built_in_function(ast_data)
16
- id = extract_text(ast_data[:str])
16
+ id = extract_text(ast_data[:str]) ||
17
+ extract_nested_text(ast_data)
17
18
  Expressir::Model::References::SimpleReference.new(id: id)
18
19
  end
19
20
 
20
21
  def build_built_in_procedure(ast_data)
21
- id = extract_text(ast_data[:str])
22
+ id = extract_text(ast_data[:str]) ||
23
+ extract_nested_text(ast_data)
22
24
  Expressir::Model::References::SimpleReference.new(id: id)
23
25
  end
24
26
 
@@ -36,7 +36,9 @@ module Expressir
36
36
  if subtype_constraint[:supertype_expression]
37
37
  supertype_expression = Builder.build({ supertype_expression: subtype_constraint[:supertype_expression] })
38
38
  elsif subtype_constraint[:list_of_entity_ref]
39
- entity_refs = Builder.build_children(Builder.ensure_array(subtype_constraint[:list_of_entity_ref][:entity_ref]).map { |d| { entity_ref: d } })
39
+ entity_refs = Builder.build_children(Builder.ensure_array(subtype_constraint[:list_of_entity_ref][:entity_ref]).map do |d|
40
+ { entity_ref: d }
41
+ end)
40
42
  supertype_expression = entity_refs.first if entity_refs.length == 1
41
43
  end
42
44
  end
@@ -47,7 +49,9 @@ module Expressir
47
49
  if list_ref.is_a?(Hash)
48
50
  entity_ref_data = list_ref[:entity_ref]
49
51
  if entity_ref_data
50
- subtype_of = Builder.build_children(Builder.ensure_array(entity_ref_data).map { |d| { entity_ref: d } })
52
+ subtype_of = Builder.build_children(Builder.ensure_array(entity_ref_data).map do |d|
53
+ { entity_ref: d }
54
+ end)
51
55
  end
52
56
  end
53
57
  end
@@ -65,12 +69,12 @@ module Expressir
65
69
 
66
70
  if entity_body[:derive_clause]
67
71
  derived = Builder.build({ derive_clause: entity_body[:derive_clause] })
68
- attributes.concat([derived]) if derived
72
+ attributes.push(derived) if derived
69
73
  end
70
74
 
71
75
  if entity_body[:inverse_clause]
72
76
  inverse = Builder.build({ inverse_clause: entity_body[:inverse_clause] })
73
- attributes.concat([inverse]) if inverse
77
+ attributes.push(inverse) if inverse
74
78
  end
75
79
 
76
80
  if entity_body[:unique_clause]