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
data/Gemfile CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # Specify your gem's dependencies in reeper.gemspec
6
5
  gemspec
7
6
 
7
+ # Use released parsanol for native extension
8
+ gem "parsanol", "~> 1.3.5"
9
+
8
10
  gem "canon"
11
+ gem "irb"
9
12
  gem "lutaml-model", github: "lutaml/lutaml-model", branch: "main"
10
13
  gem "openssl", "~> 3.0"
11
14
  gem "rake"
data/README.adoc CHANGED
@@ -117,35 +117,42 @@ Expressir uses the link:https://github.com/parsanol/parsanol-ruby[Parsanol] gem
117
117
 
118
118
  [cols="3,2,2,3"]
119
119
  |===
120
- | Mode | Time | Speedup | Notes
120
+ | Mode | Time (179K lines) | Speedup | Notes
121
121
 
122
- | Ruby (Parsanol) | 3036 ms | 1x (baseline) | Pure Ruby parsing
123
- | Native Batch (Parsanol) | 153 ms | 19.9x faster | AST via u64 array transfer
124
- | Native ZeroCopy (Parsanol) | 106 ms | 28.7x faster | Zero-copy with source positions
122
+ | Ruby (Parslet) | 507 s | 1x (baseline) | Pure Ruby parsing
123
+ | Native (Parsanol) | 29 s | 17x faster | Rust parser with AST transformation
125
124
  |===
126
125
 
127
126
  === Features
128
127
 
129
128
  When Parsanol is installed:
130
129
 
131
- * **18-44x faster parsing** - Rust native backend
132
- * **99.5% fewer allocations** - Arena-based AST construction
133
- * **Source position tracking** - Slice objects for error reporting
134
- * **Streaming Builder API** - Zero-allocation custom parsing
130
+ * **17x faster parsing** - Rust native backend
131
+ * **Lazy line/column** - Zero overhead for position info
132
+ * **Batch FFI** - Efficient u64 array transfer across language boundary
135
133
 
136
134
  === Usage
137
135
 
138
- Expressir automatically uses Parsanol when available:
136
+ Expressir automatically uses Parsanol (Rust parser) when available for better performance:
139
137
 
140
138
  [source,ruby]
141
139
  ----
142
- # Automatically uses Parsanol native parser when available
143
- repo = Expressir::Express::Parser.from_file("geometry.exp")
140
+ # Parse a single file - returns ExpFile
141
+ exp_file = Expressir::Express::Parser.from_file("geometry.exp")
142
+ schema = exp_file.schemas.first
143
+ puts "Schema: #{schema.id}"
144
144
 
145
145
  # Check if native parser is being used
146
146
  if Parsanol::Native.available?
147
147
  puts "Using Parsanol (Rust parser)"
148
148
  end
149
+
150
+ # Parse multiple files - returns Repository
151
+ files = ["schema1.exp", "schema2.exp"]
152
+ repo = Expressir::Express::Parser.from_files(files)
153
+ repo.schemas.each do |s|
154
+ puts "Schema: #{s.id}"
155
+ end
149
156
  ----
150
157
 
151
158
  For maximum performance, ensure the Parsanol gem is installed:
@@ -240,9 +247,15 @@ https://www.express-language-foundation.org/pretty-print-spec/[ELF Pretty Print
240
247
  [source,ruby]
241
248
  ----
242
249
  # Basic usage - formats with default settings
243
- repository = Expressir::Express::Parser.from_file("schema.exp")
250
+ # Parser.from_file returns an ExpFile
251
+ exp_file = Expressir::Express::Parser.from_file("schema.exp")
244
252
  formatter = Expressir::Express::PrettyFormatter.new
245
- formatted = formatter.format(repository)
253
+ formatted = formatter.format(exp_file)
254
+ puts formatted
255
+
256
+ # The formatter also accepts Repository objects
257
+ repo = Expressir::Express::Parser.from_files(["schema1.exp", "schema2.exp"])
258
+ formatted = formatter.format(repo)
246
259
  puts formatted
