parsanol 1.3.13-arm-linux

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +98 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +905 -0
  5. data/Rakefile +141 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +123 -0
  12. data/lib/parsanol/atoms/base.rb +208 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +357 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +167 -0
  24. data/lib/parsanol/atoms/lookahead.rb +85 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +277 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +195 -0
  30. data/lib/parsanol/atoms/str.rb +109 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +187 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +179 -0
  48. data/lib/parsanol/incremental_parser.rb +182 -0
  49. data/lib/parsanol/interval_tree.rb +226 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/mermaid.rb +142 -0
  52. data/lib/parsanol/native/batch_decoder.rb +255 -0
  53. data/lib/parsanol/native/dynamic.rb +238 -0
  54. data/lib/parsanol/native/parser.rb +102 -0
  55. data/lib/parsanol/native/serializer.rb +252 -0
  56. data/lib/parsanol/native/transformer.rb +604 -0
  57. data/lib/parsanol/native/types.rb +29 -0
  58. data/lib/parsanol/native.rb +223 -0
  59. data/lib/parsanol/optimizer.rb +85 -0
  60. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  61. data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
  62. data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
  63. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  64. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  65. data/lib/parsanol/options/zero_copy.rb +127 -0
  66. data/lib/parsanol/options.rb +21 -0
  67. data/lib/parsanol/parallel.rb +128 -0
  68. data/lib/parsanol/parser.rb +242 -0
  69. data/lib/parsanol/parslet.rb +151 -0
  70. data/lib/parsanol/pattern/binding.rb +91 -0
  71. data/lib/parsanol/pattern.rb +162 -0
  72. data/lib/parsanol/pool.rb +219 -0
  73. data/lib/parsanol/pools/array_pool.rb +75 -0
  74. data/lib/parsanol/pools/buffer_pool.rb +182 -0
  75. data/lib/parsanol/pools/position_pool.rb +92 -0
  76. data/lib/parsanol/pools/slice_pool.rb +64 -0
  77. data/lib/parsanol/position.rb +94 -0
  78. data/lib/parsanol/resettable.rb +29 -0
  79. data/lib/parsanol/result.rb +46 -0
  80. data/lib/parsanol/result_builder.rb +208 -0
  81. data/lib/parsanol/result_stream.rb +266 -0
  82. data/lib/parsanol/rig/rspec.rb +71 -0
  83. data/lib/parsanol/rope.rb +81 -0
  84. data/lib/parsanol/scope.rb +104 -0
  85. data/lib/parsanol/slice.rb +160 -0
  86. data/lib/parsanol/source/line_cache.rb +102 -0
  87. data/lib/parsanol/source.rb +185 -0
  88. data/lib/parsanol/source_location.rb +167 -0
  89. data/lib/parsanol/streaming_parser.rb +124 -0
  90. data/lib/parsanol/string_view.rb +198 -0
  91. data/lib/parsanol/transform.rb +226 -0
  92. data/lib/parsanol/version.rb +5 -0
  93. data/lib/parsanol/wasm/README.md +80 -0
  94. data/lib/parsanol/wasm/package.json +51 -0
  95. data/lib/parsanol/wasm/parsanol.js +252 -0
  96. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  97. data/lib/parsanol/wasm_parser.rb +240 -0
  98. data/lib/parsanol.rb +278 -0
  99. data/parsanol.gemspec +67 -0
  100. metadata +279 -0
