Skip to content

Commit 6f245ec

Browse files
committed
Merge #305: types: use GhostCell in place of mutexes in union-bound and type context
060455a types: use GhostCell in union_bound rather than mutexes (Andrew Poelstra) 671a836 types: replace LockedContext with a mutex guard around ContextInner (Andrew Poelstra) 9212b39 types: introduce helper type to hold token alongside slab (Andrew Poelstra) 7dce5f0 types: use GhostToken for type context identity (Andrew Poelstra) 79bebd9 satisfy: add inference context to the Satisfier trait (Andrew Poelstra) e18ffc0 policy: add (broken, ignored) unit test showing a problem with asm (Andrew Poelstra) 7e5df30 human_encoding: pass context object as argument to Node::to_witness_node (Andrew Poelstra) a5aa6a2 node: add inference prameter to RedeemNode::to_construct_node (Andrew Poelstra) a534de4 node: take context object as parameter to CommitNode::unfinalize_types (Andrew Poelstra) dec28f3 node: take context as argument to all ConstructNode constructors (Andrew Poelstra) 65aab33 node: thread invariant 'brand lifetime through freeking everything (Andrew Poelstra) fe8dc67 types: introduce 'Incomplete' type to represent incomplete type bounds (Andrew Poelstra) c473b17 types: move innards of Context into a ContextInner type (Andrew Poelstra) e2d1c4c types: remove Default from Context (Andrew Poelstra) 5411440 add ghost-cell dependency (Andrew Poelstra) c7bfcbf add simple RedeemNode benchmark (Andrew Poelstra) Pull request description: I recently learned about the [GhostCell](https://crates.io/crates/ghost-cell) crate, and its associated paper from ICFP 2021. There are a couple of similar projects from that era, notably `qcell` which predates the published version of ghostcell by a bit, but none of these are used much, as evidenced by the "Dependents" tab on crates.io. I learned about them from a blog post about ["generativity"](https://arhan.sh/blog/the-generativity-pattern-in-rust/) which has a pattern that makes GhostCells a little more ergonomic-looking at the cost of using macros. (I don't think this is worthwhile and I didn't use them). But the macro syntax isn't really the point. The key quote from the blog post is this >The fundamental purpose of the generativity pattern is not necessarily to improve performance, but to statically require that individual values come from or refer to the same source. The performance benefits are a symptom of this requirement. Anyway, GhostCells are extremely powerful and I don't know why they are not more popular. In effect, they allow synchronizing a single zero-sized token, and gating access to an arbitrarily-sized set of otherwise-disconnected objects on them. So if you have a `&mut Token` you can get mutable access to all of your objects. This is exactly the situation we have with our type inference context and our union bound algorithm: we have a `Context` object which has a `Mutex` controlling access to a slab of allocated bounds, and then we have a whole pile of `UbElements` which are connected in a forest structure that the Rust compiler doesn't really understand, but where we morally have exclusive access exactly when we have exclusive access to the slab. `GhostCell` lets us tell the compiler this. This PR * Eliminates all the mutexes in the type checker except the one in the `Context` object that holds the slab * Fixes multiple bugs related to inconsistent contexts, including one in the policy module which made it impossible to actually use the asm fragment functionality * Adds an invariant lifetime `'brand` which acts as a tag on all `Type`s, `ConstructNode`s and `WitnessNode`s which prevent them being combined if they come from different type inference contexts * Requires the user to do node construction inside a closure which is passed to `Context::with_context`, rather than constructing a context object which then sorta leaks into the ether by being contained in an `Arc` carried by each node The latter item sounds like a big ergonomic hit, but in practice it's not a big deal because it doesn't affect `CommitNode` or `RedeemNode`. The use of a lifetime rather than a marker type means that while every type context has a "distinct type" for `Type`, `Arrow`, `ConstructNode`, etc., the distinction is erased after typechecking. So compilation speed and program size will not be adversely affected. But conversely, the compiler error messages are pretty unclear when you use this incorrectly. ACKs for top commit: canndrew: ACK 060455a Tree-SHA512: 2f7fd1eb729ef90aaa47cb24de2f6c70d70899ef0fd152f105469673648cdec73a63f31cc753e700e759d5949dab00452fefd686f1bb3405b7b9f2d909278f86
2 parents 6934a57 + 060455a commit 6f245ec

