Skip to content

Commit f6e7c92

Browse files
committed
First idea
0 parents  commit f6e7c92

File tree

9 files changed

+343
-0
lines changed

9 files changed

+343
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.vscode
2+
.DS_Store
3+
/target
4+
Cargo.lock

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "comemo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
comemo-macros = { path = "./macros" }

macros/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "comemo-macros"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
proc-macro2 = "1"
11+
quote = "1"
12+
syn = { version = "1", features = ["full"] }

macros/src/lib.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
extern crate proc_macro;
2+
3+
macro_rules! bail {
4+
($item:expr, $fmt:literal $($tts:tt)*) => {
5+
return Err(Error::new_spanned(
6+
&$item,
7+
format!(concat!("comemo: ", $fmt) $($tts)*)
8+
))
9+
}
10+
}
11+
12+
mod memoize;
13+
mod track;
14+
15+
use proc_macro::TokenStream;
16+
use quote::{quote, quote_spanned};
17+
use syn::spanned::Spanned;
18+
use syn::{parse_quote, Error, Result};
19+
20+
/// Memoize a pure function.
21+
///
22+
/// ```
23+
/// #[memoize]
24+
/// fn describe(image: Tracked<Image>) -> String {
25+
/// let w = image.width();
26+
/// let h = image.height();
27+
/// format!("The image is {w}x{h} pixels.")
28+
/// }
29+
/// ```
30+
#[proc_macro_attribute]
31+
pub fn memoize(_: TokenStream, stream: TokenStream) -> TokenStream {
32+
let func = syn::parse_macro_input!(stream as syn::ItemFn);
33+
memoize::expand(func)
34+
.unwrap_or_else(|err| err.to_compile_error())
35+
.into()
36+
}
37+
38+
/// Make a type trackable.
39+
///
40+
/// ```
41+
/// /// A decoded raster image.
42+
/// pub struct Image {
43+
/// width: u32,
44+
/// height: u32,
45+
/// pixels: Vec<u8>,
46+
/// }
47+
///
48+
/// #[track]
49+
/// impl Image {
50+
/// fn width(&self) -> u32 {
51+
/// self.width
52+
/// }
53+
///
54+
/// fn height(&self) -> u32 {
55+
/// self.height
56+
/// }
57+
/// }
58+
/// ```
59+
///
60+
/// [`Track`]: ../comemo/trait.Track.html
61+
#[proc_macro_attribute]
62+
pub fn track(_: TokenStream, stream: TokenStream) -> TokenStream {
63+
let block = syn::parse_macro_input!(stream as syn::ItemImpl);
64+
track::expand(block)
65+
.unwrap_or_else(|err| err.to_compile_error())
66+
.into()
67+
}

