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.
- checksums.yaml +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- 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]
|