Skip to content

Commit ec8f9b3

Browse files
authored
Faster validation with tree-based constraint checking (#16)
1 parent ffaf2c7 commit ec8f9b3

File tree

11 files changed

+1234
-556
lines changed

11 files changed

+1234
-556
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ comemo-macros = { version = "0.4.0", path = "macros" }
1717
once_cell = "1.18"
1818
parking_lot = "0.12"
1919
proc-macro2 = "1"
20+
quickcheck = "1"
21+
quickcheck_macros = "1"
2022
quote = "1"
2123
rustc-hash = "2.1"
2224
serial_test = "3"
2325
siphasher = "1"
26+
slab = "0.4"
2427
syn = { version = "2", features = ["full"] }
2528

2629
[package]
@@ -45,8 +48,11 @@ comemo-macros = { workspace = true, optional = true }
4548
parking_lot = { workspace = true }
4649
rustc-hash = { workspace = true }
4750
siphasher = { workspace = true }
51+
slab = { workspace = true }
4852

4953
[dev-dependencies]
54+
quickcheck = { workspace = true }
55+
quickcheck_macros = { workspace = true }
5056
serial_test = { workspace = true }
5157

5258
[[test]]

macros/src/lib.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ use syn::{Error, Result, parse_quote};
3939
/// - _Mutably tracked:_ The argument is of the form `TrackedMut<T>`. Through
4040
/// this type, you can safely mutate an argument from within a memoized
4141
/// function. If there is a cache hit, comemo will replay all mutations.
42-
/// Mutable tracked methods can also have return values that are tracked just
43-
/// like immutable methods.
42+
/// Mutable tracked methods cannot have return values.
4443
///
4544
/// # Restrictions
4645
/// The following restrictions apply to memoized functions:
@@ -50,11 +49,40 @@ use syn::{Error, Result, parse_quote};
5049
/// expose to the hasher**. Otherwise, memoized results might get reused
5150
/// invalidly.
5251
///
53-
/// - The **only obversable impurity memoized functions may exhibit are
52+
/// - The **only observable impurity memoized functions may exhibit are
5453
/// mutations through `TrackedMut<T>` arguments.** Comemo stops you from using
5554
/// basic mutable arguments, but it cannot determine all sources of impurity,
5655
/// so this is your responsibility.
5756
///
57+
/// - Memoized functions must **call tracked methods in _reorderably
58+
/// deterministic_ fashion.** Consider two executions A and B of a memoized
59+
/// function. We define the following two properties:
60+
///
61+
/// - _In-order deterministic:_ If the first N tracked calls and their results
62+
/// are the same in A and B, then the N+1th call must also be the same. This
63+
/// is a fairly natural property as far as deterministic functions go, as,
64+
/// if the first N calls and their results were the same across two
65+
/// execution, the available information for choosing the N+1th call is the
66+
/// same. However, this property is a bit too restrictive in practice. For
67+
/// instance, a function that internally uses multi-threading may call
68+
/// tracked methods out-of-order while still producing a deterministic
69+
/// result.
70+
///
71+
/// - _Reorderably deterministic:_ If, for the first N calls in A, B has
72+
/// matching calls (same arguments, same return value) somewhere in its call
73+
/// sequence, then the N+1th call invoked by A must also occur _somewhere_
74+
/// in the call sequence of B. This is a somewhat relaxed version of
75+
/// in-order determinism that still allows comemo to perform internal
76+
/// optimizations while permitting memoization of many more functions (e.g.
77+
/// ones that use internal multi-threading in an outwardly deterministic
78+
/// fashion).
79+
///
80+
/// Reorderable determinism is necessary for efficient cache lookups. If a
81+
/// memoized function is not reorderably determinstic, comemo may panic in
82+
/// debug mode to bring your attention to this. Meanwhile, in release mode,
83+
/// memoized functions will still yield correct results, but caching may prove
84+
/// ineffective.
85+
///
5886
/// - The output of a memoized function must be `Send` and `Sync` because it is
5987
/// stored in the global cache.
6088
///
@@ -126,10 +154,6 @@ pub fn memoize(args: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
126154
/// arguments, tracking is the only option, so that comemo can replay the side
127155
/// effects when there is a cache hit.
128156
///
129-
/// If you attempt to track any mutable methods, your type must implement
130-
/// [`Clone`] so that comemo can roll back attempted mutations which did not
131-
/// result in a cache hit.
132-
///
133157
/// # Restrictions
134158
/// Tracked impl blocks or traits may not be generic and may only contain
135159
/// methods. Just like with memoized functions, certain restrictions apply to
@@ -147,6 +171,11 @@ pub fn memoize(args: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
147171
/// [`Hash`](std::hash::Hash) and **must feed all the information they expose
148172
/// to the hasher**. Otherwise, memoized results might get reused invalidly.
149173
///
174+
/// - Mutable tracked methods must not have a return value.
175+
///
176+
/// - A tracked implementation cannot have a mix of mutable and immutable
177+
/// methods.
178+
///
150179
/// - The arguments to a tracked method must be `Send` and `Sync` because they
151180
/// are stored in the global cache.
152181
///

macros/src/memoize.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ fn process(function: &Function) -> Result<TokenStream> {
148148

149149
wrapped.block = parse_quote! { {
150150
static __CACHE: ::comemo::internal::Cache<
151-
<::comemo::internal::Multi<#arg_ty_tuple> as ::comemo::internal::Input>::Constraint,
151+
<::comemo::internal::Multi<#arg_ty_tuple> as ::comemo::internal::Input>::Call,
152152
#output,
153153
> = ::comemo::internal::Cache::new(|| {
154154
::comemo::internal::register_evictor(|max_age| __CACHE.evict(max_age));
@@ -160,7 +160,7 @@ fn process(function: &Function) -> Result<TokenStream> {
160160
::comemo::internal::memoize(
161161
&__CACHE,
162162
::comemo::internal::Multi(#arg_tuple),
163-
&::core::default::Default::default(),
163+
&mut ::core::default::Default::default(),
164164
#enabled,
165165
#closure,
166166
)

macros/src/track.rs

Lines changed: 51 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ pub fn expand(item: &syn::Item) -> Result<TokenStream> {
4242
_ => bail!(item, "`track` can only be applied to impl blocks and traits"),
4343
};
4444

45+
if methods.iter().any(|m| m.mutable) && methods.iter().any(|m| !m.mutable) {
46+
bail!(
47+
item,
48+
"`track` cannot be applied to a mix of mutable and immutable methods"
49+
);
50+
}
51+
4552
// Produce the necessary items for the type to become trackable.
4653
let variants = create_variants(&methods);
4754
let scope = create(&ty, generics, trait_, &methods)?;
@@ -168,6 +175,12 @@ fn prepare_method(vis: syn::Visibility, sig: &syn::Signature) -> Result<Method>
168175
bail!(ty, "tracked methods cannot return mutable references");
169176
}
170177

178+
if let syn::ReturnType::Type(_, ty) = &sig.output
179+
&& receiver.mutability.is_some()
180+
{
181+
bail!(ty, "mutable tracked methods cannot have a return value");
182+
}
183+
171184
Ok(Method {
172185
vis,
173186
sig: sig.clone(),
@@ -225,11 +238,6 @@ fn create(
225238
let t: syn::GenericParam = parse_quote! { '__comemo_tracked };
226239
let r: syn::GenericParam = parse_quote! { '__comemo_retrack };
227240
let d: syn::GenericParam = parse_quote! { '__comemo_dynamic };
228-
let maybe_cloned = if methods.iter().any(|it| it.mutable) {
229-
quote! { ::core::clone::Clone::clone(self) }
230-
} else {
231-
quote! { self }
232-
};
233241

234242
// Prepare generics.
235243
let (impl_gen, type_gen, where_clause) = generics.split_for_impl();
@@ -245,37 +253,9 @@ fn create(
245253
impl_params_t.params.push(t.clone());
246254
type_params_t.params.push(t.clone());
247255

248-
// Prepare validations.
249256
let prefix = trait_.as_ref().map(|name| quote! { #name for });
250-
let validations: Vec<_> = methods.iter().map(create_validation).collect();
251-
let validate = if !methods.is_empty() {
252-
quote! {
253-
let mut this = #maybe_cloned;
254-
constraint.validate(|call| match &call.0 { #(#validations,)* })
255-
}
256-
} else {
257-
quote! { true }
258-
};
259-
let validate_with_id = if !methods.is_empty() {
260-
quote! {
261-
let mut this = #maybe_cloned;
262-
constraint.validate_with_id(
263-
|call| match &call.0 { #(#validations,)* },
264-
id,
265-
)
266-
}
267-
} else {
268-
quote! { true }
269-
};
270-
271-
// Prepare replying.
272-
let immutable = methods.iter().all(|m| !m.mutable);
273-
let replays = methods.iter().map(create_replay);
274-
let replay = (!immutable).then(|| {
275-
quote! {
276-
constraint.replay(|call| match &call.0 { #(#replays,)* });
277-
}
278-
});
257+
let calls: Vec<_> = methods.iter().map(create_call).collect();
258+
let calls_mut: Vec<_> = methods.iter().map(create_call_mut).collect();
279259

280260
// Prepare variants and wrapper methods.
281261
let wrapper_methods = methods
@@ -284,32 +264,18 @@ fn create(
284264
.map(|m| create_wrapper(m, false));
285265
let wrapper_methods_mut = methods.iter().map(|m| create_wrapper(m, true));
286266

287-
let constraint = if immutable {
288-
quote! { ImmutableConstraint }
289-
} else {
290-
quote! { MutableConstraint }
291-
};
292-
293267
Ok(quote! {
294-
impl #impl_params ::comemo::Track for #ty #where_clause {}
295-
296-
impl #impl_params ::comemo::Validate for #ty #where_clause {
297-
type Constraint = ::comemo::internal::#constraint<__ComemoCall>;
268+
impl #impl_params ::comemo::Track for #ty #where_clause {
269+
type Call = __ComemoCall;
298270

299271
#[inline]
300-
fn validate(&self, constraint: &Self::Constraint) -> bool {
301-
#validate
272+
fn call(&self, call: &Self::Call) -> u128 {
273+
match call.0 { #(#calls,)* }
302274
}
303275

304276
#[inline]
305-
fn validate_with_id(&self, constraint: &Self::Constraint, id: usize) -> bool {
306-
#validate_with_id
307-
}
308-
309-
#[inline]
310-
#[allow(unused_variables)]
311-
fn replay(&mut self, constraint: &Self::Constraint) {
312-
#replay
277+
fn call_mut(&mut self, call: &Self::Call) {
278+
match call.0 { #(#calls_mut,)* }
313279
}
314280
}
315281

@@ -363,41 +329,50 @@ fn create(
363329
})
364330
}
365331

366-
/// Produce a constraint validation for a method.
332+
/// Produce a call enum variant for a method.
367333
fn create_variant(method: &Method) -> TokenStream {
368334
let name = &method.sig.ident;
369335
let types = &method.types;
370336
quote! { #name(#(<#types as ::std::borrow::ToOwned>::Owned),*) }
371337
}
372338

373-
/// Produce a constraint validation for a method.
374-
fn create_validation(method: &Method) -> TokenStream {
339+
/// Produce a call branch for a method.
340+
fn create_call(method: &Method) -> TokenStream {
375341
let name = &method.sig.ident;
376342
let args = &method.args;
377343
let prepared = method.args.iter().zip(&method.kinds).map(|(arg, kind)| match kind {
378344
Kind::Normal => quote! { #arg.to_owned() },
379345
Kind::Reference => quote! { #arg },
380346
});
381-
quote! {
382-
__ComemoVariant::#name(#(#args),*)
383-
=> ::comemo::internal::hash(&this.#name(#(#prepared),*))
347+
if method.mutable {
348+
quote! {
349+
__ComemoVariant::#name(..) => 0
350+
}
351+
} else {
352+
quote! {
353+
__ComemoVariant::#name(#(ref #args),*)
354+
=> ::comemo::internal::hash(&self.#name(#(#prepared),*))
355+
}
384356
}
385357
}
386358

387-
/// Produce a constraint validation for a method.
388-
fn create_replay(method: &Method) -> TokenStream {
359+
/// Produce a mutable call branch for a method.
360+
fn create_call_mut(method: &Method) -> TokenStream {
389361
let name = &method.sig.ident;
390362
let args = &method.args;
391363
let prepared = method.args.iter().zip(&method.kinds).map(|(arg, kind)| match kind {
392364
Kind::Normal => quote! { #arg.to_owned() },
393365
Kind::Reference => quote! { #arg },
394366
});
395-
let body = method.mutable.then(|| {
367+
if method.mutable {
396368
quote! {
397-
self.#name(#(#prepared),*);
369+
__ComemoVariant::#name(#(ref #args),*) => self.#name(#(#prepared),*)
398370
}
399-
});
400-
quote! { __ComemoVariant::#name(#(#args),*) => { #body } }
371+
} else {
372+
quote! {
373+
__ComemoVariant::#name(..) => {}
374+
}
375+
}
401376
}
402377

403378
/// Produce a wrapped surface method.
@@ -417,16 +392,19 @@ fn create_wrapper(method: &Method, tracked_mut: bool) -> TokenStream {
417392
#[track_caller]
418393
#[inline]
419394
#vis #sig {
420-
let __comemo_variant = __ComemoVariant::#name(#(#args.to_owned()),*);
421-
let (__comemo_value, __comemo_constraint) = ::comemo::internal::#to_parts;
422-
let output = __comemo_value.#name(#(#args,)*);
423-
if let Some(constraint) = __comemo_constraint {
424-
constraint.push(
395+
let (__comemo_value, __comemo_sink) = ::comemo::internal::#to_parts;
396+
if let Some(__comemo_sink) = __comemo_sink {
397+
let __comemo_variant = __ComemoVariant::#name(#(#args.to_owned()),*);
398+
let output = __comemo_value.#name(#(#args,)*);
399+
::comemo::internal::Sink::emit(
400+
__comemo_sink,
425401
__ComemoCall(__comemo_variant),
426402
::comemo::internal::hash(&output),
427403
);
404+
output
405+
} else {
406+
__comemo_value.#name(#(#args,)*)
428407
}
429-
output
430408
}
431409
}
432410
}

0 commit comments

Comments
 (0)