Use mathematical notation for semantic policy Display#914
Use mathematical notation for semantic policy Display#914febyeji wants to merge 6 commits intorust-bitcoin:masterfrom
Conversation
a0d28b3 to
aee1d82
Compare
The correct inequality is |
thresh requires exactly k satisfactions to be valid, not k or more. Providing too many satisfactions makes the witness invalid. Addresses apoelstra's review feedback on PR rust-bitcoin#914.
|
Thanks for the review! Changed ≥ to = and added a doc comment to Display noting the notation and that it's not parseable. |
|
Can you squash these commits, so that there isn't one modifying code introduced by the other? |
b796520 to
73ee818
Compare
|
Do we want to close the issue if this merges? |
@tcharding Don't close #885 until issues are made for these at least
|
|
No, to close the issue we've also got to do the renames and doc changes to stop referring to semantic policies as "policies". |
|
@tcharding Looks good, thanks for updating the PR body. Happy with that. |
5a0282a to
eb6011e
Compare
|
In eb6011e: This parser is gratuitously inefficient. You should be able to just parse the new syntax directly. It is a simple CFG and I don't think it even needs lookahead. You definitely do not need to rewrite the string into the old syntax, and even if you did, this manages to be both recursive (we should not be introducing any new recursive code) and still manages to rescan the entire string on every iteration. |
…ss parser Replace the recursive `rewrite_math_notation` that rescanned the input at every depth with a single linear pass that builds `Policy<Pk>` directly using an explicit frame stack. Matches `expression::Tree`'s non-recursive conventions. Addresses apoelstra's review on rust-bitcoin#914.
eb6011e to
9852620
Compare
9201b03 to
357554e
Compare
|
I simplified the parser and added hybrid rejection. The math parser now refuses atoms whose root name is |
|
That solution is insane. I feel like I'm just arguing with a LLM by proxy here. I am not going to provide detailed feedback anymore. |
357554e to
1384586
Compare
|
@apoelstra Apologies for the back-and-forth. I wrote the parser myself, but without carefully understanding the codebase. I was trying to be conservative and reuse I've rewritten the code. If the result isn't what you had in mind, please feel free to close the PR. I will just leave one note in case it helps resolving the issue later.
|
trevarj
left a comment
There was a problem hiding this comment.
Leaving some starting feedback on the parser. If the suggestions get implemented then I will do another review and see if we can refine it more.
You may want to not force push and start stacking some commits in case Andrew or someone else doesn't like a change and then you can drop that commit and rebase later.
| } | ||
| } | ||
|
|
||
| /// Grammar: |
There was a problem hiding this comment.
Andrew said that we don't need a BNF grammar in the doc comment.
It's a pretty simple grammar, but I prefer a more simplified notation anyway (like regex style for human readability).
I don't like how the grammar includes whitespace. I think it would be better to always skip whitespace to accept pk(A) ∧ pk(B).
There was a problem hiding this comment.
Thanks for the review. I changed the code to accept whitespaces except for the digraph #{. I dropped the BNF grammar from the doc comment as well.
| kind: Kind, | ||
| } | ||
|
|
||
| fn match_sep(s: &str) -> Option<Op> { |
There was a problem hiding this comment.
Drop whitespace and then you can have a impl From<&str> for Op and have the _ case be unreachable!() instead of this.
There was a problem hiding this comment.
I refactored the code, but used From<char> instead of From<&str> since Op are of a single literal and we can avoid constructing &str from the call site. Happy to switch &str if it's a better option to consider.
There was a problem hiding this comment.
You are correct, should be char!
| } | ||
| } | ||
|
|
||
| let bytes = s.as_bytes(); |
There was a problem hiding this comment.
I would prefer using a s.chars().peekable() (or char_indices()) Iterator.
I think it will make things more readable and you won't have to index into the byte slice and skip utf8 bytes. It's not a huge problem, but I think it's just nicer Rust.
After that, then you can have really nice looking match statements on the main while loop. It's more idiomatic Rust IMO.
You'll probably need some "traditional" parser/lexer helper functions, like skipping whitespace and reading until some char, so it may be worth it to wrap the iterator in a MathParser struct or something.
There was a problem hiding this comment.
Thank you for the guidance. I refactored the code to wrap the parser state in a MathParser struct with a few helpers like peek, bump, .... Most of the byte-index manipulation is now hidden, though some match arms may still not be ergonomic to read. Please let me know if there's room to push this further.
| frame.subs.push(cur.take().unwrap()); | ||
| i += 5; // " ∧ " / " ∨ " in UTF-8 | ||
| } else if bytes[i] == b',' && bytes.get(i + 1) == Some(&b' ') { | ||
| if !matches!(frame.kind, Kind::Thresh) { |
There was a problem hiding this comment.
Derive PartialEq, Eq on Kind and you can get rid of matches! like this and just use != here and in the other areas.
There was a problem hiding this comment.
Thanks, Kind now derives the traits and directly uses == / !=.
| } | ||
| i += 3; | ||
| let k_start = i; | ||
| while i < bytes.len() && bytes[i].is_ascii_digit() { |
There was a problem hiding this comment.
For example, this is copied a couple times and could be replaced with some helper to consume until some char, I think.
There was a problem hiding this comment.
For this one, I added consume_while(predicate) helper to MathParser
trevarj
left a comment
There was a problem hiding this comment.
I think the parser is turning out pretty good, I like it.
Probably my last round of review since everything else seems ok to me.
Thanks!
| loop { | ||
| parser.skip_ws(); | ||
| let c = match parser.peek() { | ||
| Some(c) => c, | ||
| None => break, | ||
| }; |
There was a problem hiding this comment.
Does this still work if it's changed to?:
parser.skip_ws();
while let Some(c) = parser.peek() {
...
parser.skip_ws(); // <- last statementin block
}I'm not actually sure what is better...just a nit and can ignore.
There was a problem hiding this comment.
I've thought about it and would work if we put parser.skip_ws() before continue as well. For now, I kept as it is to have loop -> skip_ws -> peek-or-break format.
| enum Op { | ||
| And, | ||
| Or, | ||
| struct MathParser<'a> { |
There was a problem hiding this comment.
Can we get doc comments here and in the impl? Thanks!
| } | ||
|
|
||
| /// Parses the mathematical-notation form produced by `Display`. | ||
| fn parse_math_policy<Pk: FromStrKey>(s: &str) -> Result<Policy<Pk>, Error> { |
There was a problem hiding this comment.
Let's move this and the other parse_* functions into MathParser.
Then we can give it a good doc comment explaining the high level overview of how the parser works.
| } | ||
|
|
||
| let bytes = s.as_bytes(); | ||
| struct Frame<Pk: MiniscriptKey> { |
There was a problem hiding this comment.
Another doc comment here explaining what the use of Frame is used for.
| .map_err(Error::Parse) | ||
| } | ||
|
|
||
| fn malformed_math() -> Error { |
There was a problem hiding this comment.
I think it would be better to add a context: &str arg and display that so the user knows exactly where the policy is malformed. Need to update callers appropriately.
There was a problem hiding this comment.
Thanks, I updated this along with expect.
|
|
||
| fn peek(&mut self) -> Option<char> { self.iter.peek().map(|&(_, c)| c) } | ||
|
|
||
| fn bump(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) } |
There was a problem hiding this comment.
nit: I'd rather have next() unless you think it's better to use bump. Some people use chomp...
| fn bump(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) } | |
| fn next(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) } |
| } | ||
| } | ||
|
|
||
| fn try_consume(&mut self, want: char) -> bool { |
There was a problem hiding this comment.
| fn try_consume(&mut self, want: char) -> bool { | |
| fn expect(&mut self, want: char) -> Result<(), Error> { |
This way you can get rid of the return Err(malformed_math()); that is scattered around and just use parser.expect('(')?;
There was a problem hiding this comment.
Right, it's much clearer. Thanks!
| return Err(malformed_math("expected terminal name")); | ||
| } | ||
|
|
||
| if !self.try_consume('(') { |
There was a problem hiding this comment.
I would remove the try_consume function fully and then use:
| if !self.try_consume('(') { | |
| if let Err(e) = self.expect('(') { |
There was a problem hiding this comment.
Oh, thank you. I missed this line.
Summary
semantic::PolicyDisplayimpl to use mathematical notation:and(a,b)→(a ∧ b)or(a,b)→(a ∨ b)thresh(k,a,b,c)→#{a, b, c} = kClose #926