Skip to content

Use mathematical notation for semantic policy Display#914

Draft
febyeji wants to merge 6 commits intorust-bitcoin:masterfrom
febyeji:semantic-policy-display
Draft

Use mathematical notation for semantic policy Display#914
febyeji wants to merge 6 commits intorust-bitcoin:masterfrom
febyeji:semantic-policy-display

Conversation

@febyeji
Copy link
Copy Markdown
Contributor

@febyeji febyeji commented Apr 1, 2026

Summary

  • Change semantic::Policy Display impl to use mathematical notation:
    • and(a,b)(a ∧ b)
    • or(a,b)(a ∨ b)
    • thresh(k,a,b,c)#{a, b, c} = k

Close #926

@febyeji febyeji force-pushed the semantic-policy-display branch from a0d28b3 to aee1d82 Compare April 1, 2026 05:50
@apoelstra
Copy link
Copy Markdown
Member

  • thresh(k,a,b,c)#{a, b, c} ≥ k

The correct inequality is =, not . If you provide too many satisfactions your witness will be invalid. But concept ACK using this brace/comma notation. I think it will be easier to read in a large expression than my notation was.

febyeji added a commit to febyeji/rust-miniscript that referenced this pull request Apr 2, 2026
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.
@febyeji
Copy link
Copy Markdown
Contributor Author

febyeji commented Apr 2, 2026

Thanks for the review! Changed ≥ to = and added a doc comment to Display noting the notation and that it's not parseable.

@apoelstra
Copy link
Copy Markdown
Member

Can you squash these commits, so that there isn't one modifying code introduced by the other?

@febyeji febyeji force-pushed the semantic-policy-display branch from b796520 to 73ee818 Compare April 2, 2026 18:09
@tcharding
Copy link
Copy Markdown
Member

tcharding commented Apr 21, 2026

Do we want to close the issue if this merges?

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 73ee818

Comment thread fuzz/fuzz_targets/regression_descriptor_parse.rs Outdated
@trevarj
Copy link
Copy Markdown
Contributor

trevarj commented Apr 21, 2026

Do we want to close the issue if this merges?

@tcharding Don't close #885 until issues are made for these at least

  • We rename policy::Concrete to policy::Policy, leaving a type alias to minimize code breakage.
  • We move policy::Semantic into its own module so it becomes semantic::Semantic (we can also leave a type alias at the old location but we should deprecate it). We also move the Lift trait, which I think is in policy/mod.rs, to this own module (we can re-export it at the old location, but sadly we cannot deprecate re-exports).
  • We stop referring to policy::Semantic as a "semantic policy" or a "policy" at all anywhere in our docs
  • We change the Display impl for Semantic to output something goofy, like instead of and(a, b, c) it does (a ∧ b ∧ c). Whoever PRs to do this should provide several examples so we can debate this, especially the threshold notation. (Maybe we want (2 = a + b + c) for thresh(2, a, b, c)?)

Furthermore, I propose adding a method to "uncompile" from a Miniscript to a Policy (or even a Semantic to a Policy), but we should move that to a separate issue because I'm quite conflicted about what to do about percentages.

@apoelstra
Copy link
Copy Markdown
Member

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
Copy link
Copy Markdown
Member

@febyeji I've changed your PR description to close the new #926. Please check you are happy with that.

@febyeji
Copy link
Copy Markdown
Contributor Author

febyeji commented Apr 22, 2026

@tcharding Looks good, thanks for updating the PR body. Happy with that.

Comment thread src/policy/semantic.rs Outdated
@febyeji febyeji force-pushed the semantic-policy-display branch 2 times, most recently from 5a0282a to eb6011e Compare April 22, 2026 14:06
@apoelstra
Copy link
Copy Markdown
Member

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.