macros/src/memoize.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use super::*;
2+
3+
/// Make a type trackable.
4+
pub fn expand(mut func: syn::ItemFn) -> Result<proc_macro2::TokenStream> {
5+
let name = func.sig.ident.to_string();
6+
7+
let mut args = vec![];
8+
let mut asserts = vec![];
9+
for input in &func.sig.inputs {
10+
let typed = match input {
11+
syn::FnArg::Typed(typed) => typed,
12+
syn::FnArg::Receiver(_) => {
13+
bail!(input, "methods are not supported")
14+
}
15+
};
16+
17+
let ident = match &*typed.pat {
18+
syn::Pat::Ident(ident) => ident,
19+
_ => bail!(typed.pat, "only simple identifiers are supported"),
20+
};
21+
22+
asserts.push(quote_spanned! { ident.span() => assert_hash(&#ident); });
23+
args.push(ident);
24+
}
25+
26+
func.block.stmts.insert(0, parse_quote! { {
27+
println!("calling {}", #name);
28+
fn assert_hash<T: std::hash::Hash>(_: &T) {}
29+
// #(#asserts)*
30+
} });
31+
32+
Ok(quote! { #func })
33+
}

macros/src/track.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use super::*;
2+
3+
/// Make a type trackable.
4+
pub fn expand(block: syn::ItemImpl) -> Result<proc_macro2::TokenStream> {
5+
let ty = &block.self_ty;
6+
7+
let mut tracked_methods = vec![];
8+
for item in &block.items {
9+
let method = match item {
10+
syn::ImplItem::Method(method) => method,
11+
_ => bail!(item, "only methods are supported"),
12+
};
13+
14+
let mut tracked = method.clone();
15+
let name = &tracked.sig.ident;
16+
17+
let mut inputs = tracked.sig.inputs.iter();
18+
let receiver = match inputs.next() {
19+
Some(syn::FnArg::Receiver(recv)) => recv,
20+
_ => bail!(tracked, "method must take self"),
21+
};
22+
23+
if receiver.reference.is_none() || receiver.mutability.is_some() {
24+
bail!(receiver, "must take self by shared reference");
25+
}
26+
27+
let mut args = vec![];
28+
for input in inputs {
29+
let pat = match input {
30+
syn::FnArg::Typed(typed) => &*typed.pat,
31+
syn::FnArg::Receiver(_) => unreachable!("unexpected second self"),
32+
};
33+
34+
let ident = match pat {
35+
syn::Pat::Ident(ident) => ident,
36+
_ => bail!(pat, "only simple identifiers are supported"),
37+
};
38+
39+
args.push(ident);
40+
}
41+
42+
tracked.block = parse_quote! { { self.0.#name(#(#args),*) } };
43+
tracked_methods.push(tracked);
44+
}
45+
46+
let track_impl = quote! {
47+
impl comemo::Track for #ty {
48+
type Surface = Surface;
49+
50+
fn surface(&self) -> &Surface {
51+
unsafe { &*(self as *const Self as *const Surface) }
52+
}
53+
}
54+
55+
#[repr(transparent)]
56+
struct Surface(#ty);
57+
58+
impl Surface {
59+
#(#tracked_methods)*
60+
}
61+
};
62+
63+
Ok(quote! {
64+
#block
65+
const _: () = { #track_impl };
66+
})
67+
}

rustfmt.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
unstable_features = true
2+
3+
overflow_delimited_expr = true
4+
spaces_around_ranges = true
5+
use_field_init_shorthand = true
6+
merge_derives = false
7+
8+
max_width = 90
9+
struct_lit_width = 40
10+
chain_width = 70
11+
single_line_if_else_max_width = 60

src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*!
2+
Tracked memoization.
3+
*/
4+
5+
pub use comemo_macros::{memoize, track};
6+
7+
use std::ops::Deref;
8+
9+
/// Tracks accesses to a value.
10+
///
11+
/// Encapsulates a reference to a value and tracks all accesses to it.
12+
/// The only methods accessible on `Tracked<T>` are those defined in an impl
13+
/// block for `T` annotated with [`#[track]`](track).
14+
///
15+
/// ```
16+
/// use comemo::Track;
17+
///
18+
/// let image = Image::random(20, 40);
19+
/// let sentence = describe(image.track());
20+
/// println!("{sentence}");
21+
/// ```
22+
pub struct Tracked<'a, T>(&'a T)
23+
where
24+
T: Track;
25+
26+
/// A trackable type.
27+
pub trait Track: Sized {
28+
/// Start tracking a value.
29+
fn track(&self) -> Tracked<Self> {
30+
Tracked(self)
31+
}
32+
33+
/// The tracked API surface of the type.
34+
///
35+
/// This is an implementation detail, which shouldn't directly be used.
36+
#[doc(hidden)]
37+
type Surface;
38+
39+
/// Access the tracked API surface.
40+
///
41+
/// This is an implementation detail, which shouldn't directly be used.
42+
#[doc(hidden)]
43+
fn surface(&self) -> &Self::Surface;
44+
}
45+
46+
impl<'a, T> Deref for Tracked<'a, T>
47+
where
48+
T: Track,
49+
{
50+
type Target = T::Surface;
51+
52+
fn deref(&self) -> &Self::Target {
53+
self.0.surface()
54+
}
55+
}

src/main.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use comemo::{Track, Tracked};
2+
3+
#[rustfmt::skip]
4+
fn main() {
5+
let fonts = Fonts(vec![
6+
Face { name: "mathup", style: Style::Normal },
7+
Face { name: "mathbf", style: Style::Bold },
8+
Face { name: "mathit", style: Style::Italic },
9+
]);
10+
11+
let par = Paragraph(vec![
12+
Segment { text: "HELLO ".into(), font: "mathup" },
13+
Segment { text: "WORLD".into(), font: "mathit" },
14+
]);
15+
16+
let shaped = shape(&par, fonts.track());
17+
println!("{shaped}");
18+
19+
let shaped = shape(&par, fonts.track());
20+
println!("{shaped}");
21+
}
22+
23+
/// Shape a piece of text with fonts.
24+
#[comemo::memoize]
25+
fn shape(par: &Paragraph, fonts: Tracked<Fonts>) -> String {
26+
let mut shaped = String::new();
27+
for piece in &par.0 {
28+
let font = fonts.get(&piece.font).unwrap();
29+
for c in piece.text.chars() {
30+
shaped.push(font.map(c));
31+
}
32+
}
33+
shaped
34+
}
35+
36+
/// A paragraph of text in different fonts.
37+
#[derive(Debug, Clone, Hash)]
38+
struct Paragraph(Vec<Segment>);
39+
40+
/// A segment of text with consistent font.
41+
#[derive(Debug, Clone, Hash)]
42+
struct Segment {
43+
text: String,
44+
font: &'static str,
45+
}
46+
47+
/// A font database.
48+
struct Fonts(Vec<Face>);
49+
50+
#[comemo::track]
51+
impl Fonts {
52+
/// Select a face by family name.
53+
fn get(&self, family: &str) -> Option<&Face> {
54+
self.0.iter().find(|font| font.name == family)
55+
}
56+
}
57+
58+
/// A font face.
59+
#[derive(Debug, Clone, Hash)]
60+
struct Face {
61+
name: &'static str,
62+
style: Style,
63+
}
64+
65+
/// The style of a font face.
66+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
67+
enum Style {
68+
Normal,
69+
Bold,
70+
Italic,
71+
}
72+
73+
impl Face {
74+
fn map(&self, c: char) -> char {
75+
let base = match self.style {
76+
Style::Normal => 0x41,
77+
Style::Bold => 0x1D400,
78+
Style::Italic => 0x1D434,
79+
};
80+
81+
if c.is_ascii_alphabetic() {
82+
std::char::from_u32(base + (c as u32 - 0x41)).unwrap()
83+
} else {
84+
c
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)