ibex 0.1.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 +7 -0
- data/.rubocop.yml +43 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/Rakefile +25 -0
- data/Steepfile +10 -0
- data/docs/architecture.md +99 -0
- data/docs/compat-notes.md +37 -0
- data/docs/grammar-reference.md +127 -0
- data/docs/lexer-coverage.md +14 -0
- data/docs/phase10-extensions.md +27 -0
- data/docs/racc-migration.md +47 -0
- data/exe/ibex +6 -0
- data/gemfiles/Gemfile +7 -0
- data/gemfiles/Gemfile.lock +98 -0
- data/lib/ibex/analysis/sets.rb +194 -0
- data/lib/ibex/analysis.rb +8 -0
- data/lib/ibex/cli/counterexample_options.rb +38 -0
- data/lib/ibex/cli/outputs.rb +88 -0
- data/lib/ibex/cli.rb +333 -0
- data/lib/ibex/codegen/dot.rb +52 -0
- data/lib/ibex/codegen/html.rb +110 -0
- data/lib/ibex/codegen/rbs.rb +54 -0
- data/lib/ibex/codegen/report.rb +149 -0
- data/lib/ibex/codegen/ruby.rb +235 -0
- data/lib/ibex/codegen/symbol_labels.rb +18 -0
- data/lib/ibex/error.rb +7 -0
- data/lib/ibex/frontend/action_scanner.rb +266 -0
- data/lib/ibex/frontend/ast.rb +146 -0
- data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
- data/lib/ibex/frontend/dsl.rb +196 -0
- data/lib/ibex/frontend/generated_parser.rb +434 -0
- data/lib/ibex/frontend/generated_parser_base.rb +273 -0
- data/lib/ibex/frontend/grammar.y +156 -0
- data/lib/ibex/frontend/lexer.rb +165 -0
- data/lib/ibex/frontend/parser/declarations.rb +163 -0
- data/lib/ibex/frontend/parser/rules.rb +154 -0
- data/lib/ibex/frontend/parser.rb +23 -0
- data/lib/ibex/frontend/regenerator.rb +41 -0
- data/lib/ibex/frontend/source_cursor.rb +94 -0
- data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
- data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
- data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
- data/lib/ibex/frontend/token_adapter.rb +108 -0
- data/lib/ibex/frontend.rb +28 -0
- data/lib/ibex/ir/automaton_ir.rb +109 -0
- data/lib/ibex/ir/grammar_ir.rb +192 -0
- data/lib/ibex/ir/serialize.rb +154 -0
- data/lib/ibex/ir.rb +53 -0
- data/lib/ibex/lalr/builder.rb +270 -0
- data/lib/ibex/lalr/conflict.rb +120 -0
- data/lib/ibex/lalr/conflict_search.rb +332 -0
- data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
- data/lib/ibex/lalr/counterexample.rb +138 -0
- data/lib/ibex/lalr/default_reductions.rb +67 -0
- data/lib/ibex/lalr.rb +27 -0
- data/lib/ibex/normalize/declarations.rb +60 -0
- data/lib/ibex/normalize/diagnostics.rb +99 -0
- data/lib/ibex/normalize/expander.rb +218 -0
- data/lib/ibex/normalize/expression.rb +54 -0
- data/lib/ibex/normalize.rb +166 -0
- data/lib/ibex/runtime/parser.rb +360 -0
- data/lib/ibex/runtime.rb +8 -0
- data/lib/ibex/tables.rb +125 -0
- data/lib/ibex/version.rb +6 -0
- data/lib/ibex.rb +23 -0
- data/sig/ibex/analysis/sets.rbs +72 -0
- data/sig/ibex/analysis.rbs +6 -0
- data/sig/ibex/cli/counterexample_options.rbs +16 -0
- data/sig/ibex/cli/outputs.rbs +31 -0
- data/sig/ibex/cli.rbs +108 -0
- data/sig/ibex/codegen/dot.rbs +19 -0
- data/sig/ibex/codegen/html.rbs +35 -0
- data/sig/ibex/codegen/rbs.rbs +31 -0
- data/sig/ibex/codegen/report.rbs +39 -0
- data/sig/ibex/codegen/ruby.rbs +92 -0
- data/sig/ibex/codegen/symbol_labels.rbs +11 -0
- data/sig/ibex/error.rbs +7 -0
- data/sig/ibex/frontend/action_scanner.rbs +74 -0
- data/sig/ibex/frontend/ast.rbs +223 -0
- data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
- data/sig/ibex/frontend/dsl.rbs +124 -0
- data/sig/ibex/frontend/generated_parser.rbs +163 -0
- data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
- data/sig/ibex/frontend/lexer.rbs +61 -0
- data/sig/ibex/frontend/parser/declarations.rbs +56 -0
- data/sig/ibex/frontend/parser/rules.rbs +48 -0
- data/sig/ibex/frontend/parser.rbs +16 -0
- data/sig/ibex/frontend/regenerator.rbs +16 -0
- data/sig/ibex/frontend/source_cursor.rbs +73 -0
- data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
- data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
- data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
- data/sig/ibex/frontend/token_adapter.rbs +67 -0
- data/sig/ibex/frontend.rbs +15 -0
- data/sig/ibex/ir/automaton_ir.rbs +77 -0
- data/sig/ibex/ir/grammar_ir.rbs +143 -0
- data/sig/ibex/ir/serialize.rbs +50 -0
- data/sig/ibex/ir.rbs +43 -0
- data/sig/ibex/lalr/builder.rbs +85 -0
- data/sig/ibex/lalr/conflict.rbs +47 -0
- data/sig/ibex/lalr/conflict_search.rbs +131 -0
- data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
- data/sig/ibex/lalr/counterexample.rbs +53 -0
- data/sig/ibex/lalr/default_reductions.rbs +20 -0
- data/sig/ibex/lalr.rbs +23 -0
- data/sig/ibex/normalize/declarations.rbs +20 -0
- data/sig/ibex/normalize/diagnostics.rbs +32 -0
- data/sig/ibex/normalize/expander.rbs +66 -0
- data/sig/ibex/normalize/expression.rbs +21 -0
- data/sig/ibex/normalize.rbs +96 -0
- data/sig/ibex/runtime/parser.rbs +167 -0
- data/sig/ibex/runtime.rbs +6 -0
- data/sig/ibex/tables.rbs +50 -0
- data/sig/ibex/version.rbs +6 -0
- data/sig/ibex.rbs +6 -0
- metadata +161 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Migrating from racc
|
|
2
|
+
|
|
3
|
+
Ibex targets grammar-file compatibility, generated parser public API compatibility, and the main racc CLI options. It does not
|
|
4
|
+
copy racc's internal table arrays, internal method names, native runtime, or generated source layout.
|
|
5
|
+
|
|
6
|
+
## Typical migration
|
|
7
|
+
|
|
8
|
+
1. Run `ibex -o parser.rb grammar.y` in place of `racc -o parser.rb grammar.y`.
|
|
9
|
+
2. Change the generated-file runtime dependency from deployment packaging only; application calls to `do_parse`, `yyparse`,
|
|
10
|
+
`next_token`, `on_error`, `token_to_str`, `yyerror`, `yyerrok`, and `yyaccept` remain the same.
|
|
11
|
+
3. Use `-E` if the generated parser must be a single file with no installed Ibex gem.
|
|
12
|
+
4. Keep the default `--mode=racc` until intentionally adopting EBNF or names. Extended grammars can make that choice locally by
|
|
13
|
+
placing `pragma extended` immediately after their class header instead of requiring `--mode=extended` at each invocation.
|
|
14
|
+
|
|
15
|
+
## CLI mapping
|
|
16
|
+
|
|
17
|
+
| Option | Ibex behavior |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `-o`, `--output-file` | Select generated parser path |
|
|
20
|
+
| `-t`, `--debug`; `-g` | Generate a debug-capable parser; `-g` is accepted as an obsolete alias |
|
|
21
|
+
| `-v`, `-O` | Write the independent state report and optional path |
|
|
22
|
+
| `-e [RUBY]` | Add a shebang and executable permission |
|
|
23
|
+
| `-E`, `--embedded` | Embed the Pure Ruby runtime |
|
|
24
|
+
| `-F`, `--frozen` | Accepted; Ibex always emits frozen-string magic comments |
|
|
25
|
+
| `--rbs[=FILE]` | Ibex extension; emit a generated parser signature |
|
|
26
|
+
| `--warnings=all,error` | Ibex extension; display or promote structured grammar diagnostics |
|
|
27
|
+
| `--line-convert-all`, `-l` | Map header/inner/footer too, or disable all source mapping |
|
|
28
|
+
| `-a` | Generate methods for implicit actions |
|
|
29
|
+
| `--superclass` | Override the grammar superclass |
|
|
30
|
+
| `-C`, `-S` | Check only; show pipeline status |
|
|
31
|
+
| `-P`, `-D` | Accepted no-ops because they expose generator internals |
|
|
32
|
+
| `--version`, `--runtime-version`, `--copyright`, `--help` | Informational output |
|
|
33
|
+
|
|
34
|
+
Ibex defaults to `<input>.rb`; racc 1.8.1 was observed to default to `<input>.tab.rb`. Use `-o` for portable scripts.
|
|
35
|
+
By default, semantic actions and `inner` methods report grammar-file lines while `header` and `footer` retain generated-file
|
|
36
|
+
lines. `--line-convert-all` maps every user-code section; `-l` maps none. The mapping is retained through IR JSON resumption.
|
|
37
|
+
|
|
38
|
+
## Known differences
|
|
39
|
+
|
|
40
|
+
- Generated source and internal table representations are intentionally different.
|
|
41
|
+
- `.output` report formatting is independent and contains additional resolved-conflict and witness data.
|
|
42
|
+
- An undeclared invalid token was observed to enter racc's `error` production without `on_error`. Ibex intentionally reports the
|
|
43
|
+
unknown lookahead through `on_error` first, then recovers if the callback returns. Declared invalid tokens match in the
|
|
44
|
+
black-box recovery probe.
|
|
45
|
+
- `require "racc/parser"` replacement and previously generated racc parser table compatibility are out of scope.
|
|
46
|
+
|
|
47
|
+
See [compatibility observations](compat-notes.md) for the tested version and probe set.
|
data/exe/ibex
ADDED
data/gemfiles/Gemfile
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
ast (2.4.3)
|
|
5
|
+
concurrent-ruby (1.3.8)
|
|
6
|
+
csv (3.3.5)
|
|
7
|
+
ffi (1.17.4)
|
|
8
|
+
ffi (1.17.4-arm64-darwin)
|
|
9
|
+
fileutils (1.8.0)
|
|
10
|
+
json (2.21.1)
|
|
11
|
+
language_server-protocol (3.17.0.6)
|
|
12
|
+
listen (3.10.0)
|
|
13
|
+
logger
|
|
14
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
15
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
|
16
|
+
logger (1.7.0)
|
|
17
|
+
parser (3.3.12.0)
|
|
18
|
+
ast (~> 2.4.1)
|
|
19
|
+
racc
|
|
20
|
+
prism (1.9.0)
|
|
21
|
+
racc (1.8.1)
|
|
22
|
+
rainbow (3.1.1)
|
|
23
|
+
rb-fsevent (0.11.2)
|
|
24
|
+
rb-inotify (0.11.1)
|
|
25
|
+
ffi (~> 1.0)
|
|
26
|
+
rbs (4.0.3)
|
|
27
|
+
logger
|
|
28
|
+
prism (>= 1.6.0)
|
|
29
|
+
tsort
|
|
30
|
+
rbs-inline (0.14.0)
|
|
31
|
+
prism (>= 0.29)
|
|
32
|
+
rbs (~> 4.0)
|
|
33
|
+
securerandom (0.4.1)
|
|
34
|
+
steep (2.0.0)
|
|
35
|
+
concurrent-ruby (>= 1.1.10)
|
|
36
|
+
csv (>= 3.0.9)
|
|
37
|
+
fileutils (>= 1.1.0)
|
|
38
|
+
json (>= 2.1.0)
|
|
39
|
+
language_server-protocol (>= 3.17.0.4, < 4.0)
|
|
40
|
+
listen (~> 3.0)
|
|
41
|
+
logger (>= 1.3.0)
|
|
42
|
+
parser (>= 3.2)
|
|
43
|
+
prism (>= 0.25.0)
|
|
44
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
45
|
+
rbs (~> 4.0)
|
|
46
|
+
securerandom (>= 0.1)
|
|
47
|
+
strscan (>= 1.0.0)
|
|
48
|
+
terminal-table (>= 2, < 5)
|
|
49
|
+
uri (>= 0.12.0)
|
|
50
|
+
strscan (3.1.8)
|
|
51
|
+
terminal-table (4.0.0)
|
|
52
|
+
unicode-display_width (>= 1.1.1, < 4)
|
|
53
|
+
tsort (0.2.0)
|
|
54
|
+
unicode-display_width (3.2.0)
|
|
55
|
+
unicode-emoji (~> 4.1)
|
|
56
|
+
unicode-emoji (4.2.0)
|
|
57
|
+
uri (1.1.1)
|
|
58
|
+
|
|
59
|
+
PLATFORMS
|
|
60
|
+
arm64-darwin-24
|
|
61
|
+
ruby
|
|
62
|
+
|
|
63
|
+
DEPENDENCIES
|
|
64
|
+
rbs (= 4.0.3)
|
|
65
|
+
rbs-inline (= 0.14.0)
|
|
66
|
+
steep (= 2.0.0)
|
|
67
|
+
|
|
68
|
+
CHECKSUMS
|
|
69
|
+
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
70
|
+
bundler (4.0.13) sha256=19f08be7f27022cf0b89f27da0b044ae075e8270a9ef44ad248a932614e1ca3b
|
|
71
|
+
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
|
|
72
|
+
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
|
73
|
+
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
|
74
|
+
ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
|
|
75
|
+
fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
|
|
76
|
+
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
|
|
77
|
+
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
|
|
78
|
+
listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2
|
|
79
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
80
|
+
parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
|
|
81
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
82
|
+
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
83
|
+
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
84
|
+
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
|
85
|
+
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
|
86
|
+
rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
|
|
87
|
+
rbs-inline (0.14.0) sha256=eaf47b46690d63acddab1f6804c9cc205a4bc78e637cfc421a0b1239874ef51c
|
|
88
|
+
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
|
89
|
+
steep (2.0.0) sha256=6eb0ecc09637bbb54f0a5f2cf63daea6d3208ccace64b4f1107d976333605c30
|
|
90
|
+
strscan (3.1.8) sha256=aae2db611a225559f21ffbb71765c9a4e60fd262534a9ea84f4f11c7f32f679e
|
|
91
|
+
terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2
|
|
92
|
+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
93
|
+
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
94
|
+
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
95
|
+
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
|
96
|
+
|
|
97
|
+
BUNDLED WITH
|
|
98
|
+
4.0.13
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Analysis
|
|
5
|
+
# Computes nullable, FIRST, and FOLLOW sets over Grammar IR using integer bitsets.
|
|
6
|
+
class Sets
|
|
7
|
+
attr_reader :nullable_bits #: Integer
|
|
8
|
+
attr_reader :first_bits #: Array[Integer]
|
|
9
|
+
attr_reader :follow_bits #: Array[Integer]
|
|
10
|
+
|
|
11
|
+
# @rbs @grammar: IR::Grammar
|
|
12
|
+
|
|
13
|
+
# @rbs (IR::Grammar grammar) -> void
|
|
14
|
+
def initialize(grammar)
|
|
15
|
+
@grammar = grammar
|
|
16
|
+
@nullable_bits = 0
|
|
17
|
+
@first_bits = Array.new(grammar.symbols.length, 0)
|
|
18
|
+
@follow_bits = Array.new(grammar.symbols.length, 0)
|
|
19
|
+
grammar.terminals.each { |terminal| @first_bits[terminal.id] = bit(terminal.id) }
|
|
20
|
+
compute_nullable
|
|
21
|
+
compute_first
|
|
22
|
+
compute_follow
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> bool
|
|
26
|
+
def nullable?(symbol)
|
|
27
|
+
id = symbol_id(symbol)
|
|
28
|
+
@nullable_bits.anybits?(bit(id))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Array[String]
|
|
32
|
+
def first(symbol)
|
|
33
|
+
terminal_names(@first_bits.fetch(symbol_id(symbol)))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Array[String]
|
|
37
|
+
def follow(symbol)
|
|
38
|
+
definition = definition_for(symbol)
|
|
39
|
+
raise Ibex::Error, "(analysis):1:1: FOLLOW is only defined for nonterminals" unless definition.nonterminal?
|
|
40
|
+
|
|
41
|
+
terminal_names(@follow_bits.fetch(definition.id))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @rbs (Array[Integer] symbol_ids) -> Integer
|
|
45
|
+
def first_of_sequence(symbol_ids)
|
|
46
|
+
bits = 0
|
|
47
|
+
symbol_ids.each do |id|
|
|
48
|
+
bits |= @first_bits.fetch(id)
|
|
49
|
+
return bits unless nullable_id?(id)
|
|
50
|
+
end
|
|
51
|
+
bits
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs (Array[Integer] symbol_ids) -> bool
|
|
55
|
+
def sequence_nullable?(symbol_ids)
|
|
56
|
+
symbol_ids.all? { |id| nullable_id?(id) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# @rbs () -> void
|
|
62
|
+
def compute_nullable
|
|
63
|
+
dependencies, remaining, queue = nullable_worklist
|
|
64
|
+
|
|
65
|
+
until queue.empty?
|
|
66
|
+
id = queue.shift
|
|
67
|
+
next if nullable_id?(id)
|
|
68
|
+
|
|
69
|
+
@nullable_bits |= bit(id)
|
|
70
|
+
dependencies[id].each do |production|
|
|
71
|
+
remaining[production.id] -= 1
|
|
72
|
+
queue << production.lhs if remaining[production.id].zero?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs () -> [Array[Array[IR::Production]], Array[Integer], Array[Integer]]
|
|
78
|
+
def nullable_worklist
|
|
79
|
+
dependencies = Array.new(@grammar.symbols.length) { [] }
|
|
80
|
+
remaining = Array.new(@grammar.productions.length, 0)
|
|
81
|
+
queue = [] #: Array[Integer]
|
|
82
|
+
@grammar.productions.each do |production|
|
|
83
|
+
next if production.rhs.any? { |id| required_symbol_by_id(id).terminal? }
|
|
84
|
+
|
|
85
|
+
remaining[production.id] = production.rhs.length
|
|
86
|
+
production.rhs.each { |id| dependencies[id] << production }
|
|
87
|
+
queue << production.lhs if production.rhs.empty?
|
|
88
|
+
end
|
|
89
|
+
[dependencies, remaining, queue]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @rbs () -> void
|
|
93
|
+
def compute_first
|
|
94
|
+
dependencies = Array.new(@grammar.symbols.length) { [] }
|
|
95
|
+
@grammar.productions.each do |production|
|
|
96
|
+
production.rhs.each do |id|
|
|
97
|
+
dependencies[id] << production.lhs
|
|
98
|
+
break unless nullable_id?(id)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
propagate_bits(@first_bits, dependencies, @grammar.terminals.map(&:id))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @rbs () -> void
|
|
106
|
+
def compute_follow
|
|
107
|
+
start_definition = @grammar.symbol(@grammar.start)
|
|
108
|
+
raise Ibex::Error, "(analysis):1:1: unknown start symbol #{@grammar.start}" unless start_definition
|
|
109
|
+
|
|
110
|
+
start_id = start_definition.id
|
|
111
|
+
@follow_bits[start_id] |= bit(0)
|
|
112
|
+
dependencies = Array.new(@grammar.symbols.length) { [] }
|
|
113
|
+
@grammar.productions.each { |production| initialize_follow(production, dependencies) }
|
|
114
|
+
seeds = @grammar.nonterminals.filter_map { |symbol| symbol.id unless @follow_bits[symbol.id].zero? }
|
|
115
|
+
propagate_bits(@follow_bits, dependencies, seeds)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @rbs (IR::Production production, Array[Array[Integer]] dependencies) -> void
|
|
119
|
+
def initialize_follow(production, dependencies)
|
|
120
|
+
trailer = 0
|
|
121
|
+
suffix_nullable = true
|
|
122
|
+
production.rhs.reverse_each do |id|
|
|
123
|
+
definition = required_symbol_by_id(id)
|
|
124
|
+
if definition.nonterminal?
|
|
125
|
+
@follow_bits[id] |= trailer
|
|
126
|
+
dependencies[production.lhs] << id if suffix_nullable
|
|
127
|
+
trailer = @first_bits[id] | (nullable_id?(id) ? trailer : 0)
|
|
128
|
+
suffix_nullable &&= nullable_id?(id)
|
|
129
|
+
else
|
|
130
|
+
trailer = @first_bits[id]
|
|
131
|
+
suffix_nullable = false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs (Array[Integer] sets, Array[Array[Integer]] dependencies, Array[Integer] seeds) -> void
|
|
137
|
+
def propagate_bits(sets, dependencies, seeds)
|
|
138
|
+
queue = seeds.dup
|
|
139
|
+
queued = Array.new(@grammar.symbols.length, false)
|
|
140
|
+
queue.each { |id| queued[id] = true }
|
|
141
|
+
|
|
142
|
+
until queue.empty?
|
|
143
|
+
source = queue.shift
|
|
144
|
+
queued[source] = false
|
|
145
|
+
dependencies[source].each do |target|
|
|
146
|
+
combined = sets[target] | sets[source]
|
|
147
|
+
next if combined == sets[target]
|
|
148
|
+
|
|
149
|
+
sets[target] = combined
|
|
150
|
+
next if queued[target]
|
|
151
|
+
|
|
152
|
+
queued[target] = true
|
|
153
|
+
queue << target
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @rbs (Integer id) -> bool
|
|
159
|
+
def nullable_id?(id)
|
|
160
|
+
@nullable_bits.anybits?(bit(id))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @rbs (Integer bits) -> Array[String]
|
|
164
|
+
def terminal_names(bits)
|
|
165
|
+
@grammar.terminals.filter_map { |terminal| terminal.name if bits.anybits?(bit(terminal.id)) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Integer
|
|
169
|
+
def symbol_id(symbol)
|
|
170
|
+
definition_for(symbol).id
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> IR::GrammarSymbol
|
|
174
|
+
def definition_for(symbol)
|
|
175
|
+
return symbol if symbol.is_a?(IR::GrammarSymbol)
|
|
176
|
+
|
|
177
|
+
definition = symbol.is_a?(Integer) ? @grammar.symbol_by_id(symbol) : @grammar.symbol(symbol.to_s)
|
|
178
|
+
return definition if definition
|
|
179
|
+
|
|
180
|
+
raise Ibex::Error, "(analysis):1:1: unknown symbol #{symbol}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @rbs (Integer id) -> IR::GrammarSymbol
|
|
184
|
+
def required_symbol_by_id(id)
|
|
185
|
+
@grammar.symbol_by_id(id) || raise(Ibex::Error, "(analysis):1:1: unknown symbol id #{id}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @rbs (Integer id) -> Integer
|
|
189
|
+
def bit(id)
|
|
190
|
+
1 << id
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
# Counterexample report limits and their command-line validation.
|
|
5
|
+
module CLICounterexampleOptions
|
|
6
|
+
DEFAULTS = {
|
|
7
|
+
counterexample_max_tokens: LALR::Counterexample::DEFAULT_MAX_TOKENS,
|
|
8
|
+
counterexample_max_configurations: LALR::Counterexample::DEFAULT_MAX_CONFIGURATIONS
|
|
9
|
+
}.freeze #: Hash[Symbol, Integer]
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# @rbs (OptionParser options) -> void
|
|
14
|
+
def add_counterexample_options(options)
|
|
15
|
+
options.on("--counterexamples", "include conflict counterexamples in a report") { @options[:verbose] = true }
|
|
16
|
+
options.on("--counterexample-max-tokens=N", Integer, "maximum counterexample search token budget") do |value|
|
|
17
|
+
@options[:counterexample_max_tokens] = positive_counterexample_limit(value, "--counterexample-max-tokens")
|
|
18
|
+
end
|
|
19
|
+
options.on(
|
|
20
|
+
"--counterexample-max-configurations=N", Integer, "maximum counterexample search configuration budget"
|
|
21
|
+
) do |value|
|
|
22
|
+
@options[:counterexample_max_configurations] = positive_counterexample_limit(
|
|
23
|
+
value, "--counterexample-max-configurations"
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs (Integer value, String option) -> Integer
|
|
29
|
+
def positive_counterexample_limit(value, option)
|
|
30
|
+
if value.positive?
|
|
31
|
+
@options[:verbose] = true
|
|
32
|
+
return value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
raise Ibex::Error, "(cli):1:1: #{option} must be positive"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
# Files, warnings, and progress emitted by CLI pipeline stages.
|
|
5
|
+
module CLIOutputs
|
|
6
|
+
WARNING_MESSAGES = {
|
|
7
|
+
undeclared_terminal: ->(warning) { "undeclared terminal #{warning[:symbol]}" },
|
|
8
|
+
unused_terminal: ->(warning) { "unused terminal #{warning[:symbol]}" },
|
|
9
|
+
unreachable_nonterminal: ->(warning) { "unreachable nonterminal #{warning[:symbol]}" },
|
|
10
|
+
duplicate_production: lambda do |warning|
|
|
11
|
+
"duplicate production #{warning[:production]} (first defined as #{warning[:original]})"
|
|
12
|
+
end,
|
|
13
|
+
empty_language: ->(warning) { "start symbol #{warning[:symbol]} derives no terminal sentence" }
|
|
14
|
+
}.freeze #: Hash[Symbol, ^(IR::grammar_warning) -> String]
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# @rbs (String value) -> Array[Symbol]
|
|
19
|
+
def warning_categories(value)
|
|
20
|
+
categories = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
|
|
21
|
+
unknown = categories - %i[all error none]
|
|
22
|
+
raise OptionParser::InvalidArgument, "unknown warning category #{unknown.first}" if unknown.any?
|
|
23
|
+
if categories.empty? || (categories.include?(:none) && categories.length > 1)
|
|
24
|
+
raise OptionParser::InvalidArgument, "warning category none cannot be combined"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
categories
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs (IR::Grammar grammar, String input_path) -> void
|
|
31
|
+
def handle_grammar_warnings(grammar, input_path)
|
|
32
|
+
categories = @options[:warnings]
|
|
33
|
+
return if categories.nil? || categories.include?(:none) || grammar.warnings.empty?
|
|
34
|
+
|
|
35
|
+
messages = grammar.warnings.map { |warning| format_grammar_warning(warning, input_path) }
|
|
36
|
+
if categories.include?(:error)
|
|
37
|
+
promoted = messages.map { |message| message.sub(": warning:", ": warning treated as error:") }
|
|
38
|
+
raise Ibex::Error, promoted.join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
messages.each { |message| @stderr.puts(message) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @rbs (IR::grammar_warning warning, String input_path) -> String
|
|
45
|
+
def format_grammar_warning(warning, input_path)
|
|
46
|
+
location = warning[:loc]
|
|
47
|
+
rendered = if location
|
|
48
|
+
"#{location[:file] || input_path}:#{location[:line] || 1}:#{location[:column] || 1}"
|
|
49
|
+
else
|
|
50
|
+
"#{input_path}:1:1"
|
|
51
|
+
end
|
|
52
|
+
formatter = WARNING_MESSAGES.fetch(warning[:type]) { ->(item) { item[:type].to_s.tr("_", " ") } }
|
|
53
|
+
"#{rendered}: warning: #{formatter.call(warning)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs (IR::Automaton automaton, String input_path) -> void
|
|
57
|
+
def report_conflicts(automaton, input_path)
|
|
58
|
+
summary = automaton.conflict_summary
|
|
59
|
+
unless summary[:expectation_met]
|
|
60
|
+
@stderr.puts("#{input_path}:1:1: #{summary[:sr]} shift/reduce conflicts; expected #{summary[:expected_sr]}")
|
|
61
|
+
end
|
|
62
|
+
@stderr.puts("#{input_path}:1:1: #{summary[:rr]} reduce/reduce conflicts") if summary[:rr].positive?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @rbs (IR::Automaton automaton, String input_path) -> void
|
|
66
|
+
def write_report(automaton, input_path)
|
|
67
|
+
path = @options[:log_file] || default_output_path(input_path, ".output")
|
|
68
|
+
report = Codegen::Report.render(
|
|
69
|
+
automaton,
|
|
70
|
+
max_tokens: @options[:counterexample_max_tokens],
|
|
71
|
+
max_configurations: @options[:counterexample_max_configurations]
|
|
72
|
+
)
|
|
73
|
+
File.write(path, report)
|
|
74
|
+
report_status("wrote #{path}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs (String input_path, String extension) -> String
|
|
78
|
+
def default_output_path(input_path, extension)
|
|
79
|
+
replaced = input_path.sub(/\.[^.]+\z/, extension)
|
|
80
|
+
replaced == input_path ? "#{input_path}#{extension}" : replaced
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @rbs (String message) -> void
|
|
84
|
+
def report_status(message)
|
|
85
|
+
@stderr.puts("ibex: #{message}") if @options[:status]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|