Skip to content

CoreConstructible: remove impl for Arrow, add arrow() accessor - #377

Open
apoelstra wants to merge 7 commits into
BlockstreamResearch:masterfrom
apoelstra:2026-08/constructible
Open

CoreConstructible: remove impl for Arrow, add arrow() accessor#377
apoelstra wants to merge 7 commits into
BlockstreamResearch:masterfrom
apoelstra:2026-08/constructible

Conversation

@apoelstra

Copy link
Copy Markdown
Collaborator

This PR changes the CoreConstructible trait from "a node type that can be constructed from child node(s)" to "a node type that can be constructed from a types::Arrow and child node(s)".

This is a conceptually important shift but hopefully not one that requires many code changes. There are a couple goals here:

  • It cleans up a bunch of code, where we were using the CoreConstructible trait to represent many different kinds of things (nodes, type arrows, "hidden nodes" within policies). This enabled some code reuse, which we didn't even take advantage of, at the cost of some API friction. Lots of unused parameters and unused result return valuse.
  • The existing trait is inherently tied to type inference (all the unit/iden/etc constructors are infallible while all the comp/case/etc ones return a Result<Self, types::Error>. So you can't implement some sort of custom node type that imposes additonal restrictions. This is fine, but we could take better advantage of this. Specifically, I want to introduce infallible constructors where the caller provides a types::Arrow and promises that the arrow is correct. (If the caller lies here they will create an ill-typed program which will then fail down the line when they attempt to finalize it or otherwise convert from a ConstructNode to a useful kind of node. You can't even serialize a ConstructNode so there's not much you can do here.)

Then next PR will introduce these new constructors along with a new TypePromise construction which takes advantage of it to do infallible node construction in cases where the Rust compiler knows that the types all match.

Despite being `pub` this is not actually exported in the public API. It
is only used by the policy module, which is gated on the `elements`
feature. So when compiling with `--no-default-features` we get warnings
about it being unused.
In the next commits we will change the Constructible trait to represent
something like "can be constructed given an Arrow". It will then be a
bit hard to implement the Constructible traits for Arrow itself. (Well,
it's not *that* hard, but you have to tiptoe around the possibility that
your methods call each other in a cycle causing infinite recursion.)

These traits are already not a great fit for Arrow itself: the witness,
fail, assertl and assertr combinators all take auxiliary data that Arrow
does not care about and which we can remove. The word combinator takes a
whole word by value, but Arrow only cares about its length.

In some sense ConstructData is already the "Arrow, but wrapped to impl
the Constructible traits" type, so having Arrow implement these traits
too is wasteful.

We do lose the derived methods that we get from Constructible -- in
particular scribe() and from_inner() are pretty useful. But scribe() is
actually not that useful for Arrow (the Arrow for a scribe always has
unit source and target equal to the value's type) and is never called.
And from_inner is only called once, and requires a bunch of contortions
with the Inner mapping combinators to make this call work. The code is
actually clearer if I just inline the call (which I did, in
src/node/commit.rs in the `unfinalize_types` method).
It will become very annoying to type arrow::Arrow all the time. types::Arrow
would read much better (assuming you don't want to just type Arrow). The arrow
module only exports two symbols, Arrow and FinalArrow, so just reexport
both of these from the types module.

Leave the module `pub`, though we could now make it private, to avoid
unnecessarily breaking existing code.
This was just kinda bugging me. It's better to use `Self` than
explicitly writing `Arrow` both because it makes refactorings easier
(e.g. if we copy/paste or macroize the code, it will require no changes
to apply to a different type), and because `Self` is more specific than
`Arrow`. That is, `Self` means `Arrow<'brand>` while just `Arrow` means
`Arrow<'_>`. This improves compiler error messages when there are
lifetime problems.
In two commits we are going to refactor the `CoreConstructible` type
to add an `arrow()` accessor. That is, we are redefining a "constructible
node" to be one that has an unfinalized type arrow associated with it,
rather than just something that shares the recursive constructor
structure.

This was already implicitly the case, since CoreConstructible::pair and
others return a `Result<(), types::Error>`, but this change will make
things explicit.

To make construction of hidden nodes and "unknown programs" easier,
introduce Arrow constructors that directly provide new free arrows and
new 1-1 arrows.

We already have two "free arrow" constructors -- Arrow::fail and
Arrow::witness -- but this one is more clearly for an "unknown node"
rather than a particular node that happens to have a free arrow.

@stringhandler stringhandler left a comment

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.

utACK 8896e78

Maybe we should add a CI job with --no-default-features to catch this in future?

Comment thread src/node/hiding.rs Outdated
// for `Hiding<N>` require `N: CoreConstructible`.
impl<'brand, N: CoreConstructible<'brand>> From<N> for Hiding<'brand, N> {
// We implement From<N> for arbitrary N, to avoid but the expectation is that this is
// FIXME revisit this. does it compile?

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.

Is this still relevant?

@apoelstra apoelstra Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oops, no, this is several iterations old. Removed.

@stringhandler stringhandler left a comment

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.

utACK 1fef62f

@stringhandler

Copy link
Copy Markdown
Contributor

Maybe we should add a CI job with --no-default-features to catch this in future?

Added in #380

…ontext

In the next commit we will change CoreConstructible to add an arrow()
accessor. The `Hiding` type in particular requires significant
refactoring to hold an arrow rather than directly holding an inference
context. In particular, when creating hidden nodes, we should be
creating fresh hidden arrows for them, since each hidden node is
independent from all others. (Even if they have the same CMR, they may
infer to different type arrows.)

This is something of an annoying change -- hidden nodes don't really
have type errors; there are no constraints on their source/target types
so they can be used literally anywhere. This is arguably refrected best
by the existing code, which does not track any type arrow for hidden
nodes. However, when we change CoreConstructible to use the type arrow,
we need to have something there, so everywhere that we construct a new
hidden node, we call Arrow::hidden to get a new hidden arrow.
We are going to move type inference out of the `pair`/`comp`/`case`
combinator functions. This will let us bypass type inference in the new
"type promise" API, which in turn will eliminate the PairBuilder API in
SimplicityHL as well as a ton of unwraps/expect in "constant program
constructors".
@apoelstra
apoelstra force-pushed the 2026-08/constructible branch from 1fef62f to 617c697 Compare August 25, 2026 13:39
@stringhandler

Copy link
Copy Markdown
Contributor

utACK 617c697

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.

2 participants