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
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,386 @@
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] rescue 80
68
+ end
69
+
70
+ def clear_line
71
+ INTERACTIVE ? "\r\e[K" : ""
72
+ end
73
+
74
+ def move_to_start
75
+ INTERACTIVE ? "\r" : ""
76
+ end
77
+ end
78
+ end
79
+
80
+ # Progress bar
81
+ class ProgressBar
82
+ def initialize(total, width = 30)
83
+ @total = total
84
+ @width = width
85
+ @current = 0
86
+ end
87
+
88
+ def render(current, label = '')
89
+ @current = current
90
+ percentage = (@current.to_f / @total * 100).round(1)
91
+ filled = (@width * @current / @total.to_f).round
92
+ empty = @width - filled
93
+
94
+ bar = "#{BRIGHT_CYAN}#{'█' * filled}#{DIM}#{'░' * empty}#{RESET}"
95
+ "#{bar} #{BRIGHT_WHITE}#{percentage.to_s.rjust(5)}%#{RESET} #{DIM}#{label}#{RESET}"
96
+ end
97
+ end
98
+
99
+ def find_exp_files
100
+ Dir.glob("#{SRL_PATH}/*/*.exp").sort.reject { |f| f.include?('quantities_and_units') }
101
+ end
102
+
103
+ def count_lines(files)
104
+ files.sum { |f| File.read(f).lines.count }
105
+ end
106
+
107
+ def format_time(seconds)
108
+ if seconds < 60
109
+ "#{seconds.round(2)}s"
110
+ else
111
+ minutes = (seconds / 60).floor
112
+ secs = (seconds % 60).round(1)
113
+ "#{minutes}m #{secs}s"
114
+ end
115
+ end
116
+
117
+ def format_number(num)
118
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
119
+ end
120
+
121
+ def print_header
122
+ puts
123
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
124
+ puts "#{BRIGHT_MAGENTA}║#{RESET}#{BOLD}#{BRIGHT_WHITE} 🚀 EXPRESSIR SRL BENCHMARK 🚀 #{RESET}#{BRIGHT_MAGENTA}║#{RESET}"
125
+ puts "#{BRIGHT_MAGENTA}║#{RESET}#{DIM} Ruby vs Native Parser Showdown #{RESET}#{BRIGHT_MAGENTA}║#{RESET}"
126
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
127
+ puts
128
+ end
129
+
130
+ def print_config(files, total_lines)
131
+ puts "#{BRIGHT_CYAN}⚙️ Configuration#{RESET}"
132
+ puts "#{DIM}┌─────────────────────────────────────────────┐#{RESET}"
133
+ puts "#{DIM}│#{RESET} 📁 Files: #{BRIGHT_WHITE}#{format_number(files.size).rjust(10)}#{RESET} #{DIM}│#{RESET}"
134
+ puts "#{DIM}│#{RESET} 📊 Lines: #{BRIGHT_WHITE}#{format_number(total_lines).rjust(10)}#{RESET} #{DIM}│#{RESET}"
135
+ puts "#{DIM}│#{RESET} 🔄 Iterations: #{BRIGHT_WHITE}#{ITERATIONS.to_s.rjust(10)}#{RESET} #{DIM}│#{RESET}"
136
+ puts "#{DIM}│#{RESET} 🦀 Native: #{BRIGHT_WHITE}#{Parsanol::Native.available?.to_s.upcase.rjust(10)}#{RESET} #{DIM}│#{RESET}"
137
+ puts "#{DIM}└─────────────────────────────────────────────┘#{RESET}"
138
+ puts
139
+ end
140
+
141
+ def print_warmup_start
142
+ print "#{BRIGHT_YELLOW}🔥 Warming up...#{RESET} "
143
+ end
144
+
145
+ def print_warmup_done
146
+ puts "#{BRIGHT_GREEN}✅ Done!#{RESET}"
147
+ puts
148
+ end
149
+
150
+ class ParserBenchmark
151
+ attr_reader :name, :emoji, :color, :results
152
+
153
+ def initialize(name:, emoji:, color:, use_native:)
154
+ @name = name
155
+ @emoji = emoji
156
+ @color = color
157
+ @use_native = use_native
158
+ @results = []
159
+ end
160
+
161
+ def run(files, total_lines)
162
+ puts "#{@color}#{@emoji} #{@name}#{@RESET}"
163
+ puts "#{DIM}┌──────────────────────────────────────────────────────────┐#{RESET}"
164
+
165
+ progress_bar = ProgressBar.new(files.size, 25)
166
+ iteration_results = { success: 0, failed: 0, errors: [], time: 0, schema_times: [] }
167
+ start_time = Time.now
168
+
169
+ files.each_with_index do |file, idx|
170
+ schema_name = File.basename(file, '.exp')
171
+ file_start = Time.now
172
+ schema_lines = File.read(file).lines.count
173
+
174
+ begin
175
+ require 'timeout'
176
+ Timeout.timeout(TIMEOUT_SECONDS) do
177
+ if @use_native
178
+ content = File.read(file)
179
+ Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
180
+ else
181
+ Expressir::Express::Parser.from_file(file, skip_references: true)
182
+ end
183
+ end
184
+ iteration_results[:success] += 1
185
+ status = "#{BRIGHT_GREEN}✓#{RESET}"
186
+ rescue Timeout::Error => e
187
+ iteration_results[:failed] += 1
188
+ iteration_results[:errors] << { file: File.basename(file), error: "Timeout after #{TIMEOUT_SECONDS}s" }
189
+ status = "#{BRIGHT_YELLOW}⏱#{RESET}"
190
+ rescue => e
191
+ iteration_results[:failed] += 1
192
+ iteration_results[:errors] << { file: File.basename(file), error: e.message[0..60] }
193
+ status = "#{BRIGHT_RED}✗#{RESET}"
194
+ end
195
+
196
+ elapsed = Time.now - file_start
197
+ iteration_results[:schema_times] << { name: schema_name, time: elapsed, lines: schema_lines }
198
+
199
+ # Live progress update
200
+ if INTERACTIVE
201
+ progress_label = "#{status} #{schema_name[0..20].ljust(21)}"
202
+ print "#{Terminal.clear_line} #{progress_bar.render(idx + 1, progress_label)}"
203
+ $stdout.flush
204
+ elsif (idx + 1) % 10 == 0 || idx == 0
205
+ # Print progress every 10 files when not interactive
206
+ pct = ((idx + 1).to_f / files.size * 100).round(1)
207
+ puts " #{progress_bar.render(idx + 1, "#{pct}% complete")}"
208
+ $stdout.flush
209
+ end
210
+ end
211
+
212
+ if INTERACTIVE
213
+ puts
214
+ end
215
+
216
+ iteration_results[:time] = Time.now - start_time
217
+ @results << iteration_results
218
+ iteration_results
219
+ end
220
+
221
+ def print_summary(files, total_lines, result)
222
+ avg_time = result[:time]
223
+ lines_per_sec = (total_lines / avg_time).round(1)
224
+ files_per_sec = (files.size / avg_time).round(2)
225
+
226
+ puts "#{DIM}├──────────────────────────────────────────────────────────┤#{RESET}"
227
+ puts "#{DIM}│#{RESET} #{BOLD}Results:#{RESET}"
228
+ puts "#{DIM}│#{RESET} #{BRIGHT_GREEN}✅ Success:#{RESET} #{BRIGHT_WHITE}#{result[:success].to_s.rjust(5)}#{RESET} / #{files.size} files"
229
+ puts "#{DIM}│#{RESET} #{BRIGHT_RED}❌ Failed:#{RESET} #{BRIGHT_WHITE}#{result[:failed].to_s.rjust(5)}#{RESET} files"
230
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}⏱️ Time:#{RESET} #{BRIGHT_WHITE}#{format_time(avg_time).rjust(5)}#{RESET}"
231
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speed:#{RESET} #{BRIGHT_WHITE}#{format_number(lines_per_sec).rjust(5)}#{RESET} lines/sec"
232
+
233
+ if result[:failed] > 0 && result[:failed] <= 5
234
+ puts "#{DIM}├──────────────────────────────────────────────────────────┤#{RESET}"
235
+ puts "#{DIM}│#{RESET} #{BRIGHT_RED}⚠️ Errors:#{RESET}"
236
+ result[:errors].first(3).each do |err|
237
+ puts "#{DIM}│#{RESET} #{DIM}• #{err[:file]}: #{err[:error][0..40]}#{RESET}"
238
+ end
239
+ end
240
+
241
+ puts "#{DIM}└──────────────────────────────────────────────────────────┘#{RESET}"
242
+ puts
243
+
244
+ { avg_time: avg_time, lines_per_sec: lines_per_sec, files_per_sec: files_per_sec }
245
+ end
246
+
247
+ def print_slowest_schemas(result, count = 5)
248
+ return if result[:schema_times].empty?
249
+
250
+ slowest = result[:schema_times].sort_by { |s| -s[:time] }.first(count)
251
+
252
+ puts "#{@color}#{@emoji} Slowest Schemas#{@RESET}"
253
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
254
+ slowest.each_with_index do |s, i|
255
+ rank = "#{BRIGHT_WHITE}#{(i + 1).to_s.rjust(2)}.#{RESET}"
256
+ name = s[:name][0..25].ljust(26)
257
+ time = "#{BRIGHT_CYAN}#{s[:time].round(1)}s#{RESET}".rjust(8)
258
+ lines = "#{DIM}#{s[:lines]} lines#{RESET}"
259
+ puts "#{DIM}│#{RESET} #{rank} #{name} #{time} #{lines}"
260
+ end
261
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
262
+ puts
263
+ end
264
+ end
265
+
266
+ def print_comparison(ruby_stats, native_stats, files, total_lines)
267
+ speedup = (ruby_stats[:avg_time] / native_stats[:avg_time]).round(1)
268
+ time_saved = (ruby_stats[:avg_time] - native_stats[:avg_time]).round(2)
269
+
270
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
271
+ puts "#{BOLD}#{BRIGHT_WHITE} 📊 PERFORMANCE COMPARISON 📊 #{RESET}"
272
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
273
+ puts
274
+
275
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
276
+ puts "#{DIM}│#{RESET}#{BOLD} HEAD-TO-HEAD #{RESET}#{DIM}│#{RESET}"
277
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
278
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
279
+ puts "#{DIM}│#{RESET} #{BRIGHT_BLUE}💎 Ruby Parser#{RESET} #{format_time(ruby_stats[:avg_time]).rjust(10)} #{DIM}│#{RESET}"
280
+ puts "#{DIM}│#{RESET} #{DIM}#{format_number(ruby_stats[:lines_per_sec])} lines/sec#{RESET} #{DIM}│#{RESET}"
281
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
282
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}🦀 Native Parser#{RESET} #{format_time(native_stats[:avg_time]).rjust(10)} #{DIM}│#{RESET}"
283
+ puts "#{DIM}│#{RESET} #{DIM}#{format_number(native_stats[:lines_per_sec])} lines/sec#{RESET} #{DIM}│#{RESET}"
284
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
285
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
286
+
287
+ if speedup > 1
288
+ puts "#{DIM}│#{RESET} #{BOLD}#{BRIGHT_GREEN}🏆 WINNER: Native Parser#{RESET} #{DIM}│#{RESET}"
289
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⚡ Speedup:#{RESET} #{BOLD}#{BRIGHT_GREEN}#{speedup}x FASTER#{RESET} #{DIM}│#{RESET}"
290
+ puts "#{DIM}│#{RESET} #{BRIGHT_YELLOW}⏱️ Time Saved:#{RESET} #{BRIGHT_GREEN}#{format_time(time_saved)} per run#{RESET} #{DIM}│#{RESET}"
291
+ else
292
+ 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}"
294
+ end
295
+
296
+ puts "#{DIM}│#{RESET} #{DIM}│#{RESET}"
297
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
298
+ puts
299
+
300
+ # Summary
301
+ puts "#{DIM}┌─────────────────────────────────────────────────────┐#{RESET}"
302
+ puts "#{DIM}│#{RESET}#{BOLD} SUMMARY #{RESET}#{DIM}│#{RESET}"
303
+ puts "#{DIM}├─────────────────────────────────────────────────────┤#{RESET}"
304
+ puts "#{DIM}│#{RESET} 📦 #{format_number(files.size)} schemas (#{format_number(total_lines)} lines) #{DIM}│#{RESET}"
305
+ puts "#{DIM}│#{RESET} #{BRIGHT_BLUE}Ruby:#{RESET} #{format_time(ruby_stats[:avg_time]).rjust(8)} #{DIM}│#{RESET}"
306
+ puts "#{DIM}│#{RESET} #{BRIGHT_CYAN}Native:#{RESET} #{format_time(native_stats[:avg_time]).rjust(8)} #{DIM}│#{RESET}"
307
+ puts "#{DIM}└─────────────────────────────────────────────────────┘#{RESET}"
308
+ puts
309
+ end
310
+
311
+ def print_footer
312
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
313
+ puts "#{BOLD}#{BRIGHT_WHITE} ✨ BENCHMARK COMPLETE ✨ #{RESET}"
314
+ puts "#{BRIGHT_MAGENTA}#{'═' * 60}#{RESET}"
315
+ puts
316
+ end
317
+
318
+ # ==================== MAIN ====================
319
+
320
+ print_header
321
+
322
+ # Find all EXPRESS files
323
+ files = find_exp_files
324
+ total_lines = count_lines(files)
325
+
326
+ if files.empty?
327
+ puts "#{BRIGHT_RED}❌ ERROR: No EXPRESS files found at #{SRL_PATH}#{RESET}"
328
+ exit 1
329
+ end
330
+
331
+ print_config(files, total_lines)
332
+
333
+ # Warmup
334
+ print_warmup_start
335
+ warmup_file = files.first
336
+
337
+ begin
338
+ Expressir::Express::Parser.from_file(warmup_file, skip_references: true)
339
+ rescue => e
340
+ puts "#{BRIGHT_YELLOW}⚠️ Ruby warmup warning: #{e.message[0..40]}#{RESET}"
341
+ end
342
+
343
+ if Parsanol::Native.available?
344
+ begin
345
+ content = File.read(warmup_file)
346
+ Expressir::Express::Parser.from_exp(content, skip_references: true, use_native: true)
347
+ rescue => e
348
+ puts "#{BRIGHT_YELLOW}⚠️ Native warmup warning: #{e.message[0..40]}#{RESET}"
349
+ end
350
+ end
351
+
352
+ print_warmup_done
353
+
354
+ # Ruby Benchmark
355
+ ruby_benchmark = ParserBenchmark.new(
356
+ name: "Ruby Parser",
357
+ emoji: "💎",
358
+ color: BRIGHT_BLUE,
359
+ use_native: false
360
+ )
361
+
362
+ ruby_result = ruby_benchmark.run(files, total_lines)
363
+ ruby_stats = ruby_benchmark.print_summary(files, total_lines, ruby_result)
364
+ ruby_benchmark.print_slowest_schemas(ruby_result)
365
+
366
+ # Native Benchmark (if available)
367
+ if Parsanol::Native.available?
368
+ native_benchmark = ParserBenchmark.new(
369
+ name: "Native Parser (Rust)",
370
+ emoji: "🦀",
371
+ color: BRIGHT_CYAN,
372
+ use_native: true
373
+ )
374
+
375
+ native_result = native_benchmark.run(files, total_lines)
376
+ native_stats = native_benchmark.print_summary(files, total_lines, native_result)
377
+ native_benchmark.print_slowest_schemas(native_result)
378
+
379
+ # Print comparison
380
+ print_comparison(ruby_stats, native_stats, files, total_lines)
381
+ else
382
+ puts "#{BRIGHT_YELLOW}⚠️ Native parser not available. Run \`rake compile\` in parsanol-ruby to build.#{RESET}"
383
+ puts
384
+ end
385
+
386
+ print_footer