data/README.adoc ADDED
@@ -0,0 +1,905 @@
1
+ = Parsanol
2
+
3
+ image:https://img.shields.io/gem/v/parsanol.svg[RubyGems Version]
4
+ image:https://img.shields.io/github/license/parsanol/parsanol-ruby.svg[License]
5
+ image:https://github.com/parsanol/parsanol-ruby/actions/workflows/test.yml/badge.svg["Build", link="https://github.com/parsanol/parsanol-ruby/actions/workflows/test.yml"]
6
+
7
+ A high-performance PEG (Parsing Expression Grammar) parser construction library for Ruby with optional Rust native extensions.
8
+
9
+ == Purpose
10
+
11
+ Parsanol provides a declarative DSL for constructing parsers using PEG semantics. It offers excellent error reporting, memory efficiency through object pooling, and optional Rust native extensions for maximum performance. The library is designed as a drop-in replacement for Parslet while offering significant performance improvements.
12
+
13
+ // Inspiration attribution
14
+ [NOTE]
15
+ ====
16
+ Parsanol is inspired by the https://github.com/kschiess/parslet[Parslet] library by Kaspar Schiess.
17
+ While maintaining full API compatibility with Parslet, Parsanol features a complete independent implementation with additional performance optimizations and features.
18
+ ====
19
+
20
+ == Features
21
+
22
+ * <<basic-parsing,PEG-based Parser Construction>> - Declarative grammar definition
23
+ * <<error-reporting,Detailed Error Reporting>> - Precise failure location and context
24
+ * <<native-extension,Rust Native Extension>> - **Up to 1300x faster parsing**
25
+ * <<slice-support,Slice Support>> - Source position preservation for linters and IDEs
26
+ * <<transformation,Tree Transformation>> - Pattern-based AST construction
27
+ * <<streaming-builder,Streaming Builder API>> - Single-pass parsing with callbacks
28
+ * <<parallel-parsing,Parallel Parsing>> - Multi-core batch processing
29
+ * <<infix-expressions,Infix Expression Parsing>> - Built-in operator precedence support
30
+ * <<security-features,Security Features>> - Input size and recursion limits
31
+ * <<debug-tools,Debug Tools>> - Tracing and grammar visualization
32
+ * <<benchmarking,Benchmarking>> - Built-in performance testing tools
33
+
34
+ == Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ [source,ruby]
39
+ ----
40
+ gem 'parsanol'
41
+ ----
42
+
43
+ And then execute:
44
+
45
+ [source,shell]
46
+ ----
47
+ bundle install
48
+ ----
49
+
50
+ Or install it yourself as:
51
+
52
+ [source,shell]
53
+ ----
54
+ gem install parsanol
55
+ ----
56
+
57
+ == Examples
58
+
59
+ The repository includes several example files demonstrating different use cases:
60
+
61
+ * link:examples/benchmark_examples.rb[] - Basic performance benchmarks
62
+ * link:examples/benchmark_full.rb[] - Comprehensive benchmark suite
63
+ * link:examples/parsing_modes.rb[] - Demonstrates all parsing modes
64
+ * link:examples/parslet_migration.rb[] - Migration guide from Parslet
65
+
66
+ [source,ruby]
67
+ ----
68
+ # Run examples
69
+ bundle exec ruby examples/benchmark_examples.rb
70
+ bundle exec ruby examples/parsing_modes.rb
71
+ bundle exec ruby examples/parslet_migration.rb
72
+ ----
73
+
74
+ == Usage
75
+
76
+ [[basic-parsing]]
77
+ === Basic Parser
78
+ Define parsers by creating a class that inherits from `Parsanol::Parser` and declaring rules:
79
+
80
+ [source,ruby]
81
+ ----
82
+ require 'parsanol'
83
+
84
+ class MyParser < Parsanol::Parser
85
+ rule(:keyword) { str('if') | str('while') }
86
+ rule(:expression) { keyword >> str('(') >> expression >> str(')') }
87
+ root(:expression)
88
+ end
89
+
90
+ parser = MyParser.new
91
+ result = parser.parse('if(x)')
92
+ ----
93
+
94
+ [[error-reporting]]
95
+ === Error Reporting
96
+ Parsanol provides detailed error messages when parsing fails:
97
+
98
+ [source,ruby]
99
+ ----
100
+ begin
101
+ parser.parse('invalid input')
102
+ rescue Parsanol::ParseFailed => e
103
+ puts e.message
104
+ # => "Expected 'if' at line 1 char 1."
105
+ end
106
+ ----
107
+
108
+ [[transformation]]
109
+ === Transformation
110
+ Convert parse trees to AST using pattern-based transformations:
111
+
112
+ [source,ruby]
113
+ ----
114
+ class MyTransform < Parsanol::Transform
115
+ rule(keyword: simple(:k)) { KeywordNode.new(k) }
116
+ rule(expression: subtree(:e)) { ExpressionNode.new(e) }
117
+ end
118
+
119
+ ast = MyTransform.new.apply(parse_tree)
120
+ ----
121
+
122
+ [[native-extension]]
123
+ === Native Extension
124
+
125
+ The Rust native extension provides significant performance improvements over pure Ruby parsing:
126
+
127
+ [cols="2,1,1,1"]
128
+ |===
129
+ | Pattern Type | Ruby (i/s) | Native (i/s) | Speedup
130
+
131
+ | Simple string match | ~575 | ~775,000 | **1,340x**
132
+ | Sequence (3 parts) | ~580 | ~530,000 | **910x**
133
+ | Named capture | ~575 | ~510,000 | **880x**
134
+ | Repetition (10x digits) | ~560 | ~740,000 | **1,310x**
135
+ | Alternative (3 options) | ~575 | ~720,000 | **1,250x**
136
+ | Calculator expression | ~570 | ~180,000 | **315x**
137
+ | Repetition with name | ~575 | ~125,000 | **220x**
138
+ | Repetition (20x) | ~560 | ~720,000 | **1,280x**
139
+ |===
140
+
141
+ For maximum performance, compile the Rust native extension:
142
+
143
+ [source,shell]
144
+ ----
145
+ # Install Rust toolchain first
146
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
147
+
148
+ # Compile the extension
149
+ bundle exec rake compile
150
+ ----
151
+
152
+ Run your own benchmarks with:
153
+
154
+ [source,ruby]
155
+ ----
156
+ bundle exec ruby examples/benchmark_examples.rb
157
+ ----
158
+
159
+ [[slice-support]]
160
+ === Slice Support
161
+
162
+ All parse results include source position information through `Parsanol::Slice` objects:
163
+
164
+ [source,ruby]
165
+ ----
166
+ # Parse returns results with position info
167
+ result = parser.parse("hello world", mode: :native)
168
+ name = result[:name]
169
+
170
+ # Access the value
171
+ name.to_s # => "hello"
172
+
173
+ # Access position information
174
+ name.offset # => 0 (byte offset in original input)
175
+ name.length # => 5
176
+ name.line_and_column # => [1, 1] (1-indexed)
177
+
178
+ # Compare with strings (Slice compares by content)
179
+ name == "hello" # => true
180
+
181
+ # Extract from original source
182
+ name.extract_from(input) # => "hello"
183
+ ----
184
+
185
+ ==== JSON Output with Position Info
186
+
187
+ When using JSON mode, position information is included inline with each value:
188
+
189
+ [source,ruby]
190
+ ----
191
+ result = parser.parse("hello", mode: :json)
192
+ # => {
193
+ # "name": {
194
+ # "value": "hello",
195
+ # "offset": 0,
196
+ # "length": 5,
197
+ # "line": 1,
198
+ # "column": 1
199
+ # }
200
+ # }
201
+ ----
202
+
203
+ This format ensures position information is available for all downstream consumers including IDEs, linters, and error reporting tools.
204
+
205
+ ==== Slice API
206
+
207
+ [source,ruby]
208
+ ----
209
+ class Parsanol::Slice
210
+ # Core attributes
211
+ def content # String content
212
+ def offset # Byte offset in original input
213
+ def length # Length of the slice
214
+ def line_and_column # [line, column] tuple (requires line cache)
215
+
216
+ # String compatibility
217
+ def to_s # Returns content
218
+ def to_str # Implicit string conversion
219
+ def ==(other) # Compares content with String or Slice
220
+
221
+ # JSON serialization
222
+ def to_json # Returns { "value" => ..., "offset" => ..., ... }
223
+ def as_json # Returns hash with position info
224
+
225
+ # Utility
226
+ def to_span(input) # Returns SourceSpan object
227
+ def extract_from(input) # Extracts content from original input
228
+ end
229
+ ----
230
+
231
+ This is essential for:
232
+
233
+ * **Linters** - Map errors back to source locations
234
+ * **IDEs** - Provide go-to-definition, hover info
235
+ * **Comment attachment** - Attach remarks to AST nodes by position
236
+ * **Source extraction** - Get original text for any parsed element
237
+
238
+ == Migrating from Parslet
239
+
240
+ Parsanol provides full Parslet API compatibility with two migration modes.
241
+
242
+ === Drop-in Replacement (Zero Code Changes)
243
+
244
+ Simply replace the parslet gem with parsanol in your Gemfile:
245
+
246
+ [source,ruby]
247
+ ----
248
+ # Gemfile
249
+ - gem 'parslet'
250
+ + gem 'parsanol'
251
+ ----
252
+
253
+ Your existing code works without modification:
254
+
255
+ [source,ruby]
256
+ ----
257
+ # No changes needed!
258
+ require 'parslet' # Parsanol aliases itself
259
+
260
+ class MyParser < Parslet::Parser
261
+ rule(:number) { match('[0-9]').repeat(1) }
262
+ root(:number)
263
+ end
264
+
265
+ parser = MyParser.new
266
+ parser.parse('123') # Works exactly the same
267
+ ----
268
+
269
+ === API Compatibility Matrix
270
+
271
+ [cols="2,1,3"]
272
+ |===
273
+ | Parslet API | Status | Notes
274
+
275
+ | `str('foo')` | ✅ | Literal string match
276
+ | `match('[0-9]')` | ✅ | Character class
277
+ | `any` | ✅ | Any single character
278
+ | `>>` (sequence) | ✅ | Sequential composition
279
+ | `\|` (choice) | ✅ | Ordered choice
280
+ | `.repeat(n, m)` | ✅ | Repetition with bounds
281
+ | `.maybe` | ✅ | Optional (zero or one)
282
+ | `.as(:name)` | ✅ | Label capture
283
+ | `.absent?` | ✅ | Negative lookahead
284
+ | `.present?` | ✅ | Positive lookahead
285
+ | `infix_expression` | ✅ | Precedence climbing
286
+ | `exp('...')` | ✅ | Treetop-style expression parsing
287
+ | `Parslet::Transform` | ✅ | Tree transformation
288
+ | `simple(:x)` | ✅ | Match simple value
289
+ | `sequence(:x)` | ✅ | Match array of values
290
+ | `subtree(:x)` | ✅ | Match any subtree
291
+ | `Parslet::Slice` | ✅ | Parsanol::Slice compatible
292
+ | `.capture(:name)` | ✅ | Named capture extraction (NEW in 1.2.0)
293
+ | `scope { }` | ✅ | Isolated capture context (NEW in 1.2.0)
294
+ | `dynamic { \|ctx\| }` | ✅ | Runtime-determined parsing (NEW in 1.2.0)
295
+ |===
296
+
297
+ NOTE: The new capture, scope, and dynamic atoms provide powerful extraction and context-sensitive parsing capabilities. See the <<captures,Captures>> section for details.
298
+
299
+ == Architecture
300
+
301
+ .Parsanol architecture overview
302
+ [source]
303
+ ----
304
+ ┌─────────────────────────────────────┐
305
+ │ User Parser │
306
+ │ (inherits from Parsanol::Parser) │
307
+ └─────────────────┬───────────────────┘
308
+
309
+ ┌─────────────────▼───────────────────┐
310
+ │ Parsing Backend │
311
+ ├─────────────────┬───────────────────┤
312
+ │ Pure Ruby │ Rust Native │
313
+ │ (default) │ (optional) │
314
+ └─────────────────┴───────────────────┘
315
+
316
+ ┌─────────────────▼───────────────────┐
317
+ │ Parse Tree │
318
+ │ (with Slice position info) │
319
+ └─────────────────┬───────────────────┘
320
+
321
+ ┌─────────────────▼───────────────────┐
322
+ │ Parsanol::Transform │
323
+ │ (pattern-based transformation) │
324
+ └─────────────────┬───────────────────┘
325
+
326
+ ┌─────────────────▼───────────────────┐
327
+ │ User AST │
328
+ └─────────────────────────────────────┘
329
+ ----
330
+
331
+ === Parse Modes
332
+
333
+ Parsanol offers 3 parsing modes through the `parse` method. All modes return `Parsanol::Slice` objects with position information:
334
+
335
+ [source,ruby]
336
+ ----
337
+ result = parser.parse(input, mode: :native) # mode is optional, :native is default
338
+ ----
339
+
340
+ [cols="1,1,1,2,2"]
341
+ |===
342
+ | Mode | Backend | Keys | Values | Best For
343
+
344
+ | `:ruby` | Pure Ruby | Symbol | Slice | Debugging, fallback
345
+ | `:native` | Rust FFI | Symbol | Slice | **Production (DEFAULT)**
346
+ | `:json` | Rust FFI | String | Hash + position | APIs, serialization
347
+ |===
348
+
349
+ All modes include position info (offset, length, line, column) by default.
350
+
351
+ ==== Mode Details
352
+
353
+ **Ruby Mode** (`:ruby`)::
354
+ Pure Ruby parsing engine. Use for debugging grammar issues or when native extension is unavailable.
355
+
356
+ **Native Mode** (`:native`)::
357
+ Rust parser via FFI with automatic transformation to Ruby-friendly format (Symbol keys). ~20x faster than pure Ruby. This is the **default mode**.
358
+
359
+ **JSON Mode** (`:json`)::
360
+ Rust parser that returns JSON-serializable output with inline position information. Use for APIs and when you need JSON-compatible output.
361
+
362
+ === ZeroCopy Interface (Low-Level API)
363
+
364
+ For maximum performance (~29x faster than pure Ruby), use the ZeroCopy interface which bypasses Ruby transformation:
365
+
366
+ [source,ruby]
367
+ ----
368
+ # Low-level API: Direct Rust access, String keys
369
+ grammar = Parsanol::Native.serialize_grammar(parser.root)
370
+ result = Parsanol::Native.parse_to_ruby_objects(grammar, input)
371
+ # Returns: { "name" => Slice("hello", offset: 0, length: 5) }
372
+
373
+ # High-level ZeroCopy: Include module for direct Ruby objects
374
+ class FastParser < Parsanol::Parser
375
+ include Parsanol::ZeroCopy
376
+
377
+ rule(:number) { match('[0-9]').repeat(1) }
378
+ root(:number)
379
+
380
+ output_types(number: MyNumberClass) # Map to Ruby classes
381
+ end
382
+
383
+ parser = FastParser.new
384
+ expr = parser.parse("42") # Returns MyNumberClass instance directly
385
+ ----
386
+
387
+ [cols="1,2,2"]
388
+ |===
389
+ | Method | Keys | Use Case
390
+
391
+ | `parse_to_ruby_objects` | String | Low-level, Slice objects directly from Rust
392
+ | `Parsanol::ZeroCopy` module | Ruby objects | Maximum performance, direct object construction
393
+ |===
394
+
395
+ NOTE: ZeroCopy requires the native extension and type mapping definitions.
396
+
397
+ ==== When to Use Parse Modes vs ZeroCopy
398
+
399
+ [cols="1,2,2"]
400
+ |===
401
+ | Your Need | Use This | Why
402
+
403
+ | Building an API | JSON mode (`:json`) | Direct JSON serialization
404
+ | Building a linter/IDE | Native mode (`:native`) | Position info for errors
405
+ | Need position info | Parse Modes (not ZeroCopy) | ZeroCopy skips position tracking
406
+ | High-throughput parsing | ZeroCopy | Maximum performance
407
+ | Type-safe AST with methods | ZeroCopy | Direct typed object construction
408
+ | Debugging grammar | Ruby mode (`:ruby`) | Pure Ruby, easier to trace
409
+ |===
410
+
411
+ ==== ZeroCopy Example: Calculator with Direct Object Construction
412
+
413
+ [source,ruby]
414
+ ----
415
+ # 1. Define your AST classes with methods
416
+ module Calculator
417
+ class Expr
418
+ def eval = raise NotImplementedError
419
+ end
420
+
421
+ class Number < Expr
422
+ attr_reader :value
423
+ def initialize(value) = @value = value
424
+ def eval = @value
425
+ end
426
+
427
+ class BinOp < Expr
428
+ attr_reader :left, :op, :right
429
+ def initialize(left:, op:, right:)
430
+ @left, @op, @right = left, op, right
431
+ end
432
+ def eval
433
+ case @op
434
+ when '+' then @left.eval + @right.eval
435
+ when '-' then @left.eval - @right.eval
436
+ when '*' then @left.eval * @right.eval
437
+ when '/' then @left.eval / @right.eval
438
+ end
439
+ end
440
+ end
441
+ end
442
+
443
+ # 2. Define parser with ZeroCopy and output_types
444
+ class CalculatorParser < Parsanol::Parser
445
+ include Parsanol::ZeroCopy
446
+
447
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
448
+ rule(:expression) { (number.as(:left) >> add_op >> expression.as(:right)).as(:binop) | number }
449
+ root(:expression)
450
+
451
+ # Map rules to Ruby classes - Rust constructs these directly!
452
+ output_types(
453
+ number: Calculator::Number,
454
+ binop: Calculator::BinOp
455
+ )
456
+ end
457
+
458
+ # 3. Parse and evaluate - no transform needed!
459
+ parser = CalculatorParser.new
460
+ expr = parser.parse("2 + 3 * 4") # Returns Calculator::BinOp directly
461
+ puts expr.eval # => 14 (with proper precedence)
462
+ ----
463
+
464
+ ==== Low-Level ZeroCopy: `parse_to_ruby_objects`
465
+
466
+ When you don't need typed objects, use `parse_to_ruby_objects` for direct Slice access:
467
+
468
+ [source,ruby]
469
+ ----
470
+ # Direct FFI call - bypasses transformation, String keys
471
+ grammar = Parsanol::Native.serialize_grammar(MyParser.new.root)
472
+ result = Parsanol::Native.parse_to_ruby_objects(grammar, input)
473
+
474
+ # Result structure (String keys, Slice values):
475
+ # { "name" => Slice("hello", offset: 0, length: 5),
476
+ # "value" => Slice("42", offset: 10, length: 2) }
477
+
478
+ # Access position info directly
479
+ result["name"].offset # => 0
480
+ result["name"].to_s # => "hello"
481
+ ----
482
+
483
+ ==== ZeroCopy Requirements
484
+
485
+ The ZeroCopy module requires:
486
+
487
+ 1. **Native extension** - Run `bundle exec rake compile`
488
+ 2. **Type mapping** - Define `output_types` in your parser
489
+ 3. **Matching constructors** - Your Ruby classes must accept the parsed attributes
490
+
491
+ For complex types, you may also need Rust-side type definitions with `#[derive(RubyObject)]` for full zero-copy FFI construction.
492
+
493
+ === Parsing Backends (Rust Core)
494
+
495
+ Behind the scenes, the Rust implementation uses one of two parsing backends:
496
+
497
+ [cols="2,2,3"]
498
+ |===
499
+ | Backend | Use Case | Characteristics
500
+
501
+ | Packrat (default) | Complex grammars | O(n) guaranteed, higher memory
502
+ | Bytecode VM | Simple patterns | Lower memory, faster for linear patterns
503
+ | Auto | Variable workloads | Analyzes grammar, selects best backend
504
+ |===
505
+
506
+ The Ruby bindings automatically use the best backend for your grammar:
507
+
508
+ * Uses `Backend::Auto` by default (same as parsanol-rs)
509
+ * Detects nested repetitions, overlapping choices
510
+ * Recommends Packrat for complex grammars
511
+ * Falls back to Bytecode for simple patterns
512
+
513
+ NOTE: The backend selection is transparent to Ruby users. The parser object automatically uses the optimal backend based on grammar analysis.
514
+
515
+ For more details on backend selection and grammar analysis, see the https://parsanol.github.io/backends[Parsing Backends documentation].
516
+
517
+ [[captures]]
518
+ == Captures, Scopes, and Dynamic Atoms
519
+
520
+ Parsanol 1.2.0 introduces powerful new features for extracting and managing parsed data.
521
+
522
+ [[capture-atoms]]
523
+ === Capture Atoms
524
+
525
+ Extract named values from parsed input, similar to named groups in regular expressions:
526
+
527
+ [source,ruby]
528
+ ----
529
+ require 'parsanol/parslet'
530
+
531
+ include Parsanol::Parslet
532
+
533
+ # Basic capture
534
+ parser = str('hello').capture(:greeting)
535
+ result = parser.parse("hello")
536
+ puts result[:greeting].to_s # => "hello"
537
+
538
+ # Multiple captures - parse key=value pairs
539
+ kv_parser = match('[a-z]+').capture(:key) >>
540
+ str('=') >>
541
+ match('[a-zA-Z0-9]+').capture(:value)
542
+
543
+ result = kv_parser.parse("name=Alice")
544
+ puts result[:key].to_s # => "name"
545
+ puts result[:value].to_s # => "Alice"
546
+ ----
547
+
548
+ [[scope-atoms]]
549
+ === Scope Atoms
550
+
551
+ Create isolated capture contexts. Captures inside a scope are discarded when the scope exits:
552
+
553
+ [source,ruby]
554
+ ----
555
+ # Without scope: inner captures leak out
556
+ parser = str('a').capture(:temp) >> str('b') >> str('c').capture(:temp)
557
+
558
+ # With scope: inner captures are discarded
559
+ parser = str('prefix').capture(:outer) >>
560
+ scope { str('inner').capture(:inner) } >>
561
+ str('suffix').capture(:outer_end)
562
+
563
+ result = parser.parse("prefix inner suffix")
564
+ puts result[:inner] # => nil (discarded)
565
+ puts result[:outer] # => "prefix"
566
+ ----
567
+
568
+ Scopes are essential for:
569
+ - Parsing nested structures without capture pollution
570
+ - Recursive parsing with isolated capture state
571
+ - Memory-bounded parsing of repeated structures
572
+
573
+ [[dynamic-atoms]]
574
+ === Dynamic Atoms
575
+
576
+ Runtime-determined parsing via callbacks. The grammar can change based on context:
577
+
578
+ [source,ruby]
579
+ ----
580
+ # Type-driven value parsing
581
+ class TypeParser < Parsanol::Parser
582
+ include Parsanol::Parslet
583
+
584
+ rule(:type) { match('[a-z]+').capture(:type) }
585
+ rule(:value) do
586
+ dynamic do |ctx|
587
+ case ctx[:type].to_s
588
+ when 'int' then match('\d+')
589
+ when 'str' then match('[a-z]+')
590
+ when 'bool' then str('true') | str('false')
591
+ else match('[a-z]+')
592
+ end.capture(:value)
593
+ end
594
+ end
595
+ rule(:declaration) { type >> str(':') >> match('[a-z]+').capture(:name) >> str('=') >> value }
596
+ root :declaration
597
+ end
598
+
599
+ parser = TypeParser.new
600
+ result = parser.parse("int:count=42")
601
+ puts result[:type].to_s # => "int"
602
+ puts result[:value].to_s # => "42"
603
+ ----
604
+
605
+ The `DynamicContext` provides:
606
+ - `ctx[:name]` - Access captured values
607
+ - `ctx.remaining` - Remaining input from current position
608
+ - `ctx.pos` - Current byte position
609
+ - `ctx.input` - Full input string
610
+
611
+ [[streaming-builder]]
612
+ == Streaming Builder API
613
+ For maximum performance, use the streaming builder API which eliminates intermediate AST construction:
614
+
615
+ [source,ruby]
616
+ ----
617
+ require 'parsanol'
618
+
619
+ class StringCollector
620
+ include Parsanol::BuilderCallbacks
621
+
622
+ def initialize
623
+ @strings = []
624
+ end
625
+
626
+ def on_string(value, offset, length)
627
+ @strings << value
628
+ end
629
+
630
+ def finish
631
+ @strings
632
+ end
633
+ end
634
+
635
+ grammar = Parsanol::Native.serialize_grammar(MyParser.new.root)
636
+ builder = StringCollector.new
637
+ result = Parsanol::Native.parse_with_builder(grammar, input, builder)
638
+ # result: ["hello", "world"]
639
+ ----
640
+
641
+ ==== Available Callback Methods
642
+
643
+ [cols="1,3,2"]
644
+ |===
645
+ | Method | Description | Default
646
+
647
+ | `on_start(input)` | Parsing started | No-op
648
+ | `on_success` | Parsing succeeded | No-op
649
+ | `on_error(message)` | Parsing failed | No-op
650
+ | `on_string(value, offset, length)` | String/slice matched | No-op
651
+ | `on_int(value)` | Integer matched | No-op
652
+ | `on_float(value)` | Float matched | No-op
653
+ | `on_bool(value)` | Boolean matched | No-op
654
+ | `on_nil` | Nil matched | No-op
655
+ | `on_hash_start(size)` | Entering a hash/object | No-op
656
+ | `on_hash_key(key)` | Hash key encountered | No-op
657
+ | `on_hash_end(size)` | Exiting a hash/object | No-op
658
+ | `on_array_start(size)` | Entering an array | No-op
659
+ | `on_array_end(size)` | Exiting an array | No-op
660
+ | `finish` | Parsing complete | Returns nil
661
+ |===
662
+
663
+ [[parallel-parsing]]
664
+ == Parallel Parsing
665
+ Parse multiple inputs using all CPU cores:
666
+
667
+ [source,ruby]
668
+ ----
669
+ require 'parsanol/parallel'
670
+
671
+ grammar = MyParser.new.serialize_grammar
672
+ inputs = Dir.glob("*.json").map { |f| File.read(f) }
673
+
674
+ # Parse all files in parallel
675
+ results = Parsanol::Parallel.parse_batch(grammar, inputs)
676
+
677
+ # With configuration
678
+ config = Parsanol::Parallel::Config.new
679
+ .with_num_threads(4)
680
+ .with_min_chunk_size(50)
681
+
682
+ results = Parsanol::Parallel.parse_batch(grammar, inputs, config: config)
683
+ ----
684
+
685
+ [[infix-expressions]]
686
+ == Infix Expression Parsing
687
+ Built-in support for parsing infix expressions with operator precedence:
688
+
689
+ [source,ruby]
690
+ ----
691
+ class CalculatorParser < Parsanol::Parser
692
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
693
+ rule(:primary) { number | str('(') >> expr >> str(')') }
694
+
695
+ rule(:expr) {
696
+ infix_expression(primary,
697
+ [str('*'), 2, :left],
698
+ [str('/'), 2, :left],
699
+ [str('+'), 1, :left],
700
+ [str('-'), 1, :left],
701
+ [str('^'), 3, :right] # Right-associative
702
+ )
703
+ }
704
+ root(:expr)
705
+ end
706
+ ----
707
+
708
+ [[treetop-expressions]]
709
+ == Treetop Expression Syntax
710
+ Parsanol supports treetop-style expression strings for quick grammar definition:
711
+
712
+ [source,ruby]
713
+ ----
714
+ # Using exp() for treetop-style expressions
715
+ class QuickParser < Parsanol::Parser
716
+ rule(:word) { exp("'a' 'b' ?") } # 'a' followed by optional 'b'
717
+ root(:word)
718
+ end
719
+
720
+ # Equivalent to:
721
+ rule(:word) { str('a') >> str('b').maybe }
722
+ ----
723
+
724
+ === Treetop Syntax Reference
725
+
726
+ [cols="2,3"]
727
+ |===
728
+ | Syntax | Description
729
+
730
+ | `'hello'` | Literal string match
731
+ | `[a-z]` | Character class
732
+ | `.` | Any single character
733
+ | `'a' 'b'` | Sequence (concatenation)
734
+ | `'a' / 'b'` | Alternative (choice)
735
+ | `'a' ?` | Optional (zero or one)
736
+ | `'a' *` | Zero or more repetitions
737
+ | `'a' +` | One or more repetitions
738
+ | `'a'{2,5}` | Between 2 and 5 repetitions
739
+ | `('a' / 'b')` | Grouping
740
+ |===
741
+
742
+ [NOTE]
743
+ ====
744
+ Whitespace is required before operators: `'a' ?` not `'a'?`
745
+ ====
746
+
747
+ === Expression Parsing Performance
748
+
749
+ The expression parser is pure Ruby (not Rust-accelerated) since it runs only at grammar definition time. The resulting atoms can still be used with Rust-accelerated parsing:
750
+
751
+ [source,ruby]
752
+ ----
753
+ atom = Parsanol.exp("'a' +")
754
+
755
+ # Ruby parsing
756
+ atom.parse('aaa')
757
+
758
+ # Rust-accelerated parsing (if native extension available)
759
+ grammar = Parsanol::Native.serialize_grammar(atom)
760
+ Parsanol::Native.parse_to_ruby_objects(grammar, 'aaa')
761
+ ----
762
+
763
+ [[security-features]]
764
+ == Security Features
765
+ For parsing untrusted input, use built-in limits:
766
+
767
+ [source,ruby]
768
+ ----
769
+ result = Parsanol::Native.parse_with_limits(
770
+ grammar_json,
771
+ untrusted_input,
772
+ max_input_size: 10 * 1024 * 1024, # 10 MB max
773
+ max_recursion_depth: 100 # Limit recursion
774
+ )
775
+ ----
776
+
777
+ [[debug-tools]]
778
+ == Debug Tools
779
+ Enable tracing for debugging grammars:
780
+
781
+ [source,ruby]
782
+ ----
783
+ # Parse with trace
784
+ result, trace = Parsanol::Native.parse_with_trace(grammar_json, input)
785
+ puts trace
786
+
787
+ # Generate grammar visualization
788
+ mermaid = Parsanol::Native.grammar_to_mermaid(grammar_json)
789
+ dot = Parsanol::Native.grammar_to_dot(grammar_json)
790
+ ----
791
+
792
+ == Development
793
+
794
+ === Setup
795
+
796
+ [source,shell]
797
+ ----
798
+ bundle install
799
+ ----
800
+
801
+ === Testing
802
+
803
+ [source,shell]
804
+ ----
805
+ # Run all tests
806
+ bundle exec rake spec
807
+
808
+ # Run unit tests only
809
+ bundle exec rake spec:unit
810
+
811
+ # Run specific test file
812
+ bundle exec rspec spec/parsanol/atoms/str_spec.rb
813
+ ----
814
+
815
+ === Compiling Native Extension
816
+
817
+ [source,shell]
818
+ ----
819
+ # Install Rust (if not already installed)
820
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
821
+
822
+ # Compile the native extension
823
+ bundle exec rake compile
824
+
825
+ # Verify native extension is working
826
+ ruby -I lib -e "require 'parsanol'; puts Parsanol::Native.available?"
827
+ # => true
828
+ ----
829
+
830
+ === Running Benchmarks
831
+
832
+ [source,shell]
833
+ ----
834
+ # Quick benchmarks (examples provided in the repository)
835
+ bundle exec ruby examples/benchmark_examples.rb
836
+
837
+ # Full benchmark suite
838
+ bundle exec ruby examples/benchmark_full.rb
839
+
840
+ # Compare parsing modes
841
+ bundle exec ruby examples/parsing_modes.rb
842
+
843
+ # Parslet migration examples
844
+ bundle exec ruby examples/parslet_migration.rb
845
+ ----
846
+
847
+ [[benchmarking]]
848
+ == Benchmarking Your Own Code
849
+
850
+ Parsanol includes built-in benchmarking tools to measure performance:
851
+
852
+ [source,ruby]
853
+ ----
854
+ require 'parsanol'
855
+ require 'benchmark/ips'
856
+
857
+ # Your grammar
858
+ grammar = Parsanol.str("hello").as(:greeting)
859
+ json = Parsanol::Native.serialize_grammar(grammar)
860
+
861
+ # Benchmark
862
+ Benchmark.ips do |x|
863
+ x.config(warmup: 5, time: 3)
864
+
865
+ x.report("Ruby mode") { grammar.parse("hello", mode: :ruby) }
866
+ x.report("Native mode") { Parsanol::Native.parse(json, "hello") }
867
+
868
+ x.compare!
869
+ end
870
+ ----
871
+
872
+ === Performance Factors
873
+
874
+ The actual speedup depends on several factors:
875
+
876
+ 1. **Grammar complexity** - More complex grammars have more transformation overhead
877
+ 2. **Named captures** - More named captures mean more hash construction
878
+ 3. **Repetition patterns** - Named repetitions have more overhead than unnamed
879
+ 4. **Input size** - Larger inputs benefit more from native parsing
880
+
881
+ [cols="1,2,2"]
882
+ |===
883
+ | Pattern | Speedup | Reason for Difference
884
+
885
+ | Simple strings | ~1,300x | Minimal transformation
886
+ | Sequences | ~900x | String joining overhead
887
+ | Alternatives | ~1,250x | Fast path in Rust
888
+ | Repetitions (unnamed) | ~1,300x | Minimal transformation
889
+ | Repetitions (named) | ~220x | Array of hashes construction
890
+ | Complex expressions | ~315x | More grammar elements
891
+ |===
892
+
893
+ == License
894
+
895
+ MIT License - see LICENSE file for details.
896
+
897
+ == Acknowledgments
898
+
899
+ Parsanol is inspired by the https://github.com/kschiess/parslet[Parslet] library. We thank Kaspar Schiess and all Parslet contributors for creating an excellent parser library that served as inspiration for this project.
900
+
901
+ == Resources
902
+
903
+ * https://github.com/parsanol/parsanol-ruby[GitHub Repository]
904
+ * https://github.com/parsanol/parsanol-rs[Rust Crate]
905
+ * https://github.com/kschiess/parslet[Original Parslet Library]