File tree

33 files changed

+1653
-1251
lines changed

33 files changed

+1653
-1251
lines changed

Cargo-recent.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ dependencies = [
166166
"wasm-bindgen",
167167
]
168168

169+
[[package]]
170+
name = "ghost-cell"
171+
version = "0.2.6"
172+
source = "registry+https://github.com/rust-lang/crates.io-index"
173+
checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7"
174+
169175
[[package]]
170176
name = "hex-conservative"
171177
version = "0.1.2"
@@ -456,6 +462,7 @@ dependencies = [
456462
"byteorder",
457463
"elements",
458464
"getrandom",
465+
"ghost-cell",
459466
"hex-conservative 0.2.1",
460467
"miniscript",
461468
"santiago",

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ bitcoin = { version = "0.32.0", optional = true }
2626
bitcoin-miniscript = { package = "miniscript", version = "12.0.0" }
2727
byteorder = "1.3"
2828
elements = { version = "0.25.0", optional = true, default-features = false }
29+
ghost-cell = { version = "0.2.6", default-features = false }
2930
hashes = { package = "bitcoin_hashes", version = "0.14" }
3031
hex = { package = "hex-conservative", version = "0.2.1" }
3132
santiago = "1.3"

fuzz/fuzz_lib/program.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ impl ProgramControl {
4545
}
4646

4747
impl Extractor<'_> {
48-
pub fn extract_core_construct_node(
48+
pub fn extract_core_construct_node<'brand>(
4949
&mut self,
50+
ctx: &types::Context<'brand>,
5051
force_control: Option<ProgramControl>,
51-
) -> Option<Arc<ConstructNode<Core>>> {
52-
type ArcNode = Arc<ConstructNode<Core>>;
52+
) -> Option<Arc<ConstructNode<'brand, Core>>> {
53+
type ArcNode<'brand> = Arc<ConstructNode<'brand, Core>>;
5354

54-
let ctx = types::Context::new();
5555
let mut stack: Vec<ArcNode> = vec![];
5656

5757
let program_control =
@@ -89,8 +89,8 @@ impl Extractor<'_> {
8989
}
9090
}
9191
// 1 through 63
92-
1 => stack.push(ArcNode::unit(&ctx)),
93-
2 => stack.push(ArcNode::iden(&ctx)),
92+
1 => stack.push(ArcNode::unit(ctx)),
93+
2 => stack.push(ArcNode::iden(ctx)),
9494
3 => {
9595
use simplicity::dag::DagLike as _;
9696

@@ -105,9 +105,9 @@ impl Extractor<'_> {
105105
return None;
106106
}
107107
}
108-
stack.push(ArcNode::scribe(&ctx, &val));
108+
stack.push(ArcNode::scribe(ctx, &val));
109109
}
110-
4 if program_control.enable_witness => stack.push(ArcNode::witness(&ctx, None)),
110+
4 if program_control.enable_witness => stack.push(ArcNode::witness(ctx, None)),
111111
5 => {
112112
let child = stack.pop()?;
113113
stack.push(ArcNode::injl(&child));
@@ -139,7 +139,7 @@ impl Extractor<'_> {
139139
11 if program_control.enable_fail => {
140140
let fail_u8 = self.extract_u8()?;
141141
let fail = FailEntropy::from_byte_array([fail_u8; 64]);
142-
stack.push(ArcNode::fail(&ctx, fail));
142+
stack.push(ArcNode::fail(ctx, fail));
143143
}
144144
12 => {
145145
let rchild = stack.pop()?;
@@ -165,7 +165,7 @@ impl Extractor<'_> {
165165
_ => {
166166
let extra_bits = usize::from(control >> 6);
167167
let idx = (extra_bits << 8) + usize::from(self.extract_u8()?);
168-
stack.push(ArcNode::jet(&ctx, Core::ALL[idx % Core::ALL.len()]));
168+
stack.push(ArcNode::jet(ctx, Core::ALL[idx % Core::ALL.len()]));
169169
}
170170
}
171171
}

