5 Commits
Author SHA1 Message Date
josh 4ec4f40b36 v5.3.0 2026-11-06 08:49:14 -05:00
josh de7c126b31 Update gems 2026-11-06 08:48:09 -05:00
josh e5805d2db4 Warn on unused rules 2026-11-03 22:07:51 -05:00
josh 66af46a4f6 v5.2.0 2026-09-05 18:17:10 -04:00
josh 429ce88eb5 Add cast-to-bool operators for tree node handles for C++ and D 2026-09-05 17:56:18 -04:00
15 changed files with 216 additions and 25 deletions

No files matched your search

+18
View File
@@ -1,3 +1,21 @@
## v5.3.0
### New Features
- Warn about unused rules (rules not reachable from any start rule). The
warnings are written to the log file and, with `-w`, are printed to stderr
and treated as fatal errors.
## v5.2.0
### New Features
- Tree node handles can now be used directly as a boolean condition (e.g.
`if (node)`) in C++ (an `explicit operator bool()`) and D (an
`opCast!bool`), equivalent to testing `valid()` / `valid`. C and Rust do not
support a user-defined conversion to a boolean condition; use
`p_node_valid(node)` and `node.valid()` there.
## v5.1.0
### New Features
+27 -15
View File
@@ -2,17 +2,27 @@ GEM
remote: https://rubygems.org/
specs:
base64 (0.3.0)
date (3.5.1)
diff-lcs (1.6.2)
docile (1.4.1)
erb (6.0.4)
psych (5.4.0)
date
stringio
erb (6.0.7)
logger (1.7.0)
prism (2.1.0)
prism (2.1.0-aarch64-linux-gnu)
prism (2.1.0-aarch64-linux-musl)
prism (2.1.0-arm-linux-gnu)
prism (2.1.0-arm-linux-musl)
prism (2.1.0-arm64-darwin)
prism (2.1.0-x86_64-darwin)
prism (2.1.0-x86_64-linux-gnu)
prism (2.1.0-x86_64-linux-musl)
rake (13.4.2)
rdoc (7.2.0)
rbs (4.2.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.1.0)
erb
psych (>= 4.0.0)
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redcarpet (3.6.1)
rspec (3.13.2)
@@ -28,18 +38,20 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
stringio (3.2.0)
simplecov (1.3.2)
syntax (1.2.2)
tsort (0.2.0)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
base64
+2 -2
View File
@@ -39,8 +39,8 @@ Propane is typically invoked from the command-line as `./propane`.
debugging a grammar.
--version Show program version and exit.
-w Treat warnings as errors. This option will treat shift/reduce
conflicts as fatal errors and will print them to stderr in
addition to the log file.
conflicts and unused rules as fatal errors and will print
them to stderr in addition to the log file.
The user must specify the path to a Propane input grammar file and a path to an
output file.
+12
View File
@@ -149,6 +149,12 @@ public struct <%= @grammar.tree_prefix %>Token<%= @grammar.tree_suffix %>
return __id != 0u;
}
/** Support using this handle directly as a boolean condition. */
bool opCast(T : bool)()
{
return __id != 0u;
}
/** Return the node ID (for identity comparison). */
@property <%= @grammar.prefix %>node_id_t node_id()
{
@@ -183,6 +189,12 @@ public struct <%= @grammar.tree_prefix %><%= rule_set.name %><%= @grammar.tree_s
return __id != 0u;
}
/** Support using this handle directly as a boolean condition. */
bool opCast(T : bool)()
{
return __id != 0u;
}
/** Return the node ID (for identity comparison). */
@property <%= @grammar.prefix %>node_id_t node_id()
{
+16 -2
View File
@@ -43,8 +43,8 @@ Propane is typically invoked from the command-line as `./propane`.
debugging a grammar.
--version Show program version and exit.
-w Treat warnings as errors. This option will treat shift/reduce
conflicts as fatal errors and will print them to stderr in
addition to the log file.
conflicts and unused rules as fatal errors and will print
them to stderr in addition to the log file.
The user must specify the path to a Propane input grammar file and a path to an
output file.
@@ -360,6 +360,20 @@ accessors on a node handle:
record, for token user fields), and `node.node_id()` (for identity
comparison).
In C++ and D, a node handle can also be used directly as a boolean condition,
which is equivalent to testing `valid()` (C++) or `valid` (D):
```
if (node)
{
/* node refers to a valid (non-null) tree node. */
}
```
For C, use the `p_node_valid(node)` macro, and for Rust use the `node.valid()`
method; neither language supports a user-defined conversion to a boolean
condition.
The positional position expansions (`${$.position}`, `${N.position}`, etc...)
are not available in tree generation mode; use the position accessors above
instead.
+2 -2
View File
@@ -10,8 +10,8 @@ Options:
debugging a grammar.
--version Show program version and exit.
-w Treat warnings as errors. This option will treat shift/reduce
conflicts as fatal errors and will print them to stderr in
addition to the log file.
conflicts and unused rules as fatal errors and will print
them to stderr in addition to the log file.
EOF
class << self
+2
View File
@@ -712,6 +712,7 @@ class Propane
out << " #{p}context_t * __context;"
out << " #{p}node_id_t __id;"
out << " bool valid() const { return __id != 0u; }"
out << " explicit operator bool() const { return __id != 0u; }"
out << " #{p}node_id_t node_id() const { return __id; }"
out << " #{p}node_data_t * data() const;"
out << " #{p}position_t position() const;"
@@ -728,6 +729,7 @@ class Propane
out << " #{p}context_t * __context;"
out << " #{p}node_id_t __id;"
out << " bool valid() const { return __id != 0u; }"
out << " explicit operator bool() const { return __id != 0u; }"
out << " #{p}node_id_t node_id() const { return __id; }"
out << " #{p}node_data_t * data() const;"
out << " #{p}position_t position() const;"
+29
View File
@@ -50,6 +50,7 @@ class Propane
build_reduce_actions!
build_tables!
check_unused_rules!
write_log!
errormessage = ""
if @errors.size > 0
@@ -68,6 +69,34 @@ class Propane
private
# Record a warning for each user-defined rule that cannot be reached from
# any start rule.
#
# @return [void]
def check_unused_rules!
reachable = Set.new
pending = @grammar.rules[0...@grammar.start_rules.length].map(&:rule_set)
while rule_set = pending.pop
next if reachable.include?(rule_set)
reachable << rule_set
rule_set.rules.each do |rule|
rule.components.each do |component|
if component.is_a?(RuleSet) && !reachable.include?(component)
pending << component
end
end
end
end
@rule_sets.each do |name, rule_set|
# Skip rule sets generated internally (real start rules and optional
# component rules).
next if name.start_with?("$") || rule_set.optional?
unless reachable.include?(rule_set)
@warnings << "Unused rule #{name} (defined on line #{rule_set.rules.first.line_number})"
end
end
end
def build_tables!
@state_table = []
@shift_table = []
+1 -1
View File
@@ -1,3 +1,3 @@
class Propane
VERSION = "5.1.0"
VERSION = "5.3.0"
end
+45 -1
View File
@@ -45,7 +45,7 @@ SimpleCov.start do
# suite. The parts are merged once at the end by the spec Rake task.
coverage_dir(#{"coverage/parts/#{command_name}".inspect})
filters.clear
add_filter do |src|
skip do |src|
!(src.filename[SimpleCov.root])
end
formatter(MyFormatter)
@@ -267,6 +267,50 @@ EOF
expect(File.binread("spec/run/testparser.log")).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)}
end
it "warns on unused rules" do
write_grammar <<EOF
token a;
token b;
Start -> a;
Unused -> b Other?;
Other -> a;
EOF
results = run_propane(capture: true)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
log = File.binread("spec/run/testparser.log")
expect(log).to match %r{Unused rule Unused \(defined on line 4\)}
expect(log).to match %r{Unused rule Other \(defined on line 5\)}
expect(log).to_not match %r{Unused rule (Start|Other\?)}
end
it "errors on unused rules with -w" do
write_grammar <<EOF
token a;
token b;
Start -> a Used;
Used -> b;
Unused -> b;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to match %r{Unused rule Unused \(defined on line 5\)}
expect(results.stderr).to_not match %r{Unused rule (Start|Used)\b}
expect(results.status).to_not eq 0
end
it "does not warn about rules reachable from any of multiple start rules" do
write_grammar <<EOF
token a;
token b;
start Start Other;
Start -> a;
Other -> b;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "errors on duplicate field aliases in a rule" do
write_grammar <<EOF
token a;
+2 -2
View File
@@ -7,8 +7,8 @@ unless ENV["dist_specs"]
end
end
SimpleCov.start do
add_filter "/spec/"
add_filter "/.bundle/"
skip "/spec/"
skip "/.bundle/"
if ENV["partial_specs"]
command_name "RSpec-partial"
else
+2
View File
@@ -17,6 +17,8 @@ int main()
assert(p_node_valid(p_tree_walk_Items(items, pItem, pToken1)));
assert_eq(TOKEN_a, p_tree_walk_Items(items, pItem, pToken1, token));
assert_eq(11, p_tree_walk_Items(items, pItem, pToken1, pvalue));
/* A token node handle from an unmatched rule is invalid. */
assert(!p_node_valid(p_tree_walk_Items(items, pItem, pDual, pOne1, pToken1)));
assert(p_node_valid(p_Items_pItemsMore(items)));
ItemsMore itemsmore = p_Items_pItemsMore(items);
assert(p_node_valid(p_ItemsMore_pItem(itemsmore)));
+28
View File
@@ -12,6 +12,13 @@ int main()
Start start = p_result(context);
assert(start.pItems1().valid());
assert(start.pItems().valid());
// A rule node handle is usable directly as a boolean condition.
assert(start);
assert(start.pItems());
if (!start.pItems())
{
return 1;
}
Items items = start.pItems();
assert_ne(0u, items.node_id());
assert_eq(start.pItems().node_id(), items.node_id());
@@ -19,6 +26,21 @@ int main()
assert(items.pItem().pToken1().valid());
assert_eq(TOKEN_a, items.pItem().pToken1().token());
assert_eq(11, items.pItem().pToken1().pvalue());
// A token node handle is usable directly as a boolean condition.
Token token = items.pItem().pToken1();
assert(token);
if (!token)
{
return 1;
}
// An invalid token node handle converts to false.
Token invalid_token = items.pItem().pDual().pOne1().pToken1();
assert(!invalid_token.valid());
assert(!invalid_token);
if (invalid_token)
{
return 1;
}
assert(items.pItemsMore().valid());
ItemsMore itemsmore = items.pItemsMore();
assert(itemsmore.pItem().valid());
@@ -42,6 +64,12 @@ int main()
assert_eq(P_SUCCESS, p_parse(context));
start = p_result(context);
assert(!start.pItems().valid());
// An invalid rule node handle converts to false.
assert(!start.pItems());
if (start.pItems())
{
return 1;
}
assert_eq(0u, start.pItems().node_id());
p_context_delete(context);
+28
View File
@@ -15,11 +15,33 @@ unittest
Start start = p_result(context);
assert(start.pItems1.valid);
assert(start.pItems.valid);
// A rule node handle is usable directly as a boolean condition.
assert(start);
assert(start.pItems);
if (!start.pItems)
{
assert(false);
}
Items items = start.pItems;
assert(items.pItem.valid);
assert(items.pItem.pToken1.valid);
assert_eq(TOKEN_a, items.pItem.pToken1.token);
assert_eq(11, items.pItem.pToken1.pvalue);
// A token node handle is usable directly as a boolean condition.
Token token = items.pItem.pToken1;
assert(token);
if (!token)
{
assert(false);
}
// An invalid token node handle converts to false.
Token invalidToken = items.pItem.pDual.pOne1.pToken1;
assert(!invalidToken.valid);
assert(!invalidToken);
if (invalidToken)
{
assert(false);
}
assert(items.pItemsMore.valid);
ItemsMore itemsmore = items.pItemsMore;
assert(itemsmore.pItem.valid);
@@ -43,6 +65,12 @@ unittest
assert_eq(P_SUCCESS, p_parse(context));
start = p_result(context);
assert(!start.pItems.valid);
// An invalid rule node handle converts to false.
assert(!start.pItems);
if (start.pItems)
{
assert(false);
}
p_context_delete(context);
+2
View File
@@ -13,6 +13,8 @@ fn main() {
assert!(items.pItem().pToken1().valid());
assert_eq!(TOKEN_a, items.pItem().pToken1().token());
assert_eq!(11, items.pItem().pToken1().pvalue());
/* A token node handle from an unmatched rule is invalid. */
assert!(!items.pItem().pDual().pOne1().pToken1().valid());
let itemsmore = items.pItemsMore();
assert!(itemsmore.pItem().pItem().pItem().pToken1().valid());
assert_eq!(TOKEN_b, itemsmore.pItem().pItem().pItem().pToken1().token());