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
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../frontend"
|
|
6
|
+
|
|
7
|
+
module Ibex
|
|
8
|
+
# CLI coordination for deterministic, semantics-preserving grammar formatting.
|
|
9
|
+
# rubocop:disable Metrics/ModuleLength -- transaction lifecycle stays cohesive and auditable.
|
|
10
|
+
module CLIFormatting
|
|
11
|
+
# @rbs!
|
|
12
|
+
# type formatting_settings = {
|
|
13
|
+
# paths: Array[String],
|
|
14
|
+
# mode: Symbol,
|
|
15
|
+
# check: bool,
|
|
16
|
+
# write: bool,
|
|
17
|
+
# ?stdin_filename: String,
|
|
18
|
+
# ?help: bool
|
|
19
|
+
# }
|
|
20
|
+
# type formatting_result = {
|
|
21
|
+
# path: String,
|
|
22
|
+
# label: String,
|
|
23
|
+
# source: String,
|
|
24
|
+
# formatted: String
|
|
25
|
+
# }
|
|
26
|
+
# type formatting_target = {
|
|
27
|
+
# path: String,
|
|
28
|
+
# label: String,
|
|
29
|
+
# target: String,
|
|
30
|
+
# formatted: String,
|
|
31
|
+
# mode: Integer,
|
|
32
|
+
# directory: String,
|
|
33
|
+
# basename: String,
|
|
34
|
+
# changed: bool,
|
|
35
|
+
# installed: bool,
|
|
36
|
+
# ?stage: String,
|
|
37
|
+
# ?backup: String
|
|
38
|
+
# }
|
|
39
|
+
# type formatting_cleanup_result = {
|
|
40
|
+
# errors: Array[String],
|
|
41
|
+
# remaining: Array[String]
|
|
42
|
+
# }
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# @rbs (Array[String] arguments) -> Integer
|
|
47
|
+
def run_format_command(arguments)
|
|
48
|
+
settings = formatting_options(arguments)
|
|
49
|
+
if settings[:help]
|
|
50
|
+
@stdout.puts(formatting_option_parser(settings))
|
|
51
|
+
return 0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
validate_formatting_settings!(settings)
|
|
55
|
+
results, failed = prepare_formatting_results(settings)
|
|
56
|
+
if settings.fetch(:check)
|
|
57
|
+
check_status = check_formatting_results(results)
|
|
58
|
+
return 1 if failed
|
|
59
|
+
|
|
60
|
+
return check_status
|
|
61
|
+
end
|
|
62
|
+
return 1 if failed
|
|
63
|
+
return write_formatting_results(results) if settings.fetch(:write)
|
|
64
|
+
|
|
65
|
+
@stdout.write(results.fetch(0).fetch(:formatted))
|
|
66
|
+
0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @rbs (Array[String] arguments) -> formatting_settings
|
|
70
|
+
def formatting_options(arguments)
|
|
71
|
+
settings = { paths: [], mode: :default, check: false, write: false } #: formatting_settings
|
|
72
|
+
settings[:paths] = formatting_option_parser(settings).parse(arguments)
|
|
73
|
+
settings
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs (formatting_settings settings) -> OptionParser
|
|
77
|
+
def formatting_option_parser(settings)
|
|
78
|
+
OptionParser.new do |options|
|
|
79
|
+
options.banner = "Usage: ibex fmt [--check | --write] [options] grammar.y [...]"
|
|
80
|
+
options.on("--check", "report files that need formatting") { settings[:check] = true }
|
|
81
|
+
options.on("--write", "atomically format files in place") { settings[:write] = true }
|
|
82
|
+
options.on("--mode=MODE", %w[default extended], "grammar mode") { |value| settings[:mode] = value.to_sym }
|
|
83
|
+
options.on("--stdin-filename=FILE", "diagnostic filename for standard input") do |value|
|
|
84
|
+
settings[:stdin_filename] = value
|
|
85
|
+
end
|
|
86
|
+
options.on("--help", "show help") { settings[:help] = true }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs (formatting_settings settings) -> void
|
|
91
|
+
def validate_formatting_settings!(settings)
|
|
92
|
+
paths = settings.fetch(:paths)
|
|
93
|
+
raise Ibex::Error, "(cli):1:1: fmt requires at least one grammar file" if paths.empty?
|
|
94
|
+
|
|
95
|
+
validate_formatting_operation!(settings, paths)
|
|
96
|
+
validate_formatting_stdin!(settings, paths)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs (formatting_settings settings, Array[String] paths) -> void
|
|
100
|
+
def validate_formatting_operation!(settings, paths)
|
|
101
|
+
if settings.fetch(:check) && settings.fetch(:write)
|
|
102
|
+
raise Ibex::Error, "(cli):1:1: fmt --check and --write cannot be combined"
|
|
103
|
+
end
|
|
104
|
+
return if settings.fetch(:check) || settings.fetch(:write) || paths.length == 1
|
|
105
|
+
|
|
106
|
+
raise Ibex::Error, "(cli):1:1: fmt writes to stdout only for exactly one input"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @rbs (formatting_settings settings, Array[String] paths) -> void
|
|
110
|
+
def validate_formatting_stdin!(settings, paths)
|
|
111
|
+
raise Ibex::Error, "(cli):1:1: fmt standard input may be specified only once" if paths.count("-") > 1
|
|
112
|
+
|
|
113
|
+
if paths.include?("-") && (settings.fetch(:check) || settings.fetch(:write))
|
|
114
|
+
raise Ibex::Error, "(cli):1:1: fmt --check and --write require file inputs"
|
|
115
|
+
end
|
|
116
|
+
if settings[:stdin_filename] && formatting_control_bytes?(settings.fetch(:stdin_filename))
|
|
117
|
+
raise Ibex::Error, "(cli):1:1: --stdin-filename must not contain control characters"
|
|
118
|
+
end
|
|
119
|
+
return unless settings[:stdin_filename] && paths != ["-"]
|
|
120
|
+
|
|
121
|
+
raise Ibex::Error, "(cli):1:1: --stdin-filename requires fmt -"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @rbs (formatting_settings settings) -> [Array[formatting_result], bool]
|
|
125
|
+
def prepare_formatting_results(settings)
|
|
126
|
+
failed = false
|
|
127
|
+
results = [] #: Array[formatting_result]
|
|
128
|
+
settings.fetch(:paths).each do |path|
|
|
129
|
+
source = path == "-" ? @stdin.read : File.binread(path)
|
|
130
|
+
label = path == "-" ? settings[:stdin_filename] || "(stdin)" : formatting_label(path)
|
|
131
|
+
formatted = Frontend::Formatter.format(source, file: label, mode: settings.fetch(:mode))
|
|
132
|
+
results << { path: path, label: label, source: source, formatted: formatted }
|
|
133
|
+
rescue Ibex::Error, SystemCallError => e
|
|
134
|
+
@stderr.puts(formatting_label(e.message))
|
|
135
|
+
failed = true
|
|
136
|
+
end
|
|
137
|
+
[results, failed]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs (Array[formatting_result] results) -> Integer
|
|
141
|
+
def check_formatting_results(results)
|
|
142
|
+
changed = results.reject { |result| result.fetch(:source) == result.fetch(:formatted) }
|
|
143
|
+
changed.each { |result| @stderr.puts("#{result.fetch(:label)}: needs formatting") }
|
|
144
|
+
changed.empty? ? 0 : 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @rbs (Array[formatting_result] results) -> Integer
|
|
148
|
+
def write_formatting_results(results)
|
|
149
|
+
targets = formatting_targets(results)
|
|
150
|
+
validate_unique_formatting_targets!(targets)
|
|
151
|
+
targets = targets.select { |target| target.fetch(:changed) }
|
|
152
|
+
return 0 if targets.empty?
|
|
153
|
+
|
|
154
|
+
transactionally_write_formatted(targets)
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @rbs (Array[formatting_result] results) -> Array[formatting_target]
|
|
159
|
+
def formatting_targets(results)
|
|
160
|
+
results.map do |result|
|
|
161
|
+
target = File.realpath(result.fetch(:path))
|
|
162
|
+
{
|
|
163
|
+
path: result.fetch(:path), label: result.fetch(:label), target: target,
|
|
164
|
+
formatted: result.fetch(:formatted), mode: File.stat(target).mode & 0o7777,
|
|
165
|
+
directory: File.dirname(target), basename: File.basename(target),
|
|
166
|
+
changed: result.fetch(:source) != result.fetch(:formatted), installed: false
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
rescue SystemCallError => e
|
|
170
|
+
raise Ibex::Error, formatting_label(e.message)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
174
|
+
def validate_unique_formatting_targets!(targets)
|
|
175
|
+
collision = duplicate_formatting_target_pair(targets)
|
|
176
|
+
return unless collision
|
|
177
|
+
|
|
178
|
+
labels = collision.map { |target| target.fetch(:label) }
|
|
179
|
+
raise Ibex::Error, "(cli):1:1: fmt inputs resolve to the same target: #{labels.join(', ')}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @rbs (Array[formatting_target] targets) -> [formatting_target, formatting_target]?
|
|
183
|
+
def duplicate_formatting_target_pair(targets)
|
|
184
|
+
targets.each_with_index do |left, index|
|
|
185
|
+
targets.drop(index + 1).each do |right|
|
|
186
|
+
same_path = left.fetch(:target) == right.fetch(:target)
|
|
187
|
+
return [left, right] if same_path || File.identical?(left.fetch(:target), right.fetch(:target))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
194
|
+
def transactionally_write_formatted(targets)
|
|
195
|
+
begin
|
|
196
|
+
stage_formatting_targets(targets)
|
|
197
|
+
backup_formatting_targets(targets)
|
|
198
|
+
sync_formatting_directories!(targets)
|
|
199
|
+
install_formatting_targets(targets)
|
|
200
|
+
sync_formatting_directories!(targets)
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
rollback_errors = rollback_formatting_targets(targets)
|
|
203
|
+
cleanup = cleanup_formatting_artifacts(targets, preserve_installed_backups: true)
|
|
204
|
+
sync_errors = sync_formatting_directories_best_effort(targets)
|
|
205
|
+
raise formatting_transaction_error(e, rollback_errors, cleanup, sync_errors)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
cleanup = cleanup_formatting_artifacts(targets, preserve_installed_backups: false)
|
|
209
|
+
sync_errors = sync_formatting_directories_best_effort(targets)
|
|
210
|
+
report_formatting_cleanup_warning(cleanup, sync_errors)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
214
|
+
def stage_formatting_targets(targets)
|
|
215
|
+
targets.each do |target|
|
|
216
|
+
temporary = Tempfile.create(
|
|
217
|
+
[".ibex-fmt-", ".stage"], target.fetch(:directory)
|
|
218
|
+
)
|
|
219
|
+
target[:stage] = temporary.path.dup
|
|
220
|
+
temporary.binmode
|
|
221
|
+
temporary.write(target.fetch(:formatted))
|
|
222
|
+
temporary.flush
|
|
223
|
+
temporary.fsync
|
|
224
|
+
temporary.chmod(target.fetch(:mode))
|
|
225
|
+
temporary.fsync
|
|
226
|
+
temporary.close
|
|
227
|
+
rescue StandardError
|
|
228
|
+
begin
|
|
229
|
+
temporary&.close
|
|
230
|
+
rescue StandardError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
raise
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
238
|
+
def backup_formatting_targets(targets)
|
|
239
|
+
targets.each do |target|
|
|
240
|
+
backup = "#{target.fetch(:stage)}.backup"
|
|
241
|
+
target[:backup] = backup
|
|
242
|
+
File.link(target.fetch(:target), backup)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
247
|
+
def install_formatting_targets(targets)
|
|
248
|
+
targets.each do |target|
|
|
249
|
+
File.rename(target.fetch(:stage), target.fetch(:target))
|
|
250
|
+
target[:installed] = true
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# @rbs (Array[formatting_target] targets) -> Array[String]
|
|
255
|
+
def rollback_formatting_targets(targets)
|
|
256
|
+
errors = [] #: Array[String]
|
|
257
|
+
targets.reverse_each do |target|
|
|
258
|
+
next unless target.fetch(:installed)
|
|
259
|
+
|
|
260
|
+
File.rename(target.fetch(:backup), target.fetch(:target))
|
|
261
|
+
target[:installed] = false
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
errors << "#{target.fetch(:label)}: #{formatting_label(e.message)}"
|
|
264
|
+
end
|
|
265
|
+
errors
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# @rbs (Array[formatting_target] targets, preserve_installed_backups: bool) ->
|
|
269
|
+
# formatting_cleanup_result
|
|
270
|
+
def cleanup_formatting_artifacts(targets, preserve_installed_backups:)
|
|
271
|
+
errors = [] #: Array[String]
|
|
272
|
+
remaining = [] #: Array[String]
|
|
273
|
+
targets.each do |target|
|
|
274
|
+
artifacts = [target[:stage]] #: Array[String?]
|
|
275
|
+
backup = target[:backup]
|
|
276
|
+
if preserve_installed_backups && target.fetch(:installed) && backup
|
|
277
|
+
remaining << backup
|
|
278
|
+
else
|
|
279
|
+
artifacts << backup
|
|
280
|
+
end
|
|
281
|
+
artifacts.compact.each do |path|
|
|
282
|
+
error = remove_formatting_artifact(path)
|
|
283
|
+
next unless error
|
|
284
|
+
|
|
285
|
+
errors << error
|
|
286
|
+
remaining << path
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
{ errors: errors, remaining: remaining }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# @rbs (String path) -> String?
|
|
293
|
+
def remove_formatting_artifact(path)
|
|
294
|
+
attempts = 0
|
|
295
|
+
begin
|
|
296
|
+
attempts += 1
|
|
297
|
+
File.unlink(path)
|
|
298
|
+
nil
|
|
299
|
+
rescue Errno::ENOENT
|
|
300
|
+
nil
|
|
301
|
+
rescue StandardError => e
|
|
302
|
+
retry if attempts < 2
|
|
303
|
+
|
|
304
|
+
"#{formatting_label(path)}: #{formatting_label(e.message)}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @rbs (StandardError error, Array[String] rollback_errors,
|
|
309
|
+
# formatting_cleanup_result cleanup, Array[String] sync_errors) -> Ibex::Error
|
|
310
|
+
def formatting_transaction_error(error, rollback_errors, cleanup, sync_errors)
|
|
311
|
+
details = ["fmt write failed: #{formatting_label(error.message)}"] #: Array[String]
|
|
312
|
+
details << "rollback failed: #{rollback_errors.join('; ')}" unless rollback_errors.empty?
|
|
313
|
+
details << "cleanup failed: #{cleanup.fetch(:errors).join('; ')}" unless cleanup.fetch(:errors).empty?
|
|
314
|
+
unless cleanup.fetch(:remaining).empty?
|
|
315
|
+
paths = cleanup.fetch(:remaining).map { |path| formatting_label(path) }
|
|
316
|
+
details << "preserved artifacts: #{paths.join(', ')}"
|
|
317
|
+
end
|
|
318
|
+
details << "rollback directory sync failed: #{sync_errors.join('; ')}" unless sync_errors.empty?
|
|
319
|
+
Ibex::Error.new(details.join("; "))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# @rbs (formatting_cleanup_result cleanup, Array[String] sync_errors) -> void
|
|
323
|
+
def report_formatting_cleanup_warning(cleanup, sync_errors)
|
|
324
|
+
details = cleanup.fetch(:errors).dup
|
|
325
|
+
details.concat(sync_errors)
|
|
326
|
+
unless cleanup.fetch(:remaining).empty?
|
|
327
|
+
paths = cleanup.fetch(:remaining).map { |path| formatting_label(path) }
|
|
328
|
+
details << "remaining artifacts: #{paths.join(', ')}"
|
|
329
|
+
end
|
|
330
|
+
return if details.empty?
|
|
331
|
+
|
|
332
|
+
@stderr.puts("fmt cleanup warning (formatted targets committed): #{details.join('; ')}")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# @rbs (Array[formatting_target] targets) -> void
|
|
336
|
+
def sync_formatting_directories!(targets)
|
|
337
|
+
errors = sync_formatting_directories(targets)
|
|
338
|
+
return if errors.empty?
|
|
339
|
+
|
|
340
|
+
raise Ibex::Error, "directory sync failed: #{errors.join('; ')}"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# @rbs (Array[formatting_target] targets) -> Array[String]
|
|
344
|
+
def sync_formatting_directories_best_effort(targets)
|
|
345
|
+
sync_formatting_directories(targets)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# @rbs (Array[formatting_target] targets) -> Array[String]
|
|
349
|
+
def sync_formatting_directories(targets)
|
|
350
|
+
errors = [] #: Array[String]
|
|
351
|
+
targets.map { |target| target.fetch(:directory) }.uniq.each do |directory|
|
|
352
|
+
sync_formatting_directory(directory)
|
|
353
|
+
rescue SystemCallError, IOError => e
|
|
354
|
+
errors << "#{formatting_label(directory)}: #{formatting_label(e.message)}"
|
|
355
|
+
end
|
|
356
|
+
errors
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# @rbs (String directory) -> void
|
|
360
|
+
def sync_formatting_directory(directory)
|
|
361
|
+
File.open(directory, File::RDONLY, &:fsync)
|
|
362
|
+
rescue Errno::EINVAL, Errno::ENOTSUP, Errno::EBADF
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @rbs (String value) -> String
|
|
367
|
+
def formatting_label(value)
|
|
368
|
+
escaped = String.new(encoding: Encoding::BINARY)
|
|
369
|
+
value.b.each_byte do |byte|
|
|
370
|
+
escaped << (control_byte?(byte) ? format("\\x%02X", byte) : byte)
|
|
371
|
+
end
|
|
372
|
+
escaped.force_encoding(value.encoding)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# @rbs (String value) -> bool
|
|
376
|
+
def formatting_control_bytes?(value)
|
|
377
|
+
value.b.each_byte.any? { |byte| control_byte?(byte) }
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# @rbs (Integer byte) -> bool
|
|
381
|
+
def control_byte?(byte)
|
|
382
|
+
byte < 0x20 || byte == 0x7f
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
# rubocop:enable Metrics/ModuleLength
|
|
386
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative "../artifact_set"
|
|
5
|
+
require_relative "../generation_input"
|
|
6
|
+
require_relative "../generation_transaction"
|
|
7
|
+
|
|
8
|
+
module Ibex
|
|
9
|
+
# Collects, verifies, and transactionally publishes CLI generation outputs.
|
|
10
|
+
module CLIGenerationArtifacts
|
|
11
|
+
# @rbs!
|
|
12
|
+
# private def report_status: (String) -> void
|
|
13
|
+
# private def verify_file: (String, String, String) -> void
|
|
14
|
+
# private def default_output_path: (String, String) -> String
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# @rbs () -> void
|
|
19
|
+
def begin_artifact_generation
|
|
20
|
+
@generation_artifacts = ArtifactSet.new
|
|
21
|
+
@generation_statuses = [] #: Array[String]
|
|
22
|
+
@generation_sources = [] #: Array[String]
|
|
23
|
+
@generation_inputs = [] #: Array[GenerationInput]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs (Symbol kind, String path, String content, ?mode: Integer?, ?status: bool) -> Artifact
|
|
27
|
+
def register_artifact(kind, path, content, mode: nil, status: false)
|
|
28
|
+
artifact = generation_artifacts.add(kind: kind, path: path, content: content, mode: mode)
|
|
29
|
+
@generation_statuses << "wrote #{path}" if status
|
|
30
|
+
artifact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @rbs (Array[String] source_paths) -> Integer
|
|
34
|
+
def finish_artifact_generation(_source_paths)
|
|
35
|
+
add_generation_manifest if @options[:manifest]
|
|
36
|
+
return 0 if @defer_generation_publication
|
|
37
|
+
|
|
38
|
+
ensure_generation_inputs_stable!
|
|
39
|
+
if @options[:verify_output]
|
|
40
|
+
verify_artifact_generation
|
|
41
|
+
ensure_generation_inputs_stable!
|
|
42
|
+
elsif !generation_artifacts.empty?
|
|
43
|
+
GenerationTransaction.new(
|
|
44
|
+
generation_artifacts,
|
|
45
|
+
warning: ->(message) { @stderr.puts("ibex: warning: #{message}") },
|
|
46
|
+
stability_check: -> { generation_inputs_stable? },
|
|
47
|
+
source_records: @generation_inputs
|
|
48
|
+
).commit
|
|
49
|
+
@generation_statuses.each { |message| report_status(message) }
|
|
50
|
+
end
|
|
51
|
+
0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs () -> void
|
|
55
|
+
def add_generation_manifest
|
|
56
|
+
require_relative "../generation_manifest"
|
|
57
|
+
|
|
58
|
+
path = manifest_output_path
|
|
59
|
+
source = GenerationManifest.render(
|
|
60
|
+
generation_artifacts,
|
|
61
|
+
source_records: @generation_inputs,
|
|
62
|
+
options: generation_manifest_options
|
|
63
|
+
)
|
|
64
|
+
register_artifact(:manifest, path, source, status: true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs () -> void
|
|
68
|
+
def verify_artifact_generation
|
|
69
|
+
generation_artifacts.each do |artifact|
|
|
70
|
+
verify_file(artifact.path, artifact.content, artifact_label(artifact.kind))
|
|
71
|
+
end
|
|
72
|
+
parser = generation_artifacts.find { |artifact| artifact.kind == :parser }
|
|
73
|
+
report_status("verified #{parser.path}") if parser
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs (Symbol kind) -> String
|
|
77
|
+
def artifact_label(kind)
|
|
78
|
+
{
|
|
79
|
+
parser: "parser",
|
|
80
|
+
rbs: "RBS signature",
|
|
81
|
+
action_source: "action source",
|
|
82
|
+
manifest: "generation manifest"
|
|
83
|
+
}.fetch(kind, kind.to_s.tr("_", " "))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs () -> ArtifactSet
|
|
87
|
+
def generation_artifacts
|
|
88
|
+
@generation_artifacts || raise(Ibex::Error, "(generation):1:1: artifact collection is not active")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @rbs () -> bool
|
|
92
|
+
def generation_inputs_stable?
|
|
93
|
+
@generation_inputs.all?(&:current?)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs () -> void
|
|
97
|
+
def ensure_generation_inputs_stable!
|
|
98
|
+
return if generation_inputs_stable?
|
|
99
|
+
|
|
100
|
+
raise GenerationTransaction::SourceChanged, "(generation):1:1: source changed while rendering outputs"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @rbs (String path, String source) -> GenerationInput
|
|
104
|
+
def record_generation_input(path, source)
|
|
105
|
+
input = GenerationInput.new(path, source)
|
|
106
|
+
@generation_inputs.reject! { |entry| entry.path == input.path }
|
|
107
|
+
@generation_inputs << input
|
|
108
|
+
@generation_sources = @generation_inputs.map(&:path)
|
|
109
|
+
input
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @rbs () -> String
|
|
113
|
+
def manifest_output_path
|
|
114
|
+
configured = @options[:manifest]
|
|
115
|
+
return configured if configured.is_a?(String) && !configured.empty?
|
|
116
|
+
|
|
117
|
+
parser = generation_artifacts.find { |artifact| artifact.kind == :parser }
|
|
118
|
+
raise Ibex::Error, "(generation):1:1: generation manifest requires a parser artifact" unless parser
|
|
119
|
+
|
|
120
|
+
default_output_path(parser.path, ".ibex.json")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @rbs () -> Hash[String, untyped]
|
|
124
|
+
def generation_manifest_options
|
|
125
|
+
keys = %i[
|
|
126
|
+
action_source algorithm counterexample_max_configurations counterexample_max_tokens debug
|
|
127
|
+
embedded emit entry_isolation executable frozen line_convert line_convert_all messages mode omit_actions rbs
|
|
128
|
+
superclass table
|
|
129
|
+
]
|
|
130
|
+
options = {} #: Hash[String, untyped]
|
|
131
|
+
keys.each do |key|
|
|
132
|
+
value = @options[key]
|
|
133
|
+
options[key.to_s] = value unless value.nil?
|
|
134
|
+
end
|
|
135
|
+
options
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ibex
|
|
5
|
+
# Optional example-keyed error-message support for parser generation.
|
|
6
|
+
module CLIGenerationErrorMessages
|
|
7
|
+
# @rbs!
|
|
8
|
+
# private def record_generation_input: (String, String) -> GenerationInput
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# @rbs (OptionParser options) -> void
|
|
13
|
+
def add_error_messages_generation_option(options)
|
|
14
|
+
options.on("--messages=FILE", "embed example-keyed syntax error messages") do |value|
|
|
15
|
+
@options[:messages] = value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs (IR::Automaton automaton) -> Hash[Integer, untyped]
|
|
20
|
+
def configured_error_messages(automaton)
|
|
21
|
+
path = @options[:messages]
|
|
22
|
+
return {} unless path
|
|
23
|
+
|
|
24
|
+
require_relative "../error_messages"
|
|
25
|
+
source = File.binread(path)
|
|
26
|
+
record_generation_input(path, source)
|
|
27
|
+
document = ErrorMessages.parse(source, file: path)
|
|
28
|
+
ErrorMessages.records_for(document, automaton, file: path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @rbs () -> void
|
|
32
|
+
def validate_messages_options
|
|
33
|
+
return unless @options[:messages]
|
|
34
|
+
raise Ibex::Error, "(cli):1:1: --messages is available only with --emit=ruby" unless @options[:emit] == "ruby"
|
|
35
|
+
|
|
36
|
+
raise Ibex::Error, "(cli):1:1: --messages cannot be combined with --check-only" if @options[:check_only]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../grammar_tests"
|
|
5
|
+
|
|
6
|
+
module Ibex
|
|
7
|
+
# `%test` command parsing and human-readable result reporting.
|
|
8
|
+
module CLIGrammarTests
|
|
9
|
+
# @rbs!
|
|
10
|
+
# type grammar_test_settings = {
|
|
11
|
+
# mode: Symbol,
|
|
12
|
+
# algorithm: Symbol,
|
|
13
|
+
# entry_isolation: bool,
|
|
14
|
+
# timeout: Integer,
|
|
15
|
+
# coverage: Integer?,
|
|
16
|
+
# ?help: bool
|
|
17
|
+
# }
|
|
18
|
+
# private def print_help: (OptionParser) -> Integer
|
|
19
|
+
# private def resolve_grammar_path: (String) -> Frontend::Resolution
|
|
20
|
+
# private def handle_grammar_warnings: (IR::Grammar, String) -> void
|
|
21
|
+
# private def build_automaton: (IR::Grammar, String) -> IR::Automaton
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# @rbs (Array[String] arguments) -> Integer
|
|
26
|
+
def run_grammar_tests_command(arguments)
|
|
27
|
+
settings = {
|
|
28
|
+
mode: :default, algorithm: :lalr, entry_isolation: false, timeout: GrammarTests::DEFAULT_TIMEOUT, coverage: nil
|
|
29
|
+
} #: grammar_test_settings
|
|
30
|
+
options = grammar_test_option_parser(settings)
|
|
31
|
+
remaining = options.parse(arguments)
|
|
32
|
+
return print_help(options) if settings[:help]
|
|
33
|
+
unless settings[:coverage].nil? || settings[:coverage].between?(1, 100)
|
|
34
|
+
raise OptionParser::InvalidArgument, "coverage must be between 1 and 100"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
path = single_grammar_test_path(remaining, options)
|
|
38
|
+
@options[:mode] = settings[:mode]
|
|
39
|
+
@options[:algorithm] = settings[:algorithm]
|
|
40
|
+
@options[:entry_isolation] = settings[:entry_isolation]
|
|
41
|
+
resolution = resolve_grammar_path(path)
|
|
42
|
+
grammar = Normalizer.new(resolution, mode: settings[:mode]).normalize
|
|
43
|
+
handle_grammar_warnings(grammar, path)
|
|
44
|
+
automaton = build_automaton(grammar, path)
|
|
45
|
+
runner = GrammarTests::Runner.new(automaton, timeout: settings[:timeout])
|
|
46
|
+
results = runner.run
|
|
47
|
+
failures = render_grammar_test_results(results)
|
|
48
|
+
coverage = settings[:coverage]
|
|
49
|
+
return failures unless coverage
|
|
50
|
+
|
|
51
|
+
render_grammar_test_coverage(runner.production_coverage(results), coverage, failures)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs (grammar_test_settings settings) -> OptionParser
|
|
55
|
+
def grammar_test_option_parser(settings)
|
|
56
|
+
OptionParser.new do |options|
|
|
57
|
+
options.banner = "Usage: ibex test [options] grammarfile"
|
|
58
|
+
options.on("--mode=MODE", %w[default extended], "grammar mode") { |value| settings[:mode] = value.to_sym }
|
|
59
|
+
options.on("--algorithm=NAME", %w[slr lalr ielr lr1], "parser construction algorithm") do |value|
|
|
60
|
+
settings[:algorithm] = value.to_sym
|
|
61
|
+
end
|
|
62
|
+
options.on("--entry-isolation", "build independent state sets for each start symbol") do
|
|
63
|
+
settings[:entry_isolation] = true
|
|
64
|
+
end
|
|
65
|
+
options.on("--timeout=SECONDS", Integer, "whole-suite timeout (default: 10)") do |value|
|
|
66
|
+
raise OptionParser::InvalidArgument, "timeout must be positive" unless value.positive?
|
|
67
|
+
|
|
68
|
+
settings[:timeout] = value
|
|
69
|
+
end
|
|
70
|
+
options.on("--coverage=PERCENT", Integer, "require production coverage (1..100)") do |value|
|
|
71
|
+
settings[:coverage] = value
|
|
72
|
+
end
|
|
73
|
+
options.on("-h", "--help", "show this help") { settings[:help] = true }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs (Array[String] remaining, OptionParser options) -> String
|
|
78
|
+
def single_grammar_test_path(remaining, options)
|
|
79
|
+
return remaining.fetch(0) if remaining.one?
|
|
80
|
+
|
|
81
|
+
raise OptionParser::MissingArgument, options.banner if remaining.empty?
|
|
82
|
+
|
|
83
|
+
raise OptionParser::InvalidArgument, "test accepts exactly one grammarfile"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs (Array[GrammarTests::Result] results) -> Integer
|
|
87
|
+
def render_grammar_test_results(results)
|
|
88
|
+
failures = 0
|
|
89
|
+
results.each_with_index do |result, index|
|
|
90
|
+
label = "#{result.expectation} #{result.location[:file]}:#{result.location[:line]}"
|
|
91
|
+
if result.passed?
|
|
92
|
+
@stdout.puts("ok #{index + 1} - #{label}")
|
|
93
|
+
else
|
|
94
|
+
failures += 1
|
|
95
|
+
@stdout.puts("not ok #{index + 1} - #{label} (#{grammar_test_failure(result)})")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
@stdout.puts("#{results.length} tests, #{failures} failures")
|
|
99
|
+
failures.zero? ? 0 : 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @rbs (GrammarTests::ProductionCoverage coverage, Integer minimum, Integer failures) -> Integer
|
|
103
|
+
def render_grammar_test_coverage(coverage, minimum, failures)
|
|
104
|
+
@stdout.puts(
|
|
105
|
+
format(
|
|
106
|
+
"production coverage: %<covered>d/%<total>d (%<percentage>.1f%%), required %<minimum>d%%",
|
|
107
|
+
covered: coverage.covered_ids.length, total: coverage.production_count,
|
|
108
|
+
percentage: coverage.percentage, minimum: minimum
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
@stdout.puts("missing production ids: #{coverage.missing_ids.join(', ')}") unless coverage.complete?
|
|
112
|
+
failures.zero? && coverage.meets?(minimum) ? 0 : 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @rbs (GrammarTests::Result result) -> String
|
|
116
|
+
def grammar_test_failure(result)
|
|
117
|
+
return "parser #{result.actual}ed the input" unless result.actual == :error
|
|
118
|
+
|
|
119
|
+
"#{result.error_class}: #{result.error_message}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|