fuzz/fuzz_targets/c_rust_merkle.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ fn do_test(data: &[u8]) {
77
use simplicity::ffi::tests::{ffi::SimplicityErr, run_program, TestUpTo};
88
use simplicity::hashes::sha256::Midstate;
99
use simplicity::jet::Elements;
10+
use simplicity::types;
1011
use simplicity::{BitIter, RedeemNode};
1112

1213
// To decode the program length, we first try decoding the program using
@@ -15,15 +16,19 @@ fn do_test(data: &[u8]) {
1516
// from the error return.
1617
//
1718
// If the program doesn't decode, just use the first byte as a "length".
18-
let prog_len = {
19+
let prog_len: Option<usize> = types::Context::with_context(|ctx| {
1920
let mut iter = BitIter::from(data);
20-
match simplicity::decode::decode_expression::<_, Elements>(&mut iter) {
21-
Ok(_) => (iter.n_total_read() + 7) / 8,
21+
match simplicity::decode::decode_expression::<_, Elements>(&ctx, &mut iter) {
22+
Ok(_) => Some((iter.n_total_read() + 7) / 8),
2223
Err(_) => match data.first() {
23-
Some(&n) => core::cmp::min(data.len(), n.into()),
24-
None => return,
24+
Some(&n) => Some(core::cmp::min(data.len(), n.into())),
25+
None => return None,
2526
},
2627
}
28+
});
29+
let prog_len = match prog_len {
30+
Some(x) => x,
31+
None => return,
2732
};
2833

2934
let (program, witness) = data.split_at(prog_len);

fuzz/fuzz_targets/regression_286.rs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,40 @@
44

55
#[cfg(any(fuzzing, test))]
66
fn do_test(data: &[u8]) {
7-
use simplicity::{jet::Core, BitIter, CommitNode};
8-
9-
let mut extractor = simplicity_fuzz::Extractor::new(data);
10-
11-
let construct =
12-
match extractor.extract_core_construct_node(Some(simplicity_fuzz::ProgramControl {
13-
enable_type_bomb: false,
14-
enable_disconnect: false,
15-
enable_witness: false,
16-
enable_fail: false,
17-
enable_asserts: true,
18-
max_nodes: Some(25),
19-
})) {
7+
use simplicity::{jet::Core, types, BitIter, CommitNode};
8+
9+
types::Context::with_context(|ctx| {
10+
let mut extractor = simplicity_fuzz::Extractor::new(data);
11+
12+
let construct = match extractor.extract_core_construct_node(
13+
&ctx,
14+
Some(simplicity_fuzz::ProgramControl {
15+
enable_type_bomb: false,
16+
enable_disconnect: false,
17+
enable_witness: false,
18+
enable_fail: false,
19+
enable_asserts: true,
20+
max_nodes: Some(25),
21+
}),
22+
) {
2023
Some(x) => x,
2124
None => return,
2225
};
23-
//println!("constructed {construct}");
24-
let finalized = match construct.finalize_types() {
25-
Ok(x) => x,
26-
Err(_) => return,
27-
};
28-
//println!("finalized {finalized}");
29-
let prog = finalized.encode_to_vec();
30-
//println!("{}", simplicity::bitcoin::hex::DisplayHex::as_hex(&prog));
31-
let prog = BitIter::from(prog);
32-
let decode = CommitNode::<Core>::decode(prog).unwrap();
33-
assert_eq!(
34-
finalized, decode,
35-
"Constructed committed LHS; encoded and decoded to get RHS",
36-
);
26+
//println!("constructed {construct}");
27+
let finalized = match construct.finalize_types() {
28+
Ok(x) => x,
29+
Err(_) => return,
30+
};
31+
//println!("finalized {finalized}");
32+
let prog = finalized.encode_to_vec();
33+
//println!("{}", simplicity::bitcoin::hex::DisplayHex::as_hex(&prog));
34+
let prog = BitIter::from(prog);
35+
let decode = CommitNode::<Core>::decode(prog).unwrap();
36+
assert_eq!(
37+
finalized, decode,
38+
"Constructed committed LHS; encoded and decoded to get RHS",
39+
);
40+
});
3741
}
3842

3943
#[cfg(fuzzing)]

src/bit_encoding/bitwriter.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,10 @@ mod tests {
123123

124124
#[test]
125125
fn vec() {
126-
let program = Arc::<ConstructNode<Core>>::unit(&types::Context::new());
127-
let _ = write_to_vec(|w| program.encode_without_witness(w));
126+
types::Context::with_context(|ctx| {
127+
let program = Arc::<ConstructNode<Core>>::unit(&ctx);
128+
let _ = write_to_vec(|w| program.encode_without_witness(w));
129+
})
128130
}
129131

130132
#[test]

src/bit_encoding/decode.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::{cmp, error, fmt};
2121

2222
use super::bititer::{u2, DecodeNaturalError};
2323

24-
type ArcNode<J> = Arc<ConstructNode<J>>;
24+
type ArcNode<'brand, J> = Arc<ConstructNode<'brand, J>>;
2525

2626
/// Decoding error
2727
#[non_exhaustive]
@@ -48,13 +48,13 @@ pub enum Error {
4848
}
4949

5050
impl From<crate::EarlyEndOfStreamError> for Error {
51-
fn from(_: crate::EarlyEndOfStreamError) -> Error {
51+
fn from(_: crate::EarlyEndOfStreamError) -> Self {
5252
Error::EndOfStream
5353
}
5454
}
5555

5656
impl From<crate::BitIterCloseError> for Error {
57-
fn from(e: crate::BitIterCloseError) -> Error {
57+
fn from(e: crate::BitIterCloseError) -> Self {
5858
Error::BitIter(e)
5959
}
6060
}
@@ -153,16 +153,17 @@ impl<J: Jet> DagLike for (usize, &'_ [DecodeNode<J>]) {
153153
}
154154
}
155155

156-
pub fn decode_expression<I: Iterator<Item = u8>, J: Jet>(
156+
pub fn decode_expression<'brand, I: Iterator<Item = u8>, J: Jet>(
157+
ctx: &types::Context<'brand>,
157158
bits: &mut BitIter<I>,
158-
) -> Result<ArcNode<J>, Error> {
159-
enum Converted<J: Jet> {
160-
Node(ArcNode<J>),
159+
) -> Result<ArcNode<'brand, J>, Error> {
160+
enum Converted<'brand, J: Jet> {
161+
Node(ArcNode<'brand, J>),
161162
Hidden(Cmr),
162163
}
163164
use Converted::{Hidden, Node};
164-
impl<J: Jet> Converted<J> {
165-
fn get(&self) -> Result<&ArcNode<J>, Error> {
165+
impl<'brand, J: Jet> Converted<'brand, J> {
166+
fn get(&self) -> Result<&ArcNode<'brand, J>, Error> {
166167
match self {
167168
Node(arc) => Ok(arc),
168169
Hidden(_) => Err(Error::HiddenNode),
@@ -173,7 +174,6 @@ pub fn decode_expression<I: Iterator<Item = u8>, J: Jet>(
173174
let len = bits.read_natural::<usize>(None)?;
174175
assert_ne!(len, 0, "impossible to encode 0 in Simplicity");
175176

176-
let inference_context = types::Context::new();
177177
let mut nodes = Vec::with_capacity(cmp::min(len, 10_000));
178178
for _ in 0..len {
179179
let new_node = decode_node(bits, nodes.len())?;
@@ -191,8 +191,8 @@ pub fn decode_expression<I: Iterator<Item = u8>, J: Jet>(
191191
}
192192

193193
let new = match nodes[data.node.0] {
194-
DecodeNode::Unit => Node(ArcNode::unit(&inference_context)),
195-
DecodeNode::Iden => Node(ArcNode::iden(&inference_context)),
194+
DecodeNode::Unit => Node(ArcNode::unit(ctx)),
195+
DecodeNode::Iden => Node(ArcNode::iden(ctx)),
196196
DecodeNode::InjL(i) => Node(ArcNode::injl(converted[i].get()?)),
197197
DecodeNode::InjR(i) => Node(ArcNode::injr(converted[i].get()?)),
198198
DecodeNode::Take(i) => Node(ArcNode::take(converted[i].get()?)),
@@ -218,18 +218,16 @@ pub fn decode_expression<I: Iterator<Item = u8>, J: Jet>(
218218
converted[i].get()?,
219219
&Some(Arc::clone(converted[j].get()?)),
220220
)?),
221-
DecodeNode::Witness => Node(ArcNode::witness(&inference_context, None)),
222-
DecodeNode::Fail(entropy) => Node(ArcNode::fail(&inference_context, entropy)),
221+
DecodeNode::Witness => Node(ArcNode::witness(ctx, None)),
222+
DecodeNode::Fail(entropy) => Node(ArcNode::fail(ctx, entropy)),
223223
DecodeNode::Hidden(cmr) => {
224224
if !hidden_set.insert(cmr) {
225225
return Err(Error::SharingNotMaximal);
226226
}
227227
Hidden(cmr)
228228
}
229-
DecodeNode::Jet(j) => Node(ArcNode::jet(&inference_context, j)),
230-
DecodeNode::Word(ref w) => {
231-
Node(ArcNode::const_word(&inference_context, w.shallow_clone()))
232-
}
229+
DecodeNode::Jet(j) => Node(ArcNode::jet(ctx, j)),
230+
DecodeNode::Word(ref w) => Node(ArcNode::const_word(ctx, w.shallow_clone())),
233231
};
234232
converted.push(new);
235233
}
@@ -318,7 +316,9 @@ mod tests {
318316
let justjet = [0x6d, 0xb8, 0x80];
319317
// Should be able to decode this as an expression...
320318
let mut iter = BitIter::from(&justjet[..]);
321-
decode_expression::<_, Core>(&mut iter).unwrap();
319+
types::Context::with_context(|ctx| {
320+
decode_expression::<_, Core>(&ctx, &mut iter).unwrap();
321+
});
322322
// ...but NOT as a CommitNode
323323
let iter = BitIter::from(&justjet[..]);
324324
CommitNode::<Core>::decode(iter).unwrap_err();

src/bit_machine/mod.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -772,15 +772,17 @@ mod tests {
772772
fn crash_regression2() {
773773
use crate::node::{CoreConstructible as _, JetConstructible as _};
774774

775-
type Node = Arc<crate::ConstructNode<crate::jet::Core>>;
776-
777-
let mut bomb = Node::jet(
778-
&crate::types::Context::new(),
779-
crate::jet::Core::Ch8, // arbitrary jet with nonzero output size
780-
);
781-
for _ in 0..100 {
782-
bomb = Node::pair(&bomb, &bomb).unwrap();
783-
}
784-
let _ = bomb.finalize_pruned(&());
775+
type Node<'brand> = Arc<crate::ConstructNode<'brand, crate::jet::Core>>;
776+
777+
crate::types::Context::with_context(|ctx| {
778+
let mut bomb = Node::jet(
779+
&ctx,
780+
crate::jet::Core::Ch8, // arbitrary jet with nonzero output size
781+
);
782+
for _ in 0..100 {
783+
bomb = Node::pair(&bomb, &bomb).unwrap();
784+
}
785+
let _ = bomb.finalize_pruned(&());
786+
});
785787
}
786788
}

src/human_encoding/error.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ pub enum Error {
244244
/// us from knowing the whole program.
245245
HoleAtCommitTime {
246246
name: Arc<str>,
247-
arrow: types::arrow::Arrow,
247+
arrow_source: Arc<types::Incomplete>,
248+
arrow_target: Arc<types::Incomplete>,
248249
},
249250
/// When converting to a `CommitNode`, a disconnect node had an actual node rather
250251
/// than a hole.
@@ -303,11 +304,12 @@ impl fmt::Display for Error {
303304
),
304305
Error::HoleAtCommitTime {
305306
ref name,
306-
ref arrow,
307+
ref arrow_source,
308+
ref arrow_target,
307309
} => write!(
308310
f,
309-
"unfilled hole ?{} at commitment time; type arrow {}",
310-
name, arrow
311+
"unfilled hole ?{} at commitment time; type arrow {} -> {}",
312+
name, arrow_source, arrow_target,
311313
),
312314
Error::HoleFilledAtCommitTime => {
313315
f.write_str("disconnect node has a non-hole child at commit time")

0 commit comments

Comments
 (0)