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,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
begin
|
|
5
|
+
require 'benchmark/ips'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
return unless defined?(RSpec)
|
|
8
|
+
RSpec.describe "Native vs Ruby Performance Benchmarks", :performance do
|
|
9
|
+
it "requires benchmark-ips gem" do
|
|
10
|
+
skip "benchmark-ips gem not installed"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
require 'parsanol/native'
|
|
16
|
+
|
|
17
|
+
RSpec.describe "Native vs Ruby Performance Benchmarks", :performance do
|
|
18
|
+
# Skip if native parser not available
|
|
19
|
+
before(:all) do
|
|
20
|
+
skip "Native parser not available" unless Parsanol::Native.available?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Simple calculator parser
|
|
24
|
+
let(:calc_parser) do
|
|
25
|
+
Class.new(Parsanol::Parser) do
|
|
26
|
+
rule(:addition) {
|
|
27
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
|
28
|
+
multiplication
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rule(:multiplication) {
|
|
32
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
|
33
|
+
integer
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
rule(:integer) { digit.repeat(1).as(:i) >> space? }
|
|
37
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
|
38
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
|
39
|
+
rule(:digit) { match['0-9'] }
|
|
40
|
+
rule(:space?) { match['\s'].repeat }
|
|
41
|
+
|
|
42
|
+
root :addition
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# JSON parser
|
|
47
|
+
let(:json_parser) do
|
|
48
|
+
Class.new(Parsanol::Parser) do
|
|
49
|
+
rule(:spaces) { match('\s').repeat(1) }
|
|
50
|
+
rule(:spaces?) { spaces.maybe }
|
|
51
|
+
rule(:comma) { spaces? >> str(',') >> spaces? }
|
|
52
|
+
rule(:digit) { match('[0-9]') }
|
|
53
|
+
|
|
54
|
+
rule(:number) {
|
|
55
|
+
(
|
|
56
|
+
str('-').maybe >> (
|
|
57
|
+
str('0') | (match('[1-9]') >> digit.repeat)
|
|
58
|
+
) >> (
|
|
59
|
+
str('.') >> digit.repeat(1)
|
|
60
|
+
).maybe >> (
|
|
61
|
+
match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
|
|
62
|
+
).maybe
|
|
63
|
+
).as(:number)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rule(:string) {
|
|
67
|
+
str('"') >> (
|
|
68
|
+
str('\\') >> any | str('"').absent? >> any
|
|
69
|
+
).repeat.as(:string) >> str('"')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
rule(:array) {
|
|
73
|
+
str('[') >> spaces? >>
|
|
74
|
+
(value >> (comma >> value).repeat).maybe.as(:array) >>
|
|
75
|
+
spaces? >> str(']')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rule(:object) {
|
|
79
|
+
str('{') >> spaces? >>
|
|
80
|
+
(entry >> (comma >> entry).repeat).maybe.as(:object) >>
|
|
81
|
+
spaces? >> str('}')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rule(:value) {
|
|
85
|
+
string | number |
|
|
86
|
+
object | array |
|
|
87
|
+
str('true').as(:true) | str('false').as(:false) |
|
|
88
|
+
str('null').as(:null)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
rule(:entry) {
|
|
92
|
+
(
|
|
93
|
+
string.as(:key) >> spaces? >>
|
|
94
|
+
str(':') >> spaces? >>
|
|
95
|
+
value.as(:val)
|
|
96
|
+
).as(:entry)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
rule(:top) { spaces? >> value >> spaces? }
|
|
100
|
+
root(:top)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Identifier parser (tests regex patterns)
|
|
105
|
+
let(:identifier_parser) do
|
|
106
|
+
Class.new(Parsanol::Parser) do
|
|
107
|
+
rule(:identifier) { match('[a-zA-Z_]').repeat(1).as(:id) }
|
|
108
|
+
root :identifier
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "Simple calculator expressions" do
|
|
113
|
+
let(:input) { '1 + 2 * 3 + 4 * 5' }
|
|
114
|
+
|
|
115
|
+
# NOTE: AST structure differs between Ruby and Native parsers
|
|
116
|
+
# Ruby flattens sequences, Native returns structured output
|
|
117
|
+
# Both produce valid parse trees, just structured differently
|
|
118
|
+
it "native parser successfully parses calculator expressions" do
|
|
119
|
+
result = Parsanol::Native.parse_parslet_compatible(calc_parser.new, input)
|
|
120
|
+
expect(result).not_to be_nil
|
|
121
|
+
# Result can be Array or Hash depending on grammar structure
|
|
122
|
+
expect(result).to be_a(Array).or be_a(Hash)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "measures speedup" do
|
|
126
|
+
parser = calc_parser.new
|
|
127
|
+
grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
128
|
+
|
|
129
|
+
ruby_ips = Benchmark.ips(quiet: true) do |x|
|
|
130
|
+
x.report('ruby') { parser.parse(input) }
|
|
131
|
+
end.entries.first.ips
|
|
132
|
+
|
|
133
|
+
native_ips = Benchmark.ips(quiet: true) do |x|
|
|
134
|
+
x.report('native') { Parsanol::Native.parse(grammar, input) }
|
|
135
|
+
end.entries.first.ips
|
|
136
|
+
|
|
137
|
+
speedup = native_ips / ruby_ips
|
|
138
|
+
puts "Calculator: #{speedup.round(1)}x faster (Ruby: #{ruby_ips.round(0)} ips, Native: #{native_ips.round(0)} ips)"
|
|
139
|
+
|
|
140
|
+
# Expect at least 2x speedup for simple grammars
|
|
141
|
+
expect(speedup).to be > 1.0
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe "JSON parsing" do
|
|
146
|
+
let(:simple_json) { '{"key": "value", "number": 123}' }
|
|
147
|
+
let(:nested_json) { '{"a": [1, 2, 3], "b": {"c": "d"}}' }
|
|
148
|
+
|
|
149
|
+
# NOTE: AST structure differs between Ruby and Native parsers
|
|
150
|
+
# Both produce valid parse trees, just structured differently
|
|
151
|
+
it "native parser successfully parses simple JSON" do
|
|
152
|
+
result = Parsanol::Native.parse_parslet_compatible(json_parser.new, simple_json)
|
|
153
|
+
expect(result).not_to be_nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "measures speedup for simple JSON" do
|
|
157
|
+
parser = json_parser.new
|
|
158
|
+
grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
159
|
+
|
|
160
|
+
ruby_ips = Benchmark.ips(quiet: true) do |x|
|
|
161
|
+
x.report('ruby') { parser.parse(simple_json) }
|
|
162
|
+
end.entries.first.ips
|
|
163
|
+
|
|
164
|
+
native_ips = Benchmark.ips(quiet: true) do |x|
|
|
165
|
+
x.report('native') { Parsanol::Native.parse(grammar, simple_json) }
|
|
166
|
+
end.entries.first.ips
|
|
167
|
+
|
|
168
|
+
speedup = native_ips / ruby_ips
|
|
169
|
+
puts "JSON simple: #{speedup.round(1)}x faster (Ruby: #{ruby_ips.round(0)} ips, Native: #{native_ips.round(0)} ips)"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe "Identifier parsing (regex patterns)" do
|
|
174
|
+
# Use pattern that matches the entire input
|
|
175
|
+
let(:identifier_parser) do
|
|
176
|
+
Class.new(Parsanol::Parser) do
|
|
177
|
+
rule(:identifier) { match('[a-zA-Z_]').repeat(1).as(:id) }
|
|
178
|
+
root :identifier
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
let(:input) { 'hello_world' } # No digits, matches [a-zA-Z_]
|
|
182
|
+
|
|
183
|
+
# NOTE: Native returns char array, Ruby returns joined string
|
|
184
|
+
# Both are valid representations
|
|
185
|
+
it "native parser successfully parses identifier patterns" do
|
|
186
|
+
result = Parsanol::Native.parse_parslet_compatible(identifier_parser.new, input)
|
|
187
|
+
expect(result).not_to be_nil
|
|
188
|
+
expect(result).to have_key(:id)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "measures speedup for identifier parsing" do
|
|
192
|
+
parser = identifier_parser.new
|
|
193
|
+
grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
194
|
+
|
|
195
|
+
ruby_ips = Benchmark.ips(quiet: true) do |x|
|
|
196
|
+
x.report('ruby') { parser.parse(input) }
|
|
197
|
+
end.entries.first.ips
|
|
198
|
+
|
|
199
|
+
native_ips = Benchmark.ips(quiet: true) do |x|
|
|
200
|
+
x.report('native') { Parsanol::Native.parse(grammar, input) }
|
|
201
|
+
end.entries.first.ips
|
|
202
|
+
|
|
203
|
+
speedup = native_ips / ruby_ips
|
|
204
|
+
puts "Identifier: #{speedup.round(1)}x faster (Ruby: #{ruby_ips.round(0)} ips, Native: #{native_ips.round(0)} ips)"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe "Memory allocation comparison" do
|
|
209
|
+
it "native uses fewer allocations" do
|
|
210
|
+
input = '1 + 2 * 3'
|
|
211
|
+
parser = calc_parser.new
|
|
212
|
+
|
|
213
|
+
# Measure Ruby allocations
|
|
214
|
+
GC.start
|
|
215
|
+
before_ruby = GC.stat(:total_allocated_objects)
|
|
216
|
+
10.times { parser.parse(input) }
|
|
217
|
+
ruby_allocs = GC.stat(:total_allocated_objects) - before_ruby
|
|
218
|
+
|
|
219
|
+
# Measure Native allocations
|
|
220
|
+
grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
221
|
+
GC.start
|
|
222
|
+
before_native = GC.stat(:total_allocated_objects)
|
|
223
|
+
10.times { Parsanol::Native.parse(grammar, input) }
|
|
224
|
+
native_allocs = GC.stat(:total_allocated_objects) - before_native
|
|
225
|
+
|
|
226
|
+
reduction = (1 - native_allocs.to_f / ruby_allocs) * 100
|
|
227
|
+
puts "Allocations: Ruby=#{ruby_allocs}, Native=#{native_allocs} (#{reduction.round(1)}% reduction)"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark"
|
|
4
|
+
require "parslet"
|
|
5
|
+
require "parslet/native"
|
|
6
|
+
|
|
7
|
+
puts "=" * 70
|
|
8
|
+
puts "Phase 5 Benchmark: Grammar Hash Caching"
|
|
9
|
+
puts "=" * 70
|
|
10
|
+
|
|
11
|
+
# First ensure native extension is loaded
|
|
12
|
+
unless Parsanol::Native.available?
|
|
13
|
+
puts "ERROR: Native extension not available. Run 'rake compile' first."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A larger parser to show the impact
|
|
18
|
+
class MediumParser < Parsanol::Parser
|
|
19
|
+
rule(:space) { match(/\s/).repeat(1) }
|
|
20
|
+
rule(:space?) { space.maybe }
|
|
21
|
+
|
|
22
|
+
rule(:digit) { match(/[0-9]/) }
|
|
23
|
+
rule(:letter) { match(/[a-zA-Z]/) }
|
|
24
|
+
rule(:alnum) { match(/[a-zA-Z0-9]/) }
|
|
25
|
+
|
|
26
|
+
rule(:integer) { digit.repeat(1) }
|
|
27
|
+
rule(:float) { digit.repeat(1) >> str(".") >> digit.repeat(1) }
|
|
28
|
+
rule(:number) { (float | integer).as(:number) }
|
|
29
|
+
|
|
30
|
+
rule(:string) { str('"') >> match(/[^"]/).repeat.as(:string) >> str('"') }
|
|
31
|
+
|
|
32
|
+
rule(:identifier) { (letter >> alnum.repeat).as(:identifier) }
|
|
33
|
+
|
|
34
|
+
rule(:atom) { number | string | identifier }
|
|
35
|
+
|
|
36
|
+
rule(:add_op) { str("+") | str("-") }
|
|
37
|
+
rule(:mul_op) { str("*") | str("/") }
|
|
38
|
+
|
|
39
|
+
rule(:mul_expr) { atom >> (space? >> mul_op >> space? >> atom).repeat }
|
|
40
|
+
rule(:add_expr) { mul_expr >> (space? >> add_op >> space? >> mul_expr).repeat }
|
|
41
|
+
rule(:expression) { add_expr.as(:expression) }
|
|
42
|
+
|
|
43
|
+
rule(:comma) { str(",") >> space? }
|
|
44
|
+
rule(:arg_list) { expression >> (comma >> expression).repeat }
|
|
45
|
+
rule(:function_call) { identifier >> str("(") >> space? >> arg_list.as(:args).maybe >> str(")") }
|
|
46
|
+
|
|
47
|
+
rule(:statement) { (expression | function_call).as(:statement) >> str(";") }
|
|
48
|
+
rule(:program) { space? >> statement.repeat.as(:statements) >> space? }
|
|
49
|
+
|
|
50
|
+
root(:program)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
parser = MediumParser.new
|
|
54
|
+
test_input = 'x = 1 + 2 * 3; y = "hello"; func(x, y, 42);'
|
|
55
|
+
|
|
56
|
+
puts "\n" + "-" * 70
|
|
57
|
+
puts "Test 1: Cold Cache (first parse)"
|
|
58
|
+
puts "-" * 70
|
|
59
|
+
|
|
60
|
+
Parsanol::Native.clear_cache
|
|
61
|
+
|
|
62
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
63
|
+
result = Parsanol::Native.parse_parslet_compatible(parser, test_input)
|
|
64
|
+
cold_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
65
|
+
|
|
66
|
+
puts "Time: #{cold_time} μs"
|
|
67
|
+
puts "Cache: #{Parsanol::Native.cache_stats}"
|
|
68
|
+
|
|
69
|
+
puts "\n" + "-" * 70
|
|
70
|
+
puts "Test 2: Warm Cache (grammar hash cached)"
|
|
71
|
+
puts "-" * 70
|
|
72
|
+
|
|
73
|
+
# The key improvement: object_id cache avoids grammar structure traversal
|
|
74
|
+
times = []
|
|
75
|
+
50.times do
|
|
76
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
77
|
+
result = Parsanol::Native.parse_parslet_compatible(parser, test_input)
|
|
78
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
79
|
+
times << elapsed
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
avg_warm = times.sum / times.length
|
|
83
|
+
puts "Average time: #{avg_warm.round(2)} μs"
|
|
84
|
+
puts "Min: #{times.min.round(2)} μs, Max: #{times.max.round(2)} μs"
|
|
85
|
+
puts "Cache: #{Parsanol::Native.cache_stats}"
|
|
86
|
+
|
|
87
|
+
puts "\n" + "-" * 70
|
|
88
|
+
puts "Test 3: Repeated parsing with different inputs"
|
|
89
|
+
puts "-" * 70
|
|
90
|
+
|
|
91
|
+
inputs = [
|
|
92
|
+
'a = 1;',
|
|
93
|
+
'b = 2 + 3;',
|
|
94
|
+
'c = x * y;',
|
|
95
|
+
'func(a, b, c);',
|
|
96
|
+
'result = "test";',
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
Parsanol::Native.clear_cache
|
|
100
|
+
|
|
101
|
+
# First parse (cold)
|
|
102
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
103
|
+
inputs.each { |i| Parsanol::Native.parse_parslet_compatible(parser, i) }
|
|
104
|
+
first_batch = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
105
|
+
|
|
106
|
+
# Second batch (warm)
|
|
107
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
108
|
+
inputs.each { |i| Parsanol::Native.parse_parslet_compatible(parser, i) }
|
|
109
|
+
second_batch = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
110
|
+
|
|
111
|
+
puts "First batch (cold): #{first_batch} μs"
|
|
112
|
+
puts "Second batch (warm): #{second_batch} μs"
|
|
113
|
+
puts "Improvement: #{(first_batch.to_f / second_batch).round(1)}x faster"
|
|
114
|
+
puts "Cache: #{Parsanol::Native.cache_stats}"
|
|
115
|
+
|
|
116
|
+
puts "\n" + "=" * 70
|
|
117
|
+
puts "SUMMARY"
|
|
118
|
+
puts "=" * 70
|
|
119
|
+
|
|
120
|
+
speedup = cold_time > 0 && avg_warm > 0 ? (cold_time.to_f / avg_warm).round(0) : 0
|
|
121
|
+
|
|
122
|
+
puts <<~SUMMARY
|
|
123
|
+
|
|
124
|
+
COLD CACHE (first parse):
|
|
125
|
+
Time: #{cold_time} μs
|
|
126
|
+
|
|
127
|
+
WARM CACHE (repeated parsing):
|
|
128
|
+
Time: #{avg_warm.round(2)} μs
|
|
129
|
+
Speedup: #{speedup}x faster
|
|
130
|
+
|
|
131
|
+
TWO-LEVEL CACHE:
|
|
132
|
+
Level 1 (object_id → hash): #{Parsanol::Native.cache_stats[:hash_cache_size]} entries
|
|
133
|
+
Level 2 (hash → json): #{Parsanol::Native.cache_stats[:grammar_cache_size]} entries
|
|
134
|
+
|
|
135
|
+
OPTIMIZATION APPLIED:
|
|
136
|
+
✓ Two-level grammar caching
|
|
137
|
+
✓ Avoids grammar structure traversal on repeated parses
|
|
138
|
+
✓ Shares grammar JSON across parser instances with same structure
|
|
139
|
+
|
|
140
|
+
SUMMARY
|
|
141
|
+
|
|
142
|
+
puts "=" * 70
|
|
143
|
+
puts "Benchmark complete"
|
|
144
|
+
puts "=" * 70
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark"
|
|
4
|
+
require "parslet"
|
|
5
|
+
require "parslet/native"
|
|
6
|
+
|
|
7
|
+
puts "=" * 60
|
|
8
|
+
puts "Parsanol Profiling Analysis (Native Parser)"
|
|
9
|
+
puts "=" * 60
|
|
10
|
+
|
|
11
|
+
# First ensure native extension is loaded
|
|
12
|
+
unless Parsanol::Native.available?
|
|
13
|
+
puts "ERROR: Native extension not available. Run 'rake compile' first."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class SimpleParser < Parsanol::Parser
|
|
18
|
+
rule(:comma) { str(",") >> str(" ").maybe }
|
|
19
|
+
rule(:word) { match(/[a-z]/).repeat(1) }
|
|
20
|
+
rule(:alnum) { match(/[a-z0-9]/).repeat(1) }
|
|
21
|
+
|
|
22
|
+
rule(:value) { (word | alnum).as(:v) }
|
|
23
|
+
rule(:list) { value >> (comma >> value).repeat }
|
|
24
|
+
|
|
25
|
+
root(:list)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parser = SimpleParser.new
|
|
29
|
+
|
|
30
|
+
# Test input
|
|
31
|
+
test_input = "one, two, three, four, five, six, seven, eight, nine, ten"
|
|
32
|
+
large_input = (1..100).map { |i| "word" }.join(", ")
|
|
33
|
+
|
|
34
|
+
# Reset profile
|
|
35
|
+
Parsanol::Native.profile_reset
|
|
36
|
+
|
|
37
|
+
# Run parsing multiple times using NATIVE parser
|
|
38
|
+
puts "\nRunning 100 native parses..."
|
|
39
|
+
100.times { Parsanol::Native.parse_parslet_compatible(parser, test_input) }
|
|
40
|
+
|
|
41
|
+
# Get profile
|
|
42
|
+
profile = Parsanol::Native.profile_stats
|
|
43
|
+
|
|
44
|
+
puts "\n" + "-" * 60
|
|
45
|
+
puts "Profile Results"
|
|
46
|
+
puts "-" * 60
|
|
47
|
+
|
|
48
|
+
total_us = profile["total_parse_us"].to_i
|
|
49
|
+
if total_us == 0
|
|
50
|
+
puts "\nWARNING: No timing data collected. Parser may be using Ruby implementation."
|
|
51
|
+
else
|
|
52
|
+
puts "\nTiming (microseconds):"
|
|
53
|
+
puts " Total parse time: #{total_us} us"
|
|
54
|
+
puts " Grammar parsing: #{profile["grammar_parse_us"]} us (#{((profile["grammar_parse_us"].to_f / total_us) * 100).round(1)}%)"
|
|
55
|
+
puts " PEG matching: #{profile["peg_match_us"]} us (#{((profile["peg_match_us"].to_f / total_us) * 100).round(1)}%)"
|
|
56
|
+
puts " AST to Ruby: #{profile["ast_to_ruby_us"]} us (#{((profile["ast_to_ruby_us"].to_f / total_us) * 100).round(1)}%)"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
total_cache = profile["cache_hits"].to_i + profile["cache_misses"].to_i
|
|
60
|
+
puts "\nCache Performance:"
|
|
61
|
+
puts " Hits: #{profile["cache_hits"]}"
|
|
62
|
+
puts " Misses: #{profile["cache_misses"]}"
|
|
63
|
+
puts " Hit rate: #{profile["cache_hit_rate"]}%"
|
|
64
|
+
|
|
65
|
+
puts "\nMatch Performance:"
|
|
66
|
+
puts " Lookup matches: #{profile["lookup_matches"]}"
|
|
67
|
+
puts " Regex matches: #{profile["regex_matches"]}"
|
|
68
|
+
total_matches = profile["lookup_matches"].to_i + profile["regex_matches"].to_i
|
|
69
|
+
if total_matches > 0
|
|
70
|
+
lookup_pct = ((profile["lookup_matches"].to_f / total_matches) * 100).round(1)
|
|
71
|
+
puts " Fast path: #{lookup_pct}%"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "\nAllocation Stats:"
|
|
75
|
+
puts " AST nodes: #{profile["ast_nodes"]}"
|
|
76
|
+
puts " String allocs: #{profile["string_allocs"]}"
|
|
77
|
+
if profile["ast_nodes"].to_i > 0
|
|
78
|
+
str_per_node = (profile["string_allocs"].to_f / profile["ast_nodes"].to_i * 100).round(1)
|
|
79
|
+
puts " Strings/node: #{str_per_node}%"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Performance summary
|
|
83
|
+
puts "\n" + "=" * 60
|
|
84
|
+
puts "Analysis Summary"
|
|
85
|
+
puts "=" * 60
|
|
86
|
+
|
|
87
|
+
if total_us > 0
|
|
88
|
+
peg_pct = (profile["peg_match_us"].to_f / total_us * 100).round(1)
|
|
89
|
+
ruby_pct = (profile["ast_to_ruby_us"].to_f / total_us * 100).round(1)
|
|
90
|
+
|
|
91
|
+
puts "\nHot Path Analysis:"
|
|
92
|
+
if peg_pct > 50
|
|
93
|
+
puts " ⚠️ PEG matching is the bottleneck (#{peg_pct}%)"
|
|
94
|
+
puts " -> Consider optimizing grammar or using tokens"
|
|
95
|
+
elsif ruby_pct > 40
|
|
96
|
+
puts " ⚠️ AST to Ruby conversion is the bottleneck (#{ruby_pct}%)"
|
|
97
|
+
puts " -> Consider batch conversion or fewer allocations"
|
|
98
|
+
else
|
|
99
|
+
puts " ✓ Time is distributed, good balance"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if profile["cache_hit_rate"].to_i < 80
|
|
103
|
+
puts "\n ⚠️ Cache hit rate is low (#{profile["cache_hit_rate"]}%)"
|
|
104
|
+
puts " -> Consider larger cache or different strategy"
|
|
105
|
+
else
|
|
106
|
+
puts "\n ✓ Cache hit rate is good (#{profile["cache_hit_rate"]}%)"
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
puts "\nNo timing data available for analysis."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Large input test
|
|
113
|
+
puts "\n" + "-" * 60
|
|
114
|
+
puts "Large Input Test (#{large_input.length} chars)"
|
|
115
|
+
puts "-" * 60
|
|
116
|
+
|
|
117
|
+
Parsanol::Native.profile_reset
|
|
118
|
+
20.times { Parsanol::Native.parse_parslet_compatible(parser, large_input) }
|
|
119
|
+
profile_large = Parsanol::Native.profile_stats
|
|
120
|
+
|
|
121
|
+
puts "\nTiming:"
|
|
122
|
+
puts " Total parse time: #{profile_large["total_parse_us"]} us"
|
|
123
|
+
puts " PEG matching: #{profile_large["peg_match_us"]} us"
|
|
124
|
+
puts " AST to Ruby: #{profile_large["ast_to_ruby_us"]} us"
|
|
125
|
+
|
|
126
|
+
puts "\nCache:"
|
|
127
|
+
puts " Hit rate: #{profile_large["cache_hit_rate"]}%"
|
|
128
|
+
|
|
129
|
+
puts "\n" + "=" * 60
|
|
130
|
+
puts "Profiling complete"
|
|
131
|
+
puts "=" * 60
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark"
|
|
4
|
+
require "parslet"
|
|
5
|
+
require "parslet/native"
|
|
6
|
+
|
|
7
|
+
puts "=" * 60
|
|
8
|
+
puts "Parsanol Ruby Improvements Benchmark"
|
|
9
|
+
puts "=" * 60
|
|
10
|
+
|
|
11
|
+
# First ensure native extension is loaded
|
|
12
|
+
unless Parsanol::Native.available?
|
|
13
|
+
puts "ERROR: Native extension not available. Run 'rake compile' first."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class SimpleParser < Parsanol::Parser
|
|
18
|
+
rule(:comma) { str(",") >> str(" ").maybe }
|
|
19
|
+
rule(:word) { match(/[a-z]/).repeat(1) }
|
|
20
|
+
rule(:alnum) { match(/[a-z0-9]/).repeat(1) }
|
|
21
|
+
|
|
22
|
+
rule(:value) { (word | alnum).as(:v) }
|
|
23
|
+
rule(:list) { value >> (comma >> value).repeat }
|
|
24
|
+
|
|
25
|
+
root(:list)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# More complex parser to test caching
|
|
29
|
+
class ExpressionParser < Parsanol::Parser
|
|
30
|
+
rule(:space) { match(/\s/).repeat(1) }
|
|
31
|
+
rule(:spaces) { space.maybe }
|
|
32
|
+
|
|
33
|
+
rule(:digit) { match(/[0-9]/) }
|
|
34
|
+
rule(:number) { digit.repeat(1).as(:num) }
|
|
35
|
+
|
|
36
|
+
rule(:lparen) { str("(") >> spaces }
|
|
37
|
+
rule(:rparen) { str(")") >> spaces }
|
|
38
|
+
|
|
39
|
+
rule(:plus) { str("+") >> spaces }
|
|
40
|
+
rule(:minus) { str("-") >> spaces }
|
|
41
|
+
rule(:times) { str("*") >> spaces }
|
|
42
|
+
rule(:divide) { str("/") >> spaces }
|
|
43
|
+
|
|
44
|
+
rule(:factor) { number | (lparen >> expression >> rparen) }
|
|
45
|
+
rule(:term) { factor >> ((times | divide) >> factor).repeat }
|
|
46
|
+
rule(:expression) { term >> ((plus | minus) >> term).repeat }
|
|
47
|
+
|
|
48
|
+
root(:expression)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
parser = SimpleParser.new
|
|
52
|
+
expr_parser = ExpressionParser.new
|
|
53
|
+
|
|
54
|
+
# Test inputs
|
|
55
|
+
simple_input = "one, two, three, four, five"
|
|
56
|
+
complex_input = "1 + 2 * 3 - 4 / 5"
|
|
57
|
+
large_input = (1..100).map { |i| "word" }.join(", ")
|
|
58
|
+
|
|
59
|
+
# Clear cache first
|
|
60
|
+
Parsanol::Native.clear_cache
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# Test 1: Simple parser - first parse (cold cache)
|
|
64
|
+
# ============================================================================
|
|
65
|
+
puts "\n" + "-" * 60
|
|
66
|
+
puts "Test 1: Simple Parser (first parse, cold cache)"
|
|
67
|
+
puts "-" * 60
|
|
68
|
+
|
|
69
|
+
Parsanol::Native.profile_reset
|
|
70
|
+
result1 = Parsanol::Native.parse_parslet_compatible(parser, simple_input)
|
|
71
|
+
profile1 = Parsanol::Native.profile_stats
|
|
72
|
+
|
|
73
|
+
puts "Result: #{result1.inspect}"
|
|
74
|
+
puts "Time: #{profile1["total_parse_us"]} us"
|
|
75
|
+
|
|
76
|
+
# ============================================================================
|
|
77
|
+
# Test 2: Simple parser - repeated parses (warm cache)
|
|
78
|
+
# ============================================================================
|
|
79
|
+
puts "\n" + "-" * 60
|
|
80
|
+
puts "Test 2: Simple Parser (100 parses, warm cache)"
|
|
81
|
+
puts "-" * 60
|
|
82
|
+
|
|
83
|
+
Parsanol::Native.profile_reset
|
|
84
|
+
Parsanol::Native.clear_cache
|
|
85
|
+
|
|
86
|
+
times = []
|
|
87
|
+
100.times do
|
|
88
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
89
|
+
Parsanol::Native.parse_parslet_compatible(parser, simple_input)
|
|
90
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
91
|
+
times << elapsed
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
avg_time = times.sum / times.length
|
|
95
|
+
profile2 = Parsanol::Native.profile_stats
|
|
96
|
+
|
|
97
|
+
puts "Average time: #{avg_time} us"
|
|
98
|
+
puts "First parse: #{times.first} us"
|
|
99
|
+
puts "Last parse: #{times.last} us"
|
|
100
|
+
puts "Cache stats: #{Parsanol::Native.cache_stats}"
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Test 3: Complex parser (more grammar atoms)
|
|
104
|
+
# ============================================================================
|
|
105
|
+
puts "\n" + "-" * 60
|
|
106
|
+
puts "Test 3: Expression Parser (100 parses)"
|
|
107
|
+
puts "-" * 60
|
|
108
|
+
|
|
109
|
+
Parsanol::Native.profile_reset
|
|
110
|
+
Parsanol::Native.clear_cache
|
|
111
|
+
|
|
112
|
+
times = []
|
|
113
|
+
100.times do
|
|
114
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
115
|
+
Parsanol::Native.parse_parslet_compatible(expr_parser, complex_input)
|
|
116
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
117
|
+
times << elapsed
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
avg_time = times.sum / times.length
|
|
121
|
+
profile3 = Parsanol::Native.profile_stats
|
|
122
|
+
|
|
123
|
+
puts "Average time: #{avg_time} us"
|
|
124
|
+
puts "First parse: #{times.first} us"
|
|
125
|
+
puts "Last parse: #{times.last} us"
|
|
126
|
+
puts "Cache stats: #{Parsanol::Native.cache_stats}"
|
|
127
|
+
|
|
128
|
+
# ============================================================================
|
|
129
|
+
# Test 4: Large input
|
|
130
|
+
# ============================================================================
|
|
131
|
+
puts "\n" + "-" * 60
|
|
132
|
+
puts "Test 4: Large Input (#{large_input.length} chars, 20 parses)"
|
|
133
|
+
puts "-" * 60
|
|
134
|
+
|
|
135
|
+
Parsanol::Native.profile_reset
|
|
136
|
+
Parsanol::Native.clear_cache
|
|
137
|
+
|
|
138
|
+
times = []
|
|
139
|
+
20.times do
|
|
140
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
141
|
+
Parsanol::Native.parse_parslet_compatible(parser, large_input)
|
|
142
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start
|
|
143
|
+
times << elapsed
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
avg_time = times.sum / times.length
|
|
147
|
+
profile4 = Parsanol::Native.profile_stats
|
|
148
|
+
|
|
149
|
+
puts "Average time: #{avg_time} us"
|
|
150
|
+
puts "Cache stats: #{Parsanol::Native.cache_stats}"
|
|
151
|
+
|
|
152
|
+
# ============================================================================
|
|
153
|
+
# Summary
|
|
154
|
+
# ============================================================================
|
|
155
|
+
puts "\n" + "=" * 60
|
|
156
|
+
puts "Summary"
|
|
157
|
+
puts "=" * 60
|
|
158
|
+
|
|
159
|
+
puts "\nRuby Optimizations Applied:"
|
|
160
|
+
puts " - Structural grammar caching (hash-based)"
|
|
161
|
+
puts " - Frozen string constants"
|
|
162
|
+
puts " - Optimized AstTransformer"
|
|
163
|
+
puts " - Direct JSON output from serializer"
|
|
164
|
+
|
|
165
|
+
puts "\nPerformance Results:"
|
|
166
|
+
puts " - Grammar caching: Working (hash-based key)"
|
|
167
|
+
puts " - Cache hits after warmup: #{Parsanol::Native.cache_stats[:size]} grammars cached"
|
|
168
|
+
|
|
169
|
+
puts "\n" + "=" * 60
|
|
170
|
+
puts "Benchmark complete"
|
|
171
|
+
puts "=" * 60
|