parsanol 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of parsanol might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/HISTORY.txt +25 -0
- data/LICENSE +23 -0
- data/README.adoc +643 -0
- data/Rakefile +189 -0
- data/example/balanced-parens/basic.rb +42 -0
- data/example/balanced-parens/basic.rb.md +86 -0
- data/example/balanced-parens/parens.rb +42 -0
- data/example/balanced-parens/ruby_transform.rb +162 -0
- data/example/big.erb +73 -0
- data/example/boolean-algebra/basic.rb +70 -0
- data/example/boolean-algebra/basic.rb.md +108 -0
- data/example/boolean-algebra/ruby_transform.rb +263 -0
- data/example/calculator/basic.rb +153 -0
- data/example/calculator/basic.rb.md +120 -0
- data/example/calculator/pattern.rb +153 -0
- data/example/calculator/ruby_transform.rb +156 -0
- data/example/calculator/ruby_transform.rb.md +32 -0
- data/example/calculator/serialized.rb +257 -0
- data/example/calculator/serialized.rb.md +32 -0
- data/example/calculator/transform.rb +153 -0
- data/example/calculator/zero_copy.rb +269 -0
- data/example/calculator/zero_copy.rb.md +36 -0
- data/example/capture/basic.rb +49 -0
- data/example/capture/basic.rb.md +106 -0
- data/example/capture/example.json +39 -0
- data/example/comments/basic.rb +35 -0
- data/example/comments/basic.rb.md +110 -0
- data/example/csv/ruby_transform.rb +148 -0
- data/example/csv/ruby_transform.rb.md +131 -0
- data/example/csv/serialized.rb +201 -0
- data/example/csv/serialized.rb.md +31 -0
- data/example/csv/zero_copy.rb +276 -0
- data/example/csv/zero_copy.rb.md +36 -0
- data/example/custom_atoms/indent_atom.rb +79 -0
- data/example/deepest-errors/basic.rb +131 -0
- data/example/deepest-errors/basic.rb.md +152 -0
- data/example/documentation/basic.rb +18 -0
- data/example/documentation/basic.rb.md +97 -0
- data/example/email/basic.rb +55 -0
- data/example/email/basic.rb.md +102 -0
- data/example/email/ruby_transform.rb +106 -0
- data/example/empty/basic.rb +13 -0
- data/example/empty/basic.rb.md +73 -0
- data/example/empty/example.json +38 -0
- data/example/erb/basic.rb +47 -0
- data/example/erb/basic.rb.md +103 -0
- data/example/erb/optimized.rb +42 -0
- data/example/error-reporting/basic.rb +132 -0
- data/example/error-reporting/basic.rb.md +122 -0
- data/example/expression-evaluator/basic.rb +284 -0
- data/example/expression-evaluator/basic.rb.md +138 -0
- data/example/ini/basic.rb +154 -0
- data/example/ini/basic.rb.md +129 -0
- data/example/ini/ruby_transform.rb +154 -0
- data/example/ip-address/basic.rb +125 -0
- data/example/ip-address/basic.rb.md +139 -0
- data/example/iso-6709/basic.rb +231 -0
- data/example/iso-6709/basic.rb.md +143 -0
- data/example/iso-8601/basic.rb +275 -0
- data/example/iso-8601/basic.rb.md +149 -0
- data/example/json/basic.rb +128 -0
- data/example/json/basic.rb.md +121 -0
- data/example/json/pattern.rb +128 -0
- data/example/json/ruby_transform.rb +200 -0
- data/example/json/ruby_transform.rb.md +32 -0
- data/example/json/serialized.rb +233 -0
- data/example/json/serialized.rb.md +31 -0
- data/example/json/transform.rb +128 -0
- data/example/json/zero_copy.rb +316 -0
- data/example/json/zero_copy.rb.md +36 -0
- data/example/local/basic.rb +34 -0
- data/example/local/basic.rb.md +91 -0
- data/example/local/example.json +38 -0
- data/example/markdown/basic.rb +287 -0
- data/example/markdown/basic.rb.md +160 -0
- data/example/markup/basic.rb +173 -0
- data/example/markup/basic.rb.md +118 -0
- data/example/mathn/basic.rb +47 -0
- data/example/mathn/basic.rb.md +96 -0
- data/example/mathn/example.json +39 -0
- data/example/minilisp/basic.rb +94 -0
- data/example/minilisp/basic.rb.md +133 -0
- data/example/modularity/basic.rb +47 -0
- data/example/modularity/basic.rb.md +152 -0
- data/example/nested-errors/basic.rb +132 -0
- data/example/nested-errors/basic.rb.md +157 -0
- data/example/output/boolean_algebra.out +4 -0
- data/example/output/calc.out +1 -0
- data/example/output/capture.out +3 -0
- data/example/output/comments.out +8 -0
- data/example/output/deepest_errors.out +54 -0
- data/example/output/documentation.err +4 -0
- data/example/output/documentation.out +1 -0
- data/example/output/email_parser.out +2 -0
- data/example/output/empty.err +1 -0
- data/example/output/erb.out +7 -0
- data/example/output/ignore.out +1 -0
- data/example/output/ignore_whitespace.out +1 -0
- data/example/output/ip_address.out +9 -0
- data/example/output/json.out +5 -0
- data/example/output/local.out +3 -0
- data/example/output/mathn.out +4 -0
- data/example/output/minilisp.out +5 -0
- data/example/output/modularity.out +0 -0
- data/example/output/nested_errors.out +54 -0
- data/example/output/optimized_erb.out +1 -0
- data/example/output/parens.out +8 -0
- data/example/output/prec_calc.out +5 -0
- data/example/output/readme.out +1 -0
- data/example/output/scopes.out +1 -0
- data/example/output/seasons.out +28 -0
- data/example/output/sentence.out +1 -0
- data/example/output/simple_xml.out +2 -0
- data/example/output/string_parser.out +3 -0
- data/example/prec-calc/basic.rb +71 -0
- data/example/prec-calc/basic.rb.md +114 -0
- data/example/readme/basic.rb +30 -0
- data/example/readme/basic.rb.md +80 -0
- data/example/scopes/basic.rb +15 -0
- data/example/scopes/basic.rb.md +73 -0
- data/example/scopes/example.json +38 -0
- data/example/seasons/basic.rb +46 -0
- data/example/seasons/basic.rb.md +117 -0
- data/example/seasons/example.json +40 -0
- data/example/sentence/basic.rb +36 -0
- data/example/sentence/basic.rb.md +81 -0
- data/example/sexp/ruby_transform.rb +180 -0
- data/example/sexp/ruby_transform.rb.md +143 -0
- data/example/simple-xml/basic.rb +54 -0
- data/example/simple-xml/basic.rb.md +125 -0
- data/example/simple.lit +3 -0
- data/example/string-literal/basic.rb +77 -0
- data/example/string-literal/basic.rb.md +128 -0
- data/example/test.lit +4 -0
- data/example/toml/basic.rb +226 -0
- data/example/toml/basic.rb.md +173 -0
- data/example/url/basic.rb +219 -0
- data/example/url/basic.rb.md +142 -0
- data/example/url/ruby_transform.rb +219 -0
- data/example/yaml/basic.rb +216 -0
- data/example/yaml/basic.rb.md +148 -0
- data/ext/parsanol_native/extconf.rb +4 -0
- data/lib/parsanol/accelerator/application.rb +62 -0
- data/lib/parsanol/accelerator/engine.rb +112 -0
- data/lib/parsanol/accelerator.rb +162 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +97 -0
- data/lib/parsanol/atoms/base.rb +214 -0
- data/lib/parsanol/atoms/can_flatten.rb +192 -0
- data/lib/parsanol/atoms/capture.rb +41 -0
- data/lib/parsanol/atoms/context.rb +351 -0
- data/lib/parsanol/atoms/context_optimized.rb +42 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +62 -0
- data/lib/parsanol/atoms/dsl.rb +130 -0
- data/lib/parsanol/atoms/dynamic.rb +33 -0
- data/lib/parsanol/atoms/entity.rb +55 -0
- data/lib/parsanol/atoms/ignored.rb +28 -0
- data/lib/parsanol/atoms/infix.rb +121 -0
- data/lib/parsanol/atoms/lookahead.rb +64 -0
- data/lib/parsanol/atoms/named.rb +50 -0
- data/lib/parsanol/atoms/re.rb +61 -0
- data/lib/parsanol/atoms/repetition.rb +241 -0
- data/lib/parsanol/atoms/scope.rb +28 -0
- data/lib/parsanol/atoms/sequence.rb +157 -0
- data/lib/parsanol/atoms/str.rb +90 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +36 -0
- data/lib/parsanol/buffer.rb +130 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +101 -0
- data/lib/parsanol/context.rb +23 -0
- data/lib/parsanol/convenience.rb +35 -0
- data/lib/parsanol/edit_tracker.rb +107 -0
- data/lib/parsanol/error_reporter/contextual.rb +122 -0
- data/lib/parsanol/error_reporter/deepest.rb +106 -0
- data/lib/parsanol/error_reporter/tree.rb +68 -0
- data/lib/parsanol/error_reporter.rb +98 -0
- data/lib/parsanol/export.rb +163 -0
- data/lib/parsanol/expression/treetop.rb +94 -0
- data/lib/parsanol/expression.rb +51 -0
- data/lib/parsanol/fast_mode.rb +145 -0
- data/lib/parsanol/first_set.rb +75 -0
- data/lib/parsanol/grammar_builder.rb +177 -0
- data/lib/parsanol/graphviz.rb +97 -0
- data/lib/parsanol/incremental_parser.rb +179 -0
- data/lib/parsanol/interval_tree.rb +215 -0
- data/lib/parsanol/lazy_result.rb +178 -0
- data/lib/parsanol/lexer.rb +146 -0
- data/lib/parsanol/native/parser.rb +630 -0
- data/lib/parsanol/native/serializer.rb +245 -0
- data/lib/parsanol/native/transformer.rb +438 -0
- data/lib/parsanol/native/types.rb +41 -0
- data/lib/parsanol/native.rb +217 -0
- data/lib/parsanol/optimizer.rb +86 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +175 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +58 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +62 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/ruby_transform.rb +109 -0
- data/lib/parsanol/options/serialized.rb +94 -0
- data/lib/parsanol/options/zero_copy.rb +130 -0
- data/lib/parsanol/options.rb +20 -0
- data/lib/parsanol/parallel.rb +133 -0
- data/lib/parsanol/parsanol_native.bundle +0 -0
- data/lib/parsanol/parser.rb +151 -0
- data/lib/parsanol/parslet.rb +148 -0
- data/lib/parsanol/parslet_native.bundle +0 -0
- data/lib/parsanol/pattern/binding.rb +49 -0
- data/lib/parsanol/pattern.rb +115 -0
- data/lib/parsanol/pool.rb +220 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +173 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +89 -0
- data/lib/parsanol/result.rb +44 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +262 -0
- data/lib/parsanol/rig/rspec.rb +52 -0
- data/lib/parsanol/rope.rb +78 -0
- data/lib/parsanol/scope.rb +42 -0
- data/lib/parsanol/slice.rb +172 -0
- data/lib/parsanol/source/line_cache.rb +99 -0
- data/lib/parsanol/source.rb +171 -0
- data/lib/parsanol/source_location.rb +164 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +192 -0
- data/lib/parsanol/transform.rb +267 -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 +239 -0
- data/lib/parsanol.rb +408 -0
- data/parsanol-ruby.gemspec +56 -0
- data/spec/acceptance/examples_spec.rb +96 -0
- data/spec/acceptance/infix_parser_spec.rb +145 -0
- data/spec/acceptance/mixing_parsers_spec.rb +74 -0
- data/spec/acceptance/regression_spec.rb +329 -0
- data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
- data/spec/acceptance/unconsumed_input_spec.rb +21 -0
- data/spec/benchmark/comparative/runner_spec.rb +105 -0
- data/spec/integration/array_pooling_spec.rb +193 -0
- data/spec/integration/buffer_allocation_spec.rb +324 -0
- data/spec/integration/position_pooling_spec.rb +184 -0
- data/spec/integration/result_builder_spec.rb +282 -0
- data/spec/integration/rope_stringview_integration_spec.rb +188 -0
- data/spec/integration/slice_pooling_spec.rb +63 -0
- data/spec/integration/string_view_integration_spec.rb +125 -0
- data/spec/lexer_spec.rb +231 -0
- data/spec/parsanol/atom_results_spec.rb +39 -0
- data/spec/parsanol/atoms/alternative_spec.rb +26 -0
- data/spec/parsanol/atoms/base_spec.rb +127 -0
- data/spec/parsanol/atoms/capture_spec.rb +21 -0
- data/spec/parsanol/atoms/combinations_spec.rb +5 -0
- data/spec/parsanol/atoms/custom_spec.rb +79 -0
- data/spec/parsanol/atoms/dsl_spec.rb +7 -0
- data/spec/parsanol/atoms/entity_spec.rb +77 -0
- data/spec/parsanol/atoms/ignored_spec.rb +15 -0
- data/spec/parsanol/atoms/infix_spec.rb +5 -0
- data/spec/parsanol/atoms/lookahead_spec.rb +22 -0
- data/spec/parsanol/atoms/named_spec.rb +4 -0
- data/spec/parsanol/atoms/re_spec.rb +14 -0
- data/spec/parsanol/atoms/repetition_spec.rb +24 -0
- data/spec/parsanol/atoms/scope_spec.rb +26 -0
- data/spec/parsanol/atoms/sequence_spec.rb +28 -0
- data/spec/parsanol/atoms/str_spec.rb +15 -0
- data/spec/parsanol/atoms/visitor_spec.rb +101 -0
- data/spec/parsanol/atoms_spec.rb +488 -0
- data/spec/parsanol/auto_optimize_spec.rb +334 -0
- data/spec/parsanol/buffer_spec.rb +219 -0
- data/spec/parsanol/builder_callbacks_spec.rb +377 -0
- data/spec/parsanol/choice_optimizer_spec.rb +231 -0
- data/spec/parsanol/convenience_spec.rb +54 -0
- data/spec/parsanol/cut_inserter_spec.rb +248 -0
- data/spec/parsanol/cut_spec.rb +66 -0
- data/spec/parsanol/edit_tracker_spec.rb +218 -0
- data/spec/parsanol/error_reporter/contextual_spec.rb +122 -0
- data/spec/parsanol/error_reporter/deepest_spec.rb +82 -0
- data/spec/parsanol/error_reporter/tree_spec.rb +7 -0
- data/spec/parsanol/export_spec.rb +67 -0
- data/spec/parsanol/expression/treetop_spec.rb +75 -0
- data/spec/parsanol/first_set_spec.rb +298 -0
- data/spec/parsanol/interval_tree_spec.rb +205 -0
- data/spec/parsanol/lazy_result_spec.rb +288 -0
- data/spec/parsanol/lookahead_optimizer_spec.rb +252 -0
- data/spec/parsanol/minilisp.citrus +29 -0
- data/spec/parsanol/minilisp.tt +29 -0
- data/spec/parsanol/optimizer_spec.rb +459 -0
- data/spec/parsanol/options/parslet_compat_spec.rb +166 -0
- data/spec/parsanol/options/ruby_transform_spec.rb +70 -0
- data/spec/parsanol/options/serialized_spec.rb +69 -0
- data/spec/parsanol/options/zero_copy_spec.rb +230 -0
- data/spec/parsanol/parser_spec.rb +36 -0
- data/spec/parsanol/parslet_spec.rb +38 -0
- data/spec/parsanol/pattern_spec.rb +272 -0
- data/spec/parsanol/pool_spec.rb +392 -0
- data/spec/parsanol/pools/array_pool_spec.rb +356 -0
- data/spec/parsanol/pools/buffer_pool_spec.rb +365 -0
- data/spec/parsanol/pools/position_pool_spec.rb +118 -0
- data/spec/parsanol/pools/slice_pool_spec.rb +262 -0
- data/spec/parsanol/position_spec.rb +14 -0
- data/spec/parsanol/result_builder_spec.rb +391 -0
- data/spec/parsanol/rig/rspec_spec.rb +54 -0
- data/spec/parsanol/rope_spec.rb +207 -0
- data/spec/parsanol/scope_spec.rb +45 -0
- data/spec/parsanol/slice_spec.rb +249 -0
- data/spec/parsanol/source/line_cache_spec.rb +74 -0
- data/spec/parsanol/source_spec.rb +207 -0
- data/spec/parsanol/string_view_spec.rb +345 -0
- data/spec/parsanol/transform/context_spec.rb +56 -0
- data/spec/parsanol/transform_spec.rb +183 -0
- data/spec/parsanol/tree_memoization_spec.rb +149 -0
- data/spec/parslet_compatibility/expressir_edge_cases_spec.rb +153 -0
- data/spec/parslet_compatibility/minimal_reproduction.rb +199 -0
- data/spec/parslet_compatibility_spec.rb +399 -0
- data/spec/parslet_imported/atom_spec.rb +93 -0
- data/spec/parslet_imported/combinator_spec.rb +161 -0
- data/spec/parslet_imported/spec_helper.rb +73 -0
- data/spec/performance/batch_parsing_benchmark.rb +129 -0
- data/spec/performance/complete_optimization_summary.rb +143 -0
- data/spec/performance/grammar_caching_analysis.rb +121 -0
- data/spec/performance/grammar_caching_benchmark.rb +80 -0
- data/spec/performance/native_benchmark_spec.rb +230 -0
- data/spec/performance/phase5_benchmark.rb +144 -0
- data/spec/performance/profiling_benchmark.rb +131 -0
- data/spec/performance/ruby_improvements_benchmark.rb +171 -0
- data/spec/performance_spec.rb +374 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/support/opal.rb +8 -0
- data/spec/support/opal.rb.erb +14 -0
- metadata +485 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Boolean Algebra - Ruby Implementation
|
|
2
|
+
|
|
3
|
+
## How to Run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd parsanol-ruby/example/boolean-algebra
|
|
7
|
+
ruby basic.rb
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Code Walkthrough
|
|
11
|
+
|
|
12
|
+
### Variable Rule
|
|
13
|
+
|
|
14
|
+
Variables are `var` followed by digits:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
rule(:var) { str("var") >> match["0-9"].repeat(1).as(:var) >> space? }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `.as(:var)` captures the numeric suffix for later use.
|
|
21
|
+
|
|
22
|
+
### Operator Precedence
|
|
23
|
+
|
|
24
|
+
AND binds tighter than OR through rule ordering:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
rule(:and_operation) {
|
|
28
|
+
(primary.as(:left) >> and_operator >> and_operation.as(:right)).as(:and) |
|
|
29
|
+
primary
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
rule(:or_operation) {
|
|
33
|
+
(and_operation.as(:left) >> or_operator >> or_operation.as(:right)).as(:or) |
|
|
34
|
+
and_operation
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
OR calls AND first, giving AND higher precedence.
|
|
39
|
+
|
|
40
|
+
### Parentheses Handling
|
|
41
|
+
|
|
42
|
+
Primary handles grouping:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
rule(:primary) { lparen >> or_operation >> rparen | var }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Parenthesized expressions recurse back to the top-level OR rule.
|
|
49
|
+
|
|
50
|
+
### DNF Transform
|
|
51
|
+
|
|
52
|
+
The transform converts to Disjunctive Normal Form:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Transformer < Parsanol::Transform
|
|
56
|
+
rule(:var => simple(:var)) { [[String(var)]] }
|
|
57
|
+
|
|
58
|
+
rule(:or => { :left => subtree(:left), :right => subtree(:right) }) do
|
|
59
|
+
(left + right)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
rule(:and => { :left => subtree(:left), :right => subtree(:right) }) do
|
|
63
|
+
res = []
|
|
64
|
+
left.each do |l|
|
|
65
|
+
right.each do |r|
|
|
66
|
+
res << (l + r)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
res
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
OR concatenates alternatives; AND creates all combinations.
|
|
75
|
+
|
|
76
|
+
### DNF Output Structure
|
|
77
|
+
|
|
78
|
+
Arrays of arrays represent the formula:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# var1 and (var2 or var3)
|
|
82
|
+
# => [["1", "2"], ["1", "3"]]
|
|
83
|
+
# Means: (var1 AND var2) OR (var1 AND var3)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Output Types
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Parse tree for "var1 and (var2 or var3)":
|
|
90
|
+
{:and=>{:left=>{:var=>"1"}, :right=>{:or=>{:left=>{:var=>"2"}, :right=>{:var=>"3"}}}}}
|
|
91
|
+
|
|
92
|
+
# After transform (DNF):
|
|
93
|
+
[["1", "2"], ["1", "3"]]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Design Decisions
|
|
97
|
+
|
|
98
|
+
### Why Right Recursion?
|
|
99
|
+
|
|
100
|
+
Right recursion naturally handles left-to-right parsing while building the correct tree structure.
|
|
101
|
+
|
|
102
|
+
### Why DNF Output?
|
|
103
|
+
|
|
104
|
+
DNF is useful for query optimization and database searches. Each inner array is a conjunction that must all be true.
|
|
105
|
+
|
|
106
|
+
### Why subtree Instead of simple?
|
|
107
|
+
|
|
108
|
+
`subtree(:x)` matches any tree structure, allowing recursive matching of nested AND/OR operations.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Boolean Algebra Parser Example - RubyTransform: Ruby Transform
|
|
2
|
+
#
|
|
3
|
+
# This example demonstrates parsing boolean expressions with AND/OR operators.
|
|
4
|
+
# Shows operator precedence, parentheses handling, and evaluation.
|
|
5
|
+
#
|
|
6
|
+
# Run with: ruby -Ilib example/boolean_algebra_ruby_transform.rb
|
|
7
|
+
|
|
8
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
|
9
|
+
|
|
10
|
+
require 'parsanol'
|
|
11
|
+
|
|
12
|
+
# Step 1: Define the parser grammar
|
|
13
|
+
class BooleanAlgebraParser < Parsanol::Parser
|
|
14
|
+
root :expression
|
|
15
|
+
|
|
16
|
+
rule(:expression) { or_expr }
|
|
17
|
+
|
|
18
|
+
# OR expression (lowest precedence)
|
|
19
|
+
rule(:or_expr) {
|
|
20
|
+
(and_expr.as(:left) >> or_op >> or_expr.as(:right)).as(:or) |
|
|
21
|
+
and_expr
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# AND expression (higher precedence)
|
|
25
|
+
rule(:and_expr) {
|
|
26
|
+
(primary.as(:left) >> and_op >> and_expr.as(:right)).as(:and) |
|
|
27
|
+
primary
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Primary: variable or parenthesized expression
|
|
31
|
+
rule(:primary) {
|
|
32
|
+
lparen >> expression >> rparen |
|
|
33
|
+
variable
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
rule(:variable) { match['a-z'].repeat(1).as(:var) >> digit.repeat(1).as(:num) }
|
|
37
|
+
rule(:digit) { match('[0-9]') }
|
|
38
|
+
|
|
39
|
+
rule(:or_op) { spaces >> str('or') >> spaces }
|
|
40
|
+
rule(:and_op) { spaces >> str('and') >> spaces }
|
|
41
|
+
|
|
42
|
+
rule(:lparen) { str('(') >> spaces? }
|
|
43
|
+
rule(:rparen) { spaces? >> str(')') }
|
|
44
|
+
rule(:spaces) { match('\s').repeat(1) }
|
|
45
|
+
rule(:spaces?) { match('\s').repeat }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Step 2: Define AST classes
|
|
49
|
+
class BoolExpr
|
|
50
|
+
def eval(bindings)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class VarExpr < BoolExpr
|
|
56
|
+
attr_reader :name
|
|
57
|
+
|
|
58
|
+
def initialize(name)
|
|
59
|
+
@name = name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def eval(bindings)
|
|
63
|
+
bindings[@name] || raise("Unknown variable: #{@name}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s = @name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class AndExpr < BoolExpr
|
|
70
|
+
attr_reader :left, :right
|
|
71
|
+
|
|
72
|
+
def initialize(left, right)
|
|
73
|
+
@left = left
|
|
74
|
+
@right = right
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def eval(bindings)
|
|
78
|
+
@left.eval(bindings) && @right.eval(bindings)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_s = "(#{@left} AND #{@right})"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class OrExpr < BoolExpr
|
|
85
|
+
attr_reader :left, :right
|
|
86
|
+
|
|
87
|
+
def initialize(left, right)
|
|
88
|
+
@left = left
|
|
89
|
+
@right = right
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def eval(bindings)
|
|
93
|
+
@left.eval(bindings) || @right.eval(bindings)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_s = "(#{@left} OR #{@right})"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Step 3: Define transform
|
|
100
|
+
class BooleanTransform < Parsanol::Transform
|
|
101
|
+
rule(var: simple(:v), num: simple(:n)) { VarExpr.new("#{v}#{n}") }
|
|
102
|
+
rule(and: simple(:a)) { a }
|
|
103
|
+
rule(or: simple(:o)) { o }
|
|
104
|
+
rule(left: simple(:l), right: simple(:r)) {
|
|
105
|
+
# This handles the case where there's no explicit operator
|
|
106
|
+
l # Just return the left side for non-binary expressions
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Transform that handles binary expressions properly
|
|
111
|
+
class BooleanTransformFull < Parsanol::Transform
|
|
112
|
+
rule(var: simple(:v), num: simple(:n)) { VarExpr.new("#{v}#{n}") }
|
|
113
|
+
|
|
114
|
+
rule(left: simple(:l), right: simple(:r)) {
|
|
115
|
+
# This catches expressions without explicit and/or tags
|
|
116
|
+
# Return just the first one (this is a simplified handling)
|
|
117
|
+
l
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# These would need the actual tree structure to work correctly
|
|
121
|
+
rule(and: subtree(:a)) {
|
|
122
|
+
if a.is_a?(Hash) && a[:left] && a[:right]
|
|
123
|
+
AndExpr.new(transform(a[:left]), transform(a[:right]))
|
|
124
|
+
else
|
|
125
|
+
a
|
|
126
|
+
end
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
rule(or: subtree(:o)) {
|
|
130
|
+
if o.is_a?(Hash) && o[:left] && o[:right]
|
|
131
|
+
OrExpr.new(transform(o[:left]), transform(o[:right]))
|
|
132
|
+
else
|
|
133
|
+
o
|
|
134
|
+
end
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Manual AST builder for demonstration
|
|
139
|
+
def build_ast(tree)
|
|
140
|
+
case tree
|
|
141
|
+
when Hash
|
|
142
|
+
if tree[:var] && tree[:num]
|
|
143
|
+
VarExpr.new("#{tree[:var]}#{tree[:num]}")
|
|
144
|
+
elsif tree[:and]
|
|
145
|
+
build_and_expr(tree[:and])
|
|
146
|
+
elsif tree[:or]
|
|
147
|
+
build_or_expr(tree[:or])
|
|
148
|
+
else
|
|
149
|
+
# Try to extract from nested structure
|
|
150
|
+
tree.each_value do |v|
|
|
151
|
+
result = build_ast(v)
|
|
152
|
+
return result if result.is_a?(BoolExpr)
|
|
153
|
+
end
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
when Array
|
|
157
|
+
build_ast(tree.first)
|
|
158
|
+
when Parsanol::Slice
|
|
159
|
+
nil
|
|
160
|
+
else
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_and_expr(data)
|
|
166
|
+
case data
|
|
167
|
+
when Hash
|
|
168
|
+
left = build_ast(data[:left])
|
|
169
|
+
right = build_ast(data[:right])
|
|
170
|
+
if left && right
|
|
171
|
+
AndExpr.new(left, right)
|
|
172
|
+
else
|
|
173
|
+
build_ast(data)
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
build_ast(data)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_or_expr(data)
|
|
181
|
+
case data
|
|
182
|
+
when Hash
|
|
183
|
+
left = build_ast(data[:left])
|
|
184
|
+
right = build_ast(data[:right])
|
|
185
|
+
if left && right
|
|
186
|
+
OrExpr.new(left, right)
|
|
187
|
+
else
|
|
188
|
+
build_ast(data)
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
build_ast(data)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Step 4: Parse and transform
|
|
196
|
+
def parse_boolean(input)
|
|
197
|
+
parser = BooleanAlgebraParser.new
|
|
198
|
+
|
|
199
|
+
# RubyTransform: Parse and get tree
|
|
200
|
+
tree = parser.parse(input)
|
|
201
|
+
puts "Parse tree: #{tree.inspect}"
|
|
202
|
+
|
|
203
|
+
# Build AST
|
|
204
|
+
ast = build_ast(tree)
|
|
205
|
+
puts "AST: #{ast.to_s}"
|
|
206
|
+
|
|
207
|
+
ast
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Example usage
|
|
211
|
+
if __FILE__ == $0
|
|
212
|
+
puts "=" * 60
|
|
213
|
+
puts "Boolean Algebra Parser - RubyTransform"
|
|
214
|
+
puts "=" * 60
|
|
215
|
+
puts
|
|
216
|
+
|
|
217
|
+
expressions = [
|
|
218
|
+
"var1",
|
|
219
|
+
"var1 and var2",
|
|
220
|
+
"var1 or var2",
|
|
221
|
+
"var1 and var2 or var3",
|
|
222
|
+
"var1 or var2 and var3",
|
|
223
|
+
"(var1 or var2) and var3",
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
expressions.each do |expr_str|
|
|
227
|
+
puts "-" * 40
|
|
228
|
+
puts "Input: #{expr_str}"
|
|
229
|
+
begin
|
|
230
|
+
ast = parse_boolean(expr_str)
|
|
231
|
+
rescue => e
|
|
232
|
+
puts "Error: #{e.message}"
|
|
233
|
+
end
|
|
234
|
+
puts
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Demonstrate evaluation
|
|
238
|
+
puts "=" * 60
|
|
239
|
+
puts "Evaluation Example"
|
|
240
|
+
puts "=" * 60
|
|
241
|
+
|
|
242
|
+
bindings = { "var1" => true, "var2" => false, "var3" => true }
|
|
243
|
+
puts "Bindings: var1=true, var2=false, var3=true"
|
|
244
|
+
puts
|
|
245
|
+
|
|
246
|
+
eval_exprs = [
|
|
247
|
+
"var1 and var2",
|
|
248
|
+
"var1 or var2",
|
|
249
|
+
"var1 and var3",
|
|
250
|
+
"(var1 or var2) and var3",
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
eval_exprs.each do |expr_str|
|
|
254
|
+
begin
|
|
255
|
+
ast = parse_boolean(expr_str)
|
|
256
|
+
result = ast.eval(bindings)
|
|
257
|
+
puts " #{expr_str} = #{result}"
|
|
258
|
+
rescue => e
|
|
259
|
+
puts " #{expr_str} = ERROR: #{e.message}"
|
|
260
|
+
end
|
|
261
|
+
puts
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# A simple integer calculator to answer the question about how to do
|
|
2
|
+
# left and right associativity in parslet (PEG) once and for all.
|
|
3
|
+
|
|
4
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
|
5
|
+
|
|
6
|
+
require 'rspec'
|
|
7
|
+
require 'parsanol/parslet'
|
|
8
|
+
require 'parsanol/rig/rspec'
|
|
9
|
+
|
|
10
|
+
# This is the parsing stage. It expresses left associativity by compiling
|
|
11
|
+
# list of things that have the same associativity.
|
|
12
|
+
class CalcParser < Parsanol::Parser
|
|
13
|
+
root :addition
|
|
14
|
+
|
|
15
|
+
rule(:addition) {
|
|
16
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
|
17
|
+
multiplication
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
rule(:multiplication) {
|
|
21
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
|
22
|
+
integer }
|
|
23
|
+
|
|
24
|
+
rule(:integer) { digit.repeat(1).as(:i) >> space? }
|
|
25
|
+
|
|
26
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
|
27
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
|
28
|
+
|
|
29
|
+
rule(:digit) { match['0-9'] }
|
|
30
|
+
rule(:space?) { match['\s'].repeat }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Classes for the abstract syntax tree.
|
|
34
|
+
Int = Struct.new(:int) {
|
|
35
|
+
def eval; self end
|
|
36
|
+
def op(operation, other)
|
|
37
|
+
left = int
|
|
38
|
+
right = other.int
|
|
39
|
+
|
|
40
|
+
Int.new(
|
|
41
|
+
case operation
|
|
42
|
+
when '+'
|
|
43
|
+
left + right
|
|
44
|
+
when '-'
|
|
45
|
+
left - right
|
|
46
|
+
when '*'
|
|
47
|
+
left * right
|
|
48
|
+
when '/'
|
|
49
|
+
left / right
|
|
50
|
+
end)
|
|
51
|
+
end
|
|
52
|
+
def to_i
|
|
53
|
+
int
|
|
54
|
+
end
|
|
55
|
+
}
|
|
56
|
+
Seq = Struct.new(:sequence) {
|
|
57
|
+
def eval
|
|
58
|
+
sequence.reduce { |accum, operation|
|
|
59
|
+
operation.call(accum) }
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
LeftOp = Struct.new(:operation, :right) {
|
|
63
|
+
def call(left)
|
|
64
|
+
left = left.eval
|
|
65
|
+
right = self.right.eval
|
|
66
|
+
|
|
67
|
+
left.op(operation, right)
|
|
68
|
+
end
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Transforming intermediary syntax tree into a real AST.
|
|
72
|
+
class CalcTransform < Parsanol::Transform
|
|
73
|
+
rule(i: simple(:i)) { Int.new(Integer(i)) }
|
|
74
|
+
rule(o: simple(:o), r: simple(:i)) { LeftOp.new(o, i) }
|
|
75
|
+
rule(l: simple(:i)) { i }
|
|
76
|
+
rule(sequence(:seq)) { Seq.new(seq) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# And this calls everything in the right order.
|
|
80
|
+
def calculate(str)
|
|
81
|
+
intermediary_tree = CalcParser.new.parse(str)
|
|
82
|
+
abstract_tree = CalcTransform.new.apply(intermediary_tree)
|
|
83
|
+
result = abstract_tree.eval
|
|
84
|
+
|
|
85
|
+
result.to_i
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A test suite for the above parser
|
|
89
|
+
describe CalcParser do
|
|
90
|
+
let(:p) { described_class.new }
|
|
91
|
+
describe '#integer' do
|
|
92
|
+
let(:i) { p.integer }
|
|
93
|
+
it "parses integers" do
|
|
94
|
+
i.should parse('1')
|
|
95
|
+
i.should parse('123')
|
|
96
|
+
end
|
|
97
|
+
it "consumes trailing white space" do
|
|
98
|
+
i.should parse('123 ')
|
|
99
|
+
end
|
|
100
|
+
it "doesn't parse floats" do
|
|
101
|
+
i.should_not parse('1.3')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
describe '#multiplication' do
|
|
105
|
+
let(:m) { p.multiplication }
|
|
106
|
+
it "parses simple multiplication" do
|
|
107
|
+
m.should parse('1*2')
|
|
108
|
+
end
|
|
109
|
+
it "parses division" do
|
|
110
|
+
m.should parse('1/2')
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
describe '#addition' do
|
|
114
|
+
let(:a) { p.addition }
|
|
115
|
+
|
|
116
|
+
it "parses simple addition" do
|
|
117
|
+
a.should parse('1+2')
|
|
118
|
+
a.should parse('1+2+3-4')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
describe CalcTransform do
|
|
123
|
+
def t(obj)
|
|
124
|
+
described_class.new.apply(obj)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "transforms integers" do
|
|
128
|
+
t(i: '1').should == Int.new(1)
|
|
129
|
+
end
|
|
130
|
+
it "unwraps left operand" do
|
|
131
|
+
t(l: :obj).should == :obj
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
describe 'whole computation specs' do
|
|
135
|
+
def self.result_of(str, int)
|
|
136
|
+
it(str) { calculate(str).should == int }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
result_of '1+1', 2
|
|
140
|
+
result_of '1-1-1', -1
|
|
141
|
+
result_of '1+1+3*5/2', 9
|
|
142
|
+
result_of '123*2', 246
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Enable these if you want to change the code.
|
|
147
|
+
# RSpec::Core::Runner.run([], $stderr, $stdout)
|
|
148
|
+
|
|
149
|
+
str = ARGV.join
|
|
150
|
+
str = '123*2' if str.match(/^\s*$/)
|
|
151
|
+
|
|
152
|
+
print "#{str} (command line): -> "
|
|
153
|
+
puts calculate(str)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Calculator - Ruby Implementation
|
|
2
|
+
|
|
3
|
+
## How to Run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd parsanol-ruby/example/calculator
|
|
7
|
+
ruby basic.rb "1+2*3"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Code Walkthrough
|
|
11
|
+
|
|
12
|
+
### Left Associativity via Repetition
|
|
13
|
+
|
|
14
|
+
Addition uses repetition for left associativity:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
rule(:addition) {
|
|
18
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
|
19
|
+
multiplication
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The pattern `l >> (op >> r).repeat` creates left-associative chains like `((1+2)+3)`.
|
|
24
|
+
|
|
25
|
+
### Multiplication Rule
|
|
26
|
+
|
|
27
|
+
Multiplication has higher precedence:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
rule(:multiplication) {
|
|
31
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
|
32
|
+
integer
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Multiplication is tried first in the addition rule, giving it higher precedence.
|
|
37
|
+
|
|
38
|
+
### Operator Rules
|
|
39
|
+
|
|
40
|
+
Operators capture their symbol:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
|
44
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `:o` label marks the operator for transform matching.
|
|
48
|
+
|
|
49
|
+
### AST Node Classes
|
|
50
|
+
|
|
51
|
+
Ruby structs represent AST nodes:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
Int = Struct.new(:int) {
|
|
55
|
+
def eval; self end
|
|
56
|
+
def op(operation, other)
|
|
57
|
+
left = int
|
|
58
|
+
right = other.int
|
|
59
|
+
Int.new(
|
|
60
|
+
case operation
|
|
61
|
+
when '+' then left + right
|
|
62
|
+
when '-' then left - right
|
|
63
|
+
when '*' then left * right
|
|
64
|
+
when '/' then left / right
|
|
65
|
+
end)
|
|
66
|
+
end
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Seq = Struct.new(:sequence) {
|
|
70
|
+
def eval
|
|
71
|
+
sequence.reduce { |accum, operation|
|
|
72
|
+
operation.call(accum) }
|
|
73
|
+
end
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
LeftOp = Struct.new(:operation, :right) {
|
|
77
|
+
def call(left)
|
|
78
|
+
left = left.eval
|
|
79
|
+
right = self.right.eval
|
|
80
|
+
left.op(operation, right)
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`Int` represents values; `Seq` represents expression chains; `LeftOp` represents binary operations.
|
|
86
|
+
|
|
87
|
+
### Transform Rules
|
|
88
|
+
|
|
89
|
+
Transform builds the AST:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class CalcTransform < Parsanol::Transform
|
|
93
|
+
rule(i: simple(:i)) { Int.new(Integer(i)) }
|
|
94
|
+
rule(o: simple(:o), r: simple(:i)) { LeftOp.new(o, i) }
|
|
95
|
+
rule(l: simple(:i)) { i }
|
|
96
|
+
rule(sequence(:seq)) { Seq.new(seq) }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Pattern matching extracts components and constructs typed objects.
|
|
101
|
+
|
|
102
|
+
## Output Types
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Int.new(42) # Integer value
|
|
106
|
+
LeftOp.new('+', Int.new(2)) # Binary operation
|
|
107
|
+
Seq.new([Int.new(1), LeftOp.new('+', Int.new(2))]) # Expression chain
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
After `eval`: returns `Int` with result value.
|
|
111
|
+
|
|
112
|
+
## Design Decisions
|
|
113
|
+
|
|
114
|
+
### Why Struct for AST Nodes?
|
|
115
|
+
|
|
116
|
+
Structs are lightweight, immutable, and can define methods. They're ideal for simple AST representation.
|
|
117
|
+
|
|
118
|
+
### Why `repeat(1)` for Operators?
|
|
119
|
+
|
|
120
|
+
`repeat(1)` requires at least one operator, distinguishing `1+2` from bare `1`. The alternative handles the base case.
|