diff --git a/Cargo.lock b/Cargo.lock index 1a7e56e94..78a198b5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,6 +446,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bindlang" +version = "0.1.0" +dependencies = [ + "koto", + "koto_runtime", + "lang_bindings", + "lazy_static", + "quote", + "syn", +] + [[package]] name = "bit-vec" version = "0.6.3" @@ -1003,6 +1015,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dynfmt" version = "0.1.5" @@ -1691,6 +1709,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -1763,6 +1784,60 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "koto" +version = "0.7.0" +source = "git+https://github.com/llogiq/koto?branch=vtable#32f6537214e2f0a388515273226a06fbe1a0192c" +dependencies = [ + "koto_bytecode", + "koto_parser", + "koto_runtime", + "parking_lot", +] + +[[package]] +name = "koto_bytecode" +version = "0.7.0" +source = "git+https://github.com/llogiq/koto?branch=vtable#32f6537214e2f0a388515273226a06fbe1a0192c" +dependencies = [ + "koto_parser", + "smallvec", +] + +[[package]] +name = "koto_lexer" +version = "0.7.0" +source = "git+https://github.com/llogiq/koto?branch=vtable#32f6537214e2f0a388515273226a06fbe1a0192c" +dependencies = [ + "unicode-width", + "unicode-xid", +] + +[[package]] +name = "koto_parser" +version = "0.7.0" +source = "git+https://github.com/llogiq/koto?branch=vtable#32f6537214e2f0a388515273226a06fbe1a0192c" +dependencies = [ + "koto_lexer", +] + +[[package]] +name = "koto_runtime" +version = "0.7.0" +source = "git+https://github.com/llogiq/koto?branch=vtable#32f6537214e2f0a388515273226a06fbe1a0192c" +dependencies = [ + "downcast-rs", + "indexmap", + "koto_bytecode", + "koto_lexer", + "koto_parser", + "num_cpus", + "parking_lot", + "rustc-hash", + "smallvec", + "unicode-segmentation", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1772,6 +1847,14 @@ dependencies = [ "log", ] +[[package]] +name = "lang_bindings" +version = "0.1.0" +dependencies = [ + "chrono", + "koto", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2807,6 +2890,12 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -3433,6 +3522,7 @@ dependencies = [ "anyhow", "bimap", "bincode", + "bindlang", "bloomfilter", "chrono", "colored", @@ -3440,6 +3530,9 @@ dependencies = [ "env_logger", "fake", "humantime-serde", + "koto", + "koto_runtime", + "lang_bindings", "lazy_static", "log", "num", diff --git a/Cargo.toml b/Cargo.toml index 60ca881e6..74f0c76c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,7 @@ [workspace] members = [ + "bindlang", + "lang_bindings", "gen", "core", "synth", diff --git a/bindlang/Cargo.toml b/bindlang/Cargo.toml new file mode 100644 index 000000000..8292b50da --- /dev/null +++ b/bindlang/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bindlang" +version = "0.1.0" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["extra-traits", "full", "fold", "parsing", "proc-macro"] } +quote = "1.0" +koto = { git = "https://github.com/llogiq/koto", branch = "vtable" } +koto_runtime = { git = "https://github.com/llogiq/koto", branch = "vtable" } +lang_bindings = { path = "../lang_bindings" } +lazy_static = "1.4.0" diff --git a/bindlang/examples/ex.rs b/bindlang/examples/ex.rs new file mode 100644 index 000000000..6deb6bac3 --- /dev/null +++ b/bindlang/examples/ex.rs @@ -0,0 +1,42 @@ +use bindlang::{bindlang, bindlang_main}; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[bindlang] +pub fn bound(_b: bool, _i: i64, _f: f32, _u: u8) { + // nothing to see here +} + +#[bindlang] +pub fn anotherone() { + // still nothing +} + +#[bindlang] +#[derive(Clone, Debug, Default)] +pub struct MyType; + +//#[bindlang] +impl Display for MyType { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{:?}", self) + } +} + +#[bindlang] +impl MyType { + pub fn new() -> Self { MyType } + + pub fn answer(&self) -> usize { + 42 + } +} + +bindlang_main!(); + +fn main() -> Result<(), Box> { + let mut koto = koto::Koto::default(); + bindlang_init(&mut koto.prelude()); + koto.compile("import MyType\nMyType.new().answer()")?; + println!("{}", koto.run()?); + Ok(()) +} diff --git a/bindlang/src/lib.rs b/bindlang/src/lib.rs new file mode 100644 index 000000000..058f34fee --- /dev/null +++ b/bindlang/src/lib.rs @@ -0,0 +1,630 @@ +/// This crate supplies two procedural macros, one attribute and one bang. +/// +/// The whole language bindings have three moving parts: +/// +/// * The `FromValue`, `FromValueRef` and `IntoValue` traits from the `lang_bindings` +/// crate are there to get Rust values from and into koto (or borrow into them). +/// Since we bind our custom types as `ExternalValue`s, we need to implement the +/// eponymous trait. We also have a `StaticExternalValue` that doesn't need to +/// allocate +/// * impl blocks mostly get collected into virtual dispatch tables (VTables). Those +/// are static `ValueMap`s from function name to a wrapped function that takes care of +/// the value conversions +/// * associated or free functions go into a module, which is a table that is also +/// available from koto; functions without either a type or given module name go into +/// the `synth` module for now. You can manually override the module with +/// `#[bindlang("module_name")]`. Inherent constructors of both structs and enums are +/// also bound if both the type and all relevant fields are `pub` +/// +/// The [`#[bindlang]`]() proc_macro attribute is there to collect all items, and the +/// `bindlang_main!(..)` macro expands to the collected items. It creates both a number +/// of static VTables and the `bindlang_init(&mut ValueMap)` function that sets up the +/// modules. + +extern crate proc_macro; +extern crate quote; +extern crate syn; + +use proc_macro::TokenStream; +use quote::quote; +use syn::*; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +fn ident(s: &str) -> Ident { + syn::parse_str(s).unwrap() +} + +#[derive(Clone, Copy, Debug)] +enum BindingMode { + ByValue, + ByRef, + ByRefMut, +} + +impl From<&Type> for BindingMode { + fn from(ty: &Type) -> Self { + match ty { + Type::Reference(TypeReference { mutability: Some(_), .. }) => Self::ByRefMut, + Type::Reference(TypeReference { mutability: None, .. }) => Self::ByRef, + _ => Self::ByValue, + } + } +} + +impl From<&Field> for BindingMode { + fn from(f: &Field) -> Self { + Self::from(&f.ty) + } +} + +impl From<&FnArg> for BindingMode { + fn from(f: &FnArg) -> Self { + match &f { + FnArg::Receiver(Receiver { reference: Some(_), mutability: Some(_), ..}) => Self::ByRefMut, + FnArg::Receiver(Receiver { reference: Some(_), mutability: None, ..}) => Self::ByRef, + FnArg::Typed(PatType { ty, .. }) => Self::from(&**ty), + _ => Self::ByValue, + } + } +} + +#[derive(Debug)] +enum MethodArgs { + /// unit constructors (e.g. `MyStruct`, `My)Enum::UnitVariant` + Unit, + /// function call or tuple struct / variant, `self_ty` contains the self type for method calls + Tuple { + args: Vec, + self_ty: Option, + }, + /// named arguments, in given order + Named { names: Vec<(String, BindingMode)> }, +} + +/// all we need to wrap a method or constructor +#[derive(Debug)] +struct MethodSig { + /// This is the name on the Rust side of things (which may be different from koto's name, e.g. + /// `MyEnum::MyVariant` or `::my_function`) + path: String, + /// The arguments. There are 3 variants: Unit for unit structs/variants, Tuple for functions + /// and tuple structs/variants and Named for named structs/variants + args: MethodArgs, + /// a stringified representation of the required arguments for error reporting + inputs: String, +} + +fn is_public(vis: &Visibility) -> bool { + matches!(vis, Visibility::Public(_)) +} + +fn is_field_public(field: &Field) -> bool { + is_public(&field.vis) +} + +impl MethodSig { + fn from_fields(path: String, fields: &Fields, is_enum: bool) -> Option { + let result = Some(MethodSig { + inputs: path.clone(), //TODO: improve this + path, + args: match fields { + Fields::Named(FieldsNamed { named, .. }) => { + if is_enum || named.iter().all(is_field_public) { + MethodArgs::Named { + names: named + .iter() + .map(|f| (f.ident.as_ref().unwrap().to_string(), BindingMode::from(f))) + .collect(), + } + } else { + return None; + } + } + Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { + if is_enum || unnamed.iter().all(is_field_public) { + MethodArgs::Tuple { + args: unnamed.iter().map(BindingMode::from).collect(), + self_ty: None, + } + } else { + return None; + } + } + Fields::Unit => MethodArgs::Unit, + }, + }); + result + } + + fn from_sig(sig: &Signature, item_impl: Option<&ItemImpl>) -> Self { + let args = MethodArgs::Tuple { + args: sig.inputs.iter().map(BindingMode::from).collect(), + self_ty: sig.receiver().and_then(|_| { + item_impl.map(|i| { + let ty = &*i.self_ty; + (quote! { #ty }).to_string() + }) + }), + }; + let fn_ident = &sig.ident; + let path = match &item_impl { + // Some(_) if sig.receiver().is_some() => quote! { #fn_ident }, + Some(&ItemImpl { + ref self_ty, + trait_: Some((_, ref trait_ty, _)), + .. + }) => quote! { <#self_ty as #trait_ty>::#fn_ident }, + Some(&ItemImpl { + ref self_ty, + trait_: None, + .. + }) => quote! { #self_ty::#fn_ident }, + None => quote! { #fn_ident }, + } + .to_string(); + MethodSig { + path, + args, + inputs: (quote! { #sig }).to_string(), + } + } + + // create a tuple expr of (call, is_method) + // + // variants: + // * named struct / enum variant + // * unit struct / enum variant + // * tuple struct / enum variant + // * plain function call / plain trait function call + // * method call + + fn to_tuple(&self, koto_name: &str) -> Expr { + let path: Expr = parse_str(&self.path).unwrap(); + match &self.args { + MethodArgs::Tuple { args, self_ty } => { + let is_method = self_ty.is_some(); + let inputs = &self.inputs; + let idents: Vec<_> = (0..args.len()).map(|i| (ident(&format!("a{}", i)), ident(&format!("v{}", i)))).collect(); + let inner_idents = idents.iter().map(|(_, v)| v); + let mut expr: Expr = parse_quote! { #path(#(#inner_idents),*) }; + for (i, ((a, v), mode)) in idents.iter().zip(args.iter()).enumerate().rev() { + expr = match mode { + BindingMode::ByRef => { + parse_quote! { + ::lang_bindings::RefFromValue::ref_from_value( + &::lang_bindings::KeyPath::Index(#i, None), + #a, + |#v| #expr + )? + } + }, + BindingMode::ByRefMut => { + parse_quote! { + ::lang_bindings::RefFromValue::ref_mut_from_value( + &::lang_bindings::KeyPath::Index(#i, None), + #a, + |#v| #expr + )? + } + }, + BindingMode::ByValue => { + parse_quote! { + match ::lang_bindings::FromValue::from_value( + &::lang_bindings::KeyPath::Index(#i, None), + #a + )? { + #v => #expr + } + } + } + }; + } + let outer_idents = idents.iter().map(|(a, _)| a); + parse_quote! { + ( + #koto_name, + ::koto_runtime::ExternalFunction::new(|vm, args| match vm.get_args(args) { + [#(#outer_idents),*] => ::lang_bindings::IntoValue::into_value(#expr), + args => ::lang_bindings::fn_type_error(#koto_name, #inputs, args), + }, #is_method) + ) + } + } + MethodArgs::Named { names } => { + let idents: Vec<_> = names.iter().map(|n| ident(&n.0)).collect(); + let mut name_iter = names.iter().map(|(s, _)| s); + let mut inputs = name_iter.next().map_or(String::new(), ToOwned::to_owned); + for name in name_iter { + inputs += ", "; + inputs += name; + } + parse_quote! { + ( + #koto_name, + ::koto_runtime::ExternalFunction::new(|vm, args| { + match vm.get_args(args) { + [#(#idents),*] => ::lang_bindings::IntoValue::into_value(#path { + #(#idents: ::lang_bindings::FromValue::from_value( + &::lang_bindings::KeyPath::Field( + std::borrow::Cow::Borrowed(stringify!(#idents)), + None + ), + #idents + )?),* + }), + args => ::lang_bindings::fn_type_error(#koto_name, #inputs, args), + } + }, false) + ) + } + } + MethodArgs::Unit => parse_quote! { + ( + #koto_name, + ::koto_runtime::ExternalFunction::new(|vm, args| { + match vm.get_args(args) { + [] => ::lang_bindings::IntoValue::into_value(#path), + args => ::lang_bindings::fn_type_error(#koto_name, "()", args), + } + }, false) + ) + }, + } + } +} + +// We need some structures to keep stuff around +#[derive(Default)] +struct Context { + bare_fns: HashMap, + modules: HashMap>, + vtables: HashMap>, + types: HashMap, +} + +lazy_static::lazy_static! { + static ref CONTEXT: Arc> = Arc::new(Mutex::new(Context::default())); +} + +fn bind_sig(sig: &Signature, module: Option, item_impl: Option<&ItemImpl>) { + let ctx = &mut CONTEXT.lock().unwrap(); + let fn_ident = &sig.ident; + if let Some(i) = item_impl { + let self_ty = &*i.self_ty; + let self_ty_str = quote! { #self_ty }.to_string(); + if sig.receiver().is_some() { + &mut ctx.vtables + } else { + &mut ctx.modules + } + .entry(self_ty_str) + .or_insert_with(HashMap::new) + .insert(fn_ident.to_string(), MethodSig::from_sig(sig, item_impl)); + } else { + let mut module_entry; + if let Some(m) = module { + module_entry = ctx.modules.entry(m).or_insert_with(HashMap::new); + &mut module_entry + } else { + &mut ctx.bare_fns + } + .insert(fn_ident.to_string(), MethodSig::from_sig(sig, None)); + } +} + +fn get_attr_parens(attr: &Attribute) -> String { + attr.tokens + .to_string() + .trim_matches(&['(', ')'][..]) + .to_owned() +} + +fn get_module(attrs: &[Attribute]) -> Option { + attrs.iter().find_map(|a| { + if a.path.get_ident().map_or(false, |i| i == "bindlang") { + Some(get_attr_parens(a)) + } else { + None + } + }) +} + +// derive, (name, args) +static DERIVES: &[(&str, (&str, &[BindingMode]))] = &[ + ("Default", ("default", &[])), + ("Clone", ("clone", &[BindingMode::ByRef])), + ("Display", ("to_string", &[BindingMode::ByRef])), + //TODO: map other traits +]; + +fn get_derives(ty: String, attrs: &[Attribute]) { + for attr in attrs { + if attr.path.get_ident().map_or(false, |i| i == "derive") { + for derive in get_attr_parens(attr).split(',') { + let derive = derive.trim_matches(char::is_whitespace); + for (trt, (name, args)) in DERIVES { + if trt == &derive { + let name = *name; + let args = args.to_vec(); + let path = format!("<{} as {}>::{}", ty, trt, name); + let ctx = &mut CONTEXT.lock().unwrap(); + ctx.modules + .entry(ty.clone()) + .or_insert_with(HashMap::new) + .insert( + name.to_owned(), + MethodSig { + path, + args: MethodArgs::Tuple { + args, + self_ty: None, + }, + inputs: String::new(), + }, + ); + break; + }; + } + } + } + } +} + +fn get_pub_constructors(i: &Item) { + match i { + Item::Struct(ItemStruct { + vis, + ident, + //generics, + fields, + .. + }) => { + if !is_public(vis) { + return; + } + let ty = ident.to_string(); + if let Some(method_sig) = MethodSig::from_fields(ty.clone(), fields, false) { + CONTEXT.lock().unwrap().bare_fns.insert(ty, method_sig); + } + } + Item::Enum(ItemEnum { + vis, + ident, + variants, + .. + }) => { + if !is_public(vis) { + return; + } + let ty = ident.to_string(); + for variant in variants.iter() { + let path: String = ty.clone() + "::" + &variant.ident.to_string(); + if let Some(sig) = MethodSig::from_fields(path, &variant.fields, true) { + CONTEXT + .lock() + .unwrap() + .modules + .entry(ty.clone()) + .or_insert_with(HashMap::new) + .insert(variant.ident.to_string(), sig); + } + } + } + _ => {} + } +} + +fn create_map(items: &HashMap) -> Expr { + let members = items.iter().map(|(name, sig)| sig.to_tuple(name)); + parse_quote! { + ::koto_runtime::ValueMap::with_data(IntoIterator::into_iter([ + #(#members),* + ]).map(|(k,v): (&str, ::koto_runtime::ExternalFunction)| ( + ::koto_runtime::ValueKey::from(::koto_runtime::Value::Str(k.into())), + ::koto_runtime::Value::ExternalFunction(v) + )).collect::<::koto_runtime::ValueHashMap>()) + } +} + +fn create_vtable(vtable: &Ident, items: &HashMap) -> Item { + let map = create_map(items); + parse_quote! { + lazy_static::lazy_static! { + static ref #vtable: ::koto_runtime::ValueMap = #map; + } + } +} + +fn create_module(prelude: &Ident, name: &str, items: &HashMap) -> Stmt { + let map = create_map(items); + let lname = name.to_lowercase(); + parse_quote! { + #prelude.add_map(#lname, #map); + } +} + +//TODO: We may want to allow generics at some point, but we'd need to introduce a new type to parse them +fn create_binding(ty_name: &str, _generics: &str, ty_vtable: &Ident) -> impl Iterator { + let ty: Ident = ident(ty_name); + IntoIterator::into_iter( + //if generics.is_empty() { + [ + parse_quote! { + impl ::lang_bindings::StaticExternalValue for #ty { + fn type_str() -> &'static str { #ty_name } + } + }, + parse_quote! { + impl ::koto_runtime::ExternalValue for #ty { + fn value_type(&self) -> String { + String::from(#ty_name) + } + } + }, + parse_quote! { + impl ::lang_bindings::FromValue for #ty { + fn from_value( + key_path: &::lang_bindings::KeyPath<'_>, + value: &::koto_runtime::Value, + ) -> std::result::Result { + if let Some(result) = ::opt_from_value(key_path, value) { + return Ok(result); + } + if let ::koto_runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().write().downcast_mut::() { + Ok(v.clone()) + } else { + ::lang_bindings::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + ::lang_bindings::not_external_value(key_path, &*value) + } + } + } + }, + parse_quote! { + impl ::lang_bindings::RefFromValue for #ty { + fn ref_from_value R>( + key_path: &::lang_bindings::KeyPath<'_>, + value: &::koto_runtime::Value, + f: F, + ) -> std::result::Result { + if let ::koto_runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().read().downcast_ref::() { + Ok(f(v)) + } else { + ::lang_bindings::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + ::lang_bindings::not_external_value(key_path, &*value) + } + } + + fn ref_mut_from_value Fn(&'r Self) -> R>( + key_path: &::lang_bindings::KeyPath<'_>, + value: &::koto_runtime::Value, + f: F, + ) -> std::result::Result { + if let ::koto_runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().write().downcast_mut::() { + Ok(f(v)) + } else { + ::lang_bindings::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + ::lang_bindings::not_external_value(key_path, &*value) + } + } + } + }, + parse_quote! { + impl ::lang_bindings::IntoValue for #ty { + fn into_value(self) + -> std::result::Result<::koto_runtime::Value, ::koto_runtime::RuntimeError> { + Ok(::koto_runtime::Value::make_external_value(self, #ty_vtable.clone())) + } + } + }, + ], + ) +} + +#[proc_macro] +pub fn bindlang_main(mut code: TokenStream) -> TokenStream { + let Context { + ref bare_fns, + ref modules, + ref vtables, + ref types, + } = *CONTEXT.lock().unwrap(); + let prelude = ident("prelude"); + let vtable_idents = vtables + .keys() + .map(|ty| (ty.to_string(), vtable_ident(ty))) + .collect::>(); + let vtable_items = vtables + .iter() + .map(|(name, items)| create_vtable(&vtable_idents[name], items)); + let type_bindings = types + .iter() + .flat_map(|(name, generics)| create_binding(name, generics, &vtable_idents[name])); + let prelude_map = create_map(bare_fns); + let module_stmts = modules + .iter() + .map(|(name, items)| create_module(&prelude, name, items)); + //TODO we may insert the "synth" string below in the _code tokens + code.extend(TokenStream::from(quote! { + #(#vtable_items)* + #(#type_bindings)* + fn bindlang_init(#prelude: &mut ::koto_runtime::ValueMap) { + #prelude.add_map("synth", #prelude_map); + #(#module_stmts)* + } + })); + code +} + +fn vtable_ident(ty: &str) -> Ident { + ident(&format!("__BINDLANG_VTABLE_{}__", ty)) +} + +#[proc_macro_attribute] +pub fn bindlang(_attrs: TokenStream, code: TokenStream) -> TokenStream { + let code_cloned = code.clone(); + let input = parse_macro_input!(code_cloned as Item); + get_pub_constructors(&input); + match &input { + //TODO: bind trait impls + Item::Impl(item_impl) => { + for item in &item_impl.items { + if let ImplItem::Method(ImplItemMethod { ref sig, .. }) = item { + bind_sig(sig, get_module(&item_impl.attrs), Some(item_impl)); + } + } + } + Item::Fn(ItemFn { + ref attrs, ref sig, .. + }) => { + bind_sig(sig, get_module(attrs), None); + } + Item::Struct(ItemStruct { + attrs, + ident: ty, + generics: Generics { params, .. }, + .. + }) + | Item::Enum(ItemEnum { + attrs, + ident: ty, + generics: Generics { params, .. }, + .. + }) => { + // record the type, derives and generics (as String) + let ty_string = ty.to_string(); + get_derives(ty_string.clone(), attrs); + let mut ctx = CONTEXT.lock().unwrap(); + if !ctx.vtables.contains_key(&ty_string) { + ctx.vtables.insert(ty_string.clone(), HashMap::new()); + } + ctx.types.insert(ty_string, quote!(#params).to_string()); + } + //TODO: Do we want or need to bind other items? E.g. statics? + _ => (), //TODO: Report a usable error + } + // we emit the code as is + code +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 8f886d81a..85aceda67 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -35,3 +35,8 @@ bimap = { version = "0.6.0", features = [ "std" ] } humantime-serde = "1.0.1" bloomfilter = "1.0.5" dynfmt = { version = "0.1.5", features = [ "curly" ] } +# language bindings +lang_bindings = { path = "../lang_bindings" } +koto = { git = "https://github.com/llogiq/koto", branch = "vtable" } +koto_runtime = { git = "https://github.com/llogiq/koto", branch = "vtable" } +bindlang = { path = "../bindlang" } diff --git a/core/src/graph/mod.rs b/core/src/graph/mod.rs index 79df29528..c47ccf23a 100644 --- a/core/src/graph/mod.rs +++ b/core/src/graph/mod.rs @@ -105,12 +105,12 @@ pub type OnceInfallible = TryOnce>; macro_rules! derive_from { { - #[$attr:meta] + $(#[$attr:meta])* $vis:vis enum $id:ident { $($variant:ident$(($ty:ty))?,)* } } => { - #[$attr] + $(#[$attr])* $vis enum $id { $($variant$(($ty))?,)* } @@ -179,15 +179,31 @@ where } derive_from! { + #[bindlang::bindlang] #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Value { - Null(()), - Bool(bool), - Number(Number), - String(String), - DateTime(ChronoValue), - Object(BTreeMap), - Array(Vec), + Null(()), + Bool(bool), + Number(Number), + String(String), + DateTime(ChronoValue), + Object(BTreeMap), + Array(Vec), + } +} + +impl Display for Value { + //TODO: We want to make this something sensible for all types + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Null(_) => write!(f, "null"), + Self::Bool(bool) => write!(f, "{}", bool), + Self::Number(number) => write!(f, "{}", number), + Self::String(s) => write!(f, "{}", s), + Self::DateTime(v) => write!(f, "{:?}", v), + Self::Object(map) => write!(f, "{:?}", map), + Self::Array(v) => write!(f, "{:?}", v), + } } } diff --git a/core/src/graph/number.rs b/core/src/graph/number.rs index 2aef0e794..bfe604403 100644 --- a/core/src/graph/number.rs +++ b/core/src/graph/number.rs @@ -46,6 +46,7 @@ where } } +#[derive(Debug)] pub struct Incrementing { count: N, step: N, diff --git a/core/src/graph/string/faker.rs b/core/src/graph/string/faker.rs index f1f7b68b7..6af7ca158 100644 --- a/core/src/graph/string/faker.rs +++ b/core/src/graph/string/faker.rs @@ -13,6 +13,7 @@ use rand::RngCore; // this needs non-camel-case types because the fake crate has the same #[allow(non_camel_case_types)] +#[bindlang::bindlang] #[derive(Copy, Clone, Deserialize, Debug, Serialize, PartialEq, Eq)] /// a locale to look up names, addresses, etc. pub enum Locale { @@ -28,13 +29,38 @@ impl Default for Locale { } } +impl Display for Locale { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", match self { + Self::EN => "EN", + Self::FR_FR => "FR", + Self::ZH_TW => "TW", + Self::ZH_CN => "CN", + }) + } +} + /// The arguments for a faker +#[bindlang::bindlang] #[derive(Clone, Default, Deserialize, Debug, Serialize, PartialEq, Eq)] pub struct FakerArgs { #[serde(default)] locales: Vec, } +impl Display for FakerArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut locales = self.locales.iter(); + if let Some(l) = locales.next() { + write!(f, "{}", l)?; + for l in locales { + write!(f, ", {}", l)?; + } + } + Ok(()) + } +} + type FakerFunction = for<'r> fn(&'r mut dyn RngCore, &FakerArgs) -> String; macro_rules! fake_map_entry { diff --git a/core/src/lib.rs b/core/src/lib.rs index d36769423..efd426e8a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,7 +4,8 @@ map_first_last, box_patterns, error_iter, - try_blocks + try_blocks, + min_specialization )] #![allow(type_alias_bounds)] @@ -29,6 +30,7 @@ pub use error::Error; #[macro_use] pub mod schema; +use schema::BoolContent; pub use schema::{Content, Name, Namespace}; pub mod graph; @@ -36,3 +38,142 @@ pub use graph::Graph; pub mod compile; pub use compile::{Compile, Compiler}; + +#[test] +fn create_koto_config() { + fn _inner() -> Result<(), Box> { + let mut koto = koto::Koto::default(); + bindlang_init(&mut koto.prelude()); + let script = r#" +import synth.Namespace + +Namespace({ })"#; // for now leave the namespace empty + koto.compile(script)?; + let result_value = koto.run()?; + let namespace: Namespace = ::from_value( + &lang_bindings::KeyPath::Index(0, None), + &result_value, + )?; + assert_eq!(namespace, Namespace::default()); + Ok(()) + } + _inner().unwrap() +} + +use lang_bindings::{CustomFromValue, FromValue, KeyPath}; + +impl CustomFromValue for Namespace { + fn opt_from_value( + key_path: &lang_bindings::KeyPath<'_>, + value: &koto::runtime::Value, + ) -> Option { + if let koto::runtime::Value::Map(map) = value { + return Some(Namespace { + collections: map + .contents() + .data + .iter() + .map(|(key, value)| { + let name = if let koto::runtime::Value::Str(s) = key.value() { + s.as_str() + } else { + return None; + }; + Some(( + ::from_str(name).ok()?, + Content::from_value( + &KeyPath::Field( + std::borrow::Cow::Owned(name.to_owned()), + Some(key_path), + ), + value, + ) + .ok()?, + )) + }) + .collect::>()?, + }); + } + None + } +} + +impl CustomFromValue for Content { + fn opt_from_value( + key_path: &lang_bindings::KeyPath<'_>, + value: &koto::runtime::Value, + ) -> Option { + match value { + koto::runtime::Value::Bool(b) => Some(Content::Bool(BoolContent::Constant(*b))), + _ => None, + } + } +} + +#[test] +fn test_namespace_from_value() { + let mut koto = koto::Koto::default(); + koto.compile(r#"{"test", true}"#).unwrap(); + let result_value = koto.run().unwrap(); + let namespace: Namespace = ::from_value( + &lang_bindings::KeyPath::Index(0, None), + &result_value, + ) + .unwrap(); + assert_eq!( + namespace, + Namespace { + collections: std::iter::once(( + ::from_str("test").unwrap(), + Content::Bool(BoolContent::Constant(true)) + )) + .collect() + } + ) +} + +// this is a stop-gap measure to satisfy the `Display` requirements of koto's +// `ExternalValue` trait by forwarding to `Debug`. +macro_rules! debug_display { + ($($ty:ty),*) => { + $( + impl std::fmt::Display for $ty { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{:?}", self) + } + } + )* + } +} + +debug_display!( + schema::content::ChronoValue, + schema::content::DateTimeContent, + schema::content::SameAsContent, + schema::series::SeriesContent, + schema::series::SeriesVariant, + schema::series::Poisson, + schema::series::Incrementing, + schema::series::Cyclical, + schema::series::Zip, + schema::unique::UniqueAlgorithm, + schema::content::StringContent, + schema::content::Uuid, + schema::unique::UniqueContent, + schema::content::FormatContent, + schema::content::SerializedContent, + schema::content::ContentOrRef, + schema::content::JsonContent, + schema::content::TruncatedContent +); + +lang_bindings::external_value!(crate::schema::Categorical); +lang_bindings::external_value!(crate::schema::Categorical); + +bindlang::bindlang_main! { + use crate::graph::{string::{FakerArgs, Locale}, Value}; + use crate::schema::{ArrayContent, ChronoValue, ChronoValueType, ContentOrRef, DateTimeContent, FakerContent, FieldContent, FieldRef, + FormatContent, JsonContent, ObjectContent, OneOfContent, RegexContent, SameAsContent, SerializedContent, StringContent, TruncatedContent, Uuid, VariantContent, Weight, optional, required}; + use crate::schema::unique::{unique, UniqueAlgorithm, UniqueContent}; + use crate::schema::series::{Incrementing, SeriesContent, SeriesVariant, Poisson, Cyclical, Zip}; +} diff --git a/core/src/schema/content/array.rs b/core/src/schema/content/array.rs index 0462a21f5..d3b27fcf9 100644 --- a/core/src/schema/content/array.rs +++ b/core/src/schema/content/array.rs @@ -2,6 +2,7 @@ use super::prelude::*; use crate::graph::prelude::content::number::number_content::U64; use crate::schema::{NumberContent, RangeStep}; +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] @@ -10,6 +11,12 @@ pub struct ArrayContent { pub content: Box, } +impl Display for ArrayContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}; {}]", self.content, self.length) + } +} + impl ArrayContent { pub fn from_content_default_length(content: Content) -> Self { Self { diff --git a/core/src/schema/content/bool.rs b/core/src/schema/content/bool.rs index 22170dee6..b3e998879 100644 --- a/core/src/schema/content/bool.rs +++ b/core/src/schema/content/bool.rs @@ -2,6 +2,7 @@ use super::prelude::*; use super::Categorical; +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] @@ -21,12 +22,23 @@ impl BoolContent { } } +#[bindlang::bindlang] impl Default for BoolContent { fn default() -> Self { Self::Frequency(0.5) } } +impl Display for BoolContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Frequency(freq) => write!(f, "{}% true", freq), + Self::Constant(b) => write!(f, "{}", b), + Self::Categorical(cat) => write!(f, "{}", cat), + } + } +} + impl Compile for BoolContent { fn compile<'a, C: Compiler<'a>>(&'a self, _compiler: C) -> Result { let random_bool = match self { diff --git a/core/src/schema/content/categorical.rs b/core/src/schema/content/categorical.rs index 1ba87f260..943d3996e 100644 --- a/core/src/schema/content/categorical.rs +++ b/core/src/schema/content/categorical.rs @@ -1,9 +1,9 @@ use super::prelude::*; use std::collections::BTreeMap; -pub trait CategoricalType: Eq + Hash + Clone + FromStr + Ord + PartialOrd {} +pub trait CategoricalType: Eq + Hash + Clone + Display + FromStr + Ord + PartialOrd {} -impl CategoricalType for T where T: Eq + Hash + Clone + FromStr + Ord + PartialOrd {} +impl CategoricalType for T where T: Eq + Hash + Clone + Display + FromStr + Ord + PartialOrd {} #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(from = "CategoricalShadow")] @@ -36,6 +36,22 @@ where Ok(hm_final) } +impl Display for Categorical { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for (t, n) in self.seen.iter() { + write!( + f, + "{}{}×{}", + if std::mem::replace(&mut first, false) { "{" } else { ", "}, + n, + t + )?; + } + f.write_str("}") + } +} + impl Categorical { pub fn push(&mut self, t: T) { match self.seen.get_mut(&t) { diff --git a/core/src/schema/content/mod.rs b/core/src/schema/content/mod.rs index e900357d9..18046c62b 100644 --- a/core/src/schema/content/mod.rs +++ b/core/src/schema/content/mod.rs @@ -24,8 +24,8 @@ pub use number::{number_content, NumberContent, NumberContentKind, NumberKindExt mod string; pub use string::{ - ChronoValue, ChronoValueFormatter, ChronoValueType, DateTimeContent, FakerContent, - FakerContentArgument, RegexContent, StringContent, Uuid, + ChronoValue, ChronoValueFormatter, ChronoValueType, ContentOrRef, DateTimeContent, FakerContent, + FakerContentArgument, FormatContent, JsonContent, RegexContent, SerializedContent, StringContent, TruncatedContent, Uuid, }; mod array; @@ -51,6 +51,24 @@ use super::FieldRef; use crate::schema::content::series::SeriesContent; use crate::schema::unique::UniqueContent; +/// make an optional FieldContent +#[bindlang::bindlang(FieldContent)] +pub fn optional(content: Content) -> FieldContent { + FieldContent { + optional: true, + content: Box::new(content), + } +} + +/// make a non-optional FieldContent +#[bindlang::bindlang(FieldContent)] +pub fn required(content: Content) -> FieldContent { + FieldContent { + optional: true, + content: Box::new(content), + } +} + pub trait Find { fn find(&self, reference: I) -> Result<&C> where @@ -79,6 +97,7 @@ pub trait Find { R: AsRef; } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(deny_unknown_fields)] pub struct SameAsContent { @@ -86,6 +105,7 @@ pub struct SameAsContent { pub ref_: FieldRef, } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[serde(tag = "type")] @@ -199,6 +219,7 @@ impl Content { } } +#[bindlang::bindlang] impl Default for Content { fn default() -> Self { Self::Null @@ -211,8 +232,8 @@ impl std::fmt::Display for Content { } } -impl<'r> From<&'r Value> for Content { - fn from(value: &'r Value) -> Self { +impl From<&'_ Value> for Content { + fn from(value: &'_ Value) -> Self { match value { // TODO not sure what the correct behaviour is here Value::Null => Content::Null, @@ -339,6 +360,7 @@ where .map(|suggest| format!(", did you mean '{}'?", suggest.as_ref())) } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(try_from = "f64")] pub struct Weight(f64); @@ -365,6 +387,12 @@ impl Default for Weight { } } +impl Display for Weight { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + #[cfg(test)] pub mod tests { use super::*; diff --git a/core/src/schema/content/number.rs b/core/src/schema/content/number.rs index 61f9eba2a..935dcbc7d 100644 --- a/core/src/schema/content/number.rs +++ b/core/src/schema/content/number.rs @@ -79,6 +79,16 @@ macro_rules! number_content { $as(number_content::$as), )* } + + impl Display for NumberContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + $( + NumberContent::$as(value) => value.fmt(f), + )* + } + } + } impl Serialize for NumberContent { @@ -159,6 +169,12 @@ macro_rules! number_content { Self::$as(value) } } + + impl std::fmt::Display for $as { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) //TODO: Make something more useful + } + } )* } diff --git a/core/src/schema/content/object.rs b/core/src/schema/content/object.rs index 3988b1096..cc7893035 100644 --- a/core/src/schema/content/object.rs +++ b/core/src/schema/content/object.rs @@ -8,11 +8,18 @@ use std::collections::BTreeMap; use std::fmt; use std::ops::Not; +#[bindlang::bindlang] #[derive(Debug, Clone, PartialEq)] pub struct ObjectContent { pub fields: BTreeMap, } +impl Display for ObjectContent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) //TODO: Do something more clever than using Debug + } +} + fn add_type_underscore(key: &str) -> Cow { if key.starts_with("type") && key[4..].bytes().all(|b| b == b'_') { Cow::Owned(key.to_string() + "_") @@ -135,6 +142,7 @@ impl ObjectContent { } } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct FieldContent { #[serde(default, skip_serializing_if = "Not::not")] @@ -143,6 +151,12 @@ pub struct FieldContent { pub content: Box, } +impl Display for FieldContent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.content, if self.optional { "|null" } else { "" }) + } +} + impl FieldContent { pub fn new>(content: I) -> Self { FieldContent { @@ -156,6 +170,7 @@ impl FieldContent { } } +#[bindlang::bindlang] impl Default for ObjectContent { fn default() -> Self { Self { diff --git a/core/src/schema/content/one_of.rs b/core/src/schema/content/one_of.rs index 692f6403c..d3a3c30d4 100644 --- a/core/src/schema/content/one_of.rs +++ b/core/src/schema/content/one_of.rs @@ -2,12 +2,22 @@ use super::prelude::*; use super::Weight; +#[bindlang::bindlang] #[derive(Debug, Default, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct OneOfContent { pub variants: Vec, } +impl Display for OneOfContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for v in &self.variants { + v.fmt(f)?; + } + Ok(()) + } +} + impl PartialEq for OneOfContent { fn eq(&self, other: &Self) -> bool { for left in self.variants.iter() { @@ -24,6 +34,7 @@ impl PartialEq for OneOfContent { } } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VariantContent { #[serde(default)] @@ -32,6 +43,12 @@ pub struct VariantContent { pub content: Box, } +impl Display for VariantContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}: {}", self.content, self.weight) + } +} + impl PartialEq for VariantContent { fn eq(&self, other: &Self) -> bool { self.content.eq(&other.content) diff --git a/core/src/schema/content/series.rs b/core/src/schema/content/series.rs index 3a1b8bb40..016000654 100644 --- a/core/src/schema/content/series.rs +++ b/core/src/schema/content/series.rs @@ -4,8 +4,10 @@ use crate::graph::series::{ }; use crate::{Compile, Compiler, Graph}; use anyhow::Result; +use bindlang::bindlang; use std::convert::TryInto; +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(deny_unknown_fields)] pub struct SeriesContent { @@ -14,6 +16,7 @@ pub struct SeriesContent { pub variant: SeriesVariant, } +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] @@ -24,6 +27,7 @@ pub enum SeriesVariant { Zip(Zip), } +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Incrementing { pub(crate) start: String, @@ -31,6 +35,7 @@ pub struct Incrementing { pub(crate) increment: std::time::Duration, } +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Poisson { pub(crate) start: String, @@ -38,6 +43,7 @@ pub struct Poisson { pub(crate) rate: std::time::Duration, } +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Cyclical { pub(crate) start: String, @@ -49,6 +55,7 @@ pub struct Cyclical { pub(crate) max_rate: std::time::Duration, } +#[bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Zip { series: Vec, diff --git a/core/src/schema/content/string.rs b/core/src/schema/content/string.rs index b1825eace..24f6018c4 100644 --- a/core/src/schema/content/string.rs +++ b/core/src/schema/content/string.rs @@ -1,6 +1,7 @@ use super::prelude::*; use super::Categorical; +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] @@ -15,6 +16,7 @@ pub enum StringContent { Format(FormatContent), } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Uuid; @@ -33,6 +35,7 @@ impl StringContent { } } +#[bindlang::bindlang] #[derive(Debug, Clone)] pub struct RegexContent(String, RandRegex); @@ -104,6 +107,7 @@ impl Serialize for RegexContent { } } +#[bindlang::bindlang] impl Default for RegexContent { fn default() -> Self { let pattern = "[a-zA-Z0-9]*".to_string(); @@ -111,6 +115,7 @@ impl Default for RegexContent { } } +#[bindlang::bindlang] impl Default for StringContent { fn default() -> Self { Self::Pattern(RegexContent::default()) @@ -152,6 +157,7 @@ impl Serialize for FakerContentArgument { } } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct FakerContent { pub generator: String, @@ -169,6 +175,13 @@ impl FakerContent { } } +impl Display for FakerContent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({})", self.generator, self.args) + } +} + +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] #[serde(tag = "serializer")] @@ -176,6 +189,7 @@ pub enum SerializedContent { Json(JsonContent), } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub struct TruncatedContent { @@ -183,6 +197,7 @@ pub struct TruncatedContent { length: usize, } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub struct FormatContent { @@ -190,6 +205,7 @@ pub struct FormatContent { arguments: HashMap, } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "lowercase")] #[serde(untagged)] @@ -198,11 +214,13 @@ pub enum ContentOrRef { FieldRef(FieldRef), } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct JsonContent { content: Box, } +#[bindlang::bindlang] #[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Hash)] pub enum ChronoValue { NaiveDate(NaiveDate), @@ -292,6 +310,7 @@ impl ChronoValue { } } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum ChronoValueType { @@ -307,6 +326,7 @@ impl Default for ChronoValueType { } } +#[bindlang::bindlang] #[derive(Debug, Clone, PartialEq)] pub struct DateTimeContent { pub format: String, diff --git a/core/src/schema/content/unique.rs b/core/src/schema/content/unique.rs index 4fc92f6d8..cfd6a4548 100644 --- a/core/src/schema/content/unique.rs +++ b/core/src/schema/content/unique.rs @@ -4,17 +4,20 @@ use crate::{Compiler, Content, Graph}; use anyhow::Result; use serde::{Deserialize, Serialize}; +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum UniqueAlgorithm { Hash { retries: Option }, } +#[bindlang::bindlang] impl Default for UniqueAlgorithm { fn default() -> Self { Self::Hash { retries: None } } } +#[bindlang::bindlang] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(deny_unknown_fields)] pub struct UniqueContent { @@ -23,6 +26,15 @@ pub struct UniqueContent { pub content: Box, } +/// Make a unique content of some content +#[bindlang::bindlang] +pub fn unique(content: Content) -> Content { + Content::Unique(UniqueContent { + algorithm: UniqueAlgorithm::default(), + content: Box::new(content) + }) +} + impl Compile for UniqueContent { fn compile<'a, C: Compiler<'a>>(&'a self, compiler: C) -> Result { let graph = self.content.compile(compiler)?; diff --git a/core/src/schema/inference/mod.rs b/core/src/schema/inference/mod.rs index 5951a40f6..fbc989a1f 100644 --- a/core/src/schema/inference/mod.rs +++ b/core/src/schema/inference/mod.rs @@ -470,7 +470,7 @@ pub mod tests { let error_margin = f64::EPSILON; OptionalMergeStrategy - .try_merge(&mut master, &"15".parse().unwrap()) + .try_merge(&mut master, &"15".parse::().unwrap()) .unwrap(); match master { @@ -483,10 +483,10 @@ pub mod tests { } OptionalMergeStrategy - .try_merge(&mut master, &"-10".parse().unwrap()) + .try_merge(&mut master, &"-10".parse::().unwrap()) .unwrap(); OptionalMergeStrategy - .try_merge(&mut master, &"20".parse().unwrap()) + .try_merge(&mut master, &"20".parse::().unwrap()) .unwrap(); match master { @@ -499,10 +499,10 @@ pub mod tests { } OptionalMergeStrategy - .try_merge(&mut master, &"-13.6".parse().unwrap()) + .try_merge(&mut master, &"-13.6".parse::().unwrap()) .unwrap(); OptionalMergeStrategy - .try_merge(&mut master, &"20.6".parse().unwrap()) + .try_merge(&mut master, &"20.6".parse::().unwrap()) .unwrap(); match master { diff --git a/core/src/schema/mod.rs b/core/src/schema/mod.rs index 076e75f6d..ee761178e 100644 --- a/core/src/schema/mod.rs +++ b/core/src/schema/mod.rs @@ -67,6 +67,7 @@ pub fn bool_from_str<'de, D: Deserializer<'de>>(d: D) -> std::result::Result Deserialize<'de> for Name { } } +#[bindlang::bindlang] #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct FieldRef { collection: Name, fields: Vec, } +#[bindlang::bindlang] impl From for FieldRef { fn from(collection: Name) -> Self { Self { diff --git a/core/src/schema/namespace.rs b/core/src/schema/namespace.rs index c63f699c8..2f9857138 100644 --- a/core/src/schema/namespace.rs +++ b/core/src/schema/namespace.rs @@ -15,6 +15,7 @@ use crate::graph::{Graph, KeyValueOrNothing}; #[allow(dead_code)] type JsonObject = Map; +#[bindlang::bindlang] #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct Namespace { #[serde(flatten)] @@ -27,6 +28,12 @@ impl AsRef> for Namespace { } } +impl std::fmt::Display for Namespace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:#?}", self) + } +} + impl IntoIterator for Namespace { type Item = (Name, Content); @@ -45,6 +52,13 @@ impl FromIterator<(Name, Content)> for Namespace { } } +#[bindlang::bindlang] +impl Namespace { + pub fn new(collections: BTreeMap) -> Self { + Self { collections } + } +} + impl Namespace { #[cfg(test)] pub fn accepts(&self, name: &Name, value: &Value) -> Result<()> { diff --git a/lang_bindings/Cargo.toml b/lang_bindings/Cargo.toml new file mode 100644 index 000000000..725d355a8 --- /dev/null +++ b/lang_bindings/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "lang_bindings" +version = "0.1.0" +edition = "2018" + +[dependencies] +# for now, we use my koto branch wholesale +koto = { git = "https://github.com/llogiq/koto", branch = "vtable" } +chrono = "0.4.19" + diff --git a/lang_bindings/src/lib.rs b/lang_bindings/src/lib.rs new file mode 100644 index 000000000..f6ef576d6 --- /dev/null +++ b/lang_bindings/src/lib.rs @@ -0,0 +1,629 @@ +#![feature(min_specialization)] + +/// bindings for our koto-derived language +/// +/// for now we use koto wholesale, will add custom syntax later +// re-export Value for derive macro +pub use koto::runtime::Value; +use koto::runtime::{runtime_error, RuntimeError, ValueNumber}; + +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; + +pub enum KeyPath<'parent> { + Index(usize, Option<&'parent KeyPath<'parent>>), + Field(Cow<'static, str>, Option<&'parent KeyPath<'parent>>), +} + +fn field(name: &'static str) -> KeyPath<'static> { + KeyPath::Field(Cow::Borrowed(name), None) +} + +impl Display for KeyPath<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + &KeyPath::Index(i, parent) => { + if let Some(p) = parent { + write!(f, "{}@{}", p, i) + } else { + write!(f, "{}", i) + } + } + &KeyPath::Field(ref i, parent) => { + if let Some(p) = parent { + write!(f, "{}.{}", p, i) + } else { + write!(f, "{}", i) + } + } + } + } +} + +pub trait StaticExternalValue { + fn type_str() -> &'static str; +} + +pub trait FromValue: Sized { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result; +} + +pub trait CustomFromValue: Sized { + fn opt_from_value(_key_path: &KeyPath<'_>, _value: &Value) -> Option; +} + +impl CustomFromValue for T { + default fn opt_from_value(_key_path: &KeyPath<'_>, _value: &Value) -> Option { + None + } +} + +pub trait RefFromValue { + fn ref_from_value R>( + key_path: &KeyPath<'_>, + value: &Value, + f: F, + ) -> Result; + + fn ref_mut_from_value Fn(&'r Self) -> R>( + key_path: &KeyPath<'_>, + value: &Value, + _f: F, + ) -> Result { + runtime_error!( + "Cannot mutate a primitive value ({}) at {}", + value.type_as_string(), + key_path + ) + } +} + +#[cold] +pub fn fn_type_error( + fn_name: &str, + inputs: &str, + args: &[Value], +) -> Result { + let mut types = args.iter().map(|v| v.type_as_string()); + let mut argspec = types.next().unwrap_or_else(String::new); + for ty in types { + argspec += ", "; + argspec += &ty; + } + runtime_error!("expected {}({:?}), got {}", fn_name, inputs, argspec,) +} + +/// Return an error for a missing item +#[cold] +pub fn missing(key_path: &KeyPath<'_>) -> Result { + runtime_error!("Missing value at {}", key_path) +} + +/// Return an error for an item of the wrong type +#[cold] +pub fn wrong_type( + ty: &'static str, + key_path: &KeyPath<'_>, + value: &Value, +) -> Result { + runtime_error!( + "expected value of type {} at {}, found {}", + ty, + key_path, + value.type_as_string(), + ) +} + +#[cold] +pub fn not_external_value( + key_path: &KeyPath<'_>, + value: &Value, +) -> Result { + runtime_error!("expected external value at {}, found {}", key_path, value) +} + +impl FromValue for () { + fn from_value(_key_path: &KeyPath<'_>, _value: &Value) -> Result { + Ok(()) + } +} + +impl FromValue for bool { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let Value::Bool(b) = value { + Ok(*b) + } else { + wrong_type("bool", key_path, value) + } + } +} + +impl RefFromValue for bool { + fn ref_from_value R>( + key_path: &KeyPath<'_>, + value: &Value, + f: F, + ) -> Result { + if let Value::Bool(b) = value { + Ok(f(b)) + } else { + wrong_type("bool", key_path, value) + } + } +} + +macro_rules! impl_from_value_num { + (one $ty:ty, $value:path, $category:expr) => { + impl FromValue for $ty { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let Value::Number($value(i)) = value { + Ok((*i) as $ty) + } else { + wrong_type($category, key_path, value) + } + } + } + + impl RefFromValue for $ty { + fn ref_from_value R>( + key_path: &KeyPath<'_>, + value: &Value, + f: F, + ) -> Result { + if let Value::Number($value(i)) = value { + Ok(f(&(*i as $ty))) + } else { + wrong_type("integer", key_path, value) + } + } + } + }; + ($category:expr, $value:path, $($ty:ty),*) => { + $(impl_from_value_num!(one $ty, $value, $category);)* + } +} + +impl_from_value_num!( + "integer", + ValueNumber::I64, + u8, + u16, + u32, + u64, + usize, + i8, + i16, + i32, + i64, + isize +); +impl_from_value_num!("float", ValueNumber::F64, f32, f64); + +impl FromValue for String { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let Value::Str(s) = value { + Ok(s.as_str().to_owned()) + } else { + wrong_type("string", key_path, value) + } + } +} + +impl RefFromValue for str { + fn ref_from_value R>( + key_path: &KeyPath<'_>, + value: &Value, + f: F, + ) -> Result { + if let Value::Str(s) = value { + Ok(f(s.as_str())) + } else { + wrong_type("string", key_path, value) + } + } +} + +impl FromValue for Vec { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let Value::List(elems) = value { + elems + .data() + .iter() + .enumerate() + .map(|(i, v)| { + let item = T::from_value(&KeyPath::Index(i, Some(key_path)), v); + item + }) + .collect() + } else { + wrong_type("list of items", key_path, value) + } + } +} + +impl FromValue for BTreeMap { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let Value::Map(map) = value { + map.contents() + .data + .iter() + .map(|(k, v)| { + let kval = k.value(); + Ok(( + K::from_value(key_path, kval)?, + V::from_value(&KeyPath::Field(kval.to_string().into(), Some(key_path)), v)?, + )) + }) + .collect() + } else { + wrong_type("map", key_path, value) + } + } +} + +impl FromValue for Box { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + T::from_value(key_path, value).map(Box::new) + } +} + +impl FromValue for Result { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + T::from_value(&KeyPath::Field(Cow::Borrowed("ok"), Some(key_path)), value).map(Ok) + } +} + +impl FromValue for Option { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + if let &Value::Empty = value { + Ok(None) + } else { + T::from_value(key_path, value).map(Some) + } + } +} + +type ValueResult = Result; + +/// Make a koto Value out of some Rust value +pub trait IntoValue: Sized { + fn into_value(self) -> ValueResult; +} + +impl IntoValue for Option { + fn into_value(self) -> ValueResult { + self.map_or(Ok(Value::Empty), IntoValue::into_value) + } +} + +impl IntoValue for Result +where + T: IntoValue, + E: Error, +{ + fn into_value(self) -> ValueResult { + match self { + Ok(ok) => ok.into_value(), + Err(e) => runtime_error!("{}", e), + } + } +} + +impl IntoValue for () { + fn into_value(self) -> ValueResult { + Ok(Value::Empty) + } +} + +impl IntoValue for bool { + fn into_value(self) -> ValueResult { + Ok(Value::Bool(self)) + } +} + +impl IntoValue for Box { + fn into_value(self) -> ValueResult { + (*self).into_value() + } +} + +impl IntoValue for &T { + fn into_value(self) -> ValueResult { + self.clone().into_value() + } +} + +impl IntoValue for String { + fn into_value(self) -> ValueResult { + Ok(Value::Str(self.into())) + } +} + +impl IntoValue for &str { + fn into_value(self) -> ValueResult { + Ok(Value::Str(self.into())) + } +} + +macro_rules! impl_into_value_num { + (one $ty:ty, $as_ty:ty, $variant:path) => { + impl IntoValue for $ty { + fn into_value(self) -> ValueResult { + Ok(Value::Number($variant(self as $as_ty))) + } + } + }; + ($variant:path, $as_ty:ty; $($tys:ty),*) => { + $( + impl_into_value_num!(one $tys, $as_ty, $variant); + )* + }; +} + +impl_into_value_num!(ValueNumber::I64, i64; u8, u16, u32, u64, usize, i8, i16, i32, i64, isize); +impl_into_value_num!(ValueNumber::F64, f64; f32, f64); + +mod chrono { + // we cannot implement ExternalValue for chrono types, alas. So my + // solution for now is to allow strings in standard format and + + use super::{field, FromValue, IntoValue, RefFromValue, Value, ValueResult}; + use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; + + macro_rules! impl_chrono_value { + ($ty:ty, $tup_pat:pat, $ok:expr, $fmt:expr, $($accessors:ident),*) => { + impl FromValue for $ty { + fn from_value( + key_path: &crate::KeyPath<'_>, + value: &koto::runtime::Value + ) -> Result { + match value { + Value::Tuple(tuple) => if let &$tup_pat = &tuple.data() { + Ok($ok) + } else { + crate::wrong_type(stringify!($ty), key_path, value) + }, + Value::Str(s) => Self::parse_from_str(s.as_str(), $fmt) + .or_else(|e| ::koto::runtime::runtime_error!("{}", e)), + value => crate::wrong_type(stringify!($ty), key_path, value) + } + } + } + + impl RefFromValue for $ty { + fn ref_from_value R>( + key_path: &crate::KeyPath<'_>, + value: &Value, + f: F, + ) -> Result { + Ok(f(&Self::from_value(key_path, value)?)) + } + } + + impl IntoValue for $ty { + fn into_value(self) -> ValueResult { + #[allow(unused_imports)] + use ::chrono::{Datelike, Timelike}; + Ok(Value::Tuple(::koto::runtime::ValueTuple::from( + &[$(self.$accessors().into_value()?),*][..] + ))) + } + } + }; + } + + impl_chrono_value!( + NaiveDate, + [y, m, d], + NaiveDate::from_ymd( + i32::from_value(&field("year"), &y)?, + u32::from_value(&field("month"), &m)?, + u32::from_value(&field("day"), &d)? + ), + "%Y-%m-%d", + year, + month, + day + ); + + impl_chrono_value!( + NaiveTime, + [h, m, s], + NaiveTime::from_hms( + u32::from_value(&field("hour"), &h)?, + u32::from_value(&field("minute"), &m)?, + u32::from_value(&field("second"), &s)? + ), + "%H:%M:%S", + hour, + minute, + second + ); + + impl_chrono_value!( + NaiveDateTime, + [y, m, d, h, min, s], + NaiveDate::from_ymd( + i32::from_value(&field("year"), &y)?, + u32::from_value(&field("month"), &m)?, + u32::from_value(&field("day"), &d)? + ) + .and_hms( + u32::from_value(&field("hour"), &h)?, + u32::from_value(&field("minute"), &min)?, + u32::from_value(&field("second"), &s)? + ), + "%Y-%m-%d %H:%M:%S", + year, + month, + day, + hour, + minute, + second + ); + + impl_chrono_value!( + DateTime, + [y, m, d, h, min, s, tz], + DateTime::from_utc( + NaiveDate::from_ymd( + i32::from_value(&field("year"), &y)?, + u32::from_value(&field("month"), &m)?, + u32::from_value(&field("day"), &d)? + ) + .and_hms( + u32::from_value(&field("hour"), &h)?, + u32::from_value(&field("minute"), &min)?, + u32::from_value(&field("second"), &s)? + ), + FixedOffset::east(i32::from_value(&field("timezone"), &tz)?), + ), + "%Y %b %d %H:%M:%S%.3f %z", + year, + month, + day, + hour, + minute, + second, + timezone + ); + + impl IntoValue for chrono::FixedOffset { + fn into_value(self) -> ValueResult { + Ok(Value::Empty) //TODO: Have to read up on what to do with an offset + } + } +} + +use std::time::Duration; + +/// Duration is either given as number of seconds (either float or integer) or a tuple of +/// (seconds, nanoseconds), both integer +impl FromValue for Duration { + fn from_value(key_path: &KeyPath<'_>, value: &Value) -> Result { + match value { + Value::Number(ValueNumber::I64(secs)) => return Ok(Duration::from_secs(*secs as u64)), + Value::Number(ValueNumber::F64(secs)) => return Ok(Duration::from_secs_f64(*secs)), + Value::Tuple(tup) => { + if let [secs, nanos] = tup.data() { + return Ok( + Duration::from_secs( + u64::from_value(&field("seconds"), secs)? + ) + Duration::from_nanos( + u64::from_value(&field("nanoseconds"), nanos)? + ), + ); + } + } + _ => {} + } + wrong_type("Duration", key_path, value) + } +} + +impl RefFromValue for Duration { + fn ref_from_value R>( + key_path: &KeyPath<'_>, + value: &Value, + f: F, + ) -> Result { + Ok(f(&Duration::from_value(key_path, value)?)) + } +} + +/// Duration is always turned into a integer tuple of (seconds, nanoseconds) +impl IntoValue for Duration { + fn into_value(self) -> ValueResult { + Ok(Value::Tuple(koto::runtime::ValueTuple::from( + &[ + self.as_secs().into_value()?, + self.subsec_nanos().into_value()?, + ][..], + ))) + } +} + +#[macro_export] +macro_rules! external_value { + ($($ty:ty),*) => { + $( + impl $crate::StaticExternalValue for $ty { + fn type_str() -> &'static str { stringify!($ty) } + } + + impl ::koto::runtime::ExternalValue for $ty { + fn value_type(&self) -> String { + String::from(stringify!($ty)) + } + } + + impl $crate::FromValue for $ty { + fn from_value( + key_path: &$crate::KeyPath<'_>, + value: &::koto::runtime::Value, + ) -> std::result::Result { + if let Some(result) = ::opt_from_value(key_path, value) { + return Ok(result); + } + if let ::koto::runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().write().downcast_mut::() { + Ok(v.clone()) + } else { + $crate::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + $crate::not_external_value(key_path, &*value) + } + } + } + + impl $crate::RefFromValue for $ty { + fn ref_from_value R>( + key_path: &$crate::KeyPath<'_>, + value: &::koto::runtime::Value, + f: F, + ) -> Result { + if let ::koto::runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().read().downcast_ref::() { + Ok(f(v)) + } else { + $crate::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + $crate::not_external_value(key_path, &*value) + } + } + + fn ref_mut_from_value Fn(&'r Self) -> R>( + key_path: &$crate::KeyPath<'_>, + value: &::koto::runtime::Value, + f: F, + ) -> Result { + if let ::koto::runtime::Value::ExternalValue(exval, ..) = value { + if let Some(v) = exval.as_ref().write().downcast_mut::() { + Ok(f(v)) + } else { + $crate::wrong_type( + ::type_str(), + key_path, + &value, + ) + } + } else { + $crate::not_external_value(key_path, &*value) + } + } + } + )* + } +} \ No newline at end of file