febyeji added a commit to febyeji/rust-miniscript that referenced this pull request Apr 23, 2026
…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.
@febyeji febyeji force-pushed the semantic-policy-display branch from eb6011e to 9852620 Compare April 23, 2026 11:12
Comment thread src/policy/semantic.rs Outdated
@febyeji febyeji force-pushed the semantic-policy-display branch 2 times, most recently from 9201b03 to 357554e Compare April 24, 2026 05:04
@febyeji
Copy link
Copy Markdown
Contributor Author

febyeji commented Apr 24, 2026

I simplified the parser and added hybrid rejection. The math parser now refuses atoms whose root name is and/or/thresh, so strings like (a ∧ or(b, c)) and similar won't parse.

@apoelstra
Copy link
Copy Markdown
Member

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.

@febyeji febyeji marked this pull request as draft April 27, 2026 03:23
@febyeji febyeji force-pushed the semantic-policy-display branch from 357554e to 1384586 Compare April 27, 2026 05:29
@febyeji
Copy link
Copy Markdown
Contributor Author

febyeji commented Apr 27, 2026

@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 expression::Tree, which compounded the mess since I had also misunderstood your intent and feedback.

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.

In the current code, FromStr accepts the legacy form as well to avoid breaking existing tests and users, but I'm not sure if FromStr should accept only the new format.

Copy link
Copy Markdown
Contributor

@trevarj trevarj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/policy/semantic.rs Outdated
}
}

/// Grammar:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/policy/semantic.rs Outdated
kind: Kind,
}

fn match_sep(s: &str) -> Option<Op> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop whitespace and then you can have a impl From<&str> for Op and have the _ case be unreachable!() instead of this.

Copy link
Copy Markdown
Contributor Author

@febyeji febyeji Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, should be char!

Comment thread src/policy/semantic.rs Outdated
}
}

let bytes = s.as_bytes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/policy/semantic.rs Outdated
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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Derive PartialEq, Eq on Kind and you can get rid of matches! like this and just use != here and in the other areas.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Kind now derives the traits and directly uses == / !=.

Comment thread src/policy/semantic.rs Outdated
}
i += 3;
let k_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, this is copied a couple times and could be replaced with some helper to consume until some char, I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this one, I added consume_while(predicate) helper to MathParser

Copy link
Copy Markdown
Contributor

@trevarj trevarj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread src/policy/semantic.rs Outdated
Comment on lines +438 to +443
loop {
parser.skip_ws();
let c = match parser.peek() {
Some(c) => c,
None => break,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/policy/semantic.rs
enum Op {
And,
Or,
struct MathParser<'a> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get doc comments here and in the impl? Thanks!

Comment thread src/policy/semantic.rs Outdated
}

/// Parses the mathematical-notation form produced by `Display`.
fn parse_math_policy<Pk: FromStrKey>(s: &str) -> Result<Policy<Pk>, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/policy/semantic.rs
}

let bytes = s.as_bytes();
struct Frame<Pk: MiniscriptKey> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another doc comment here explaining what the use of Frame is used for.

Comment thread src/policy/semantic.rs Outdated
.map_err(Error::Parse)
}

fn malformed_math() -> Error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I updated this along with expect.

Comment thread src/policy/semantic.rs Outdated

fn peek(&mut self) -> Option<char> { self.iter.peek().map(|&(_, c)| c) }

fn bump(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd rather have next() unless you think it's better to use bump. Some people use chomp...

Suggested change
fn bump(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) }
fn next(&mut self) -> Option<char> { self.iter.next().map(|(_, c)| c) }

Comment thread src/policy/semantic.rs Outdated
}
}

fn try_consume(&mut self, want: char) -> bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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('(')?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it's much clearer. Thanks!

Comment thread src/policy/semantic.rs Outdated
return Err(malformed_math("expected terminal name"));
}

if !self.try_consume('(') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove the try_consume function fully and then use:

Suggested change
if !self.try_consume('(') {
if let Err(e) = self.expect('(') {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thank you. I missed this line.

Copy link
Copy Markdown
Contributor

@trevarj trevarj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 511e81b

Don't forget to squash commits later. Probably last 5 into a single commit?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix Display impl for Semantic

4 participants