ibex 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +247 -104
- data/docs/architecture.md +322 -28
- data/docs/cst-migration.md +107 -0
- data/docs/cst.md +213 -0
- data/docs/development.md +129 -0
- data/docs/editor-setup.md +25 -0
- data/docs/error-ux.md +60 -0
- data/docs/grammar-reference.md +458 -18
- data/docs/lexer-migration.md +51 -0
- data/docs/racc-migration.md +34 -6
- data/docs/release-readiness.md +163 -0
- data/docs/stability.md +124 -0
- data/examples/README.md +60 -0
- data/examples/calculator.y +44 -0
- data/examples/csv.y +32 -0
- data/examples/ini.y +70 -0
- data/examples/json.y +48 -0
- data/examples/tiny_language.y +75 -0
- data/lib/ibex/analysis/sets.rb +5 -4
- data/lib/ibex/artifact_set.rb +63 -0
- data/lib/ibex/cli/ambiguity.rb +66 -0
- data/lib/ibex/cli/counterexample_options.rb +6 -2
- data/lib/ibex/cli/coverage.rb +173 -0
- data/lib/ibex/cli/debug.rb +106 -0
- data/lib/ibex/cli/diagnostics.rb +116 -0
- data/lib/ibex/cli/documentation.rb +67 -0
- data/lib/ibex/cli/error_messages.rb +177 -0
- data/lib/ibex/cli/explain.rb +76 -0
- data/lib/ibex/cli/formatting.rb +386 -0
- data/lib/ibex/cli/generation_artifacts.rb +138 -0
- data/lib/ibex/cli/generation_error_messages.rb +39 -0
- data/lib/ibex/cli/grammar_tests.rb +122 -0
- data/lib/ibex/cli/ir_tools.rb +184 -0
- data/lib/ibex/cli/lsp.rb +33 -0
- data/lib/ibex/cli/outputs.rb +91 -8
- data/lib/ibex/cli/racc_migration.rb +87 -0
- data/lib/ibex/cli/samples.rb +92 -0
- data/lib/ibex/cli/watch.rb +103 -0
- data/lib/ibex/cli.rb +506 -37
- data/lib/ibex/codegen/action_locations.rb +103 -0
- data/lib/ibex/codegen/action_method_source.rb +210 -0
- data/lib/ibex/codegen/action_source.rb +118 -0
- data/lib/ibex/codegen/ambiguity.rb +173 -0
- data/lib/ibex/codegen/cst_metadata.rb +171 -0
- data/lib/ibex/codegen/documentation.rb +170 -0
- data/lib/ibex/codegen/explain.rb +312 -0
- data/lib/ibex/codegen/generated_action_abi.rb +288 -0
- data/lib/ibex/codegen/html.rb +94 -10
- data/lib/ibex/codegen/mermaid.rb +43 -0
- data/lib/ibex/codegen/railroad.rb +197 -0
- data/lib/ibex/codegen/railroad_documentation.rb +59 -0
- data/lib/ibex/codegen/rbs.rb +323 -2
- data/lib/ibex/codegen/report.rb +46 -6
- data/lib/ibex/codegen/ruby.rb +334 -59
- data/lib/ibex/codegen/ruby_actions.rb +143 -0
- data/lib/ibex/codegen/ruby_ast.rb +121 -0
- data/lib/ibex/codegen/ruby_error_messages.rb +28 -0
- data/lib/ibex/codegen/ruby_lexer.rb +83 -0
- data/lib/ibex/codegen/ruby_syntax.rb +90 -0
- data/lib/ibex/codegen/ruby_table_metadata.rb +57 -0
- data/lib/ibex/codegen/ruby_value_printers.rb +56 -0
- data/lib/ibex/codegen/symbol_labels.rb +1 -1
- data/lib/ibex/coverage/collector.rb +188 -0
- data/lib/ibex/coverage/event_stream.rb +97 -0
- data/lib/ibex/coverage/report.rb +259 -0
- data/lib/ibex/coverage/runtime_event_validator.rb +160 -0
- data/lib/ibex/coverage.rb +13 -0
- data/lib/ibex/error_messages/parser.rb +159 -0
- data/lib/ibex/error_messages/parser_v2.rb +198 -0
- data/lib/ibex/error_messages/renderer.rb +65 -0
- data/lib/ibex/error_messages/sentence_search.rb +196 -0
- data/lib/ibex/error_messages/update.rb +169 -0
- data/lib/ibex/error_messages.rb +165 -0
- data/lib/ibex/frontend/ast.rb +145 -6
- data/lib/ibex/frontend/bootstrap_parser.rb +47 -4
- data/lib/ibex/frontend/diagnostic.rb +81 -0
- data/lib/ibex/frontend/diagnostic_recovery.rb +268 -0
- data/lib/ibex/frontend/dsl.rb +66 -7
- data/lib/ibex/frontend/formatter.rb +407 -0
- data/lib/ibex/frontend/generated_parser.rb +614 -190
- data/lib/ibex/frontend/generated_parser_base.rb +164 -30
- data/lib/ibex/frontend/generated_parser_includes.rb +61 -0
- data/lib/ibex/frontend/generated_parser_metadata.rb +61 -0
- data/lib/ibex/frontend/generated_parser_parameters.rb +60 -0
- data/lib/ibex/frontend/generation.rb +33 -0
- data/lib/ibex/frontend/lexer.rb +162 -10
- data/lib/ibex/frontend/lexer_recovery.rb +84 -0
- data/lib/ibex/frontend/parser/declarations.rb +247 -11
- data/lib/ibex/frontend/parser/parameters.rb +82 -0
- data/lib/ibex/frontend/parser/rules.rb +43 -6
- data/lib/ibex/frontend/parser.rb +182 -4
- data/lib/ibex/frontend/regenerator.rb +26 -1
- data/lib/ibex/frontend/resolution.rb +69 -0
- data/lib/ibex/frontend/resolver.rb +217 -0
- data/lib/ibex/frontend/rule_documentation.rb +103 -0
- data/lib/ibex/frontend/source_cursor.rb +132 -8
- data/lib/ibex/frontend/source_document.rb +229 -0
- data/lib/ibex/frontend/source_loader.rb +150 -0
- data/lib/ibex/frontend/source_span.rb +81 -0
- data/lib/ibex/frontend/token_adapter/declaration_document_state.rb +47 -0
- data/lib/ibex/frontend/token_adapter/declaration_lexer_state.rb +83 -0
- data/lib/ibex/frontend/token_adapter/declaration_state.rb +216 -26
- data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +8 -2
- data/lib/ibex/frontend/token_adapter/rule_state.rb +60 -2
- data/lib/ibex/frontend/token_adapter.rb +8 -3
- data/lib/ibex/frontend.rb +13 -2
- data/lib/ibex/generation_input.rb +57 -0
- data/lib/ibex/generation_manifest.rb +200 -0
- data/lib/ibex/generation_transaction.rb +261 -0
- data/lib/ibex/generation_transaction_recovery.rb +109 -0
- data/lib/ibex/generation_transaction_validation.rb +196 -0
- data/lib/ibex/grammar_tests.rb +206 -0
- data/lib/ibex/ir/automaton_ir.rb +38 -5
- data/lib/ibex/ir/grammar_ir.rb +137 -24
- data/lib/ibex/ir/lexer_ir.rb +76 -0
- data/lib/ibex/ir/migration.rb +120 -0
- data/lib/ibex/ir/serialize.rb +110 -19
- data/lib/ibex/ir/validator/automaton.rb +345 -0
- data/lib/ibex/ir/validator/base.rb +129 -0
- data/lib/ibex/ir/validator/grammar.rb +604 -0
- data/lib/ibex/ir/validator/lexer.rb +113 -0
- data/lib/ibex/ir/validator.rb +62 -0
- data/lib/ibex/ir.rb +57 -4
- data/lib/ibex/lalr/build_metrics.rb +23 -0
- data/lib/ibex/lalr/builder.rb +379 -52
- data/lib/ibex/lalr/conflict.rb +1 -0
- data/lib/ibex/lalr/conflict_search.rb +11 -5
- data/lib/ibex/lalr/counterexample.rb +20 -5
- data/lib/ibex/lalr/direct_lookaheads.rb +236 -0
- data/lib/ibex/lalr/ielr_partition.rb +152 -0
- data/lib/ibex/lalr/on_error_reductions.rb +74 -0
- data/lib/ibex/lalr.rb +7 -0
- data/lib/ibex/location.rb +129 -0
- data/lib/ibex/lsp/document_handlers.rb +66 -0
- data/lib/ibex/lsp/document_store.rb +264 -0
- data/lib/ibex/lsp/document_store_diagnostics.rb +44 -0
- data/lib/ibex/lsp/document_store_validation.rb +61 -0
- data/lib/ibex/lsp/initialization_handlers.rb +78 -0
- data/lib/ibex/lsp/navigation_handlers.rb +56 -0
- data/lib/ibex/lsp/position_codec.rb +109 -0
- data/lib/ibex/lsp/protocol_error.rb +33 -0
- data/lib/ibex/lsp/request_handlers.rb +45 -0
- data/lib/ibex/lsp/request_support.rb +87 -0
- data/lib/ibex/lsp/server.rb +146 -0
- data/lib/ibex/lsp/symbol_index.rb +241 -0
- data/lib/ibex/lsp/symbol_index_builder.rb +267 -0
- data/lib/ibex/lsp/symbol_index_precedence_references.rb +44 -0
- data/lib/ibex/lsp/symbol_index_source_queries.rb +61 -0
- data/lib/ibex/lsp/symbol_occurrence.rb +17 -0
- data/lib/ibex/lsp/transport.rb +118 -0
- data/lib/ibex/lsp/workspace.rb +127 -0
- data/lib/ibex/lsp/workspace_analyzer.rb +199 -0
- data/lib/ibex/lsp.rb +32 -0
- data/lib/ibex/normalize/declarations.rb +140 -7
- data/lib/ibex/normalize/diagnostics.rb +50 -5
- data/lib/ibex/normalize/expander.rb +59 -55
- data/lib/ibex/normalize/expression.rb +60 -39
- data/lib/ibex/normalize/inline_expansion.rb +414 -0
- data/lib/ibex/normalize/inline_validation.rb +174 -0
- data/lib/ibex/normalize/lexer.rb +131 -0
- data/lib/ibex/normalize/named_references.rb +60 -0
- data/lib/ibex/normalize/nodes.rb +46 -0
- data/lib/ibex/normalize/parameter_ebnf_lowering.rb +69 -0
- data/lib/ibex/normalize/parameter_lowering.rb +126 -0
- data/lib/ibex/normalize/parameter_substitution.rb +125 -0
- data/lib/ibex/normalize/parameter_validation.rb +140 -0
- data/lib/ibex/normalize/parameters.rb +199 -0
- data/lib/ibex/normalize/recovery_declarations.rb +84 -0
- data/lib/ibex/normalize.rb +179 -14
- data/lib/ibex/racc_migration/checker.rb +122 -0
- data/lib/ibex/racc_migration/harness.rb +177 -0
- data/lib/ibex/racc_migration/report.rb +94 -0
- data/lib/ibex/racc_migration.rb +12 -0
- data/lib/ibex/rake_task.rb +116 -0
- data/lib/ibex/samples.rb +186 -0
- data/lib/ibex/table_simulation/result.rb +51 -0
- data/lib/ibex/table_simulation/simulator.rb +253 -0
- data/lib/ibex/table_simulation/step.rb +60 -0
- data/lib/ibex/table_simulation/text.rb +31 -0
- data/lib/ibex/table_simulation.rb +13 -0
- data/lib/ibex/tables.rb +9 -70
- data/lib/ibex/version.rb +1 -1
- data/lib/ibex/watch/runner.rb +172 -0
- data/lib/ibex/watch/source_snapshot.rb +93 -0
- data/lib/ibex/watch.rb +11 -0
- data/lib/ibex.rb +25 -1
- data/schema/automaton-ir-v1.schema.json +401 -0
- data/schema/automaton-ir-v2.schema.json +58 -0
- data/schema/benchmark-v1.schema.json +212 -0
- data/schema/benchmark-v2.schema.json +61 -0
- data/schema/cst-v1.json +170 -0
- data/schema/error-ux-v1.schema.json +258 -0
- data/schema/explain-v1.schema.json +433 -0
- data/schema/frontend-diagnostics-v1.schema.json +154 -0
- data/schema/generation-manifest-v1.schema.json +115 -0
- data/schema/grammar-ir-v1.schema.json +426 -0
- data/schema/grammar-ir-v2.schema.json +779 -0
- data/schema/lexer-ir-v1.schema.json +215 -0
- data/schema/migration-check-v1.schema.json +60 -0
- data/schema/performance-comparison-v1.schema.json +395 -0
- data/schema/public-performance-comparison-v1.schema.json +506 -0
- data/schema/public-performance-profile-v1.schema.json +360 -0
- data/schema/runtime-coverage-v1.schema.json +86 -0
- data/schema/runtime-event-v1.schema.json +308 -0
- data/schema/table-simulation-v1.schema.json +70 -0
- data/sig/ibex/artifact_set.rbs +37 -0
- data/sig/ibex/cli/ambiguity.rbs +22 -0
- data/sig/ibex/cli/counterexample_options.rbs +2 -0
- data/sig/ibex/cli/coverage.rbs +53 -0
- data/sig/ibex/cli/debug.rbs +28 -0
- data/sig/ibex/cli/diagnostics.rbs +38 -0
- data/sig/ibex/cli/documentation.rbs +25 -0
- data/sig/ibex/cli/error_messages.rbs +57 -0
- data/sig/ibex/cli/explain.rbs +25 -0
- data/sig/ibex/cli/formatting.rbs +103 -0
- data/sig/ibex/cli/generation_artifacts.rbs +50 -0
- data/sig/ibex/cli/generation_error_messages.rbs +19 -0
- data/sig/ibex/cli/grammar_tests.rbs +36 -0
- data/sig/ibex/cli/ir_tools.rbs +51 -0
- data/sig/ibex/cli/lsp.rbs +14 -0
- data/sig/ibex/cli/outputs.rbs +17 -0
- data/sig/ibex/cli/racc_migration.rbs +30 -0
- data/sig/ibex/cli/samples.rbs +30 -0
- data/sig/ibex/cli/watch.rbs +43 -0
- data/sig/ibex/cli.rbs +104 -5
- data/sig/ibex/codegen/action_locations.rbs +45 -0
- data/sig/ibex/codegen/action_method_source.rbs +65 -0
- data/sig/ibex/codegen/action_source.rbs +50 -0
- data/sig/ibex/codegen/ambiguity.rbs +60 -0
- data/sig/ibex/codegen/cst_metadata.rbs +59 -0
- data/sig/ibex/codegen/documentation.rbs +50 -0
- data/sig/ibex/codegen/explain.rbs +85 -0
- data/sig/ibex/codegen/generated_action_abi.rbs +101 -0
- data/sig/ibex/codegen/html.rbs +18 -2
- data/sig/ibex/codegen/mermaid.rbs +16 -0
- data/sig/ibex/codegen/railroad.rbs +82 -0
- data/sig/ibex/codegen/railroad_documentation.rbs +31 -0
- data/sig/ibex/codegen/rbs.rbs +84 -4
- data/sig/ibex/codegen/report.rbs +8 -0
- data/sig/ibex/codegen/ruby.rbs +97 -23
- data/sig/ibex/codegen/ruby_actions.rbs +54 -0
- data/sig/ibex/codegen/ruby_ast.rbs +34 -0
- data/sig/ibex/codegen/ruby_error_messages.rbs +16 -0
- data/sig/ibex/codegen/ruby_lexer.rbs +25 -0
- data/sig/ibex/codegen/ruby_syntax.rbs +25 -0
- data/sig/ibex/codegen/ruby_table_metadata.rbs +26 -0
- data/sig/ibex/codegen/ruby_value_printers.rbs +28 -0
- data/sig/ibex/coverage/collector.rbs +76 -0
- data/sig/ibex/coverage/event_stream.rbs +42 -0
- data/sig/ibex/coverage/report.rbs +100 -0
- data/sig/ibex/coverage/runtime_event_validator.rbs +68 -0
- data/sig/ibex/coverage.rbs +7 -0
- data/sig/ibex/error_messages/parser.rbs +58 -0
- data/sig/ibex/error_messages/parser_v2.rbs +67 -0
- data/sig/ibex/error_messages/renderer.rbs +23 -0
- data/sig/ibex/error_messages/sentence_search.rbs +80 -0
- data/sig/ibex/error_messages/update.rbs +43 -0
- data/sig/ibex/error_messages.rbs +85 -0
- data/sig/ibex/frontend/ast.rbs +208 -19
- data/sig/ibex/frontend/bootstrap_parser.rbs +11 -0
- data/sig/ibex/frontend/diagnostic.rbs +53 -0
- data/sig/ibex/frontend/diagnostic_recovery.rbs +98 -0
- data/sig/ibex/frontend/dsl.rbs +33 -4
- data/sig/ibex/frontend/formatter.rbs +135 -0
- data/sig/ibex/frontend/generated_parser.rbs +218 -68
- data/sig/ibex/frontend/generated_parser_base.rbs +61 -10
- data/sig/ibex/frontend/generated_parser_includes.rbs +23 -0
- data/sig/ibex/frontend/generated_parser_metadata.rbs +23 -0
- data/sig/ibex/frontend/generated_parser_parameters.rbs +24 -0
- data/sig/ibex/frontend/generation.rbs +6 -0
- data/sig/ibex/frontend/lexer.rbs +49 -2
- data/sig/ibex/frontend/lexer_recovery.rbs +25 -0
- data/sig/ibex/frontend/parser/declarations.rbs +54 -0
- data/sig/ibex/frontend/parser/parameters.rbs +28 -0
- data/sig/ibex/frontend/parser/rules.rbs +3 -0
- data/sig/ibex/frontend/parser.rbs +66 -0
- data/sig/ibex/frontend/regenerator.rbs +11 -0
- data/sig/ibex/frontend/resolution.rbs +33 -0
- data/sig/ibex/frontend/resolver.rbs +91 -0
- data/sig/ibex/frontend/rule_documentation.rbs +42 -0
- data/sig/ibex/frontend/source_cursor.rbs +36 -3
- data/sig/ibex/frontend/source_document.rbs +119 -0
- data/sig/ibex/frontend/source_loader.rbs +66 -0
- data/sig/ibex/frontend/source_span.rbs +53 -0
- data/sig/ibex/frontend/token_adapter/declaration_document_state.rbs +21 -0
- data/sig/ibex/frontend/token_adapter/declaration_lexer_state.rbs +27 -0
- data/sig/ibex/frontend/token_adapter/declaration_state.rbs +73 -5
- data/sig/ibex/frontend/token_adapter/rule_state.rbs +20 -0
- data/sig/ibex/frontend/token_adapter.rbs +5 -2
- data/sig/ibex/frontend.rbs +1 -1
- data/sig/ibex/generation_input.rbs +37 -0
- data/sig/ibex/generation_manifest.rbs +67 -0
- data/sig/ibex/generation_transaction.rbs +82 -0
- data/sig/ibex/generation_transaction_recovery.rbs +36 -0
- data/sig/ibex/generation_transaction_validation.rbs +65 -0
- data/sig/ibex/grammar_tests.rbs +93 -0
- data/sig/ibex/ir/automaton_ir.rbs +8 -2
- data/sig/ibex/ir/grammar_ir.rbs +75 -15
- data/sig/ibex/ir/lexer_ir.rbs +57 -0
- data/sig/ibex/ir/migration.rbs +34 -0
- data/sig/ibex/ir/serialize.rbs +23 -6
- data/sig/ibex/ir/validator/automaton.rbs +109 -0
- data/sig/ibex/ir/validator/base.rbs +65 -0
- data/sig/ibex/ir/validator/grammar.rbs +184 -0
- data/sig/ibex/ir/validator/lexer.rbs +37 -0
- data/sig/ibex/ir/validator.rbs +16 -0
- data/sig/ibex/ir.rbs +38 -4
- data/sig/ibex/lalr/build_metrics.rbs +20 -0
- data/sig/ibex/lalr/builder.rbs +95 -15
- data/sig/ibex/lalr/conflict_search.rbs +7 -3
- data/sig/ibex/lalr/counterexample.rbs +5 -2
- data/sig/ibex/lalr/direct_lookaheads.rbs +86 -0
- data/sig/ibex/lalr/ielr_partition.rbs +59 -0
- data/sig/ibex/lalr/on_error_reductions.rbs +22 -0
- data/sig/ibex/lalr.rbs +6 -0
- data/sig/ibex/location.rbs +67 -0
- data/sig/ibex/lsp/document_handlers.rbs +24 -0
- data/sig/ibex/lsp/document_store.rbs +94 -0
- data/sig/ibex/lsp/document_store_diagnostics.rbs +20 -0
- data/sig/ibex/lsp/document_store_validation.rbs +26 -0
- data/sig/ibex/lsp/initialization_handlers.rbs +30 -0
- data/sig/ibex/lsp/navigation_handlers.rbs +30 -0
- data/sig/ibex/lsp/position_codec.rbs +40 -0
- data/sig/ibex/lsp/protocol_error.rbs +32 -0
- data/sig/ibex/lsp/request_handlers.rbs +26 -0
- data/sig/ibex/lsp/request_support.rbs +40 -0
- data/sig/ibex/lsp/server.rbs +55 -0
- data/sig/ibex/lsp/symbol_index.rbs +78 -0
- data/sig/ibex/lsp/symbol_index_builder.rbs +87 -0
- data/sig/ibex/lsp/symbol_index_precedence_references.rbs +18 -0
- data/sig/ibex/lsp/symbol_index_source_queries.rbs +26 -0
- data/sig/ibex/lsp/symbol_occurrence.rbs +25 -0
- data/sig/ibex/lsp/transport.rbs +35 -0
- data/sig/ibex/lsp/workspace.rbs +41 -0
- data/sig/ibex/lsp/workspace_analyzer.rbs +69 -0
- data/sig/ibex/lsp.rbs +7 -0
- data/sig/ibex/normalize/declarations.rbs +31 -0
- data/sig/ibex/normalize/diagnostics.rbs +9 -0
- data/sig/ibex/normalize/expander.rbs +20 -14
- data/sig/ibex/normalize/expression.rbs +10 -10
- data/sig/ibex/normalize/inline_expansion.rbs +120 -0
- data/sig/ibex/normalize/inline_validation.rbs +40 -0
- data/sig/ibex/normalize/lexer.rbs +37 -0
- data/sig/ibex/normalize/named_references.rbs +20 -0
- data/sig/ibex/normalize/nodes.rbs +14 -0
- data/sig/ibex/normalize/parameter_ebnf_lowering.rbs +32 -0
- data/sig/ibex/normalize/parameter_lowering.rbs +35 -0
- data/sig/ibex/normalize/parameter_substitution.rbs +42 -0
- data/sig/ibex/normalize/parameter_validation.rbs +44 -0
- data/sig/ibex/normalize/parameters.rbs +46 -0
- data/sig/ibex/normalize/recovery_declarations.rbs +24 -0
- data/sig/ibex/normalize.rbs +123 -18
- data/sig/ibex/racc_migration/checker.rbs +36 -0
- data/sig/ibex/racc_migration/harness.rbs +16 -0
- data/sig/ibex/racc_migration/report.rbs +53 -0
- data/sig/ibex/racc_migration.rbs +8 -0
- data/sig/ibex/rake_task.rbs +51 -0
- data/sig/ibex/samples.rbs +51 -0
- data/sig/ibex/table_simulation/result.rbs +32 -0
- data/sig/ibex/table_simulation/simulator.rbs +98 -0
- data/sig/ibex/table_simulation/step.rbs +40 -0
- data/sig/ibex/table_simulation/text.rbs +14 -0
- data/sig/ibex/table_simulation.rbs +7 -0
- data/sig/ibex/tables.rbs +0 -26
- data/sig/ibex/watch/runner.rbs +48 -0
- data/sig/ibex/watch/source_snapshot.rbs +42 -0
- data/sig/ibex/watch.rbs +7 -0
- data/sig/ibex.rbs +2 -0
- metadata +301 -16
- data/.rubocop.yml +0 -43
- data/CHANGELOG.md +0 -30
- data/Rakefile +0 -25
- data/Steepfile +0 -10
- data/docs/compat-notes.md +0 -37
- data/docs/lexer-coverage.md +0 -14
- data/docs/phase10-extensions.md +0 -27
- data/gemfiles/Gemfile +0 -7
- data/gemfiles/Gemfile.lock +0 -98
- data/lib/ibex/frontend/grammar.y +0 -156
- data/lib/ibex/runtime/parser.rb +0 -360
- data/lib/ibex/runtime.rb +0 -8
- data/sig/ibex/runtime/parser.rbs +0 -167
- data/sig/ibex/runtime.rbs +0 -6
data/lib/ibex/frontend/parser.rb
CHANGED
|
@@ -4,19 +4,197 @@ module Ibex
|
|
|
4
4
|
module Frontend
|
|
5
5
|
# Public grammar parser backed by Ibex's generated LR frontend.
|
|
6
6
|
class Parser
|
|
7
|
+
DEFAULT_MAX_DIAGNOSTICS = 20 #: Integer
|
|
8
|
+
STRICT_LEXICAL_DIAGNOSTIC_LIMIT = 1 #: Integer
|
|
9
|
+
private_constant :STRICT_LEXICAL_DIAGNOSTIC_LIMIT
|
|
10
|
+
|
|
7
11
|
attr_reader :implementation #: GeneratedParser
|
|
8
12
|
|
|
13
|
+
# @rbs @source_document: SourceDocument?
|
|
14
|
+
# @rbs @parsed_document: SourceDocument?
|
|
15
|
+
# @rbs @parsed_node: AST::Root | AST::Fragment | nil
|
|
16
|
+
# @rbs @parse_error_message: String?
|
|
17
|
+
# @rbs @source_document_error_message: String?
|
|
18
|
+
# @rbs @tokens: Array[Token]
|
|
19
|
+
# @rbs @mode: Symbol
|
|
20
|
+
# @rbs @lexical_diagnostics: Array[Diagnostic]
|
|
21
|
+
|
|
9
22
|
# @rbs (String | Array[Token] source, ?file: String, ?mode: Symbol) -> void
|
|
10
|
-
def initialize(source, file: "(grammar)", mode: :
|
|
11
|
-
raise ArgumentError, "mode must be :
|
|
23
|
+
def initialize(source, file: "(grammar)", mode: :default)
|
|
24
|
+
raise ArgumentError, "mode must be :default or :extended" unless %i[default extended].include?(mode)
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
@mode = mode
|
|
27
|
+
@lexical_diagnostics = [] #: Array[Diagnostic]
|
|
28
|
+
@parse_error_message = nil
|
|
29
|
+
@source_document_error_message = nil
|
|
30
|
+
tokens = source.is_a?(Array) ? source : tokenize_source(source, file)
|
|
31
|
+
@tokens = tokens
|
|
14
32
|
@implementation = GeneratedParser.new(tokens, mode: mode)
|
|
15
33
|
end
|
|
16
34
|
|
|
17
35
|
# @rbs () -> AST::Root
|
|
18
36
|
def parse
|
|
19
|
-
|
|
37
|
+
node = parse_node
|
|
38
|
+
return node if node.is_a?(AST::Root)
|
|
39
|
+
|
|
40
|
+
raise Ibex::Error, "#{node.loc}: fragment input requires Parser#parse_fragment"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Parse an explicit fragment without resolving its includes.
|
|
44
|
+
# @rbs () -> AST::Fragment
|
|
45
|
+
def parse_fragment
|
|
46
|
+
node = parse_node
|
|
47
|
+
return node if node.is_a?(AST::Fragment)
|
|
48
|
+
|
|
49
|
+
raise Ibex::Error, "#{node.loc}: root grammar input cannot be parsed as a fragment"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parse the grammar and return its lossless source model.
|
|
53
|
+
# @rbs () -> SourceDocument
|
|
54
|
+
def parse_document
|
|
55
|
+
parsed_document = @parsed_document
|
|
56
|
+
return parsed_document if parsed_document && parsed_document.ast.is_a?(AST::Root)
|
|
57
|
+
|
|
58
|
+
@parsed_document = source_document!.with_ast(parse)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parse either a root grammar or an explicit fragment and return its lossless source model.
|
|
62
|
+
# @rbs () -> SourceDocument
|
|
63
|
+
def parse_source_document
|
|
64
|
+
parsed_document = @parsed_document
|
|
65
|
+
return parsed_document if parsed_document
|
|
66
|
+
|
|
67
|
+
@parsed_document = source_document!.with_ast(parse_node)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Parse with conservative boundary recovery and collect multiple errors.
|
|
71
|
+
# @rbs (?max_diagnostics: Integer) -> ParseResult
|
|
72
|
+
def parse_with_diagnostics(max_diagnostics: DEFAULT_MAX_DIAGNOSTICS)
|
|
73
|
+
unless max_diagnostics.is_a?(Integer) && max_diagnostics.positive?
|
|
74
|
+
raise ArgumentError, "max_diagnostics must be a positive integer"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
lexical_diagnostics = lexical_diagnostics_for(max_diagnostics)
|
|
78
|
+
if @source_document.nil? && @lexical_diagnostics.any?
|
|
79
|
+
return ParseResult.new(diagnostics: lexical_diagnostics, ast: nil, document: nil)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
recovery = DiagnosticRecovery.new(
|
|
83
|
+
@tokens, mode: @mode, max_diagnostics: max_diagnostics
|
|
84
|
+
)
|
|
85
|
+
node, syntax_diagnostics = recovery.parse
|
|
86
|
+
node = enrich_rule_documentation(node) if node
|
|
87
|
+
ast, syntax_diagnostics = root_diagnostic_result(node, syntax_diagnostics)
|
|
88
|
+
diagnostics = merge_diagnostics(lexical_diagnostics, syntax_diagnostics, max_diagnostics)
|
|
89
|
+
document = if diagnostics.empty? && ast
|
|
90
|
+
@source_document&.with_ast(ast)
|
|
91
|
+
else
|
|
92
|
+
@source_document
|
|
93
|
+
end
|
|
94
|
+
ParseResult.new(diagnostics: diagnostics, ast: ast, document: document)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# @rbs () -> SourceDocument
|
|
100
|
+
def source_document!
|
|
101
|
+
source_document_error_message = @source_document_error_message
|
|
102
|
+
raise Ibex::Error, source_document_error_message if source_document_error_message
|
|
103
|
+
|
|
104
|
+
source_document = @source_document
|
|
105
|
+
raise ArgumentError, "source document parsing requires String source" unless source_document
|
|
106
|
+
|
|
107
|
+
source_document
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @rbs () -> (AST::Root | AST::Fragment)
|
|
111
|
+
def parse_node
|
|
112
|
+
lexical = @lexical_diagnostics.first
|
|
113
|
+
raise Ibex::Error, lexical.to_s if lexical
|
|
114
|
+
|
|
115
|
+
parse_error_message = @parse_error_message
|
|
116
|
+
raise Ibex::Error, parse_error_message if parse_error_message
|
|
117
|
+
|
|
118
|
+
node = @parsed_node
|
|
119
|
+
return node if node
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
@parsed_node = enrich_rule_documentation(@implementation.parse)
|
|
123
|
+
rescue Ibex::Error => e
|
|
124
|
+
@parse_error_message = e.message.dup.freeze
|
|
125
|
+
raise
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @rbs (AST::Root | AST::Fragment node) -> (AST::Root | AST::Fragment)
|
|
130
|
+
def enrich_rule_documentation(node)
|
|
131
|
+
source_document = @source_document
|
|
132
|
+
return node unless source_document
|
|
133
|
+
|
|
134
|
+
RuleDocumentation.enrich(node, source_document)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @rbs (AST::Root | AST::Fragment | nil node, Array[Diagnostic] diagnostics) ->
|
|
138
|
+
# [AST::Root?, Array[Diagnostic]]
|
|
139
|
+
def root_diagnostic_result(node, diagnostics)
|
|
140
|
+
return [node, diagnostics] unless node.is_a?(AST::Fragment)
|
|
141
|
+
|
|
142
|
+
diagnostic = Diagnostic.new(
|
|
143
|
+
code: "frontend.syntax_error", phase: :syntax,
|
|
144
|
+
message: "fragment input requires Parser#parse_fragment", location: node.loc
|
|
145
|
+
)
|
|
146
|
+
[nil, diagnostics + [diagnostic]]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @rbs (String source, String file) -> Array[Token]
|
|
150
|
+
def tokenize_source(source, file)
|
|
151
|
+
document, diagnostics = Lexer.new(source, file: file).tokenize_document_recovering(
|
|
152
|
+
max_diagnostics: STRICT_LEXICAL_DIAGNOSTIC_LIMIT
|
|
153
|
+
)
|
|
154
|
+
@source_document = document
|
|
155
|
+
@lexical_diagnostics = diagnostics
|
|
156
|
+
document.tokens
|
|
157
|
+
rescue Ibex::Error => e
|
|
158
|
+
location = Location.new(file: file, line: 1, column: 1)
|
|
159
|
+
@lexical_diagnostics = [
|
|
160
|
+
Diagnostic.new(code: "frontend.lexical_error", phase: :lexical, message: e.message,
|
|
161
|
+
location: location, rendered: e.message)
|
|
162
|
+
]
|
|
163
|
+
@source_document_error_message = e.message.dup.freeze
|
|
164
|
+
[Token.new(type: :eof, value: nil, location: location, span: nil)]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @rbs (Integer max_diagnostics) -> Array[Diagnostic]
|
|
168
|
+
def lexical_diagnostics_for(max_diagnostics)
|
|
169
|
+
return @lexical_diagnostics if @lexical_diagnostics.empty? || max_diagnostics == 1
|
|
170
|
+
|
|
171
|
+
source_document = @source_document
|
|
172
|
+
return @lexical_diagnostics.first(max_diagnostics) unless source_document
|
|
173
|
+
|
|
174
|
+
_, diagnostics = Lexer.new(source_document.source, file: source_document.file)
|
|
175
|
+
.tokenize_document_recovering(max_diagnostics: max_diagnostics)
|
|
176
|
+
diagnostics
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @rbs (Array[Diagnostic] lexical, Array[Diagnostic] syntax, Integer limit) -> Array[Diagnostic]
|
|
180
|
+
def merge_diagnostics(lexical, syntax, limit)
|
|
181
|
+
(lexical + syntax)
|
|
182
|
+
.uniq { |diagnostic| diagnostic_key(diagnostic) }
|
|
183
|
+
.sort_by { |diagnostic| diagnostic_sort_key(diagnostic) }
|
|
184
|
+
.first(limit)
|
|
185
|
+
.freeze
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @rbs (Diagnostic diagnostic) -> [String, String, Integer, Integer, String]
|
|
189
|
+
def diagnostic_key(diagnostic)
|
|
190
|
+
location = diagnostic.location
|
|
191
|
+
[diagnostic.phase.to_s, location.file, location.line, location.column, diagnostic.message]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @rbs (Diagnostic diagnostic) -> [String, Integer, Integer, String, String]
|
|
195
|
+
def diagnostic_sort_key(diagnostic)
|
|
196
|
+
location = diagnostic.location
|
|
197
|
+
[location.file, location.line, location.column, diagnostic.phase.to_s, diagnostic.code]
|
|
20
198
|
end
|
|
21
199
|
end
|
|
22
200
|
end
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../error"
|
|
4
4
|
require_relative "../tables"
|
|
5
|
+
require_relative "source_span"
|
|
5
6
|
require_relative "source_cursor"
|
|
7
|
+
require_relative "source_document"
|
|
6
8
|
require_relative "action_scanner"
|
|
7
9
|
require_relative "lexer"
|
|
8
10
|
require_relative "ast"
|
|
9
11
|
require_relative "parser/declarations"
|
|
10
12
|
require_relative "parser/rules"
|
|
13
|
+
require_relative "parser/parameters"
|
|
11
14
|
require_relative "bootstrap_parser"
|
|
12
15
|
require_relative "../ir"
|
|
13
16
|
require_relative "../normalize"
|
|
@@ -20,6 +23,7 @@ module Ibex
|
|
|
20
23
|
# Builds the committed frontend parser from its canonical Ibex grammar.
|
|
21
24
|
module Regenerator
|
|
22
25
|
GRAMMAR_PATH = File.expand_path("grammar.y", File.dirname(__FILE__)) #: String
|
|
26
|
+
SHADOW_GRAMMAR_PATH = File.expand_path("shadow_grammar.y", File.dirname(__FILE__)) #: String
|
|
23
27
|
|
|
24
28
|
module_function
|
|
25
29
|
|
|
@@ -29,13 +33,34 @@ module Ibex
|
|
|
29
33
|
ast = BootstrapParser.new(source, file: relative_grammar_path).parse
|
|
30
34
|
grammar = Normalizer.new(ast).normalize
|
|
31
35
|
automaton = LALR::Builder.new(grammar).build
|
|
32
|
-
Codegen::Ruby.new(
|
|
36
|
+
Codegen::Ruby.new(
|
|
37
|
+
automaton, table: :compact, line_convert: false, runtime_require: nil
|
|
38
|
+
).generate
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build the non-published parameterized/inline grammar used to verify
|
|
42
|
+
# that preview composition features can describe the production
|
|
43
|
+
# frontend without changing its observable AST.
|
|
44
|
+
# @rbs () -> String
|
|
45
|
+
def generate_shadow
|
|
46
|
+
source = File.read(SHADOW_GRAMMAR_PATH)
|
|
47
|
+
ast = BootstrapParser.new(source, file: relative_shadow_grammar_path, mode: :extended).parse
|
|
48
|
+
grammar = Normalizer.new(ast, mode: :extended).normalize
|
|
49
|
+
automaton = LALR::Builder.new(grammar).build
|
|
50
|
+
Codegen::Ruby.new(
|
|
51
|
+
automaton, table: :compact, line_convert: false, runtime_require: nil
|
|
52
|
+
).generate
|
|
33
53
|
end
|
|
34
54
|
|
|
35
55
|
# @rbs () -> String
|
|
36
56
|
def relative_grammar_path
|
|
37
57
|
"lib/ibex/frontend/grammar.y"
|
|
38
58
|
end
|
|
59
|
+
|
|
60
|
+
# @rbs () -> String
|
|
61
|
+
def relative_shadow_grammar_path
|
|
62
|
+
"lib/ibex/frontend/shadow_grammar.y"
|
|
63
|
+
end
|
|
39
64
|
end
|
|
40
65
|
end
|
|
41
66
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Frontend
|
|
5
|
+
# Immutable result of resolving one root grammar and its fragment closure.
|
|
6
|
+
class Resolution
|
|
7
|
+
EMPTY_CHAIN = Array.new(0).freeze #: Array[IR::source_provenance]
|
|
8
|
+
private_constant :EMPTY_CHAIN
|
|
9
|
+
|
|
10
|
+
attr_reader :root #: AST::Root
|
|
11
|
+
attr_reader :root_path #: String
|
|
12
|
+
attr_reader :root_directory #: String
|
|
13
|
+
attr_reader :files #: Array[String]
|
|
14
|
+
|
|
15
|
+
# @rbs (root: AST::Root, root_path: String, root_directory: String, files: Array[String],
|
|
16
|
+
# include_chains: Hash[AST::Rule, Array[IR::source_provenance]]) -> void
|
|
17
|
+
def initialize(root:, root_path:, root_directory:, files:, include_chains:)
|
|
18
|
+
@root = deep_freeze_ast(root)
|
|
19
|
+
@root_path = root_path.dup.freeze
|
|
20
|
+
@root_directory = root_directory.dup.freeze
|
|
21
|
+
@files = files.map { |file| file.dup.freeze }.freeze
|
|
22
|
+
chains = {} #: Hash[AST::Rule, Array[IR::source_provenance]]
|
|
23
|
+
@include_chains = chains.compare_by_identity
|
|
24
|
+
include_chains.each do |rule, chain|
|
|
25
|
+
frozen_chain = chain.map { |entry| copy_provenance(entry) }.freeze
|
|
26
|
+
@include_chains[rule] = frozen_chain
|
|
27
|
+
end
|
|
28
|
+
@include_chains.freeze
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs (AST::Rule rule) -> Array[IR::source_provenance]
|
|
33
|
+
def include_chain_for(rule)
|
|
34
|
+
@include_chains.fetch(rule, EMPTY_CHAIN)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# @rbs (untyped value) -> untyped
|
|
40
|
+
def deep_freeze_ast(value)
|
|
41
|
+
case value
|
|
42
|
+
when Struct
|
|
43
|
+
value.each_pair { |_name, child| deep_freeze_ast(child) }
|
|
44
|
+
when Array
|
|
45
|
+
value.each { |child| deep_freeze_ast(child) }
|
|
46
|
+
when Hash
|
|
47
|
+
value.each do |key, child|
|
|
48
|
+
deep_freeze_ast(key)
|
|
49
|
+
deep_freeze_ast(child)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
value.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs (IR::source_provenance entry) -> IR::source_provenance
|
|
56
|
+
def copy_provenance(entry)
|
|
57
|
+
span = entry[:byte_span]
|
|
58
|
+
frozen_span = span && { start: span[:start], end: span[:end] } #: IR::byte_span?
|
|
59
|
+
frozen_span&.freeze
|
|
60
|
+
provenance = {
|
|
61
|
+
file: entry[:file]&.dup&.freeze,
|
|
62
|
+
root: entry[:root]&.dup&.freeze,
|
|
63
|
+
byte_span: frozen_span
|
|
64
|
+
} #: IR::source_provenance
|
|
65
|
+
provenance.freeze
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Frontend
|
|
5
|
+
# An actual filesystem read failure while resolving a grammar closure.
|
|
6
|
+
class ResolutionIOError < Ibex::Error; end
|
|
7
|
+
|
|
8
|
+
# Loads an explicit root grammar and resolves its extended-mode fragment graph.
|
|
9
|
+
class Resolver
|
|
10
|
+
GLOB_CHARACTERS = /[*?\[\]{}]/ #: Regexp
|
|
11
|
+
WINDOWS_ABSOLUTE = %r{\A(?:[A-Za-z]:[\\/]|\\\\)} #: Regexp
|
|
12
|
+
|
|
13
|
+
# @rbs @input_path: String
|
|
14
|
+
# @rbs @mode: Symbol
|
|
15
|
+
# @rbs @loader: SourceLoader
|
|
16
|
+
# @rbs @root_path: String
|
|
17
|
+
# @rbs @root_directory: String
|
|
18
|
+
# @rbs @files: Array[String]
|
|
19
|
+
# @rbs @visiting: Array[String]
|
|
20
|
+
# @rbs @loaded: Hash[String, bool]
|
|
21
|
+
# @rbs @include_chains: Hash[AST::Rule, Array[IR::source_provenance]]
|
|
22
|
+
# @rbs @attempted_paths: Array[String]
|
|
23
|
+
|
|
24
|
+
attr_reader :attempted_paths #: Array[String]
|
|
25
|
+
|
|
26
|
+
# @rbs (String path, ?mode: Symbol, ?loader: SourceLoader) -> void
|
|
27
|
+
def initialize(path, mode: :default, loader: SourceLoader.new)
|
|
28
|
+
raise ArgumentError, "mode must be :default or :extended" unless %i[default extended].include?(mode)
|
|
29
|
+
|
|
30
|
+
@input_path = path
|
|
31
|
+
@mode = mode
|
|
32
|
+
@loader = loader
|
|
33
|
+
@attempted_paths = [File.expand_path(path)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs () -> Resolution
|
|
37
|
+
def resolve
|
|
38
|
+
cached = @resolution
|
|
39
|
+
return cached if cached
|
|
40
|
+
|
|
41
|
+
prepare_resolution
|
|
42
|
+
parsed_root = parse_root(@root_path)
|
|
43
|
+
declarations, rules = expand_node(parsed_root, [])
|
|
44
|
+
root = AST::Root.new(
|
|
45
|
+
class_name: parsed_root.class_name, superclass: parsed_root.superclass,
|
|
46
|
+
declarations: declarations, rules: rules, user_code: parsed_root.user_code, loc: parsed_root.loc,
|
|
47
|
+
extended: parsed_root.extended, cst: parsed_root.cst
|
|
48
|
+
)
|
|
49
|
+
@loaded[@root_path] = true
|
|
50
|
+
@visiting.pop
|
|
51
|
+
@resolution = Resolution.new(
|
|
52
|
+
root: root, root_path: @root_path, root_directory: @root_directory,
|
|
53
|
+
files: @files, include_chains: @include_chains
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs () -> Array[String]
|
|
58
|
+
def dependencies
|
|
59
|
+
resolve.files
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Source bytes actually consumed by the parser, in resolution order.
|
|
63
|
+
# @rbs () -> Array[GenerationInput]
|
|
64
|
+
def source_records
|
|
65
|
+
@loader.read_records
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# @rbs () -> void
|
|
71
|
+
def prepare_resolution
|
|
72
|
+
@root_path = canonical_root
|
|
73
|
+
@root_directory = File.dirname(@root_path)
|
|
74
|
+
@files = [@root_path] #: Array[String]
|
|
75
|
+
@visiting = [@root_path] #: Array[String]
|
|
76
|
+
@loaded = {} #: Hash[String, bool]
|
|
77
|
+
chains = {} #: Hash[AST::Rule, Array[IR::source_provenance]]
|
|
78
|
+
@include_chains = chains.compare_by_identity
|
|
79
|
+
@attempted_paths = [File.expand_path(@input_path)]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @rbs () -> String
|
|
83
|
+
def canonical_root
|
|
84
|
+
path = @loader.canonical_path(@input_path)
|
|
85
|
+
raise Ibex::Error, "#{@input_path}:1:1: root grammar must be a file" unless @loader.file?(path)
|
|
86
|
+
|
|
87
|
+
@loader.record_access(path, @input_path)
|
|
88
|
+
path
|
|
89
|
+
rescue SystemCallError => e
|
|
90
|
+
raise ResolutionIOError, "#{@input_path}:1:1: cannot read root grammar: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @rbs (String path) -> AST::Root
|
|
94
|
+
def parse_root(path)
|
|
95
|
+
Parser.new(@loader.read(path), file: path, mode: @mode).parse
|
|
96
|
+
rescue SystemCallError => e
|
|
97
|
+
raise ResolutionIOError, "#{path}:1:1: cannot read grammar: #{e.message}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs (String path) -> AST::Fragment
|
|
101
|
+
def parse_fragment(path)
|
|
102
|
+
Parser.new(@loader.read(path), file: path, mode: :extended).parse_fragment
|
|
103
|
+
rescue SystemCallError => e
|
|
104
|
+
raise ResolutionIOError, "#{path}:1:1: cannot read fragment: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @rbs (AST::Root | AST::Fragment node, Array[IR::source_provenance] chain) ->
|
|
108
|
+
# [Array[AST::declaration], Array[AST::Rule]]
|
|
109
|
+
def expand_node(node, chain)
|
|
110
|
+
declarations = [] #: Array[AST::declaration]
|
|
111
|
+
rules = [] #: Array[AST::Rule]
|
|
112
|
+
node.declarations.each do |declaration|
|
|
113
|
+
if declaration.is_a?(AST::Include)
|
|
114
|
+
included_declarations, included_rules = expand_include(declaration, chain)
|
|
115
|
+
declarations.concat(included_declarations)
|
|
116
|
+
rules.concat(included_rules)
|
|
117
|
+
else
|
|
118
|
+
declarations << declaration
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
node.rules.each do |rule|
|
|
122
|
+
@include_chains[rule] = chain
|
|
123
|
+
rules << rule
|
|
124
|
+
end
|
|
125
|
+
[declarations, rules]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @rbs (AST::Include include_node, Array[IR::source_provenance] chain) ->
|
|
129
|
+
# [Array[AST::declaration], Array[AST::Rule]]
|
|
130
|
+
def expand_include(include_node, chain)
|
|
131
|
+
target = canonical_include(include_node)
|
|
132
|
+
reject_cycle(include_node, target)
|
|
133
|
+
return [[], []] if @loaded[target]
|
|
134
|
+
|
|
135
|
+
@files << target
|
|
136
|
+
@visiting << target
|
|
137
|
+
fragment = parse_fragment(target)
|
|
138
|
+
next_chain = chain + [source_provenance(target)]
|
|
139
|
+
declarations, rules = expand_node(fragment, next_chain)
|
|
140
|
+
@visiting.pop
|
|
141
|
+
@loaded[target] = true
|
|
142
|
+
[declarations, rules]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @rbs (AST::Include include_node) -> String
|
|
146
|
+
def canonical_include(include_node)
|
|
147
|
+
validate_include_path(include_node)
|
|
148
|
+
candidate = File.expand_path(include_node.path, File.dirname(include_node.loc.file))
|
|
149
|
+
@attempted_paths << candidate unless @attempted_paths.include?(candidate)
|
|
150
|
+
canonical = @loader.canonical_path(candidate)
|
|
151
|
+
unless @loader.file?(canonical)
|
|
152
|
+
fail_include(include_node, "include path is not a file: #{include_node.path.inspect}")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
unless inside_root?(canonical)
|
|
156
|
+
message = "include resolves outside the root grammar directory: #{include_node.path.inspect}"
|
|
157
|
+
fail_include(include_node, message)
|
|
158
|
+
end
|
|
159
|
+
@loader.record_access(canonical, candidate)
|
|
160
|
+
canonical
|
|
161
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
162
|
+
fail_include(include_node, "include file does not exist: #{include_node.path.inspect}")
|
|
163
|
+
rescue SystemCallError => e
|
|
164
|
+
message = "#{include_node.loc}: cannot read include #{include_node.path.inspect}: #{e.message}"
|
|
165
|
+
raise ResolutionIOError, message
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @rbs (AST::Include include_node) -> void
|
|
169
|
+
def validate_include_path(include_node)
|
|
170
|
+
path = include_node.path
|
|
171
|
+
fail_include(include_node, "include path must not be empty") if path.empty?
|
|
172
|
+
fail_include(include_node, "include path must not contain NUL") if path.include?("\0")
|
|
173
|
+
if path.start_with?("/") || path.match?(WINDOWS_ABSOLUTE)
|
|
174
|
+
fail_include(include_node, "include path must be relative")
|
|
175
|
+
end
|
|
176
|
+
if path.split(%r{[\\/]}).include?("..")
|
|
177
|
+
fail_include(include_node, "include path must not contain parent traversal")
|
|
178
|
+
end
|
|
179
|
+
return unless path.match?(GLOB_CHARACTERS)
|
|
180
|
+
|
|
181
|
+
fail_include(include_node, "include path must not contain glob metacharacters")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @rbs (String path) -> bool
|
|
185
|
+
def inside_root?(path)
|
|
186
|
+
directory = File.dirname(path)
|
|
187
|
+
loop do
|
|
188
|
+
return true if directory == @root_directory
|
|
189
|
+
|
|
190
|
+
parent = File.dirname(directory)
|
|
191
|
+
return false if parent == directory
|
|
192
|
+
|
|
193
|
+
directory = parent
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @rbs (AST::Include include_node, String target) -> void
|
|
198
|
+
def reject_cycle(include_node, target)
|
|
199
|
+
start = @visiting.index(target)
|
|
200
|
+
return unless start
|
|
201
|
+
|
|
202
|
+
cycle = @visiting.drop(start) + [target]
|
|
203
|
+
fail_include(include_node, "include cycle: #{cycle.join(' -> ')}")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @rbs (String file) -> IR::source_provenance
|
|
207
|
+
def source_provenance(file)
|
|
208
|
+
{ file: file, root: @root_directory, byte_span: nil }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @rbs (AST::Include include_node, String message) -> bot
|
|
212
|
+
def fail_include(include_node, message)
|
|
213
|
+
raise Ibex::Error, "#{include_node.loc}: #{message}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Frontend
|
|
5
|
+
# Attaches immediately preceding lossless line-comment blocks to parsed rules.
|
|
6
|
+
class RuleDocumentation
|
|
7
|
+
EMPTY_SEGMENTS = Array.new(0).freeze #: Array[Segment]
|
|
8
|
+
private_constant :EMPTY_SEGMENTS
|
|
9
|
+
|
|
10
|
+
# @rbs (AST::Root | AST::Fragment node, SourceDocument document) -> (AST::Root | AST::Fragment)
|
|
11
|
+
def self.enrich(node, document)
|
|
12
|
+
new(document).enrich(node)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs (SourceDocument document) -> void
|
|
16
|
+
def initialize(document)
|
|
17
|
+
@segments_by_line = index_segments(document) #: Hash[Integer, Array[Segment]]
|
|
18
|
+
@inline_lines = document.tokens.filter_map do |token|
|
|
19
|
+
token.location.line if token.type == :inline
|
|
20
|
+
end #: Array[Integer]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs (AST::Root | AST::Fragment node) -> (AST::Root | AST::Fragment)
|
|
24
|
+
def enrich(node)
|
|
25
|
+
rules = node.rules.map { |rule| documented_rule(rule) }
|
|
26
|
+
if node.is_a?(AST::Root)
|
|
27
|
+
AST::Root.new(
|
|
28
|
+
class_name: node.class_name, superclass: node.superclass, declarations: node.declarations,
|
|
29
|
+
rules: rules, user_code: node.user_code, loc: node.loc, extended: node.extended, cst: node.cst
|
|
30
|
+
)
|
|
31
|
+
else
|
|
32
|
+
AST::Fragment.new(declarations: node.declarations, rules: rules, loc: node.loc)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @rbs (AST::Rule rule) -> AST::Rule
|
|
39
|
+
def documented_rule(rule)
|
|
40
|
+
AST::Rule.new(
|
|
41
|
+
lhs: rule.lhs, parameters: rule.parameters, alternatives: rule.alternatives, loc: rule.loc,
|
|
42
|
+
documentation: documentation_before(documentation_anchor(rule)), inline: rule.inline
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @rbs (AST::Rule rule) -> Integer
|
|
47
|
+
def documentation_anchor(rule)
|
|
48
|
+
return rule.loc.line unless rule.inline
|
|
49
|
+
|
|
50
|
+
@inline_lines.reverse_each.find { |line| line <= rule.loc.line } || rule.loc.line
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs (Integer rule_line) -> String?
|
|
54
|
+
def documentation_before(rule_line)
|
|
55
|
+
lines = [] #: Array[String]
|
|
56
|
+
line = rule_line - 1
|
|
57
|
+
while line.positive?
|
|
58
|
+
content = documentation_line(line)
|
|
59
|
+
break unless content
|
|
60
|
+
|
|
61
|
+
lines.unshift(content)
|
|
62
|
+
line -= 1
|
|
63
|
+
end
|
|
64
|
+
lines.empty? ? nil : lines.join("\n").freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs (Integer line) -> String?
|
|
68
|
+
def documentation_line(line)
|
|
69
|
+
segments = @segments_by_line.fetch(line, EMPTY_SEGMENTS)
|
|
70
|
+
comments = segments.select { |segment| segment.kind == :line_comment }
|
|
71
|
+
return unless comments.length == 1
|
|
72
|
+
|
|
73
|
+
comment = comments.fetch(0)
|
|
74
|
+
return unless comment.text.start_with?("##")
|
|
75
|
+
return unless segments.all? { |segment| documentation_line_segment?(segment, comment) }
|
|
76
|
+
|
|
77
|
+
comment.text.delete_prefix("##").delete_prefix(" ")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs (Segment segment, Segment comment) -> bool
|
|
81
|
+
def documentation_line_segment?(segment, comment)
|
|
82
|
+
segment.equal?(comment) || %i[whitespace newline].include?(segment.kind)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @rbs (SourceDocument document) -> Hash[Integer, Array[Segment]]
|
|
86
|
+
def index_segments(document)
|
|
87
|
+
indexed = Hash.new { |hash, line| hash[line] = [] } #: Hash[Integer, Array[Segment]]
|
|
88
|
+
document.cst.each do |segment|
|
|
89
|
+
covered_lines(segment).each { |line| indexed[line] << segment }
|
|
90
|
+
end
|
|
91
|
+
indexed
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @rbs (Segment segment) -> Array[Integer]
|
|
95
|
+
def covered_lines(segment)
|
|
96
|
+
first = segment.span.start.line
|
|
97
|
+
return [first] if segment.kind == :newline
|
|
98
|
+
|
|
99
|
+
(first..segment.span.finish.line).to_a
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|