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,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
begin
|
|
5
|
+
require 'benchmark/ips'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
# Skip this spec file if benchmark/ips is not available
|
|
8
|
+
return unless defined?(RSpec)
|
|
9
|
+
RSpec.describe "Performance Regression Tests", :performance do
|
|
10
|
+
it "requires benchmark-ips gem for performance tests" do
|
|
11
|
+
skip "benchmark-ips gem not installed. Install with: gem install benchmark-ips"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
RSpec.describe "Performance Regression Tests", :performance do
|
|
18
|
+
# Baseline performance expectations (adjusted for opt-in optimization model)
|
|
19
|
+
# These are conservative targets that should pass on most systems
|
|
20
|
+
# Note: These thresholds are set low to accommodate CI environments
|
|
21
|
+
# and debug builds. Production builds should be significantly faster.
|
|
22
|
+
BASELINE_IPS = {
|
|
23
|
+
simple_calc: 500, # Adjusted for CI/debug environments
|
|
24
|
+
json_parse: 400, # Adjusted for CI/debug environments
|
|
25
|
+
xml_parse: 400 # Adjusted for CI/debug environments
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Note: The 13.3x speedup is cumulative from all optimization phases (1-50b)
|
|
29
|
+
# vs parslet 2.0, not just from optimize_rules! alone
|
|
30
|
+
|
|
31
|
+
# Test parsers from examples
|
|
32
|
+
let(:calc_parser) do
|
|
33
|
+
Class.new(Parsanol::Parser) do
|
|
34
|
+
optimize_rules!
|
|
35
|
+
|
|
36
|
+
rule(:addition) {
|
|
37
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
|
38
|
+
multiplication
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
rule(:multiplication) {
|
|
42
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
|
43
|
+
integer
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
rule(:integer) { digit.repeat(1).as(:i) >> space? }
|
|
47
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
|
48
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
|
49
|
+
rule(:digit) { match['0-9'] }
|
|
50
|
+
rule(:space?) { match['\s'].repeat }
|
|
51
|
+
|
|
52
|
+
root :addition
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
let(:unoptimized_calc_parser) do
|
|
57
|
+
Class.new(Parsanol::Parser) do
|
|
58
|
+
# Same rules but without optimize_rules!
|
|
59
|
+
rule(:addition) {
|
|
60
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
|
61
|
+
multiplication
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
rule(:multiplication) {
|
|
65
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
|
66
|
+
integer
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rule(:integer) { digit.repeat(1).as(:i) >> space? }
|
|
70
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
|
71
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
|
72
|
+
rule(:digit) { match['0-9'] }
|
|
73
|
+
rule(:space?) { match['\s'].repeat }
|
|
74
|
+
|
|
75
|
+
root :addition
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
let(:json_parser) do
|
|
80
|
+
Class.new(Parsanol::Parser) do
|
|
81
|
+
optimize_rules!
|
|
82
|
+
|
|
83
|
+
rule(:spaces) { match('\s').repeat(1) }
|
|
84
|
+
rule(:spaces?) { spaces.maybe }
|
|
85
|
+
rule(:comma) { spaces? >> str(',') >> spaces? }
|
|
86
|
+
rule(:digit) { match('[0-9]') }
|
|
87
|
+
|
|
88
|
+
rule(:number) {
|
|
89
|
+
(
|
|
90
|
+
str('-').maybe >> (
|
|
91
|
+
str('0') | (match('[1-9]') >> digit.repeat)
|
|
92
|
+
) >> (
|
|
93
|
+
str('.') >> digit.repeat(1)
|
|
94
|
+
).maybe >> (
|
|
95
|
+
match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
|
|
96
|
+
).maybe
|
|
97
|
+
).as(:number)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rule(:string) {
|
|
101
|
+
str('"') >> (
|
|
102
|
+
str('\\') >> any | str('"').absent? >> any
|
|
103
|
+
).repeat.as(:string) >> str('"')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rule(:array) {
|
|
107
|
+
str('[') >> spaces? >>
|
|
108
|
+
(value >> (comma >> value).repeat).maybe.as(:array) >>
|
|
109
|
+
spaces? >> str(']')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
rule(:object) {
|
|
113
|
+
str('{') >> spaces? >>
|
|
114
|
+
(entry >> (comma >> entry).repeat).maybe.as(:object) >>
|
|
115
|
+
spaces? >> str('}')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
rule(:value) {
|
|
119
|
+
string | number |
|
|
120
|
+
object | array |
|
|
121
|
+
str('true').as(:true) | str('false').as(:false) |
|
|
122
|
+
str('null').as(:null)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
rule(:entry) {
|
|
126
|
+
(
|
|
127
|
+
string.as(:key) >> spaces? >>
|
|
128
|
+
str(':') >> spaces? >>
|
|
129
|
+
value.as(:val)
|
|
130
|
+
).as(:entry)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rule(:top) { spaces? >> value >> spaces? }
|
|
134
|
+
root(:top)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
let(:xml_parser) do
|
|
139
|
+
Class.new(Parsanol::Parser) do
|
|
140
|
+
optimize_rules!
|
|
141
|
+
|
|
142
|
+
rule(:document) {
|
|
143
|
+
tag(close: false).as(:o) >> document.as(:i) >> tag(close: true).as(:c) |
|
|
144
|
+
text
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def tag(opts={})
|
|
148
|
+
close = opts[:close] || false
|
|
149
|
+
|
|
150
|
+
parslet = str('<')
|
|
151
|
+
parslet = parslet >> str('/') if close
|
|
152
|
+
parslet = parslet >> (str('>').absent? >> match("[a-zA-Z]")).repeat(1).as(:name)
|
|
153
|
+
parslet = parslet >> str('>')
|
|
154
|
+
|
|
155
|
+
parslet
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
rule(:text) {
|
|
159
|
+
match('[^<>]').repeat(0)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
root :document
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
context "with optimizations enabled" do
|
|
167
|
+
describe "optimization safety" do
|
|
168
|
+
it "optimized parser does not significantly degrade performance" do
|
|
169
|
+
input = '1 + 2 * 3 + 4'
|
|
170
|
+
|
|
171
|
+
# Create parser instances
|
|
172
|
+
optimized = calc_parser.new
|
|
173
|
+
unoptimized = unoptimized_calc_parser.new
|
|
174
|
+
|
|
175
|
+
# Warm up
|
|
176
|
+
3.times do
|
|
177
|
+
optimized.parse(input)
|
|
178
|
+
unoptimized.parse(input)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Benchmark both
|
|
182
|
+
unoptimized_result = Benchmark.ips(quiet: true) do |x|
|
|
183
|
+
x.report('unoptimized') { unoptimized.parse(input) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
optimized_result = Benchmark.ips(quiet: true) do |x|
|
|
187
|
+
x.report('optimized') { optimized.parse(input) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
unoptimized_ips = unoptimized_result.entries.first.ips
|
|
191
|
+
optimized_ips = optimized_result.entries.first.ips
|
|
192
|
+
slowdown_ratio = optimized_ips / unoptimized_ips
|
|
193
|
+
|
|
194
|
+
# Ensure optimizer doesn't make things significantly worse
|
|
195
|
+
# Allow up to 20% slowdown for safety (optimizer should not harm performance)
|
|
196
|
+
expect(slowdown_ratio).to be >= 0.8,
|
|
197
|
+
"Optimizer caused significant slowdown: #{slowdown_ratio.round(2)}x " \
|
|
198
|
+
"(unoptimized: #{unoptimized_ips.round(0)} ips, optimized: #{optimized_ips.round(0)} ips)"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
describe "baseline performance" do
|
|
203
|
+
it "parses calculator expressions within performance bounds" do
|
|
204
|
+
parser = calc_parser.new
|
|
205
|
+
input = '1 + 2 * 3'
|
|
206
|
+
|
|
207
|
+
result = Benchmark.ips(quiet: true) do |x|
|
|
208
|
+
x.report('calc') { parser.parse(input) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
actual_ips = result.entries.first.ips
|
|
212
|
+
|
|
213
|
+
# Allow 50% variance for different environments
|
|
214
|
+
min_acceptable = BASELINE_IPS[:simple_calc] * 0.5
|
|
215
|
+
|
|
216
|
+
expect(actual_ips).to be >= min_acceptable,
|
|
217
|
+
"Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "parses JSON within performance bounds" do
|
|
221
|
+
parser = json_parser.new
|
|
222
|
+
input = '{"key": "value", "array": [1,2,3]}'
|
|
223
|
+
|
|
224
|
+
result = Benchmark.ips(quiet: true) do |x|
|
|
225
|
+
x.report('json') { parser.parse(input) }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
actual_ips = result.entries.first.ips
|
|
229
|
+
|
|
230
|
+
# Allow 50% variance for different environments
|
|
231
|
+
min_acceptable = BASELINE_IPS[:json_parse] * 0.5
|
|
232
|
+
|
|
233
|
+
expect(actual_ips).to be >= min_acceptable,
|
|
234
|
+
"Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "parses XML within performance bounds" do
|
|
238
|
+
parser = xml_parser.new
|
|
239
|
+
input = '<tag>content</tag>'
|
|
240
|
+
|
|
241
|
+
result = Benchmark.ips(quiet: true) do |x|
|
|
242
|
+
x.report('xml') { parser.parse(input) }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
actual_ips = result.entries.first.ips
|
|
246
|
+
|
|
247
|
+
# Allow 50% variance for different environments
|
|
248
|
+
min_acceptable = BASELINE_IPS[:xml_parse] * 0.5
|
|
249
|
+
|
|
250
|
+
expect(actual_ips).to be >= min_acceptable,
|
|
251
|
+
"Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
describe "cache efficiency" do
|
|
256
|
+
it "maintains reasonable cache hit rate with repetition" do
|
|
257
|
+
parser = Class.new(Parsanol::Parser) do
|
|
258
|
+
optimize_rules!
|
|
259
|
+
rule(:digits) { match('[0-9]').repeat(3) }
|
|
260
|
+
root :digits
|
|
261
|
+
end.new
|
|
262
|
+
|
|
263
|
+
# Parse multiple times to warm cache
|
|
264
|
+
10.times { parser.parse('123') }
|
|
265
|
+
|
|
266
|
+
# Get cache stats if available
|
|
267
|
+
if parser.respond_to?(:cache_stats)
|
|
268
|
+
stats = parser.cache_stats
|
|
269
|
+
hit_rate = stats[:hits].to_f / (stats[:hits] + stats[:misses])
|
|
270
|
+
|
|
271
|
+
expect(hit_rate).to be >= 0.05,
|
|
272
|
+
"Expected cache hit rate ≥5%, got #{(hit_rate * 100).round(2)}%"
|
|
273
|
+
else
|
|
274
|
+
# Skip if cache stats not available
|
|
275
|
+
skip "Cache stats not available in this parser"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "keeps allocations under threshold for medium inputs" do
|
|
280
|
+
parser = json_parser.new
|
|
281
|
+
input = '{"a":1,"b":2,"c":3,"d":4,"e":5}'
|
|
282
|
+
|
|
283
|
+
# Warm up
|
|
284
|
+
parser.parse(input)
|
|
285
|
+
|
|
286
|
+
# Measure allocations
|
|
287
|
+
allocations = count_allocations { parser.parse(input) }
|
|
288
|
+
|
|
289
|
+
expect(allocations).to be < 30_000,
|
|
290
|
+
"Expected <30,000 allocations, got #{allocations}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
context "optimizer semantic equivalence" do
|
|
296
|
+
describe "calculator parser" do
|
|
297
|
+
it "produces identical parse trees with/without optimization" do
|
|
298
|
+
test_cases = [
|
|
299
|
+
'1 + 2',
|
|
300
|
+
'1 * 2 + 3',
|
|
301
|
+
'10 + 20 * 30',
|
|
302
|
+
'1+2+3+4+5'
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
test_cases.each do |input|
|
|
306
|
+
optimized = calc_parser.new.parse(input)
|
|
307
|
+
unoptimized = unoptimized_calc_parser.new.parse(input)
|
|
308
|
+
|
|
309
|
+
# Strip positions for comparison
|
|
310
|
+
expect(strip_positions(optimized)).to eq(strip_positions(unoptimized)),
|
|
311
|
+
"Parse trees differ for input: #{input}"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
describe "JSON parser" do
|
|
317
|
+
it "produces identical parse trees for various JSON inputs" do
|
|
318
|
+
test_cases = [
|
|
319
|
+
'{"key": "value"}',
|
|
320
|
+
'[1, 2, 3]',
|
|
321
|
+
'{"a": [1, 2], "b": {"c": 3}}',
|
|
322
|
+
'null',
|
|
323
|
+
'true',
|
|
324
|
+
'false',
|
|
325
|
+
'123',
|
|
326
|
+
'"string"'
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
test_cases.each do |input|
|
|
330
|
+
parser = json_parser.new
|
|
331
|
+
|
|
332
|
+
# Parse twice to ensure consistency
|
|
333
|
+
first_parse = parser.parse(input)
|
|
334
|
+
second_parse = json_parser.new.parse(input)
|
|
335
|
+
|
|
336
|
+
expect(strip_positions(first_parse)).to eq(strip_positions(second_parse)),
|
|
337
|
+
"Parse trees differ for input: #{input}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
describe "XML parser" do
|
|
343
|
+
it "produces identical parse trees for various XML inputs" do
|
|
344
|
+
test_cases = [
|
|
345
|
+
'<tag>content</tag>',
|
|
346
|
+
'<a><b>text</b></a>',
|
|
347
|
+
'<root><child>data</child></root>'
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
test_cases.each do |input|
|
|
351
|
+
parser = xml_parser.new
|
|
352
|
+
|
|
353
|
+
# Parse twice to ensure consistency
|
|
354
|
+
first_parse = parser.parse(input)
|
|
355
|
+
second_parse = xml_parser.new.parse(input)
|
|
356
|
+
|
|
357
|
+
expect(strip_positions(first_parse)).to eq(strip_positions(second_parse)),
|
|
358
|
+
"Parse trees differ for input: #{input}"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Helper methods
|
|
365
|
+
private
|
|
366
|
+
|
|
367
|
+
def count_allocations(&block)
|
|
368
|
+
GC.start
|
|
369
|
+
before = GC.stat(:total_allocated_objects)
|
|
370
|
+
block.call
|
|
371
|
+
after = GC.stat(:total_allocated_objects)
|
|
372
|
+
after - before
|
|
373
|
+
end
|
|
374
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'support/opal' if RUBY_ENGINE == 'opal'
|
|
4
|
+
|
|
5
|
+
require 'parsanol/parslet'
|
|
6
|
+
require 'parsanol/rig/rspec'
|
|
7
|
+
require 'parsanol/atoms/visitor'
|
|
8
|
+
require 'parsanol/export'
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
require 'ae'
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# AE not available
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
# Allow both old and new syntax for backward compatibility
|
|
18
|
+
config.expect_with :rspec do |expectations|
|
|
19
|
+
expectations.syntax = %i[should expect]
|
|
20
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Use rspec mocks
|
|
24
|
+
config.mock_with :rspec do |mocks|
|
|
25
|
+
mocks.verify_partial_doubles = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Shared context and examples
|
|
29
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
30
|
+
|
|
31
|
+
# This is not a Rails project, so no Rails-specific configuration needed
|
|
32
|
+
|
|
33
|
+
# Exclude other ruby versions by giving :ruby => '2.7' or :ruby => '3.0'
|
|
34
|
+
config.filter_run_excluding ruby: lambda { |version|
|
|
35
|
+
RUBY_VERSION !~ /^#{Regexp.escape(version.to_s)}/
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Exclude specs that require optional dependencies not available
|
|
39
|
+
begin
|
|
40
|
+
require 'benchmark/ips'
|
|
41
|
+
rescue LoadError
|
|
42
|
+
config.filter_run_excluding :benchmark => true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run specs in random order to surface order dependencies
|
|
46
|
+
config.order = :random
|
|
47
|
+
Kernel.srand config.seed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def catch_failed_parse
|
|
51
|
+
exception = nil
|
|
52
|
+
begin
|
|
53
|
+
yield
|
|
54
|
+
rescue Parsanol::ParseFailed => e
|
|
55
|
+
exception = e
|
|
56
|
+
end
|
|
57
|
+
exception&.parse_failure_cause
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def slet(name, &block)
|
|
61
|
+
let(name, &block)
|
|
62
|
+
subject(&block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Helper method to convert Parsanol::Slice objects to plain strings for comparison
|
|
66
|
+
def strip_positions(obj)
|
|
67
|
+
case obj
|
|
68
|
+
when Parsanol::Slice
|
|
69
|
+
obj.to_s
|
|
70
|
+
when Hash
|
|
71
|
+
obj.transform_values { |v| strip_positions(v) }
|
|
72
|
+
when Array
|
|
73
|
+
obj.map { |item| strip_positions(item) }
|
|
74
|
+
when String
|
|
75
|
+
obj.gsub(/@\d+/, '') # Remove slice of text
|
|
76
|
+
else
|
|
77
|
+
obj
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# A set of patches required to run Opal-RSpec
|
|
2
|
+
require 'nodejs/yaml'
|
|
3
|
+
|
|
4
|
+
module AE
|
|
5
|
+
# Compile in AE metadata
|
|
6
|
+
@metadata =
|
|
7
|
+
{ 'revision' => 2013, 'type' => 'ruby', 'sources' => ['var'], 'authors' => [{ 'name' => 'Trans', 'email' => 'transfire@gmail.com' }], 'organizations' => [], 'requirements' => [{ 'name' => 'ansi' }, { 'groups' => ['build'], 'development' => true, 'name' => 'detroit' }, { 'groups' => ['test'], 'development' => true, 'name' => 'qed' }], 'conflicts' => [], 'alternatives' => [], 'resources' => [{ 'type' => 'home', 'uri' => 'http://rubyworks.github.com/ae', 'label' => 'Homepage' }, { 'type' => 'code', 'uri' => 'http://github.com/rubyworks/ae', 'label' => 'Source Code' }, { 'type' => 'docs', 'uri' => 'http://rubydoc.info/gems/ae', 'label' => 'Documentation' }, { 'type' => 'wiki', 'uri' => 'http://wiki.github.com/rubyworks/ae', 'label' => 'User Guide' }, { 'type' => 'bugs', 'uri' => 'http://github.com/rubyworks/ae/issues', 'label' => 'Issue Tracker' }, { 'type' => 'mail', 'uri' => 'http://groups.google.com/group/rubyworks-mailinglist', 'label' => 'Mailing List' }], 'repositories' => [{ 'name' => 'upstream', 'scm' => 'git', 'uri' => 'git://github.com/rubyworks/ae.git' }], 'categories' => [], 'copyrights' => [{ 'holder' => 'Rubyworks', 'year' => '2008', 'license' => 'BSD-2-Clause' }], 'customs' => [], 'paths' => { 'lib' => ['lib'] }, 'created' => '2008-08-17', 'summary' => 'Assertive Expressive', 'title' => 'AE', 'version' => '1.8.2', 'name' => 'ae', 'description' => "Assertive Expressive is an assertions library specifically designed \nfor reuse by other test frameworks.", 'date' => '2013-02-18' }
|
|
8
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# A set of patches required to run Opal-RSpec
|
|
2
|
+
require 'nodejs/yaml'
|
|
3
|
+
|
|
4
|
+
module AE
|
|
5
|
+
# Compile in AE metadata
|
|
6
|
+
@metadata = (
|
|
7
|
+
<%=
|
|
8
|
+
require 'yaml'
|
|
9
|
+
YAML.load(
|
|
10
|
+
File.read(
|
|
11
|
+
File.expand_path('lib/ae.yml', Gem::Specification.find_by_name('ae').gem_dir)))
|
|
12
|
+
%>
|
|
13
|
+
)
|
|
14
|
+
end
|