mod handler;
pub(crate) mod user;
use std::collections::HashMap;
use std::io;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use crate::build::BuildError;
use crate::codegen::{CodegenError, Features};
use crate::parser::{Program, Relation};
pub(crate) fn gen_input_module(
program: &Program,
features: &Features,
) -> Result<TokenStream, CodegenError> {
let edbs = program.edbs();
let string_intern = features.string_intern();
let preamble = gen_preamble(program, features);
let input_structs = edbs
.iter()
.map(|rel| handler::gen_input_struct(rel, program.facts().get(rel.name()), string_intern))
.collect::<Result<Vec<_>, _>>()?;
let inputs_container = gen_inputs_container(&edbs);
Ok(quote! {
#preamble
#(#input_structs)*
#inputs_container
})
}
pub(crate) fn pascal_case(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut capitalize = true;
for c in name.chars() {
if c == '_' || c == '-' {
capitalize = true;
continue;
}
if capitalize {
out.extend(c.to_uppercase());
capitalize = false;
} else {
out.push(c);
}
}
out
}
pub(crate) fn inputs_field_ident(rel: &Relation) -> Ident {
format_ident!("in_{}", rel.name())
}
pub(crate) fn results_field_ident(rel: &Relation) -> Ident {
format_ident!("{}", rel.name())
}
pub(crate) fn printsize_field_ident(rel: &Relation) -> Ident {
format_ident!("{}_size", rel.name())
}
pub(crate) fn user_struct_ident(rel: &Relation) -> Ident {
format_ident!("{}", pascal_case(rel.name()))
}
pub(crate) fn input_struct_ident(rel: &Relation) -> Ident {
format_ident!("{}Input", pascal_case(rel.name()))
}
pub(crate) fn validate_api_surface(program: &Program) -> Result<(), BuildError> {
let mut fields: HashMap<String, String> = HashMap::new();
for rel in program.output_idbs() {
ensure_plain_ident(rel.name(), rel.raw_name(), "a results field")?;
ensure_unique(
&mut fields,
rel.name().to_string(),
rel.raw_name(),
"results field",
)?;
}
for rel in program.printsize_idbs() {
let field = printsize_field_ident(rel).to_string();
ensure_unique(&mut fields, field, rel.raw_name(), "results field")?;
}
let mut aliases: HashMap<String, String> = HashMap::new();
for rel in user::collect_user_rels(program) {
let stem = pascal_case(rel.name());
ensure_plain_ident(&stem, rel.raw_name(), "a `rel::` type alias")?;
ensure_unique(&mut aliases, stem, rel.raw_name(), "`rel::` type alias")?;
}
let mut input_structs: HashMap<String, String> = HashMap::new();
for rel in program.edbs() {
let stem = input_struct_ident(rel).to_string();
ensure_unique(
&mut input_structs,
stem,
rel.raw_name(),
"input-handler struct",
)?;
}
Ok(())
}
fn ensure_plain_ident(name: &str, raw_name: &str, what: &str) -> Result<(), BuildError> {
if syn::parse_str::<syn::Ident>(name).is_err() {
return Err(BuildError::from(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"relation `{raw_name}` cannot be exposed through the library API: \
`{name}` is not usable as {what} (it is a Rust keyword) — rename \
the relation, or drop its `.output`/`.printsize` directive"
),
)));
}
Ok(())
}
fn ensure_unique(
owners: &mut HashMap<String, String>,
ident: String,
raw_name: &str,
what: &str,
) -> Result<(), BuildError> {
if let Some(prev) = owners.insert(ident.clone(), raw_name.to_string()) {
return Err(BuildError::from(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"relations `{prev}` and `{raw_name}` would both surface as the \
{what} `{ident}` in the generated library API — rename one of them"
),
)));
}
Ok(())
}
fn gen_preamble(program: &Program, features: &Features) -> TokenStream {
let facts = program.facts();
let edbs = program.edbs();
let has_any_inline = edbs.iter().any(|rel| facts.contains_key(rel.name()));
let needs_ordered_float = edbs
.iter()
.any(|rel| rel.data_type().iter().any(|dt| dt.is_float()));
let intern_import = if features.string_intern() {
quote! {
use super::intern;
use lasso::Spur;
}
} else {
quote! {}
};
let ordered_float_import = if needs_ordered_float {
quote! { use ordered_float::OrderedFloat; }
} else {
quote! {}
};
let semiring_one_import = if has_any_inline {
quote! { use super::SEMIRING_ONE; }
} else {
quote! {}
};
quote! {
use differential_dataflow::input::InputSession;
use super::{Diff, Ts};
#semiring_one_import
#intern_import
#ordered_float_import
}
}
fn gen_inputs_container(edbs: &[&Relation]) -> TokenStream {
if edbs.is_empty() {
return quote! {};
}
let fields: Vec<TokenStream> = edbs
.iter()
.map(|rel| {
let f = inputs_field_ident(rel);
let ty = input_struct_ident(rel);
quote! { pub #f: #ty }
})
.collect();
let fn_params: Vec<TokenStream> = edbs
.iter()
.map(|rel| {
let p = format_ident!("h_{}", rel.name());
let ty = input_struct_ident(rel);
quote! { #p: #ty }
})
.collect();
let inits: Vec<TokenStream> = edbs
.iter()
.map(|rel| {
let f = inputs_field_ident(rel);
let p = format_ident!("h_{}", rel.name());
quote! { #f: #p }
})
.collect();
let per_field = |method: TokenStream| -> Vec<TokenStream> {
edbs.iter()
.map(|rel| {
let f = inputs_field_ident(rel);
quote! { self.#f.#method; }
})
.collect()
};
let apply_inline = per_field(quote! { apply_inline(index) });
let close = per_field(quote! { close() });
let advance = per_field(quote! { advance_to(t) });
let flush = per_field(quote! { flush() });
quote! {
pub(crate) struct Inputs {
#(#fields,)*
}
impl Inputs {
pub fn new(#(#fn_params),*) -> Self {
Self { #(#inits,)* }
}
pub fn apply_inline_all(&mut self, index: usize) {
#(#apply_inline)*
}
pub fn close_all(&mut self) {
#(#close)*
}
pub fn advance_to_all(&mut self, t: Ts) {
#(#advance)*
}
pub fn flush_all(&mut self) {
#(#flush)*
}
}
}
}
#[cfg(test)]
mod api_surface_tests {
use std::collections::HashMap;
use super::{ensure_plain_ident, ensure_unique, pascal_case};
#[test]
fn keywords_are_rejected_as_plain_idents() {
for kw in [
"type", "match", "in", "loop", "self", "Self", "crate", "super", "yield", "try",
] {
assert!(
ensure_plain_ident(kw, kw, "a results field").is_err(),
"keyword {kw:?} should be rejected as a verbatim API ident"
);
}
}
#[test]
fn ordinary_names_are_accepted() {
for name in ["varpointsto", "method_lookup", "crate_", "self_", "type_"] {
assert!(
ensure_plain_ident(name, name, "a results field").is_ok(),
"name {name:?} should be accepted"
);
}
}
#[test]
fn duplicate_idents_are_rejected() {
let mut owners = HashMap::new();
ensure_unique(&mut owners, "x_size".into(), "x_size", "results field").unwrap();
let err = ensure_unique(&mut owners, "x_size".into(), "x", "results field").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("x_size") && msg.contains('x'), "{msg}");
}
#[test]
fn pascal_namespace_hazards_are_caught() {
assert_eq!(pascal_case("foo_bar"), pascal_case("foo__bar"));
let mut owners = HashMap::new();
ensure_unique(
&mut owners,
pascal_case("foo_bar"),
"foo_bar",
"`rel::` type alias",
)
.unwrap();
assert!(
ensure_unique(
&mut owners,
pascal_case("foo__bar"),
"foo__bar",
"`rel::` type alias"
)
.is_err()
);
assert_eq!(pascal_case("self"), "Self");
assert!(ensure_plain_ident(&pascal_case("self"), "self", "a `rel::` type alias").is_err());
}
}