nyanoblog/macros/src/define_json_ns.rs
fef 3d71b04338
ap: refactor to use custom vocab and iri types
IndexVocabulary and IriIndex have been replaced
with NyaVocabulary and NyaIri, and the JSON-LD
namespaces are defined through a proc macro now
2023-08-10 06:41:16 +02:00

523 lines
17 KiB
Rust

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use std::iter::Peekable;
use std::sync::atomic::{AtomicIsize, Ordering};
use crate::util::*;
// "forgive me, rustc, for i have sinned"
//
// good luck to whoever is trying to read not to mention understand this lmao
/// This is a global counter for unique indices because every invocation
/// of this macro should yield an enum with unique numeric representations
static INDEX: AtomicIsize = AtomicIsize::new(1);
pub fn invoke(input: TokenStream) -> Result<TokenStream> {
let mut stream = input.into_iter().peekable();
// parse the thing
let (mut meta, mut classes, mut props) = (None, None, None);
while let Some(tt) = stream.next() {
if let TokenTree::Ident(ident) = &tt {
let name = ident.to_string();
match name.as_str() {
"meta" => {
let body = eat_group(&mut stream, Delimiter::Brace)?;
if meta.replace(eat_meta(body.stream())?).is_some() {
return err("Duplicate definition of metadata", ident.span());
}
}
"class" => {
let body = eat_group(&mut stream, Delimiter::Brace)?;
let body = eat_enum_members(body.stream())?;
if classes.replace((body, ident.span())).is_some() {
return err("Duplicate class definition", ident.span());
}
}
"prop" => {
let body = eat_group(&mut stream, Delimiter::Brace)?;
let body = eat_enum_members(body.stream())?;
if props.replace((body, ident.span())).is_some() {
return err("Duplicate props definition", ident.span());
}
}
name => return err(format!("Unknown section \"{name}\""), ident.span()),
}
eat_maybe_comma(&mut stream);
}
}
// now spit out some new tokens
let meta = meta.ok_or_else(|| error("Missing meta block", None))?;
let mut ts = TokenStream::new();
if let Some((classes, span)) = classes {
ts.extend(compile_enum(
&meta,
make_ident_tt("__Class", None),
&classes,
span,
));
}
if let Some((props, span)) = props {
ts.extend(compile_enum(
&meta,
make_ident_tt("__Prop", None),
&props,
span,
));
}
let mut module = rust_code!(pub mod );
module.extend([meta.mod_name, make_group_tt(Delimiter::Brace, ts)]);
//println!("{module}");
Ok(module)
}
struct NsMeta {
mod_name: TokenTree,
iri_base: String,
alias_base: Option<String>,
}
#[derive(Clone)]
struct EnumMember {
name: Ident,
}
//
// parser
//
// meta { key1 = val1, key2 = val2, ... }
fn eat_meta(stream: TokenStream) -> Result<NsMeta> {
let mut stream = stream.into_iter().peekable();
let mut mod_name = None;
let mut iri_base = None;
let mut alias_base = None;
let mut had_preceding_comma = true;
while let Some(ident) = eat_ident_or_end(&mut stream)? {
if !had_preceding_comma {
return err("Expected a comma", ident.span());
}
let name = ident.to_string();
match name.as_str() {
"mod_name" => {
if mod_name.replace(eat_assign_ident(&mut stream)?).is_some() {
return err("Duplicate definition of key \"mod_name\"", ident.span());
}
}
"iri_base" => {
if iri_base.replace(eat_assign_string(&mut stream)?).is_some() {
return err("Duplicate definition of key \"iri_base\"", ident.span());
}
}
"alias_base" => {
if alias_base
.replace(eat_assign_string(&mut stream)?)
.is_some()
{
return err("Duplicate definition of key \"alias_base\"", ident.span());
}
}
name => return err(format!("Unknown key \"{name}\""), ident.span()),
}
had_preceding_comma = eat_maybe_comma(&mut stream);
}
Ok(NsMeta {
mod_name: mod_name.ok_or_else(|| error("Missing key \"mod_name\"", None))?,
iri_base: iri_base.ok_or_else(|| error("Missing key \"iri_base\"", None))?,
alias_base,
})
}
// qualifier { Member1, Member2, ... }
fn eat_enum_members(stream: TokenStream) -> Result<Vec<EnumMember>> {
let mut stream = stream.into_iter().peekable();
let mut members = Vec::new();
let mut had_preceding_comma = true;
while let Some(member) = eat_enum_member(&mut stream)? {
if !had_preceding_comma {
return err_unexpected(",", &member.name, member.name.span());
}
members.push(member);
had_preceding_comma = eat_maybe_comma(&mut stream);
}
Ok(members)
}
fn eat_enum_member(stream: &mut impl TtIter) -> Result<Option<EnumMember>> {
match stream.next() {
Some(tt) => {
if let TokenTree::Ident(name) = tt {
Ok(Some(EnumMember { name }))
} else {
err_unexpected("ident", &tt, tt.span())
}
}
None => Ok(None),
}
}
fn eat_assign_ident(stream: &mut Peekable<impl TtIter>) -> Result<TokenTree> {
eat_eq(stream)?;
Ok(TokenTree::Ident(eat_ident(stream)?))
}
fn eat_assign_string(stream: &mut impl TtIter) -> Result<String> {
let _ = eat_eq(stream)?;
match stream.next() {
Some(TokenTree::Literal(literal)) => {
let s = literal.to_string();
let sb = s.as_bytes();
if sb.len() >= 2 && sb[0] == b'"' && sb[sb.len() - 1] == b'"' {
Ok(String::from_utf8(Vec::from(&sb[1..(sb.len() - 1)])).unwrap())
} else {
err_unexpected("string", &literal, literal.span())
}
}
Some(tt) => err_unexpected("string", &tt, tt.span()),
None => err_end(),
}
}
fn eat_group(stream: &mut impl TtIter, delimiter: impl Into<Option<Delimiter>>) -> Result<Group> {
match stream.next() {
Some(TokenTree::Group(grp)) => {
if let Some(delim) = delimiter.into() {
if grp.delimiter() == delim {
Ok(grp)
} else {
err("Expected a `{`", grp.span())
}
} else {
Ok(grp)
}
}
Some(tt) => err_unexpected("{", &tt, tt.span()),
None => err_end(),
}
}
fn eat_ident(stream: &mut impl TtIter) -> Result<Ident> {
eat_ident_or_end(stream).and_then(|o| o.ok_or_else(error_end))
}
fn eat_ident_or_end(stream: &mut impl TtIter) -> Result<Option<Ident>> {
match stream.next() {
Some(TokenTree::Ident(ident)) => Ok(Some(ident)),
Some(tt) => err_unexpected("ident", &tt, tt.span()),
None => Ok(None),
}
}
fn eat_eq(stream: &mut impl TtIter) -> Result<TokenTree> {
match stream.next() {
Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => Ok(TokenTree::Punct(punct)),
Some(tt) => err_unexpected("=", &tt, tt.span()),
None => err_end(),
}
}
fn eat_maybe_comma(stream: &mut Peekable<impl TtIter>) -> bool {
stream
.next_if(|tt| {
if let TokenTree::Punct(punct) = tt {
punct.as_char() == ','
} else {
false
}
})
.is_some()
}
//
// compiler
//
fn compile_enum(
meta: &NsMeta,
name: TokenTree,
members: &[EnumMember],
kw_span: Span,
) -> TokenStream {
// enum Name { Member1, Member2, ... }
let mut ts = rust_code!(
#[derive(Copy, Clone, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub
);
ts.extend([
make_ident_tt("enum", kw_span),
name.clone(),
make_group_tt(
Delimiter::Brace,
members.iter().flat_map(|m| {
[
TokenTree::Ident(m.name.clone()),
TokenTree::Punct(Punct::new('=', Spacing::Alone)),
TokenTree::Literal(Literal::isize_unsuffixed(
INDEX.fetch_add(1, Ordering::Relaxed),
)),
TokenTree::Punct(Punct::new(',', Spacing::Alone)),
]
}),
),
]);
ts.extend(rust_code!(pub use ));
ts.extend([name.clone()]);
ts.extend(rust_code!(::*;));
// impl Enum { ... }
ts.extend(compile_internal_impl(meta, &name, members));
// impl HasContext for Enum { ... }
ts.extend(compile_trait_impl(meta, &name, members));
ts
}
fn compile_internal_impl(meta: &NsMeta, name: &TokenTree, members: &[EnumMember]) -> TokenStream {
let mut ts = rust_code!(impl);
ts.extend([name.clone()]);
let impl_body = {
let mut impl_body = rust_code!(const __MEMBERS: );
impl_body.extend([
// [(Self, Iri<'static>); $n]
make_group_tt(
Delimiter::Bracket,
[
make_group_tt(
Delimiter::Parenthesis,
rust_code!(Self, ::iref::Iri<'static>),
),
make_punct_alone(';'),
TokenTree::Literal(Literal::usize_unsuffixed(members.len())),
],
),
make_punct_alone('='),
]);
impl_body.extend([
// [ (Enum::Name1, iri!("iri1")), (Enum::Name2, iri!("iri2")), ... ]
make_group_tt(
Delimiter::Bracket,
members.iter().flat_map(|memb| {
[
make_group_tt(Delimiter::Parenthesis, {
let mut ts = TokenStream::from_iter([
name.clone(),
make_punct_joint(':'),
make_punct_alone(':'),
TokenTree::Ident(memb.name.clone()),
make_punct_alone(','),
]);
ts.extend(compile_iri_macro_call(&meta.iri_base, Some(memb)));
ts
}),
make_punct_alone(','),
]
.into_iter()
}),
),
TokenTree::Punct(Punct::new(';', Spacing::Alone)),
]);
impl_body.extend(rust_code!(fn __from_str(s: &str) -> Option<Self>));
let fn_body = {
let mut fn_body = rust_code!(match s);
let match_body = {
let mut match_body = TokenStream::new();
for memb in members {
let memb_name = memb.name.to_string();
match_body.extend([
TokenTree::Literal(Literal::string(&memb_name)),
make_punct_joint('='),
make_punct_alone('>'),
make_ident_tt("Some", None),
make_group_tt(
Delimiter::Parenthesis,
[
name.clone(),
make_punct_joint(':'),
make_punct_alone(':'),
TokenTree::Ident(memb.name.clone()),
],
),
make_punct_alone(','),
]);
}
match_body.extend(rust_code!(_ => None,));
make_group_ts(Delimiter::Brace, match_body)
};
fn_body.extend(match_body);
make_group_ts(Delimiter::Brace, fn_body)
};
impl_body.extend(fn_body);
make_group_ts(Delimiter::Brace, impl_body)
};
ts.extend(impl_body);
ts
}
fn compile_trait_impl(meta: &NsMeta, name: &TokenTree, members: &[EnumMember]) -> TokenStream {
// impl HasContext for Enum
let mut ts = rust_code!(impl);
ts.extend(absolute_path(None, rust_code!(ap::vocab::HasContext)));
ts.extend(rust_code!(for));
ts.extend([name.clone()]);
let impl_body = {
// const ... = ...; const ... = ...; ...
let mut impl_body = compile_impl_const_members(meta, members);
// fn from_index(...) -> ... { ... }
impl_body.extend(compile_impl_from_index(name, members));
impl_body.extend(rust_code!(
fn as_index(&self) -> usize {
*self as usize
}
));
// fn from_iri(...) -> ... { ... }
impl_body.extend(compile_impl_from_iri());
make_group_ts(Delimiter::Brace, impl_body)
};
ts.extend(impl_body);
ts
}
fn compile_impl_const_members(meta: &NsMeta, members: &[EnumMember]) -> TokenStream {
let mut ts = TokenStream::new();
ts.extend(rust_code!(const COUNT: usize = ));
ts.extend([
TokenTree::Literal(Literal::usize_suffixed(members.len())),
TokenTree::Punct(Punct::new(';', Spacing::Alone)),
]);
ts.extend(rust_code!(const OFFSET: usize = ));
ts.extend([
TokenTree::Literal(Literal::usize_suffixed(
INDEX.load(Ordering::Relaxed) as usize - members.len(),
)),
TokenTree::Punct(Punct::new(';', Spacing::Alone)),
]);
ts.extend(rust_code!(const IRI_BASE: ::iref::Iri<'static> = ));
ts.extend(compile_iri_macro_call(&meta.iri_base, None));
ts.extend([make_punct_alone(';')]);
ts.extend(rust_code!(const ALIAS_BASE: ::core::option::Option<::iref::Iri<'static>> =));
match meta.alias_base.as_ref() {
Some(s) => {
ts.extend(rust_code!(Some));
ts.extend(make_group_ts(
Delimiter::Parenthesis,
compile_iri_macro_call(s, None),
));
}
None => ts.extend(rust_code!(None)),
}
ts.extend(rust_code!(;));
// this is so we get a ref with static lifetime; see compile_internal_impl()
ts.extend(rust_code!(
const MEMBERS: &'static [(Self, ::iref::Iri<'static>)] = &Self::__MEMBERS;
));
ts
}
fn compile_impl_from_index(name: &TokenTree, members: &[EnumMember]) -> TokenStream {
let mut ts = rust_code!(fn from_index(index: usize) -> Option<Self>);
let fn_body = {
let mut fn_body = rust_code!(match index);
let match_body = {
let mut match_body = TokenStream::new();
for member in members {
match_body.extend(rust_code!(x if x == ));
match_body.extend([
name.clone(),
make_punct_joint(':'),
make_punct_alone(':'),
TokenTree::Ident(member.name.clone()),
]);
match_body.extend(rust_code!(as usize => Some));
match_body.extend(make_group_ts(
Delimiter::Parenthesis,
[
name.clone(),
make_punct_joint(':'),
make_punct_alone(':'),
TokenTree::Ident(member.name.clone()),
],
));
match_body.extend([make_punct_alone(',')]);
}
match_body.extend(rust_code!(_ => None,));
make_group_ts(Delimiter::Brace, match_body)
};
fn_body.extend(match_body);
make_group_ts(Delimiter::Brace, fn_body)
};
ts.extend(fn_body);
ts
}
fn compile_impl_from_iri() -> TokenStream {
rust_code!(
fn from_iri(iri: &::iref::Iri) -> Option<Self> {
let iri = iri.as_str();
let iri_base = Self::IRI_BASE.as_str();
if iri.starts_with(iri_base) {
Self::__from_str(unsafe {
::std::str::from_utf8_unchecked(&iri.as_bytes()[iri_base.len()..])
})
} else if let Some(alias_base) = Self::ALIAS_BASE.as_ref() {
let alias_base = alias_base.as_str();
if iri.starts_with(alias_base) {
Self::__from_str(unsafe {
::std::str::from_utf8_unchecked(&iri.as_bytes()[alias_base.len()..])
})
} else {
None
}
} else {
None
}
}
)
}
fn compile_iri_macro_call(iri_base: &str, member: Option<&EnumMember>) -> TokenStream {
let mut ts = rust_code!(::static_iref::iri!);
let iri = match member {
Some(member) => format!("{iri_base}{}", member.name),
None => String::from(iri_base),
};
ts.extend(make_group_ts(
Delimiter::Parenthesis,
[TokenTree::Literal(Literal::string(&iri))],
));
ts
}