@parsanol/wasm 0.8.11

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.
package/README.md ADDED
@@ -0,0 +1,2058 @@
1
+ # Parsanol-rs
2
+
3
+ A high-performance PEG (Parsing Expression Grammar) parser library for Rust with packrat memoization and arena allocation.
4
+
5
+ [![Crates.io](https://img.shields.io/crates/v/parsanol.svg)](https://crates.io/crates/parsanol)
6
+ [![Documentation](https://docs.rs/parsanol/badge.svg)](https://docs.rs/parsanol)
7
+ [![License](https://img.shields.io/github/license/parsanol/parsanol-rs.svg)](https://github.com/parsanol/parsanol-rs/blob/main/LICENSE)
8
+ [![CI](https://github.com/parsanol/parsanol-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/parsanol/parsanol-rs/actions/workflows/ci.yml)
9
+
10
+ ## Purpose
11
+
12
+ Parsanol-rs is a generic, domain-agnostic PEG parser library written in
13
+ Rust. It provides high-performance parsing capabilities with a focus on:
14
+
15
+ - **Speed**: Packrat memoization for O(n) parsing complexity
16
+
17
+ - **Memory efficiency**: Arena allocation for zero-copy AST construction
18
+
19
+ - **Developer experience**: Fluent API for building grammars, rich error
20
+ reporting
21
+
22
+ - **Flexibility**: Transform system for converting parse trees to typed
23
+ Rust structs via derive macros
24
+
25
+ ## Features
26
+
27
+ - [Quick Start](#quick-start) - Get started in minutes
28
+ - [Backend Abstraction](#backend-abstraction) - Extensible backend trait system
29
+ - [Bytecode Backend](#bytecode-backend) - Optional VM backend for linear patterns
30
+ - [Parser DSL](#parser-dsl) - Fluent API for grammar definition
31
+ - [Capture Atoms](#capture-atoms) - Extract named values during parsing
32
+ - [Scope Atoms](#scope-atoms) - Isolated capture contexts
33
+ - [Dynamic Atoms](#dynamic-atoms) - Runtime-determined parsing via callbacks
34
+ - [Streaming with Captures](#streaming-with-captures) - Memory-efficient parsing with capture support
35
+ - [Transform System](#transform-system) - Convert parse trees to typed structs
36
+ - [Derive Macros](#derive-macros) - Automatic typed AST generation
37
+ - [Streaming Builder](#streaming-builder) - Single-pass parsing with custom output
38
+ - [Parallel Parsing](#parallel-parsing) - Multi-file parsing with rayon
39
+ - [Infix Expression Parsing](#infix-expression-parsing) - Built-in operator precedence
40
+ - [Rich Error Reporting](#rich-error-reporting) - Tree-structured error messages
41
+ - [Source Location Tracking](#source-location-tracking) - Line/column tracking through transforms
42
+ - [Grammar Composition](#grammar-composition) - Import and compose grammars
43
+ - [Ruby FFI](#ruby-ffi) - Optional Ruby bindings
44
+ - [WASM Support](#wasm-support) - Optional WebAssembly bindings
45
+
46
+ # Bytecode Backend
47
+
48
+ Parsanol-rs supports two parsing backends:
49
+
50
+ 1. **Packrat (default)**: Memoization-based parser with O(n) time complexity for all grammars
51
+ 2. **Bytecode VM**: Stack-based virtual machine with optimization passes
52
+
53
+ ## Backend Comparison
54
+
55
+ Both backends produce **identical parsing results** for all valid inputs. The difference lies in performance characteristics:
56
+
57
+ | Aspect | Packrat | Bytecode VM |
58
+ |--------|---------|-------------|
59
+ | **Time Complexity** | Guaranteed O(n) | O(n) to O(2^n) depending on grammar |
60
+ | **Memory Usage** | Higher (memoization table) | Lower (stack-based) |
61
+ | **Compilation** | None required | Pre-compilation needed |
62
+ | **Nested Repetitions** | Handles efficiently | Can be exponential |
63
+ | **Simple Patterns** | Good | Excellent |
64
+ | **Predictability** | Consistent performance | Varies by grammar |
65
+
66
+ ### Performance Characteristics
67
+
68
+ **Packrat Backend:**
69
+ - Memoization stores parse results at each position
70
+ - Guarantees O(n) time complexity regardless of grammar structure
71
+ - Memory overhead scales with input size and grammar complexity
72
+ - Ideal when predictable performance is required
73
+
74
+ **Bytecode VM Backend:**
75
+ - Stack-based execution with backtracking
76
+ - O(n) for linear patterns (most common case)
77
+ - Can exhibit O(2^n) behavior for pathological patterns like `(a*)*`
78
+ - Lower memory footprint, good for memory-constrained environments
79
+ - Pre-compilation enables optimization passes
80
+
81
+ ### Decision Matrix
82
+
83
+ | Grammar Type | Recommended Backend | Reason |
84
+ |--------------|---------------------|--------|
85
+ | JSON, XML, config files | Either | Linear patterns, both perform well |
86
+ | Programming languages | Packrat | Complex grammar with nested structures |
87
+ | Log parsing | Bytecode | Simple patterns, streaming potential |
88
+ | Nested repetitions `(a*)*` | Packrat | Avoids exponential backtracking |
89
+ | Memory-constrained | Bytecode | Lower memory footprint |
90
+ | Need predictable O(n) | Packrat | Guaranteed linear time |
91
+
92
+ ### Automatic Selection
93
+
94
+ Use `Backend::Auto` (the default) to let parsanol analyze your grammar:
95
+
96
+ ```rust
97
+ // Automatic selection (default)
98
+ let mut parser = Parser::auto(grammar);
99
+
100
+ // Or explicitly:
101
+ let mut parser = Parser::new(grammar, Backend::Auto);
102
+
103
+ // Check the analysis
104
+ let analysis = parser.analysis();
105
+ println!("Has nested repetitions: {}", analysis.has_nested_repetition);
106
+ println!("Recommended: {:?}", analysis.recommended_backend());
107
+ ```
108
+
109
+ ### Why Nested Repetitions Are the Criterion
110
+
111
+ The backend selection is based on a **single hard rule**:
112
+
113
+ - **Has nested repetitions** (e.g., `(a*)*`) → **Packrat**
114
+ - **Otherwise** → **Bytecode**
115
+
116
+ This is the only criterion because nested repetitions are the **only pattern that causes exponential time complexity** in the bytecode backend. Here's why:
117
+
118
+ **The Algorithmic Problem:**
119
+
120
+ When a repetition contains another repetition, the parser must try all possible ways to divide the input. For pattern `(a*)*` on input "aaa":
121
+
122
+ ```
123
+ Division 1: (aaa) - outer * matches 1 group
124
+ Division 2: (aa)(a) - outer * matches 2 groups
125
+ Division 3: (a)(aa) - outer * matches 2 groups (different split)
126
+ Division 4: (a)(a)(a) - outer * matches 3 groups
127
+ ... and so on
128
+ ```
129
+
130
+ The number of ways to partition n characters is O(2^n). The bytecode VM tries each possibility via backtracking, leading to exponential time.
131
+
132
+ **Why Packrat Solves It:**
133
+
134
+ Packrat memoizes results by (position, rule). Once `(a*)` is evaluated at position i, the result is cached. Subsequent evaluations at the same position are O(1) cache hits. This guarantees O(n) total time.
135
+
136
+ **Why Other Patterns Don't Matter:**
137
+
138
+ | Pattern | Time Impact | Backend Difference |
139
+ |---------|-------------|-------------------|
140
+ | Overlapping choices (`"a" \| "aa"`) | Linear backtracking | Both handle identically |
141
+ | Deep nesting | Stack depth increases | Both handle fine |
142
+ | Many alternatives | More choice points | Linear in alternative count |
143
+ | Left recursion | Infinite loop | **Both fail** - not a backend issue |
144
+
145
+ ### How the Analysis Works
146
+
147
+ The grammar analysis is deliberately simple:
148
+
149
+ ```rust
150
+ pub struct GrammarAnalysis {
151
+ /// Total atoms in the grammar
152
+ pub atom_count: usize,
153
+ /// Whether any Repetition contains another Repetition
154
+ pub has_nested_repetition: bool,
155
+ }
156
+ ```
157
+
158
+ The algorithm iterates through all atoms and checks: "Is this a Repetition whose inner atom is also a Repetition?"
159
+
160
+ ```rust
161
+ for atom in &grammar.atoms {
162
+ if let Atom::Repetition { atom: inner_idx, .. } = atom {
163
+ if let Some(inner) = grammar.get_atom(*inner_idx) {
164
+ if matches!(inner, Atom::Repetition { .. }) {
165
+ has_nested_repetition = true;
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ This is O(atoms) time and detects the only pattern that matters for backend selection.
174
+
175
+ ### When to Override Auto-Selection
176
+
177
+ The auto-selection only considers **time complexity**. You may want to manually select based on:
178
+
179
+ | Scenario | Manual Selection | Rationale |
180
+ |----------|------------------|-----------|
181
+ | **Memory-constrained** (embedded, WASM) | `Backend::Bytecode` | Lower memory: O(depth) vs O(n×rules) |
182
+ | **Very large files** (>100MB) | `Backend::Bytecode` | Packrat table grows with input size |
183
+ | **Predictable latency required** | `Backend::Packrat` | Guaranteed O(n), no pathological cases |
184
+ | **Streaming parsing** | `Backend::Bytecode` | Packrat requires full input in memory |
185
+ | **Incremental re-parsing** | `Backend::Packrat` | Memo table can be reused for unchanged portions |
186
+ | **Grammar has nested repetitions but input is bounded** | Either | If input is always small, exponential doesn't matter |
187
+ | **Testing/debugging** | `Backend::Packrat` | Consistent behavior across all inputs |
188
+
189
+ ```rust
190
+ // Memory-constrained environment
191
+ let mut parser = Parser::bytecode(grammar);
192
+
193
+ // Safety-critical with guaranteed O(n)
194
+ let mut parser = Parser::packrat(grammar);
195
+
196
+ // Explicit choice regardless of analysis
197
+ let mut parser = Parser::new(grammar, Backend::Packrat);
198
+ ```
199
+
200
+ ### Problematic Grammar Patterns
201
+
202
+ The following patterns can cause exponential O(2^n) behavior in the Bytecode backend.
203
+ They are **safe with Packrat** due to memoization. If your grammar contains these,
204
+ use Packrat explicitly or rely on `Backend::Auto`.
205
+
206
+ **Critical Pattern: Nested Repetitions**
207
+ ```
208
+ (a*)* // CRITICAL: Outer * tries O(2^n) ways to divide input
209
+ (a+)+ // Same issue
210
+ ((a|b)*)* // Even worse with choice
211
+
212
+ // Safe alternatives:
213
+ a* // Single repetition - O(n)
214
+ (a b)* // Fixed sequence inside - O(n)
215
+ ```
216
+
217
+ **Moderate Pattern: Overlapping Choice Prefixes**
218
+ ```
219
+ // Problematic: All start with 'a'
220
+ ("a" | "aa" | "aaa")+
221
+
222
+ // Better: Distinct first characters
223
+ ("a" | "b" | "c")+
224
+ ```
225
+
226
+ **Safe Pattern: Deep Recursion (Both handle well)**
227
+ ```
228
+ expr = term (("+" | "-") term)*
229
+ // Recursive but structured - both backends handle efficiently
230
+ ```
231
+
232
+ ### Analyzing Your Grammar
233
+
234
+ Use the GrammarAnalysis API to check for nested repetitions:
235
+
236
+ ```rust
237
+ use parsanol::portable::{
238
+ parser_dsl::{str, re, GrammarBuilder},
239
+ bytecode::{Backend, GrammarAnalysis, Parser},
240
+ };
241
+
242
+ fn main() {
243
+ let grammar = GrammarBuilder::new()
244
+ .rule("expr", re(r"[0-9]+"))
245
+ .build();
246
+
247
+ // Analyze the grammar
248
+ let analysis = GrammarAnalysis::analyze(&grammar);
249
+
250
+ // The only field that matters for backend selection
251
+ if analysis.has_nested_repetition {
252
+ println!("⚠️ Nested repetitions detected - use Packrat!");
253
+ } else {
254
+ println!("✅ No nested repetitions - Bytecode is efficient");
255
+ }
256
+
257
+ // Get recommendation (hard rule: nested repetition → Packrat, else → Bytecode)
258
+ println!("Recommended: {:?}", analysis.recommended_backend());
259
+ }
260
+ ```
261
+
262
+ **GrammarAnalysis Fields:**
263
+
264
+ | Field | Type | Purpose |
265
+ |-------|------|---------|
266
+ | `atom_count` | `usize` | Number of atoms in grammar (informational) |
267
+ | `has_nested_repetition` | `bool` | **The criterion** - if true, use Packrat |
268
+
269
+ **The `recommended_backend()` Method:**
270
+
271
+ Returns `Backend::Packrat` if `has_nested_repetition` is true, otherwise `Backend::Bytecode`. This is what `Backend::Auto` uses internally.
272
+
273
+ ## Using the Bytecode Backend
274
+
275
+ ```rust
276
+ use parsanol::portable::{
277
+ parser_dsl::{str, re, GrammarBuilder},
278
+ bytecode::{Backend, Parser},
279
+ };
280
+
281
+ let grammar = GrammarBuilder::new()
282
+ .rule("number", re(r"[0-9]+"))
283
+ .build();
284
+
285
+ // Create parser with bytecode backend
286
+ let mut parser = Parser::new(grammar, Backend::Bytecode);
287
+ let result = parser.parse("42");
288
+
289
+ // Or use auto-selection (analyzes grammar complexity)
290
+ let mut parser = Parser::auto(grammar);
291
+ let result = parser.parse("42");
292
+ ```
293
+
294
+ ### Known Differences
295
+
296
+ Both backends produce **identical results** for the vast majority of patterns. However, there are edge cases where behavior differs:
297
+
298
+ **Alternatives in Sequences**: For patterns like `("a" | "aa") "b"` on input `"aab"`:
299
+ - **Packrat**: May succeed due to memoization re-evaluation
300
+ - **Bytecode**: Fails (standard PEG semantics - once "a" succeeds, "aa" is not tried)
301
+
302
+ This difference only affects patterns with:
303
+ - Alternatives containing overlapping prefixes ("a" vs "aa")
304
+ - The alternative is followed by content that fails
305
+ - The later alternative would allow the following content to succeed
306
+
307
+ For most practical grammars, this difference never manifests. Use `Backend::Auto` to let parsanol choose the appropriate backend.
308
+
309
+ ## Backend Abstraction
310
+
311
+ Parsanol provides a trait-based backend abstraction for extensibility. You can implement custom backends or use the built-in ones interchangeably.
312
+
313
+ ### Using the ParsingBackend Trait
314
+
315
+ ```rust
316
+ use parsanol::portable::backend::{ParsingBackend, PackratBackend, BytecodeBackend, Backend};
317
+
318
+ // Use Packrat backend for predictable O(n) performance
319
+ let mut packrat = PackratBackend::new();
320
+ let result = packrat.parse(&grammar, input)?;
321
+
322
+ // Use Bytecode backend for lower memory usage
323
+ let mut bytecode = BytecodeBackend::new();
324
+ let result = bytecode.parse(&grammar, input)?;
325
+
326
+ // Configure backends
327
+ let packrat = PackratBackend::new()
328
+ .with_max_recursion_depth(500)
329
+ .with_timeout_ms(5000);
330
+
331
+ let bytecode = BytecodeBackend::new()
332
+ .with_auto_fallback(true); // Falls back to Packrat for complex grammars
333
+ ```
334
+
335
+ ### Runtime Backend Selection
336
+
337
+ ```rust
338
+ use parsanol::portable::backend::Backend;
339
+
340
+ // Select backend at runtime
341
+ let backend_type = Backend::default_for_grammar(&grammar);
342
+
343
+ match backend_type {
344
+ Backend::Packrat => {
345
+ let mut parser = PackratBackend::new();
346
+ parser.parse(&grammar, input)?
347
+ }
348
+ Backend::Bytecode => {
349
+ let mut parser = BytecodeBackend::new();
350
+ parser.parse(&grammar, input)?
351
+ }
352
+ };
353
+ ```
354
+
355
+ ### Backend Characteristics
356
+
357
+ Each backend documents its performance characteristics:
358
+
359
+ ```rust
360
+ use parsanol::portable::backend::{ParsingBackend, PackratBackend};
361
+
362
+ let backend = PackratBackend::new();
363
+ let chars = backend.characteristics();
364
+
365
+ println!("Time: {}", chars.time_complexity); // "O(n)"
366
+ println!("Memory: {}", chars.memory_complexity); // "O(n × r)"
367
+ println!("Memoization: {}", chars.uses_memoization); // true
368
+ println!("Streaming: {}", chars.supports_streaming); // false
369
+ println!("Incremental: {}", chars.supports_incremental); // true
370
+ println!("Safe: {}", chars.safe_for_all_grammars); // true
371
+ ```
372
+
373
+ ### Implementing Custom Backends
374
+
375
+ ```rust
376
+ use parsanol::portable::backend::{ParsingBackend, BackendCharacteristics, BackendResult};
377
+ use parsanol::portable::grammar::Grammar;
378
+
379
+ struct MyCustomBackend;
380
+
381
+ impl ParsingBackend for MyCustomBackend {
382
+ fn parse(&mut self, grammar: &Grammar, input: &str) -> BackendResult {
383
+ // Custom parsing logic here
384
+ todo!()
385
+ }
386
+
387
+ fn name(&self) -> &'static str {
388
+ "my-custom"
389
+ }
390
+
391
+ fn characteristics(&self) -> BackendCharacteristics {
392
+ BackendCharacteristics {
393
+ time_complexity: "O(n log n)",
394
+ memory_complexity: "O(n)",
395
+ uses_memoization: false,
396
+ supports_streaming: true,
397
+ supports_incremental: false,
398
+ safe_for_all_grammars: true,
399
+ }
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### Dynamic Backend Dispatch
405
+
406
+ For runtime polymorphism:
407
+
408
+ ```rust
409
+ use parsanol::portable::backend::{DynBackend, PackratBackend, BytecodeBackend};
410
+
411
+ fn get_backend(use_packrat: bool) -> DynBackend {
412
+ if use_packrat {
413
+ Box::new(PackratBackend::new())
414
+ } else {
415
+ Box::new(BytecodeBackend::new())
416
+ }
417
+ }
418
+
419
+ let mut backend: DynBackend = get_backend(true);
420
+ let result = backend.parse(&grammar, input)?;
421
+ ```
422
+
423
+ ## Quick Start Examples
424
+
425
+ Using the bytecode backend explicitly:
426
+
427
+ ```rust
428
+ use parsanol::portable::{
429
+ parser_dsl::{str, re, GrammarBuilder},
430
+ bytecode::{Backend, Parser},
431
+ };
432
+
433
+ let grammar = GrammarBuilder::new()
434
+ .rule("number", re(r"[0-9]+"))
435
+ .build();
436
+
437
+ // Create parser with bytecode backend
438
+ let mut parser = Parser::new(grammar, Backend::Bytecode);
439
+ let result = parser.parse("42");
440
+ ```
441
+
442
+ Using packrat backend explicitly:
443
+
444
+ ```rust
445
+ let mut parser = Parser::new(grammar, Backend::Packrat);
446
+ let result = parser.parse("42");
447
+ ```
448
+
449
+ ## Optimization Passes
450
+
451
+ The bytecode backend applies 11 optimization passes automatically:
452
+
453
+ 1. `DeadCodeElimination` - Remove unreachable code
454
+ 2. `JumpChainSimplification` - Simplify jump chains
455
+ 3. `JumpToReturnSimplification` - Direct returns
456
+ 4. `JumpToFailSimplification` - Direct failures
457
+ 5. `CombineAdjacentChars` - Char merging
458
+ 6. `SpanOptimization` - CharSet* to Span
459
+ 7. `FullCaptureOptimization` - Capture pairs to FullCapture
460
+ 8. `TestCharOptimization` - Choice patterns to TestChar
461
+ 9. `TestSetOptimization` - Choice patterns to TestSet
462
+ 10. `TailCallOptimization` - Tail calls to jumps
463
+ 11. `LookaheadOptimization` - Choice to PredChoice for predicates
464
+
465
+ ## Bytecode VM Architecture
466
+
467
+ ```
468
+ Grammar (Atoms) ──► Compiler ──► Program (bytecode)
469
+ │
470
+ ▼
471
+ Input ──────────────────────────► VM ──► AstNode
472
+ ```
473
+
474
+ The bytecode VM uses:
475
+ - **Backtracking stack**: For choice point management
476
+ - **Capture stack**: For building AST nodes
477
+ - **Instruction pointer**: Sequential execution
478
+ - **Optimization passes**: Peephole optimization on compiled bytecode
479
+
480
+ ## Instruction Set
481
+
482
+ The VM supports 28 instructions covering all PEG operations:
483
+
484
+ | Category | Instructions |
485
+ |----------|-------------|
486
+ | Matching | `Char`, `CharSet`, `String`, `Regex`, `Any`, `Custom` |
487
+ | Control Flow | `Jump`, `Call`, `Return`, `End` |
488
+ | Backtracking | `Choice`, `Commit`, `PartialCommit`, `BackCommit`, `Fail`, `FailTwice` |
489
+ | Captures | `OpenCapture`, `CloseCapture`, `FullCapture` |
490
+ | Tests | `TestChar`, `TestSet`, `TestAny` |
491
+ | Special | `Behind`, `Span`, `NoOp`, `PredChoice` |
492
+
493
+ # Architecture
494
+
495
+ ┌─────────────────────────────────────────────────────────────┐
496
+ │ PARSANOL-RS │
497
+ │ (Generic PEG Parser Library) │
498
+ ├─────────────────────────────────────────────────────────────┤
499
+ │ • Parser combinators (PEG atoms) │
500
+ │ • Grammar representation │
501
+ │ • Packrat memoization │
502
+ │ • Arena allocation │
503
+ │ • Infix expression parsing │
504
+ │ • Rich error reporting (tree structure) │
505
+ │ • Transform DSL (pattern matching) │
506
+ │ • Derive macros for typed ASTs │
507
+ │ • Optional Ruby FFI / WASM bindings │
508
+ └─────────────────────────────────────────────────────────────┘
509
+ ▲ ▲
510
+ │ (build ON TOP) │ (build ON TOP)
511
+ │ │
512
+ ┌─────────┴──────────┐ ┌─────────┴─────────┐
513
+ │ parsanol-express │ │ Your Language │
514
+ │ (EXPRESS lexer) │ │ (Your DSL) │
515
+ └────────────────────┘ └───────────────────┘
516
+
517
+ > [!IMPORTANT]
518
+ > Parsanol-rs is a **GENERIC** parser library. It has no knowledge of
519
+ > any specific domain (EXPRESS, Ruby, JSON, YAML, etc.). Domain-specific
520
+ > parsers should be built ON TOP of this library.
521
+
522
+ # Workspace Structure
523
+
524
+ This repository uses a Cargo workspace with two crates:
525
+
526
+ ```
527
+ parsanol-rs/
528
+ ├── parsanol/ # Main parser library
529
+ │ ├── src/
530
+ │ └── Cargo.toml
531
+ ├── parsanol-derive/ # Derive macros (always included)
532
+ │ ├── src/
533
+ │ └── Cargo.toml
534
+ ├── examples/ # 39 example parsers
535
+ └── Cargo.toml # Workspace root
536
+ ```
537
+
538
+ # Installation
539
+
540
+ Add this to your `Cargo.toml`:
541
+
542
+ ```toml
543
+ [dependencies]
544
+ parsanol = "0.1"
545
+ ```
546
+
547
+ The `parsanol-derive` crate is automatically included as a dependency,
548
+ providing the `#[derive(FromAst)]` macro for typed AST conversion.
549
+
550
+ ## Optional Features
551
+
552
+ - `ruby` - Enable Ruby FFI bindings (requires `magnus`)
553
+
554
+ - `wasm` - Enable WebAssembly bindings (requires `wasm-bindgen`,
555
+ `js-sys`)
556
+
557
+ - `parallel` - Enable parallel parsing (requires `rayon`)
558
+
559
+ ```toml
560
+ [dependencies]
561
+ parsanol = { version = "0.1", features = ["ruby", "parallel"] }
562
+ ```
563
+
564
+ # Quick Start
565
+
566
+ ## Basic Parsing
567
+
568
+ ```rust
569
+ use parsanol::portable::{Grammar, PortableParser, AstArena, parser_dsl::*};
570
+
571
+ // Build a simple grammar
572
+ let grammar = GrammarBuilder::new()
573
+ .rule("greeting", str("hello").then(str("world")))
574
+ .build();
575
+
576
+ let input = "helloworld";
577
+ let mut arena = AstArena::for_input(input.len());
578
+ let mut parser = PortableParser::new(&grammar, input, &mut arena);
579
+
580
+ match parser.parse() {
581
+ Ok(ast) => println!("Parsed successfully: {:?}", ast),
582
+ Err(e) => println!("Parse error: {:?}", e),
583
+ }
584
+ ```
585
+
586
+ ## Calculator with Operator Precedence
587
+
588
+ ```rust
589
+ use parsanol::portable::{
590
+ GrammarBuilder, PortableParser, AstArena, Grammar,
591
+ parser_dsl::{str, re, ref_, seq, choice, dynamic},
592
+ infix::{InfixBuilder, Assoc},
593
+ };
594
+
595
+ fn build_calculator_grammar() -> Grammar {
596
+ let mut builder = GrammarBuilder::new();
597
+
598
+ // Define atoms
599
+ builder = builder.rule("number", re(r"[0-9]+"));
600
+ builder = builder.rule("primary", choice(vec![
601
+ dynamic(seq(vec![
602
+ dynamic(str("(")),
603
+ dynamic(ref_("expr")),
604
+ dynamic(str(")")),
605
+ ])),
606
+ dynamic(ref_("number")),
607
+ ]));
608
+
609
+ // Build infix with precedence
610
+ let expr_atom = InfixBuilder::new()
611
+ .primary(ref_("primary"))
612
+ .op("*", 2, Assoc::Left)
613
+ .op("/", 2, Assoc::Left)
614
+ .op("+", 1, Assoc::Left)
615
+ .op("-", 1, Assoc::Left)
616
+ .build(&mut builder);
617
+
618
+ builder.update_rule("expr", expr_atom);
619
+ builder.build()
620
+ }
621
+ ```
622
+
623
+ # Parser DSL
624
+
625
+ ## Atom Types
626
+
627
+ | Atom | Description | Example |
628
+ |------|-------------|---------|
629
+ | `str("literal")` | Match exact string | `str("hello")` |
630
+ | `re("pattern")` | Match regex pattern | `re(r"[0-9]+")` |
631
+ | `any()` | Match any single character | `any()` |
632
+ | `ref_("rule")` | Reference to named rule | `ref_("expr")` |
633
+ | `seq([...])` | Sequence of atoms | `seq(vec![a, b, c])` |
634
+ | `choice([...])` | Alternative atoms | `choice(vec![a, b])` |
635
+ | `cut()` | Commit to this branch (prevent backtracking) | `cut()` |
636
+ | `capture("name", atom)` | Extract named value during parsing | `capture("id", re(r"[a-z]+"))` |
637
+ | `scope(atom)` | Create isolated capture context | `scope(seq([...]))` |
638
+ | `dynamic(callback)` | Runtime-determined parsing via callback | `dynamic(callback_id)` |
639
+
640
+ ## Combinators
641
+
642
+ All atoms implement the `ParsletExt` trait with these methods:
643
+
644
+ ```rust
645
+ use parsanol::portable::parser_dsl::*;
646
+
647
+ // Sequence: A >> B
648
+ let parser = str("hello").then(str("world"));
649
+
650
+ // Alternative: A | B
651
+ let parser = str("foo").or(str("bar"));
652
+
653
+ // Repetition
654
+ let parser = str("a").repeat(1, None); // One or more
655
+ let parser = str("a").repeat(0, Some(3)); // Zero to three
656
+ let parser = str("a").many(); // Zero or more
657
+ let parser = str("a").many1(); // One or more
658
+ let parser = str("a").optional(); // Zero or one
659
+
660
+ // Named capture
661
+ let parser = re(r"[0-9]+").label("number");
662
+
663
+ // Ignore (don't include in AST)
664
+ let parser = str(" ").ignore();
665
+
666
+ // Lookahead (don't consume)
667
+ let parser = str("hello").lookahead(); // Positive: must match
668
+ let parser = str("hello").not_ahead(); // Negative: must NOT match
669
+ ```
670
+
671
+ ## Grammar Macro
672
+
673
+ For declarative grammar definition:
674
+
675
+ ```rust
676
+ use parsanol::portable::parser_dsl::grammar;
677
+
678
+ let grammar = grammar! {
679
+ "hello" => str("hello"),
680
+ "world" => str("world"),
681
+ "greeting" => ref_("hello").then(ref_("world")),
682
+ };
683
+ ```
684
+
685
+ # Capture Atoms
686
+
687
+ Capture atoms extract named values during parsing, similar to regex named groups. They work with all backends (Packrat, Bytecode, Streaming).
688
+
689
+ ## Basic Usage
690
+
691
+ ```rust
692
+ use parsanol::portable::{
693
+ parser_dsl::{capture, dynamic, re, seq, GrammarBuilder},
694
+ PortableParser, AstArena,
695
+ };
696
+
697
+ let grammar = GrammarBuilder::new()
698
+ .rule("greeting", seq(vec![
699
+ capture("word", dynamic(re(r"[a-zA-Z]+"))),
700
+ ]))
701
+ .build();
702
+
703
+ let mut arena = AstArena::for_input(64);
704
+ let mut parser = PortableParser::packrat(grammar);
705
+ let result = parser.parse_from_pos(0, "hello world", &mut arena)?;
706
+
707
+ // Access captures
708
+ if let Some(text) = result.get_capture("word", "hello world") {
709
+ println!("Captured: {}", text); // Prints: "hello"
710
+ }
711
+ ```
712
+
713
+ ## Capture API
714
+
715
+ ```rust
716
+ // Get a single capture by name
717
+ let value = result.get_capture("name", input);
718
+
719
+ // Get all capture names
720
+ for name in result.capture_names() {
721
+ println!("Capture: {}", name);
722
+ }
723
+
724
+ // Check if capture exists
725
+ if result.has_capture("name") {
726
+ // ...
727
+ }
728
+ ```
729
+
730
+ ## Backend Compatibility
731
+
732
+ | Backend | Capture Support | Notes |
733
+ |---------|-----------------|-------|
734
+ | Packrat | Full | Native support |
735
+ | Bytecode | Full | Uses capture instructions |
736
+ | Streaming | Full | Captures persist across chunks |
737
+
738
+ # Scope Atoms
739
+
740
+ Scope atoms create isolated capture contexts. Captures made inside a scope are discarded when the scope exits, preventing pollution of the parent context.
741
+
742
+ ## Use Cases
743
+
744
+ - Nested parsing where inner captures shouldn't affect outer state
745
+ - Repetitive patterns where each iteration starts fresh
746
+ - Context isolation in recursive grammars
747
+
748
+ ## Basic Usage
749
+
750
+ ```rust
751
+ use parsanol::portable::parser_dsl::{scope, seq, capture, dynamic, re, GrammarBuilder};
752
+
753
+ let grammar = GrammarBuilder::new()
754
+ .rule("outer", seq(vec![
755
+ capture("outer_name", dynamic(re(r"[a-z]+"))),
756
+ scope(seq(vec![
757
+ capture("inner_name", dynamic(re(r"[0-9]+"))),
758
+ ])),
759
+ // "inner_name" is NOT available here
760
+ ]))
761
+ .build();
762
+ ```
763
+
764
+ # Dynamic Atoms
765
+
766
+ Dynamic atoms enable runtime-determined parsing via registered callbacks. This allows context-sensitive parsing where the grammar itself depends on input or previously captured values.
767
+
768
+ ## Registering a Callback
769
+
770
+ ```rust
771
+ use parsanol::portable::{
772
+ Grammar, Atom, Parser,
773
+ dynamic::{DynamicCallback, DynamicContext, register_dynamic_callback},
774
+ parser_dsl::*,
775
+ };
776
+
777
+ struct KeywordCallback;
778
+
779
+ impl DynamicCallback for KeywordCallback {
780
+ fn call(&self, ctx: &DynamicContext) -> Option<Atom> {
781
+ // Access current position
782
+ let pos = ctx.pos();
783
+ // Access input
784
+ let input = ctx.input();
785
+ // Access captures made so far
786
+ if let Some(lang) = ctx.get_capture("language") {
787
+ match lang {
788
+ "ruby" => Some(Atom::Str { pattern: "def".into() }),
789
+ "python" => Some(Atom::Str { pattern: "lambda".into() }),
790
+ _ => None,
791
+ }
792
+ } else {
793
+ None
794
+ }
795
+ }
796
+
797
+ fn description(&self) -> &str {
798
+ "keyword_callback"
799
+ }
800
+ }
801
+
802
+ let callback_id = register_dynamic_callback(Box::new(KeywordCallback));
803
+ ```
804
+
805
+ ## Using Dynamic Atoms in Grammars
806
+
807
+ ```rust
808
+ let grammar = GrammarBuilder::new()
809
+ .rule("keyword", dynamic_with_id(callback_id))
810
+ .build();
811
+ ```
812
+
813
+ ## Backend Compatibility
814
+
815
+ | Backend | Dynamic Support | Notes |
816
+ |---------|-----------------|-------|
817
+ | Packrat | Full | Native support (recommended) |
818
+ | Bytecode | Fallback | Uses Packrat internally |
819
+ | Streaming | Fallback | Uses Packrat internally |
820
+
821
+ **Note:** For heavy dynamic atom usage, prefer the Packrat backend for best performance.
822
+
823
+ # Streaming with Captures
824
+
825
+ The streaming parser supports captures while maintaining bounded memory usage. Captures persist across streaming parse operations.
826
+
827
+ ## Basic Usage
828
+
829
+ ```rust
830
+ use parsanol::portable::{
831
+ parser_dsl::{capture, dynamic, re, GrammarBuilder},
832
+ streaming::{StreamingParser, ChunkConfig},
833
+ arena::AstArena,
834
+ };
835
+ use std::io::Cursor;
836
+
837
+ let grammar = GrammarBuilder::new()
838
+ .rule("word", capture("word", dynamic(re(r"[a-zA-Z]+"))))
839
+ .build();
840
+
841
+ let config = ChunkConfig {
842
+ chunk_size: 65536, // 64 KB chunks
843
+ window_size: 2, // Keep 2 chunks in memory
844
+ };
845
+
846
+ let mut parser = StreamingParser::new(&grammar, config);
847
+ let mut arena = AstArena::for_input(65536);
848
+ let mut cursor = Cursor::new(input.as_bytes());
849
+
850
+ let result = parser.parse_from_reader(&mut cursor, &mut arena)?;
851
+
852
+ if let Some(captures) = &result.capture_state {
853
+ for name in captures.names() {
854
+ if let Some(value) = captures.get(&name) {
855
+ println!("{} = {:?}", name, value.get_text(input));
856
+ }
857
+ }
858
+ }
859
+ ```
860
+
861
+ ## Chunk Configuration
862
+
863
+ | Preset | Chunk Size | Window | Use Case |
864
+ |--------|------------|--------|----------|
865
+ | `small()` | 16 KB | 2 | Real-time feeds |
866
+ | `medium()` | 64 KB | 3 | Default |
867
+ | `large()` | 256 KB | 4 | Log files |
868
+ | `huge()` | 1 MB | 5 | Large files |
869
+
870
+ ## Performance Notes
871
+
872
+ - Memory: O(chunk_size × window_size + capture_state)
873
+ - Captures accumulate during parse, available at end
874
+ - For very large captures, use `reset()` to process incrementally
875
+
876
+ # Transform System
877
+
878
+ The transform system converts generic parse trees into typed Rust data
879
+ structures, similar to Parslet’s transformation system.
880
+
881
+ ## Value Types
882
+
883
+ The `Value` enum represents transformed data:
884
+
885
+ ```rust
886
+ pub enum Value {
887
+ Nil,
888
+ Bool(bool),
889
+ Int(i64),
890
+ Float(f64),
891
+ String(String),
892
+ Array(Vec<Value>),
893
+ Hash(HashMap<String, Value>),
894
+ }
895
+ ```
896
+
897
+ ## Basic Transformations
898
+
899
+ ```rust
900
+ use parsanol::portable::transform::{Transform, Value, TransformError};
901
+
902
+ let transform = Transform::new()
903
+ // Transform "int" captures by doubling
904
+ .rule("int", |v| {
905
+ let n = v.as_int().ok_or_else(|| TransformError::Custom("not int".into()))?;
906
+ Ok(Value::int(n * 2))
907
+ });
908
+
909
+ let value = Value::hash(vec![("int", Value::int(21))]);
910
+ let result = transform.apply(&value)?;
911
+ assert_eq!(result.as_int(), Some(42));
912
+ ```
913
+
914
+ ## Pattern Matching
915
+
916
+ Pattern-based transformations similar to Parslet:
917
+
918
+ ```rust
919
+ use parsanol::portable::transform::{Transform, Pattern, Value};
920
+
921
+ let transform = Transform::new()
922
+ // Match hash with specific fields
923
+ .pattern(
924
+ Pattern::hash()
925
+ .field("left", "l")
926
+ .field("op", Pattern::str("+"))
927
+ .field("right", "r"),
928
+ |bindings| {
929
+ let l = bindings.get_int("l")?;
930
+ let r = bindings.get_int("r")?;
931
+ Ok(Value::int(l + r))
932
+ }
933
+ );
934
+ ```
935
+
936
+ ## Pattern Types
937
+
938
+ | Pattern | Description | Example |
939
+ |----|----|----|
940
+ | `Pattern::simple("x")` | Match any leaf value and bind to variable | `Pattern::simple("n")` matches `42` |
941
+ | `Pattern::str("value")` | Match a specific string value | `Pattern::str("+")` matches `"+"` |
942
+ | `Pattern::int(n)` | Match a specific integer | `Pattern::int(42)` matches `42` |
943
+ | `Pattern::sequence("x")` | Match an array and bind to variable | `Pattern::sequence("items")` |
944
+ | `Pattern::subtree("x")` | Match anything and bind to variable | `Pattern::subtree("node")` |
945
+ | `Pattern::hash()` | Match a hash with specific fields | See example above |
946
+
947
+ ## Converting AST to Value
948
+
949
+ ```rust
950
+ use parsanol::portable::transform::{ast_to_value, Value};
951
+
952
+ // After parsing
953
+ let ast = parser.parse()?;
954
+ let value = ast_to_value(&ast, &arena, input);
955
+
956
+ // Now apply transforms
957
+ let result = transform.apply(&value)?;
958
+ ```
959
+
960
+ # Derive Macros
961
+
962
+ The `FromAst` derive macro automatically generates code to convert `Value`
963
+ types into typed Rust structs and enums. This eliminates boilerplate code
964
+ for AST transformation.
965
+
966
+ ## Basic Usage
967
+
968
+ ```rust
969
+ use parsanol::derive::FromAst;
970
+ use parsanol::portable::transform::Value;
971
+
972
+ #[derive(FromAst, Debug)]
973
+ pub enum Expr {
974
+ #[parsanol(tag = "number")]
975
+ Number(i64),
976
+
977
+ #[parsanol(tag = "binop")]
978
+ BinOp {
979
+ left: Box<Expr>,
980
+ op: String,
981
+ right: Box<Expr>,
982
+ },
983
+ }
984
+
985
+ // Convert Value to typed Expr
986
+ let value: Value = /* ... parsed value ... */;
987
+ let expr: Expr = value.try_into()?;
988
+ ```
989
+
990
+ ## Container Attributes
991
+
992
+ | Attribute | Description |
993
+ |-----------|-------------|
994
+ | `#[parsanol(rule = "name")]` | Specify the grammar rule name |
995
+
996
+ ## Variant Attributes (for enums)
997
+
998
+ | Attribute | Description |
999
+ |-----------|-------------|
1000
+ | `#[parsanol(tag = "literal")]` | Match by literal tag string |
1001
+ | `#[parsanol(tag_expr = expr)]` | Match by expression (for dynamic tags) |
1002
+
1003
+ ## Field Attributes
1004
+
1005
+ | Attribute | Description |
1006
+ |-----------|-------------|
1007
+ | `#[parsanol(field = "name")]` | Map to different hash field name |
1008
+ | `#[parsanol(default)]` | Use `Default::default()` if missing |
1009
+ | `#[parsanol(default = expr)]` | Use expression if missing |
1010
+
1011
+ ## Complete Example
1012
+
1013
+ ```rust
1014
+ use parsanol::derive::FromAst;
1015
+ use parsanol::portable::transform::Value;
1016
+
1017
+ #[derive(FromAst, Debug)]
1018
+ #[parsanol(rule = "statement")]
1019
+ pub enum Statement {
1020
+ #[parsanol(tag = "assignment")]
1021
+ Assignment {
1022
+ #[parsanol(field = "name")]
1023
+ variable: String,
1024
+ value: Box<Expr>,
1025
+ },
1026
+
1027
+ #[parsanol(tag = "return")]
1028
+ Return {
1029
+ #[parsanol(default)]
1030
+ value: Option<Box<Expr>>,
1031
+ },
1032
+
1033
+ #[parsanol(tag = "if")]
1034
+ If {
1035
+ condition: Box<Expr>,
1036
+ then_block: Vec<Statement>,
1037
+ #[parsanol(default)]
1038
+ else_block: Option<Vec<Statement>>,
1039
+ },
1040
+ }
1041
+
1042
+ // Usage
1043
+ fn parse_statement(value: Value) -> Result<Statement, parsanol::derive::FromAstError> {
1044
+ value.try_into()
1045
+ }
1046
+ ```
1047
+
1048
+ ## Single-Field Tuple Structs
1049
+
1050
+ Single-field tuple structs automatically get transparent conversion:
1051
+
1052
+ ```rust
1053
+ #[derive(FromAst)]
1054
+ pub struct Identifier(pub String);
1055
+
1056
+ // Value::String("foo") directly converts to Identifier("foo")
1057
+ ```
1058
+
1059
+ ## Error Handling
1060
+
1061
+ ```rust
1062
+ use parsanol::derive::FromAstError;
1063
+
1064
+ match value.try_into() {
1065
+ Ok(expr) => println!("Parsed: {:?}", expr),
1066
+ Err(FromAstError::MissingField(field)) => {
1067
+ eprintln!("Missing field: {}", field);
1068
+ }
1069
+ Err(FromAstError::UnknownTag) => {
1070
+ eprintln!("Unknown tag in enum");
1071
+ }
1072
+ Err(e) => eprintln!("Conversion error: {}", e),
1073
+ }
1074
+ ```
1075
+
1076
+ # Streaming Builder
1077
+
1078
+ The streaming builder API allows single-pass parsing without
1079
+ intermediate AST construction. This is ideal for:
1080
+
1081
+ - Maximum performance (eliminates AST allocation)
1082
+
1083
+ - Custom output formats
1084
+
1085
+ - Memory-constrained environments
1086
+
1087
+ ## Implementing StreamingBuilder
1088
+
1089
+ ```rust
1090
+ use parsanol::portable::streaming_builder::{StreamingBuilder, BuildResult, BuildError};
1091
+
1092
+ // Custom builder that collects all strings
1093
+ struct StringCollector {
1094
+ strings: Vec<String>,
1095
+ }
1096
+
1097
+ impl StreamingBuilder for StringCollector {
1098
+ type Output = Vec<String>;
1099
+
1100
+ fn on_string(&mut self, value: &str, _offset: usize, _length: usize) -> BuildResult<()> {
1101
+ self.strings.push(value.to_string());
1102
+ Ok(())
1103
+ }
1104
+
1105
+ fn finish(&mut self) -> BuildResult<Self::Output> {
1106
+ Ok(std::mem::take(&mut self.strings))
1107
+ }
1108
+ }
1109
+ ```
1110
+
1111
+ ## Using parse_with_builder
1112
+
1113
+ ```rust
1114
+ use parsanol::portable::{Grammar, PortableParser, AstArena};
1115
+
1116
+ let grammar = /* ... */;
1117
+ let input = "hello world";
1118
+ let mut arena = AstArena::for_input(input.len());
1119
+ let mut parser = PortableParser::new(&grammar, input, &mut arena);
1120
+
1121
+ // Create builder
1122
+ let mut builder = StringCollector { strings: vec![] };
1123
+
1124
+ // Parse with streaming builder
1125
+ let result = parser.parse_with_builder(&mut builder)?;
1126
+ // result: Vec<String>
1127
+ ```
1128
+
1129
+ ## Built-in Builders
1130
+
1131
+ Several useful builders are provided:
1132
+
1133
+ | Builder | Description |
1134
+ |--------------------------|----------------------------------------------|
1135
+ | `DebugBuilder` | Collects all events as strings for debugging |
1136
+ | `BuilderStringCollector` | Collects all string values |
1137
+ | `BuilderNodeCounter` | Counts nodes by type |
1138
+
1139
+ ## Ruby Integration
1140
+
1141
+ The streaming builder works with Ruby callbacks via FFI:
1142
+
1143
+ ```ruby
1144
+ require 'parsanol'
1145
+
1146
+ class MyBuilder
1147
+ include Parsanol::BuilderCallbacks
1148
+
1149
+ def initialize
1150
+ @strings = []
1151
+ end
1152
+
1153
+ def on_string(value, offset, length)
1154
+ @strings << value
1155
+ end
1156
+
1157
+ def finish
1158
+ @strings
1159
+ end
1160
+ end
1161
+
1162
+ builder = MyBuilder.new
1163
+ result = Parsanol::Native.parse_with_builder(grammar_json, input, builder)
1164
+ ```
1165
+
1166
+ # Parallel Parsing
1167
+
1168
+ Parse multiple inputs in parallel using rayon for linear speedup on
1169
+ multi-core systems.
1170
+
1171
+ ## Enabling Parallel Feature
1172
+
1173
+ ```toml
1174
+ [dependencies]
1175
+ parsanol = { version = "0.1", features = ["parallel"] }
1176
+ ```
1177
+
1178
+ ## Batch Parallel Parsing
1179
+
1180
+ ```rust
1181
+ use parsanol::portable::{Grammar, parse_batch_parallel};
1182
+
1183
+ let grammar = /* ... */;
1184
+ let inputs = vec!["file1.exp", "file2.exp", "file3.exp"];
1185
+
1186
+ // Parse all inputs in parallel
1187
+ let results = parse_batch_parallel(&grammar, &inputs);
1188
+
1189
+ // Results are in same order as inputs
1190
+ for (i, result) in results.iter().enumerate() {
1191
+ match result {
1192
+ Ok(ast) => println!("File {} parsed successfully", i),
1193
+ Err(e) => eprintln!("File {} failed: {}", i, e),
1194
+ }
1195
+ }
1196
+ ```
1197
+
1198
+ ## Parallel Configuration
1199
+
1200
+ ```rust
1201
+ use parsanol::portable::parallel::{parse_batch_parallel, ParallelConfig};
1202
+
1203
+ let config = ParallelConfig::new()
1204
+ .with_num_threads(4) // Use 4 threads
1205
+ .with_min_chunk_size(10); // Minimum inputs per thread
1206
+
1207
+ let results = parse_batch_parallel(&grammar, &inputs);
1208
+ ```
1209
+
1210
+ ## Performance
1211
+
1212
+ | Scenario | Speedup |
1213
+ |--------------------|----------------------------------------|
1214
+ | 8 cores, 100 files | ~8x faster than sequential |
1215
+ | 4 cores, 50 files | ~4x faster than sequential |
1216
+ | Single core | Same as sequential (graceful fallback) |
1217
+
1218
+ When the `parallel` feature is not enabled, the functions fall back to
1219
+ sequential parsing automatically.
1220
+
1221
+ # Infix Expression Parsing
1222
+
1223
+ Built-in support for parsing infix expressions with operator precedence
1224
+ and associativity.
1225
+
1226
+ ## Using InfixBuilder
1227
+
1228
+ ```rust
1229
+ use parsanol::portable::infix::{InfixBuilder, Assoc};
1230
+
1231
+ let mut builder = GrammarBuilder::new();
1232
+
1233
+ let expr_idx = InfixBuilder::new()
1234
+ .primary(ref_("atom")) // Base expression (numbers, parens)
1235
+ .op("*", 2, Assoc::Left) // Higher precedence
1236
+ .op("/", 2, Assoc::Left)
1237
+ .op("+", 1, Assoc::Left) // Lower precedence
1238
+ .op("-", 1, Assoc::Left)
1239
+ .op("^", 3, Assoc::Right) // Right-associative
1240
+ .build(&mut builder);
1241
+ ```
1242
+
1243
+ ## Associativity
1244
+
1245
+ | Associativity | Meaning | Example |
1246
+ |----|----|----|
1247
+ | `Assoc::Left` | Left-to-right evaluation | `a` `-` `b` `-` `c` = `(a` `-` `b)` `-` `c` |
1248
+ | `Assoc::Right` | Right-to-left evaluation | `a` `=` `b` `=` `c` = `a` `=` `(b` `=` `c)` |
1249
+ | `Assoc::NonAssoc` | Cannot chain | `a` `<` `b` `<` `c` is an error |
1250
+
1251
+ # Rich Error Reporting
1252
+
1253
+ Tree-structured error messages similar to Parslet for better debugging.
1254
+
1255
+ ## Basic Usage
1256
+
1257
+ ```rust
1258
+ use parsanol::portable::error::{RichError, ErrorBuilder, Span};
1259
+
1260
+ // Create rich errors
1261
+ let error = ErrorBuilder::new("Failed to parse expression")
1262
+ .at(10, 2, 5) // offset, line, column
1263
+ .context("expression")
1264
+ .child(
1265
+ ErrorBuilder::new("Expected '+' or '-'")
1266
+ .at(10, 2, 5)
1267
+ .build(),
1268
+ )
1269
+ .build();
1270
+
1271
+ // Print as ASCII tree
1272
+ println!("{}", error.ascii_tree());
1273
+ ```
1274
+
1275
+ ## Example Output
1276
+
1277
+ Error at line 3, column 5:
1278
+ `- Failed to parse expression (in expression)
1279
+ `- Expected '+' or '-'
1280
+
1281
+ ## Source Context
1282
+
1283
+ ```rust
1284
+ // Format error with source code context
1285
+ let formatted = error.format_with_source(input);
1286
+ println!("{}", formatted);
1287
+ ```
1288
+
1289
+ Output:
1290
+
1291
+ Error at line 3, column 5:
1292
+ let x = foo bar
1293
+ ^
1294
+ `- Failed to parse expression (in expression)
1295
+ `- Expected '+' or '-'
1296
+
1297
+ # Source Location Tracking
1298
+
1299
+ Track source positions through the parsing and transformation pipeline.
1300
+
1301
+ ## Using SourceSpan
1302
+
1303
+ ```rust
1304
+ use parsanol::portable::source_location::{SourceSpan, SourcePosition};
1305
+ use parsanol::portable::transform::{ast_to_value_with_span};
1306
+
1307
+ // Create a span from offsets
1308
+ let span = SourceSpan::from_offsets(input, 10, 20);
1309
+ println!("Line {}, Column {}", span.start.line, span.start.column);
1310
+
1311
+ // Merge adjacent spans
1312
+ let merged = span1.merge(&span2);
1313
+
1314
+ // Check overlap
1315
+ if span1.overlaps(&span2) {
1316
+ // Spans overlap
1317
+ }
1318
+
1319
+ // Transform AST with source spans preserved
1320
+ let (value, spans) = ast_to_value_with_span(&ast, &arena, input);
1321
+ ```
1322
+
1323
+ # Grammar Composition
1324
+
1325
+ Build complex grammars by importing and composing smaller grammars.
1326
+
1327
+ ## Importing Grammars
1328
+
1329
+ ```rust
1330
+ use parsanol::portable::parser_dsl::*;
1331
+
1332
+ let mut builder = GrammarBuilder::new();
1333
+
1334
+ // Import another grammar with a prefix
1335
+ builder.import(&expression_grammar, Some("expr"));
1336
+ builder.import(&type_grammar, Some("type"));
1337
+
1338
+ // Reference imported rules
1339
+ let combined = seq(vec![
1340
+ ref_("expr:root"), // References expression_grammar's root
1341
+ str(":"),
1342
+ ref_("type:root"), // References type_grammar's root
1343
+ ]);
1344
+
1345
+ builder.rule("typed_expr", combined);
1346
+ let grammar = builder.build();
1347
+ ```
1348
+
1349
+ # Ruby FFI
1350
+
1351
+ Parsanol-rs can be compiled as a Ruby extension for use with
1352
+ parsanol-ruby.
1353
+
1354
+ ## Features
1355
+
1356
+ The Ruby FFI provides:
1357
+
1358
+ - **17x faster** parsing than pure Ruby (Parslet)
1359
+ - **Single `parse()` API** - no confusing options
1360
+ - **Lazy line/column** - zero overhead unless needed
1361
+ - **Batch FFI** - efficient u64 array transfer across language boundary
1362
+
1363
+ ## Building for Ruby
1364
+
1365
+ > **Ruby 4.0 Support**: This version uses unreleased magnus 0.9.0 and rb-sys HEAD
1366
+ > for Ruby 4.0 compatibility. The workspace `Cargo.toml` patches rb-sys automatically.
1367
+
1368
+ ```bash
1369
+ # Build with Ruby support
1370
+ cargo build --features ruby
1371
+
1372
+ # The resulting library can be loaded as a Ruby extension
1373
+ ```
1374
+
1375
+ ## Ruby API
1376
+
1377
+ ```ruby
1378
+ require 'parsanol/native'
1379
+
1380
+ # Serialize grammar once
1381
+ grammar = str('hello').as(:greeting) >> str(' ').maybe >> match('[a-z]').repeat(1).as(:name)
1382
+ grammar_json = Parsanol::Native.serialize_grammar(grammar)
1383
+
1384
+ # Parse - simple and clean
1385
+ result = Parsanol::Native.parse(grammar_json, "hello world")
1386
+ # => {greeting: "hello"@0, name: "world"@6}
1387
+
1388
+ # Line/column available when needed (computed lazily)
1389
+ result[:greeting].line_and_column # => [1, 1]
1390
+ result[:name].line_and_column # => [1, 7]
1391
+ ```
1392
+
1393
+ ## Lazy Line/Column
1394
+
1395
+ Slice objects support lazy line/column computation:
1396
+
1397
+ - `slice.offset` - character position (always available, zero cost)
1398
+ - `slice.content` - string value (always available, zero cost)
1399
+ - `slice.line_and_column` - [line, column] tuple (computed lazily, cached)
1400
+
1401
+ This provides **zero overhead** for users who don't need position info,
1402
+ while keeping line/column **always available** when needed.
1403
+
1404
+ ## Streaming Builder (Ruby)
1405
+
1406
+ For maximum performance, use the streaming builder API:
1407
+
1408
+ ```ruby
1409
+ require 'parsanol'
1410
+
1411
+ # Define a builder class
1412
+ class StringCollector
1413
+ include Parsanol::BuilderCallbacks
1414
+
1415
+ def initialize
1416
+ @strings = []
1417
+ end
1418
+
1419
+ def on_string(value, offset, length)
1420
+ @strings << value
1421
+ end
1422
+
1423
+ def on_int(value)
1424
+ @strings << value.to_s
1425
+ end
1426
+
1427
+ def finish
1428
+ @strings
1429
+ end
1430
+ end
1431
+
1432
+ # Parse with streaming builder
1433
+ builder = StringCollector.new
1434
+ result = Parsanol::Native.parse_with_builder(grammar_json, input, builder)
1435
+ # result: ["42", "+", "8"]
1436
+ ```
1437
+
1438
+ See [parsanol-ruby](https://github.com/parsanol/parsanol-ruby) for full
1439
+ documentation.
1440
+
1441
+ # WASM Support
1442
+
1443
+ Parsanol-rs can be compiled to WebAssembly for use in browsers or
1444
+ Node.js.
1445
+
1446
+ ## Building for WASM
1447
+
1448
+ ```bash
1449
+ # Install wasm-pack
1450
+ cargo install wasm-pack
1451
+
1452
+ # Build for web
1453
+ wasm-pack build --features wasm --target web
1454
+ ```
1455
+
1456
+ ## JavaScript API
1457
+
1458
+ ```javascript
1459
+ import { Parser, Grammar } from 'parsanol';
1460
+
1461
+ const grammar = Grammar.fromJson({
1462
+ atoms: [
1463
+ { Str: { pattern: "hello" } }
1464
+ ],
1465
+ root: 0
1466
+ });
1467
+
1468
+ const parser = new Parser(grammar);
1469
+ const result = parser.parse("hello");
1470
+ ```
1471
+
1472
+ # Debug Tools
1473
+
1474
+ ## Parser Tracing
1475
+
1476
+ Enable tracing for debugging:
1477
+
1478
+ ```rust
1479
+ let (result, trace) = parser.parse_with_trace();
1480
+
1481
+ // Print trace
1482
+ println!("{}", trace.format(&grammar));
1483
+ ```
1484
+
1485
+ ## Grammar Visualization
1486
+
1487
+ ```rust
1488
+ use parsanol::portable::debug::GrammarVisualizer;
1489
+
1490
+ let viz = GrammarVisualizer::new(&grammar);
1491
+
1492
+ // Generate Mermaid diagram
1493
+ println!("{}", viz.to_mermaid());
1494
+
1495
+ // Generate GraphViz DOT
1496
+ println!("{}", viz.to_dot());
1497
+ ```
1498
+
1499
+ # Performance
1500
+
1501
+ Parsanol-rs is designed for high performance:
1502
+
1503
+ - **18-44x Faster** than pure Ruby parsers (Parslet)
1504
+
1505
+ - **99.5% Fewer Allocations** through arena allocation
1506
+
1507
+ - **O(n) Parsing** via packrat memoization
1508
+
1509
+ - **SIMD Optimization**: Fast character matching via memchr
1510
+
1511
+ - **AHash**: Fast hashing for cache lookups
1512
+
1513
+ - **SmallVec**: Stack-allocated small collections
1514
+
1515
+ ## Benchmarks
1516
+
1517
+ | Parser | Input Size | Time |
1518
+ |------------------------------|------------|--------|
1519
+ | parsanol-rs (Ruby Transform) | 1KB JSON | ~50µs |
1520
+ | parsanol-rs (Serialized) | 1KB JSON | ~30µs |
1521
+ | parsanol-rs (Native) | 1KB JSON | ~20µs |
1522
+ | Pure Ruby (Parslet) | 1KB JSON | ~800µs |
1523
+
1524
+ # Security
1525
+
1526
+ Parsanol-rs includes built-in protection against denial-of-service
1527
+ attacks.
1528
+
1529
+ ## Default Limits
1530
+
1531
+ | Limit | Default Value | Description |
1532
+ |----|----|----|
1533
+ | `max_input_size` | 100 MB | Maximum input size in bytes |
1534
+ | `max_recursion_depth` | 1000 | Maximum recursion depth for nested structures |
1535
+
1536
+ ## Custom Limits
1537
+
1538
+ For untrusted input, configure custom limits:
1539
+
1540
+ ```rust
1541
+ use parsanol::portable::{PortableParser, AstArena, Grammar, ParseError};
1542
+
1543
+ // For untrusted input, use stricter limits
1544
+ let mut parser = PortableParser::with_limits(
1545
+ &grammar,
1546
+ input,
1547
+ &mut arena,
1548
+ 10 * 1024 * 1024, // 10 MB max input
1549
+ 100, // 100 max recursion depth
1550
+ );
1551
+
1552
+ match parser.parse() {
1553
+ Ok(ast) => { /* success */ },
1554
+ Err(ParseError::InputTooLarge { input_size, max_size }) => {
1555
+ eprintln!("Input too large: {} > {}", input_size, max_size);
1556
+ },
1557
+ Err(ParseError::RecursionLimitExceeded { depth, max_depth }) => {
1558
+ eprintln!("Recursion too deep: {} > {}", depth, max_depth);
1559
+ },
1560
+ Err(e) => { /* other errors */ },
1561
+ }
1562
+ ```
1563
+
1564
+ ## Best Practices
1565
+
1566
+ 1. **Always limit input size** when parsing untrusted data
1567
+ 2. **Use external timeouts** for network services (e.g., `tokio::time::timeout`)
1568
+ 3. **Monitor memory usage** in production environments
1569
+
1570
+ See [SECURITY.md](SECURITY.md) for complete security documentation.
1571
+
1572
+ # Module Reference
1573
+
1574
+ ## Core Modules
1575
+
1576
+ | Module | Description |
1577
+ |-------------------------------|---------------------------------------------|
1578
+ | `portable::parser` | PEG parsing engine with packrat memoization |
1579
+ | `portable::grammar` | Grammar representation and serialization |
1580
+ | `portable::ast` | AST node types |
1581
+ | `portable::arena` | Arena allocator for AST nodes |
1582
+ | `portable::cache` | Dense cache for memoization |
1583
+ | `portable::parser_dsl` | Fluent API for grammar definition |
1584
+ | `portable::transform` | Transform system for converting parse trees |
1585
+ | `portable::error` | Rich error reporting |
1586
+ | `portable::infix` | Infix expression parsing with precedence |
1587
+ | `portable::debug` | Debugging and visualization tools |
1588
+ | `portable::source_location` | Source span tracking with line/column info |
1589
+ | `portable::streaming` | Streaming parser support for large inputs |
1590
+ | `portable::streaming_builder` | Single-pass parsing with custom builders |
1591
+ | `portable::parallel` | Parallel parsing for batch processing |
1592
+ | `portable::incremental` | Incremental parsing for editor integration |
1593
+ | `portable::visitor` | AST visitor pattern implementation |
1594
+ | `portable::source_map` | Source map generation for debugging |
1595
+
1596
+ # Examples
1597
+
1598
+ See the `examples/` directory for 39 complete examples demonstrating
1599
+ real-world parsing scenarios:
1600
+
1601
+ ## Expression Parsers
1602
+
1603
+ | Example | Description |
1604
+ |----|----|
1605
+ | `calculator-pattern` | Parse expressions with pattern-based transforms |
1606
+ | `calculator-transform` | Parse and evaluate expressions with native transforms |
1607
+ | `boolean-algebra` | Parse boolean expressions with AND, OR, NOT operators |
1608
+ | `expression-evaluator` | Evaluate expressions with variables and function calls |
1609
+ | `prec-calc` | Precedence climbing algorithm for infix expressions |
1610
+
1611
+ ## Data Formats
1612
+
1613
+ | Example | Description |
1614
+ |--------------|------------------------------------------------------------|
1615
+ | `json-pattern` | JSON parser with pattern matching |
1616
+ | `json-transform` | JSON parser with native transforms |
1617
+ | `csv-pattern` | CSV parser handling quoted fields (pattern mode) |
1618
+ | `csv-transform` | CSV parser handling quoted fields (transform mode) |
1619
+ | `ini` | INI configuration file parser |
1620
+ | `simple-xml` | XML parser with tag matching |
1621
+ | `markup` | Lightweight markup language parser |
1622
+ | `toml` | TOML configuration file parser |
1623
+ | `yaml` | YAML subset parser |
1624
+ | `markdown` | Markdown subset parser with headers and lists |
1625
+ | `iso-8601` | ISO 8601 date/time/duration parser |
1626
+ | `iso-6709` | ISO 6709 geographic coordinate parser |
1627
+
1628
+ ## URLs & Network
1629
+
1630
+ | Example | Description |
1631
+ |--------------|-----------------------------------------------|
1632
+ | `url` | URL parser with scheme, host, path components |
1633
+ | `email` | Email address parser with validation |
1634
+ | `ip-address` | IPv4/IPv6 address parser with validation |
1635
+
1636
+ ## Code & Templates
1637
+
1638
+ | Example | Description |
1639
+ |------------|--------------------------------------------------|
1640
+ | `erb` | ERB template parser for Ruby templates |
1641
+ | `sexp` | S-expression parser for Lisp-style syntax |
1642
+ | `minilisp` | MiniLisp parser demonstrating recursive grammars |
1643
+
1644
+ ## Text Processing
1645
+
1646
+ | Example | Description |
1647
+ |-------------------|---------------------------------------------|
1648
+ | `balanced-parens` | Balanced parentheses parser |
1649
+ | `string-literal` | String literal parser with escape sequences |
1650
+ | `sentence` | Sentence parser with Unicode support |
1651
+ | `comments` | Comment parser (line and block comments) |
1652
+
1653
+ ## Error Handling
1654
+
1655
+ | Example | Description |
1656
+ |-------------------|------------------------------------------|
1657
+ | `error-reporting` | Rich error reporting with tree structure |
1658
+ | `error-recovery` | Error recovery strategies |
1659
+ | `deepest-errors` | Deepest error point tracking |
1660
+ | `nested-errors` | Nested error tree visualization |
1661
+
1662
+ ## Advanced Features
1663
+
1664
+ | Example | Description |
1665
+ |-----------------|----------------------------------------|
1666
+ | `streaming` | Streaming parser for large inputs |
1667
+ | `incremental` | Incremental parsing for editor integration |
1668
+ | `linter` | Code linter with custom validation |
1669
+ | `custom-atoms` | Custom atom registration |
1670
+ | `modularity` | Grammar composition from modules |
1671
+
1672
+ Run examples with:
1673
+
1674
+ ```bash
1675
+ cargo run --example calculator-transform
1676
+ cargo run --example json-pattern
1677
+ cargo run --example url
1678
+ ```
1679
+
1680
+ Full documentation and interactive examples available at [the
1681
+ website](https://parsanol.github.io/examples).
1682
+
1683
+ # API Stability
1684
+
1685
+ The API is currently in active development. Version 0.x indicates that
1686
+ breaking changes may occur.
1687
+
1688
+ Stable APIs:
1689
+
1690
+ - `Grammar` and `GrammarBuilder`
1691
+
1692
+ - `PortableParser` basic parsing
1693
+
1694
+ - `AstArena` and `AstNode`
1695
+
1696
+ - Parser DSL combinators
1697
+
1698
+ - Streaming builder trait and built-in builders
1699
+
1700
+ - Parallel parsing functions
1701
+
1702
+ Experimental APIs (may change):
1703
+
1704
+ - `Transform` and pattern matching
1705
+
1706
+ - Rich error reporting
1707
+
1708
+ - Infix expression parsing
1709
+
1710
+ - Debug/trace tools
1711
+
1712
+ # Documentation
1713
+
1714
+ ## Architecture
1715
+
1716
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the overall system architecture.
1717
+
1718
+ ## Development
1719
+
1720
+ - [docs/refactoring-plan.md](docs/refactoring-plan.md) - Current refactoring roadmap
1721
+ - [docs/continuation-prompt.md](docs/continuation-prompt.md) - Prompt for continuing work
1722
+ - [docs/MIGRATION.md](docs/MIGRATION.md) - Migration guide from Parslet
1723
+
1724
+ # License
1725
+
1726
+ MIT License - see [LICENSE](LICENSE) file for details.
1727
+
1728
+ # Contributing
1729
+
1730
+ Contributions are welcome! Please feel free to submit issues and pull
1731
+ requests at [GitHub](https://github.com/parsanol/parsanol-rs).
1732
+
1733
+ ## Development Setup
1734
+
1735
+ ```bash
1736
+ # Clone the repository
1737
+ git clone https://github.com/parsanol/parsanol-rs.git
1738
+ cd parsanol-rs
1739
+
1740
+ # Build (workspace)
1741
+ cargo build
1742
+
1743
+ # Run tests (234 unit tests)
1744
+ cargo test --lib
1745
+
1746
+ # Run all examples
1747
+ cargo build --examples
1748
+
1749
+ # Run benchmarks
1750
+ cargo bench
1751
+
1752
+ # Check code quality
1753
+ cargo clippy --lib -- -D warnings
1754
+ cargo fmt --check
1755
+ ```
1756
+
1757
+ ## Testing
1758
+
1759
+ The test suite consists of multiple types of tests:
1760
+
1761
+ **Unit tests:** 234 tests covering internal functionality of each module
1762
+ (parser, arena, cache, transform, derive, etc.).
1763
+
1764
+ **Integration tests:** Located in `tests/` directory, test end-to-end parsing scenarios.
1765
+
1766
+ **Examples:** 39 runnable parsers in `examples/` directory demonstrating real-world usage.
1767
+ Examples are compiled and tested via `cargo build --examples`.
1768
+
1769
+ **Documentation tests (doc tests):** Code examples in documentation comments. Note that many doc tests are marked with `ignore` because they show **incomplete code snippets** (e.g., method signatures or pseudocode) rather than complete runnable examples. This is intentional - the doc tests illustrate API patterns, while the `examples/` directory contains fully runnable code that is verified by CI.
1770
+
1771
+ To run all tests:
1772
+
1773
+ ```bash
1774
+ # Unit + integration tests
1775
+ cargo test
1776
+
1777
+ # Include doc tests (most will be ignored as designed)
1778
+ cargo test --doc
1779
+
1780
+ # Test examples compile
1781
+ cargo build --examples
1782
+
1783
+ # Run ignored doc tests (will fail if not complete)
1784
+ cargo test -- --ignored
1785
+ ```
1786
+
1787
+ # Release Process
1788
+
1789
+ This project uses [release-plz](https://release-plz.dev/) for automated releases.
1790
+
1791
+ ## How It Works
1792
+
1793
+ 1. **Push to main** → release-plz creates/updates a Release PR
1794
+
1795
+ 2. **Review and merge the Release PR** → Version is updated in main
1796
+
1797
+ 3. **After merge** → release-plz automatically:
1798
+ - Creates a git tag (e.g., `v0.1.2`)
1799
+ - Publishes to crates.io
1800
+ - Creates a GitHub release
1801
+
1802
+ 4. **Build artifacts** → CI builds native libraries and uploads them to the GitHub release
1803
+
1804
+ ## Maintainer Workflow
1805
+
1806
+ ### Normal Release (Recommended)
1807
+
1808
+ Just push commits with conventional commit messages:
1809
+
1810
+ ```bash
1811
+ git commit -m "feat: add new parser combinator"
1812
+ git push origin main
1813
+ ```
1814
+
1815
+ release-plz will:
1816
+ 1. Create a Release PR with version bump (e.g., `0.1.1` → `0.1.2` for `feat:`)
1817
+ 2. Wait for you to review and merge
1818
+ 3. Publish automatically after merge
1819
+
1820
+ ### Manual Release
1821
+
1822
+ If you need to trigger a release manually:
1823
+
1824
+ 1. Go to **Actions** → **Release** workflow
1825
+ 2. Click **Run workflow**
1826
+ 3. Select action:
1827
+ - `auto` (default): Let release-plz decide
1828
+ - `release-pr`: Just create/update the Release PR
1829
+ - `release`: Force a release immediately
1830
+
1831
+ ### Version Bump Rules
1832
+
1833
+ release-plz uses [conventional commits](https://www.conventionalcommits.org/):
1834
+
1835
+ | Commit Type | Version Bump |
1836
+ |-------------|--------------|
1837
+ | `feat:` | Minor (0.1.0 → 0.2.0) |
1838
+ | `fix:` | Patch (0.1.0 → 0.1.1) |
1839
+ | `feat!:` or `fix!:` | Major (0.1.0 → 1.0.0) |
1840
+ | `docs:`, `chore:`, etc. | No bump (changelog only) |
1841
+
1842
+ ### What Gets Released
1843
+
1844
+ - **crates.io**: `parsanol` crate
1845
+ - **GitHub Release**: With release notes
1846
+ - **Build Artifacts**: Native libraries for Linux, macOS, Windows (x64, ARM64)
1847
+
1848
+ ### Troubleshooting
1849
+
1850
+ **"Already published" error:**
1851
+ - release-plz sees an existing tag and thinks the version is already published
1852
+ - Solution: Ensure Cargo.toml version matches what you want to publish
1853
+
1854
+ **No Release PR created:**
1855
+ - Check that commits follow conventional commit format
1856
+ - Check GitHub Actions logs for the `release-pr` job
1857
+
1858
+ **Publish failed:**
1859
+ - Check crates.io API token is valid
1860
+ - Check version doesn't already exist on crates.io
1861
+
1862
+ ## FFI Feature Testing
1863
+
1864
+ This crate supports optional Ruby and WebAssembly (WASM) FFI features.
1865
+ These must be tested explicitly.
1866
+
1867
+ > [!IMPORTANT]
1868
+ > FFI features require additional setup and may not compile/link in all
1869
+ > environments. Always verify FFI code compiles before pushing to CI.
1870
+
1871
+ ### Ruby FFI Testing
1872
+
1873
+ The Ruby FFI uses the `magnus` crate to provide Ruby bindings.
1874
+
1875
+ **Prerequisites:**
1876
+ - Ruby 3.0+ installed
1877
+ - Ruby development headers (macOS: `brew install ruby`, Ubuntu: `sudo apt-get install ruby-dev`)
1878
+
1879
+ **Testing:**
1880
+ ```bash
1881
+ # Compile-time check (no Ruby required for linking)
1882
+ cargo check --features ruby
1883
+ cargo clippy --features ruby --lib -- -D warnings
1884
+
1885
+ # Full integration tests (requires Ruby runtime)
1886
+ # Note: These tests are marked #[ignore] - run manually
1887
+ cargo test --features ruby --test ruby_ffi -- --ignored
1888
+ ```
1889
+
1890
+ **Test coverage:**
1891
+ - `tests/ruby_ffi.rs` - Comprehensive tests for RubyBuilder, RubyObject trait
1892
+ - Magnus type annotations (e.g., `funcall::<&str, (), Value>`)
1893
+ - Error handling from Ruby callbacks
1894
+ - Parse result conversion
1895
+
1896
+ ### WebAssembly FFI Testing
1897
+
1898
+ The WASM FFI uses `wasm-bindgen` for JavaScript bindings.
1899
+
1900
+ **Prerequisites:**
1901
+ - `wasm-pack` installed (`cargo install wasm-pack`)
1902
+ - `.cargo/config.toml` configures WASM-specific rustflags (auto-applied)
1903
+
1904
+ **Testing:**
1905
+ ```bash
1906
+ # Compile-time check
1907
+ cargo check --features wasm
1908
+ cargo clippy --features wasm --lib -- -D warnings
1909
+
1910
+ # Build for WASM target
1911
+ cargo build --features wasm --target wasm32-unknown-unknown
1912
+
1913
+ # Full WASM build and test
1914
+ wasm-pack build --features wasm
1915
+ wasm-pack test --node --features wasm
1916
+ ```
1917
+
1918
+ **Test coverage:**
1919
+ - `tests/wasm_ffi.rs` - Tests for WASM exports, grammar serialization
1920
+ - JsValue conversions
1921
+ - Error handling for WASM
1922
+
1923
+ ### CI Integration
1924
+
1925
+ CI automatically tests FFI features:
1926
+
1927
+ ``` yaml
1928
+ # From .github/workflows/ci.yml
1929
+ strategy:
1930
+ matrix:
1931
+ feature: ["", "logging", "ruby", "wasm"]
1932
+ ```
1933
+
1934
+ The Ruby and WASM feature tests run on every push to catch FFI
1935
+ regressions early.
1936
+
1937
+ # Release Process
1938
+
1939
+ This project uses [release-plz](https://release-plz.dev/) for automated releases.
1940
+
1941
+ ## How It Works
1942
+
1943
+ ```
1944
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1945
+ │ RELEASE-PLZ WORKFLOW │
1946
+ └─────────────────────────────────────────────────────────────────────────────┘
1947
+
1948
+ Push to main
1949
+ │
1950
+ ▼
1951
+ ┌─────────────────┐
1952
+ │ release-pr job │ Creates/updates Release PR
1953
+ └────────┬────────┘
1954
+ │
1955
+ ▼
1956
+ ┌─────────────────┐
1957
+ │ Release PR │ Contains version bump + changelog
1958
+ │ (on GitHub) │
1959
+ └────────┬────────┘
1960
+ │
1961
+ │ Maintainer reviews and merges
1962
+ ▼
1963
+ ┌─────────────────┐
1964
+ │ release job │ Runs release-plz release
1965
+ └────────┬────────┘
1966
+ │
1967
+ ├──────────────────────────────┐
1968
+ │ │
1969
+ ▼ ▼
1970
+ ┌─────────────────┐ ┌─────────────────┐
1971
+ │ Create tag │ │ Publish to │
1972
+ │ (v0.1.2) │ │ crates.io │
1973
+ └────────┬────────┘ └─────────────────┘
1974
+ │
1975
+ ▼
1976
+ ┌─────────────────┐
1977
+ │ GitHub Release │ With release notes
1978
+ └────────┬────────┘
1979
+ │
1980
+ ▼
1981
+ ┌─────────────────┐
1982
+ │ Build jobs │ Build native libraries
1983
+ └────────┬────────┘
1984
+ │
1985
+ ▼
1986
+ ┌─────────────────┐
1987
+ │ Update Release │ Upload artifacts
1988
+ └─────────────────┘
1989
+ ```
1990
+
1991
+ ## Maintainer Workflow
1992
+
1993
+ ### Normal Release (Recommended)
1994
+
1995
+ Just push commits with conventional commit messages:
1996
+
1997
+ ```bash
1998
+ git commit -m "feat: add new parser combinator"
1999
+ git push origin main
2000
+ ```
2001
+
2002
+ release-plz will:
2003
+ 1. Create a Release PR with version bump (e.g., `0.1.1` → `0.1.2` for `feat:`)
2004
+ 2. Wait for you to review and merge
2005
+ 3. Publish automatically after merge
2006
+
2007
+ ### Manual Release
2008
+
2009
+ If you need to trigger a release manually:
2010
+
2011
+ 1. Go to **Actions** → **Release** workflow
2012
+ 2. Click **Run workflow**
2013
+ 3. Select action:
2014
+ - `auto` (default): Let release-plz decide
2015
+ - `release-pr`: Just create/update the Release PR
2016
+ - `release`: Force a release immediately
2017
+
2018
+ ### Version Bump Rules
2019
+
2020
+ release-plz uses [conventional commits](https://www.conventionalcommits.org/):
2021
+
2022
+ | Commit Type | Version Bump |
2023
+ |-------------|--------------|
2024
+ | `feat:` | Minor (0.1.0 → 0.2.0) |
2025
+ | `fix:` | Patch (0.1.0 → 0.1.1) |
2026
+ | `feat!:` or `fix!:` | Major (0.1.0 → 1.0.0) |
2027
+ | `docs:`, `chore:`, etc. | No bump (changelog only) |
2028
+
2029
+ ### What Gets Released
2030
+
2031
+ - **crates.io**: `parsanol` crate
2032
+ - **GitHub Release**: With release notes
2033
+ - **Build Artifacts**: Native libraries for Linux, macOS, Windows (x64, ARM64)
2034
+
2035
+ ### Troubleshooting
2036
+
2037
+ **"Already published" error:**
2038
+ - release-plz sees an existing tag and thinks the version is already published
2039
+ - Solution: Ensure Cargo.toml version matches what you want to publish
2040
+
2041
+ **No Release PR created:**
2042
+ - Check that commits follow conventional commit format
2043
+ - Check GitHub Actions logs for the `release-pr` job
2044
+
2045
+ **Publish failed:**
2046
+ - Check that the `crates.io` environment is configured in repository settings
2047
+ - Check that trusted publishing is enabled
2048
+
2049
+ # See Also
2050
+
2051
+ - [parsanol-ruby](https://github.com/parsanol/parsanol-ruby) - Ruby
2052
+ bindings
2053
+
2054
+ - [Documentation
2055
+ Website](https://github.com/parsanol/parsanol.github.io)
2056
+
2057
+ - [Parslet](https://github.com/kschiess/parslet) - Original Ruby PEG
2058
+ parser (inspiration)