247
260
  ----
248
261
 
@@ -1319,15 +1332,19 @@ The library provides two main methods for parsing EXPRESS files.
1319
1332
 
1320
1333
  ==== Parsing a single file
1321
1334
 
1322
- Use the `from_file` method to parse a single EXPRESS schema file:
1335
+ Use the `from_file` method to parse a single EXPRESS schema file.
1336
+ This returns an `ExpFile` object containing the parsed schemas:
1323
1337
 
1324
1338
  [source,ruby]
1325
1339
  ----
1326
- # Parse a single file
1327
- repository = Expressir::Express::Parser.from_file("path/to/schema.exp")
1340
+ # Parse a single file - returns ExpFile
1341
+ exp_file = Expressir::Express::Parser.from_file("path/to/schema.exp")
1342
+
1343
+ # Access schemas from the file
1344
+ schema = exp_file.schemas.first
1328
1345
 
1329
1346
  # With options
1330
- repository = Expressir::Express::Parser.from_file(
1347
+ exp_file = Expressir::Express::Parser.from_file(
1331
1348
  "path/to/schema.exp",
1332
1349
  skip_references: false, # Set to true to skip resolving references
1333
1350
  include_source: true, # Set to true to include original source in the model
@@ -1342,7 +1359,7 @@ error:
1342
1359
  [source,ruby]
1343
1360
  ----
1344
1361
  begin
1345
- repository = Expressir::Express::Parser.from_file("path/to/schema.exp")
1362
+ exp_file = Expressir::Express::Parser.from_file("path/to/schema.exp")
1346
1363
  rescue Expressir::Express::Error::SchemaParseFailure => e
1347
1364
  puts "Failed to parse schema: #{e.message}"
1348
1365
  puts "Filename: #{e.filename}"
@@ -1352,13 +1369,19 @@ end
1352
1369
 
1353
1370
  ==== Parsing multiple files
1354
1371
 
1355
- Use the `from_files` method to parse multiple EXPRESS schema files:
1372
+ Use the `from_files` method to parse multiple EXPRESS schema files.
1373
+ This returns a `Repository` object containing all parsed files:
1356
1374
 
1357
1375
  [source,ruby]
1358
1376
  ----
1359
- # Parse multiple files
1377
+ # Parse multiple files - returns Repository
1360
1378
  files = ["schema1.exp", "schema2.exp", "schema3.exp"]
1361
1379
  repository = Expressir::Express::Parser.from_files(files)
1380
+
1381
+ # Access all schemas across all files
1382
+ repository.schemas.each do |schema|
1383
+ puts "Schema: #{schema.id}"
1384
+ end
1362
1385
  ----
1363
1386
 
1364
1387
  You can provide a block to track loading progress and handle errors:
@@ -1407,7 +1430,12 @@ Example:
1407
1430
 
1408
1431
  [source,ruby]
1409
1432
  ----
1410
- repo = Expressir::Express::Parser.from_file("path/to/file.exp")
1433
+ # Single file (ExpFile)
1434
+ exp_file = Expressir::Express::Parser.from_file("path/to/file.exp")
1435
+ file_drop = exp_file.to_liquid
1436
+
1437
+ # Multiple files (Repository)
1438
+ repo = Expressir::Express::Parser.from_files(["file1.exp", "file2.exp"])
1411
1439
  repo_drop = repo.to_liquid
1412
1440
  ----
1413
1441
 
@@ -1438,10 +1466,15 @@ and `Expressir::Liquid::Declarations::SchemaDrop` has same attribute `file`.
1438
1466
 
1439
1467
  [source,ruby]
1440
1468
  ----
1441
- repo = Expressir::Express::Parser.from_file("path/to/file.exp")
1442
- repo_drop = repo.to_liquid
1443
- schema = repo_drop.schemas.first
1469
+ # Parse file (returns ExpFile)
1470
+ exp_file = Expressir::Express::Parser.from_file("path/to/file.exp")
1471
+ file_drop = exp_file.to_liquid
1472
+ schema = file_drop.schemas.first
1444
1473
  schema.file = "path/to/file.exp"
1474
+
1475
+ # Or parse multiple files (returns Repository)
1476
+ repo = Expressir::Express::Parser.from_files(["file1.exp", "file2.exp"])
1477
+ repo_drop = repo.to_liquid
1445
1478
  ----
1446
1479
 
1447
1480
  === Documentation coverage analysis
@@ -1454,9 +1487,13 @@ analyze and report on documentation coverage of EXPRESS schemas.
1454
1487
  # Create a coverage report from a file
1455
1488
  report = Expressir::Coverage::Report.from_file("path/to/schema.exp")
1456
1489
 
1490
+ # Or create a report from an ExpFile
1491
+ exp_file = Expressir::Express::Parser.from_file("path/to/schema.exp")
1492
+ report = Expressir::Coverage::Report.from_exp_file(exp_file)
1493
+
1457
1494
  # Or create a report from a repository
1458
- repository = Expressir::Express::Parser.from_file("path/to/schema.exp")
1459
- report = Expressir::Coverage::Report.from_repository(repository)
1495
+ repo = Expressir::Express::Parser.from_files(["schema1.exp", "schema2.exp"])
1496
+ report = Expressir::Coverage::Report.from_repository(repo)
1460
1497
 
1461
1498
  # Access overall statistics
1462
1499
  puts "Overall coverage: #{report.coverage_percentage}%"
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # SRL Benchmark Script for Expressir
5
+ # Compares Parsanol Ruby vs Native performance on full STEPmod Resource Library
6
+ # Features: Live progress, emojis, colors, per-schema stats
7
+
8
+ require "bundler/setup"
9
+ require "benchmark"
10
+ require "fileutils"
11
+
12
+ # Force loading of native extension
13
+ require "parsanol"
14
+ require "parsanol/native"
15
+
16
+ # Now require expressir
17
+ require "expressir"
18
+
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
23
+
24
+ # Check if we're running in an interactive terminal
25
+ INTERACTIVE = $stdout.tty?
26
+
27
+ # ANSI Color codes
28
+ module Colors
29
+ RESET = "\e[0m"
30
+ BOLD = "\e[1m"
31
+ DIM = "\e[2m"
32
+
33
+ # Foreground colors
34
+ BLACK = "\e[30m"
35
+ RED = "\e[31m"
36
+ GREEN = "\e[32m"
37
+ YELLOW = "\e[33m"
38
+ BLUE = "\e[34m"
39
+ MAGENTA = "\e[35m"
40
+ CYAN = "\e[36m"
41
+ WHITE = "\e[37m"
42
+
43
+ # Bright foreground colors
44
+ BRIGHT_RED = "\e[91m"
45
+ BRIGHT_GREEN = "\e[92m"
46
+ BRIGHT_YELLOW = "\e[93m"
47
+ BRIGHT_BLUE = "\e[94m"
48
+ BRIGHT_MAGENTA = "\e[95m"
49
+ BRIGHT_CYAN = "\e[96m"
50
+ BRIGHT_WHITE = "\e[97m"
51
+
52
+ # Background colors
53
+ BG_RED = "\e[41m"
54
+ BG_GREEN = "\e[42m"
55
+ BG_YELLOW = "\e[43m"
56
+ BG_BLUE = "\e[44m"
57
+ BG_MAGENTA = "\e[45m"
58
+ BG_CYAN = "\e[46m"
59
+ end
60
+
61
+ include Colors
62
+
63
+ # Terminal utilities
64
+ module Terminal
65
+ class << self
66
+ def width
67
+ IO.console.winsize[1]
68
+ rescue StandardError
69
+ 80
70
+ end
71
+
72
+ def clear_line
73
+ INTERACTIVE ? "\r\e[K" : ""
74
+ end
75
+
76
+ def move_to_start
77
+ INTERACTIVE ? "\r" : ""
78
+ end
79
+ end
80
+ end
81
+
82
+ # Progress bar
83
+ class ProgressBar
84
+ def initialize(total, width = 30)
85
+ @total = total
86
+ @width = width
87
+ @current = 0
88
+ end
89
+
90
+ def render(current, label = "")
91
+ @current = current
92
+ percentage = (@current.to_f / @total * 100).round(1)
93
+ filled = (@width * @current / @total.to_f).round
94
+ empty = @width - filled
95
+
96
+ bar = "#{BRIGHT_CYAN}#{'█' * filled}#{DIM}#{'░' * empty}#{RESET}"
97
+ "#{bar} #{BRIGHT_WHITE}#{percentage.to_s.rjust(5)}%#{RESET} #{DIM}#{label}#{RESET}"
98
+ end
99
+ end
100
+
101
+ def find_exp_files
102
+ Dir.glob("#{SRL_PATH}/*/*.exp").reject do |f|
103
+ f.include?("quantities_and_units")
104
+ end
105
+ end
106
+
107
+ def count_lines(files)
108
+ files.sum { |f| File.read(f).lines.count }
109
+ end
110
+
111
+ def format_time(seconds)
112
+ if seconds < 60
113
+ "#{seconds.round(2)}s"
114
+ else
115
+ minutes = (seconds / 60).floor
116
+ secs = (seconds % 60).round(1)
117
+ "#{minutes}m #{secs}s"
118
+ end
119
+ end
120
+
121
+ def format_number(num)
122
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
123
+ end
124
+
125
+ def print_header
126
+ puts
127
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
128
+ puts "#{BRIGHT_MAGENTA}║#{RESET}#{BOLD}#{BRIGHT_WHITE} 🚀 EXPRESSIR SRL BENCHMARK 🚀 #{RESET}#{BRIGHT_MAGENTA}║#{RESET}"
129
+ puts "#{BRIGHT_MAGENTA}║#{RESET}#{DIM} Ruby vs Native Parser Showdown #{RESET}#{BRIGHT_MAGENTA}║#{RESET}"
130
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
131
+ puts
132
+ end
133
+
134
+ def print_config(files, total_lines)
135
+ puts "#{BRIGHT_CYAN}⚙️ Configuration#{RESET}"
136
+ puts "#{DIM}┌─────────────────────────────────────────────┐#{RESET}"
137
+ puts "#{DIM}│#{RESET} 📁 Files: #{BRIGHT_WHITE}#{format_number(files.size).rjust(10)}#{RESET} #{DIM}│#{RESET}"
138
+ puts "#{DIM}│#{RESET} 📊 Lines: #{BRIGHT_WHITE}#{format_number(total_lines).rjust(10)}#{RESET} #{DIM}│#{RESET}"
139
+ puts "#{DIM}│#{RESET} 🔄 Iterations: #{BRIGHT_WHITE}#{ITERATIONS.to_s.rjust(10)}#{RESET} #{DIM}│#{RESET}"
140
+ puts "#{DIM}│#{RESET} 🦀 Native: #{BRIGHT_WHITE}#{Parsanol::Native.available?.to_s.upcase.rjust(10)}#{RESET} #{DIM}│#{RESET}"
141
+ puts "#{DIM}└─────────────────────────────────────────────┘#{RESET}"
142
+ puts
143
+ end
144
+
145
+ def print_warmup_start
146
+ print "#{BRIGHT_YELLOW}🔥 Warming up...#{RESET} "
147
+ end
148
+
149
+ def print_warmup_done
150
+ puts "#{BRIGHT_GREEN}✅ Done!#{RESET}"
151
+ puts
152
+ end
153
+
154
+ class ParserBenchmark
155
+ attr_reader :name, :emoji, :color, :results
156
+
157
+ def initialize(name:, emoji:, color:, use_native:)
158
+ @name = name
159
+ @emoji = emoji
160
+ @color = color
161
+ @use_native = use_native
162
+ @results = []
163
+ end
164
+
165
+ def run(files, _total_lines)
166
+ puts "#{@color}#{@emoji} #{@name}#{@RESET}"
167
+ puts "#{DIM}┌──────────────────────────────────────────────────────────┐#{RESET}"
168
+
169
+ progress_bar = ProgressBar.new(files.size, 25)
170
+ iteration_results = { success: 0, failed: 0, errors: [], time: 0,
171
+ schema_times: [] }
172
+ start_time = Time.now
173
+
174
+ files.each_with_index do |file, idx|
175
+ schema_name = File.basename(file, ".exp")
176
+ file_start = Time.now
177
+ schema_lines = File.read(file).lines.count
178
+
179
+ begin
180
+ require "timeout"
181
+ Timeout.timeout(TIMEOUT_SECONDS) do
182
+ if @use_native
183
+ content = File.read(file)
184
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
185
+ use_native: true)
186
+ else
187
+ Expressir::Express::Parser.from_file(file, skip_references: true)
188
+ end
189
+ end
190
+ iteration_results[:success] += 1
191
+ status = "#{BRIGHT_GREEN}✓#{RESET}"
192
+ rescue Timeout::Error
193
+ iteration_results[:failed] += 1
194
+ iteration_results[:errors] << { file: File.basename(file),
195
+ error: "Timeout after #{TIMEOUT_SECONDS}s" }
196
+ status = "#{BRIGHT_YELLOW}⏱#{RESET}"
197
+ rescue StandardError => e
198
+ iteration_results[:failed] += 1
199
+ iteration_results[:errors] << { file: File.basename(file),
200
+ error: e.message[0..60] }
201
+ status = "#{BRIGHT_RED}✗#{RESET}"
202
+ end
203
+
204
+ elapsed = Time.now - file_start
205
+ iteration_results[:schema_times] << { name: schema_name, time: elapsed,
206
+ lines: schema_lines }
207
+
208
+ # Live progress update
209
+ if INTERACTIVE
210
+ progress_label = "#{status} #{schema_name[0..20].ljust(21)}"
211
+ print "#{Terminal.clear_line} #{progress_bar.render(idx + 1,
212
+ progress_label)}"
213
+ $stdout.flush
214
+ elsif ((idx + 1) % 10).zero? || idx.zero?
215
+ # Print progress every 10 files when not interactive
216
+ pct = ((idx + 1).to_f / files.size * 100).round(1)
217
+ puts " #{progress_bar.render(idx + 1, "#{pct}% complete")}"
218
+ $stdout.flush
219
+ end
220
+ end
221
+
222
+ if INTERACTIVE
223
+ puts
224
+ end
225
+
226
+ iteration_results[:time] = Time.now - start_time
227
+ @results << iteration_results
228
+ iteration_results
229
+ end
230
+
231
+ def print_summary(files, total_lines, result)
232
+ avg_time = result[:time]
233
+ lines_per_sec = (total_lines / avg_time).round(1)
234
+ files_per_sec = (files.size / avg_time).round(2)
235
+
236
+ puts "#{DIM}├──────────────────────────────────────────────────────────┤#{RESET}"
237
+ puts "#{DIM}│#{RESET} #{BOLD}Results:#{RESET}"
238
+ puts "#{DIM}│#{RESET} #{BRIGHT_GREEN}✅ Success:#{RESET} #{BRIGHT_WHITE}#{result[:success].to_s.rjust(5)}#{RESET} / #{files.size} files"
239
+ puts "#{DIM}│#{RESET} #{BRIGHT_RED}❌ Failed:#{RESET} #{BRIGHT_WHITE}#{result[:failed].to_s.rjust(5)}#{RESET} files"
240
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}⏱️ Time:#{RESET} #{BRIGHT_WHITE}#{format_time(avg_time).rjust(5)}#{RESET}"
241
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speed:#{RESET} #{BRIGHT_WHITE}#{format_number(lines_per_sec).rjust(5)}#{RESET} lines/sec"
242
+
243
+ if result[:failed].positive? && result[:failed] <= 5
244
+ puts "#{DIM}├──────────────────────────────────────────────────────────┤#{RESET}"
245
+ puts "#{DIM}│#{RESET} #{BRIGHT_RED}⚠️ Errors:#{RESET}"
246
+ result[:errors].first(3).each do |err|
247
+ puts "#{DIM}│#{RESET} #{DIM}• #{err[:file]}: #{err[:error][0..40]}#{RESET}"
248
+ end
249
+ end
250
+
251
+ puts "#{DIM}└──────────────────────────────────────────────────────────┘#{RESET}"
252
+ puts
253
+
254
+ { avg_time: avg_time, lines_per_sec: lines_per_sec,
255
+ files_per_sec: files_per_sec }
256
+ end
257
+
258
+ def print_slowest_schemas(result, count = 5)
259
+ return if result[:schema_times].empty?
260
+
261
+ slowest = result[:schema_times].sort_by { |s| -s[:time] }.first(count)
262
+
263
+ puts "#{@color}#{@emoji} Slowest Schemas#{@RESET}"
264
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
265
+ slowest.each_with_index do |s, i|
266
+ rank = "#{BRIGHT_WHITE}#{(i + 1).to_s.rjust(2)}.#{RESET}"
267
+ name = s[:name][0..25].ljust(26)
268
+ time = "#{BRIGHT_CYAN}#{s[:time].round(1)}s#{RESET}".rjust(8)
269
+ lines = "#{DIM}#{s[:lines]} lines#{RESET}"
270
+ puts "#{DIM}│#{RESET} #{rank} #{name} #{time} #{lines}"
271
+ end
272
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
273
+ puts
274
+ end
275
+ end
276
+
277
+ def print_comparison(ruby_stats, native_stats, files, total_lines)
278
+ speedup = (ruby_stats[:avg_time] / native_stats[:avg_time]).round(1)
279
+ time_saved = (ruby_stats[:avg_time] - native_stats[:avg_time]).round(2)
280
+
281
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
282
+ puts "#{BOLD}#{BRIGHT_WHITE} 📊 PERFORMANCE COMPARISON 📊 #{RESET}"
283
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
284
+ puts
285
+
286
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
287
+ puts "#{DIM}│#{RESET}#{BOLD} HEAD-TO-HEAD #{RESET}#{DIM}│#{RESET}"
288
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
289
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
290
+ puts "#{DIM}│#{RESET} #{BRIGHT_BLUE}💎 Ruby Parser#{RESET} #{format_time(ruby_stats[:avg_time]).rjust(10)} #{DIM}│#{RESET}"
291
+ puts "#{DIM}│#{RESET} #{DIM}#{format_number(ruby_stats[:lines_per_sec])} lines/sec#{RESET} #{DIM}│#{RESET}"
292
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
293
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}🦀 Native Parser#{RESET} #{format_time(native_stats[:avg_time]).rjust(10)} #{DIM}│#{RESET}"
294
+ puts "#{DIM}│#{RESET} #{DIM}#{format_number(native_stats[:lines_per_sec])} lines/sec#{RESET} #{DIM}│#{RESET}"
295
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
296
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
297
+
298
+ if speedup > 1
299
+ puts "#{DIM}│#{RESET} #{BOLD}#{BRIGHT_GREEN}🏆 WINNER: Native Parser#{RESET} #{DIM}│#{RESET}"
300
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speedup:#{RESET} #{BOLD}#{BRIGHT_GREEN}#{speedup}x FASTER#{RESET} #{DIM}│#{RESET}"
301
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⏱️ Time Saved:#{RESET} #{BRIGHT_GREEN}#{format_time(time_saved)} per run#{RESET} #{DIM}│#{RESET}"
302
+ else
303
+ puts "#{DIM}│#{RESET} #{BOLD}#{BRIGHT_BLUE}🏆 WINNER: Ruby Parser#{RESET} #{DIM}│#{RESET}"
304
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speedup:#{RESET} #{BOLD}#{BRIGHT_BLUE}#{(1 / speedup).round(1)}x FASTER#{RESET} #{DIM}│#{RESET}"
305
+ end
306
+
307
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
308
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
309
+ puts
310
+
311
+ # Summary
312
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
313
+ puts "#{DIM}│#{RESET}#{BOLD} SUMMARY #{RESET}#{DIM}│#{RESET}"
314
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
315
+ puts "#{DIM}│#{RESET} 📦 #{format_number(files.size)} schemas (#{format_number(total_lines)} lines) #{DIM}│#{RESET}"
316
+ puts "#{DIM}│#{RESET} #{BRIGHT_BLUE}Ruby:#{RESET} #{format_time(ruby_stats[:avg_time]).rjust(8)} #{DIM}│#{RESET}"
317
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}Native:#{RESET} #{format_time(native_stats[:avg_time]).rjust(8)} #{DIM}│#{RESET}"
318
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
319
+ puts
320
+ end
321
+
322
+ def print_footer
323
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
324
+ puts "#{BOLD}#{BRIGHT_WHITE} ✨ BENCHMARK COMPLETE ✨ #{RESET}"
325
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
326
+ puts
327
+ end
328
+
329
+ # ==================== MAIN ====================
330
+
331
+ print_header
332
+
333
+ # Find all EXPRESS files
334
+ files = find_exp_files
335
+ total_lines = count_lines(files)
336
+
337
+ if files.empty?
338
+ puts "#{BRIGHT_RED}❌ ERROR: No EXPRESS files found at #{SRL_PATH}#{RESET}"
339
+ exit 1
340
+ end
341
+
342
+ print_config(files, total_lines)
343
+
344
+ # Warmup
345
+ print_warmup_start
346
+ warmup_file = files.first
347
+
348
+ begin
349
+ Expressir::Express::Parser.from_file(warmup_file, skip_references: true)
350
+ rescue StandardError => e
351
+ puts "#{BRIGHT_YELLOW}⚠️ Ruby warmup warning: #{e.message[0..40]}#{RESET}"
352
+ end
353
+
354
+ if Parsanol::Native.available?
355
+ begin
356
+ content = File.read(warmup_file)
357
+ Expressir::Express::Parser.from_exp(content, skip_references: true,
358
+ use_native: true)
359
+ rescue StandardError => e
360
+ puts "#{BRIGHT_YELLOW}⚠️ Native warmup warning: #{e.message[0..40]}#{RESET}"
361
+ end
362
+ end
363
+
364
+ print_warmup_done
365
+
366
+ # Ruby Benchmark
367
+ ruby_benchmark = ParserBenchmark.new(
368
+ name: "Ruby Parser",
369
+ emoji: "💎",
370
+ color: BRIGHT_BLUE,
371
+ use_native: false,
372
+ )
373
+
374
+ ruby_result = ruby_benchmark.run(files, total_lines)
375
+ ruby_stats = ruby_benchmark.print_summary(files, total_lines, ruby_result)
376
+ ruby_benchmark.print_slowest_schemas(ruby_result)
377
+
378
+ # Native Benchmark (if available)
379
+ if Parsanol::Native.available?
380
+ native_benchmark = ParserBenchmark.new(
381
+ name: "Native Parser (Rust)",
382
+ emoji: "🦀",
383
+ color: BRIGHT_CYAN,
384
+ use_native: true,
385
+ )
386
+
387
+ native_result = native_benchmark.run(files, total_lines)
388
+ native_stats = native_benchmark.print_summary(files, total_lines,
389
+ native_result)
390
+ native_benchmark.print_slowest_schemas(native_result)
391
+
392
+ # Print comparison
393
+ print_comparison(ruby_stats, native_stats, files, total_lines)
394
+ else
395
+ puts "#{BRIGHT_YELLOW}⚠️ Native parser not available. Run `rake compile` in parsanol-ruby to build.#{RESET}"
396
+ puts
397
+ end
398
+
399
+ print_footer