diff --git a/crates/rune-core/src/protocol.rs b/crates/rune-core/src/protocol.rs index 054917ba9..d19414b6e 100644 --- a/crates/rune-core/src/protocol.rs +++ b/crates/rune-core/src/protocol.rs @@ -177,6 +177,22 @@ define! { doc: ["Allows an indexing set operation to work."], }; + /// The function to access a field dynamically. + pub const [DYNAMIC_FIELD_GET, DYNAMIC_FIELD_GET_HASH]: Protocol = Protocol { + name: "dynamic_get", + hash: 0x6dda58b140dfeaf9, + repr: Some("let output = $value"), + doc: ["Allows a dynamic get operation to work."], + }; + + /// The function to set a field dynamically. + pub const [DYNAMIC_FIELD_SET, DYNAMIC_FIELD_SET_HASH]: Protocol = Protocol { + name: "dynamic_set", + hash: 0xbe28c02896ca0b64, + repr: Some("$value = input"), + doc: ["Allows a dynamic set operation to work."], + }; + /// Check two types for partial equality. pub const [PARTIAL_EQ, PARTIAL_EQ_HASH]: Protocol = Protocol { name: "partial_eq", diff --git a/crates/rune-macros/src/any.rs b/crates/rune-macros/src/any.rs index 5d89d08bf..d32fb0c17 100644 --- a/crates/rune-macros/src/any.rs +++ b/crates/rune-macros/src/any.rs @@ -2,11 +2,12 @@ use std::collections::BTreeMap; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; -use rune_core::Hash; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::Token; +use rune_core::Hash; + use crate::context::{Context, Generate, GenerateTarget, Tokens, TypeAttr}; /// An internal call to the macro. diff --git a/crates/rune-macros/src/context.rs b/crates/rune-macros/src/context.rs index 33e26ab0d..dee11a32e 100644 --- a/crates/rune-macros/src/context.rs +++ b/crates/rune-macros/src/context.rs @@ -1,6 +1,5 @@ use std::cell::RefCell; -use crate::internals::*; use proc_macro2::Span; use proc_macro2::TokenStream; use quote::quote_spanned; @@ -10,6 +9,8 @@ use syn::punctuated::Punctuated; use syn::spanned::Spanned as _; use syn::Token; +use crate::internals::*; + /// Parsed `#[rune(..)]` field attributes. #[derive(Default)] pub(crate) struct FieldAttrs { diff --git a/crates/rune-macros/src/internals.rs b/crates/rune-macros/src/internals.rs index 8d1771e90..1707a05a9 100644 --- a/crates/rune-macros/src/internals.rs +++ b/crates/rune-macros/src/internals.rs @@ -1,6 +1,7 @@ +use std::fmt; + use proc_macro2::Span; use quote::{ToTokens, TokenStreamExt as _}; -use std::fmt; #[derive(Copy, Clone)] pub struct Symbol(&'static str); @@ -19,12 +20,12 @@ pub const NAME: Symbol = Symbol("name"); pub const ITEM: Symbol = Symbol("item"); pub const MODULE: Symbol = Symbol("module"); pub const INSTALL_WITH: Symbol = Symbol("install_with"); - pub const CONSTRUCTOR: Symbol = Symbol("constructor"); pub const BUILTIN: Symbol = Symbol("builtin"); pub const STATIC_TYPE: Symbol = Symbol("static_type"); pub const FROM_VALUE: Symbol = Symbol("from_value"); pub const FROM_VALUE_PARAMS: Symbol = Symbol("from_value_params"); + pub const GET: Symbol = Symbol("get"); pub const SET: Symbol = Symbol("set"); pub const COPY: Symbol = Symbol("copy"); diff --git a/crates/rune/Cargo.toml b/crates/rune/Cargo.toml index 09b7e5116..88ab72c04 100644 --- a/crates/rune/Cargo.toml +++ b/crates/rune/Cargo.toml @@ -27,6 +27,7 @@ disable-io = ["alloc"] fmt = ["alloc"] std = ["alloc", "num/std", "serde/std", "rune-core/std", "rune-alloc/std", "musli/std", "musli-storage/std", "once_cell/std", "anyhow/std"] alloc = ["anyhow", "rune-alloc/alloc", "rune-core/alloc", "once_cell/alloc", "serde/alloc"] +dynamic_fields = [] [dependencies] rune-macros = { version = "=0.13.1", path = "../rune-macros" } diff --git a/crates/rune/src/any.rs b/crates/rune/src/any.rs index 0ccbb8464..95d6d3240 100644 --- a/crates/rune/src/any.rs +++ b/crates/rune/src/any.rs @@ -1,8 +1,5 @@ use core::any; -use crate::compile::Named; -use crate::hash::Hash; - /// Macro to mark a value as external, which will implement all the appropriate /// traits. /// @@ -56,6 +53,9 @@ use crate::hash::Hash; /// ``` pub use rune_macros::Any; +use crate::compile::Named; +use crate::hash::Hash; + /// A trait which can be stored inside of an [AnyObj](crate::runtime::AnyObj). /// /// We use our own marker trait that must be explicitly derived to prevent other @@ -72,6 +72,7 @@ pub use rune_macros::Any; /// name: String, /// } /// ``` + pub trait Any: Named + any::Any { /// The type hash of the type. /// diff --git a/crates/rune/src/compile.rs b/crates/rune/src/compile.rs index 8cc997df9..c24125b9e 100644 --- a/crates/rune/src/compile.rs +++ b/crates/rune/src/compile.rs @@ -3,78 +3,61 @@ //! The main entry to compiling rune source is [prepare][crate::prepare] which //! uses this compiler. In here you'll just find compiler-specific types. -mod assembly; -pub(crate) use self::assembly::{Assembly, AssemblyInst}; - -pub(crate) mod attrs; - -pub(crate) mod error; -pub use self::error::{Error, ImportStep, MetaError}; -pub(crate) use self::error::{ErrorKind, IrErrorKind}; +pub use meta_info::MetaInfo; +pub use rune_core::{Component, ComponentRef, IntoComponent, Item, ItemBuf}; -mod compile_visitor; +pub(crate) use self::assembly::{Assembly, AssemblyInst}; +pub(crate) use self::compile::compile; pub use self::compile_visitor::CompileVisitor; #[cfg(feature = "std")] pub(crate) use self::compile_visitor::NoopCompileVisitor; - -pub(crate) mod context; pub use self::context::Context; - -pub(crate) mod context_error; pub use self::context_error::ContextError; - -pub(crate) mod meta_info; -pub use meta_info::MetaInfo; - -mod docs; pub(crate) use self::docs::Docs; - -mod prelude; +pub use self::error::{Error, ImportStep, MetaError}; +pub(crate) use self::error::{ErrorKind, IrErrorKind}; +pub(crate) use self::location::DynLocation; +pub use self::location::{Located, Location}; +pub(crate) use self::meta::{Doc, ItemMeta}; +pub use self::meta::{MetaRef, SourceMeta}; +pub use self::named::Named; +pub(crate) use self::names::Names; +pub use self::options::{Options, ParseOptionError}; +pub(crate) use self::pool::{ItemId, ModId, ModMeta, Pool}; pub(crate) use self::prelude::Prelude; - -pub(crate) mod ir; - -pub use rune_core::{Component, ComponentRef, IntoComponent, Item, ItemBuf}; - -mod source_loader; #[cfg(feature = "std")] pub use self::source_loader::FileSourceLoader; pub use self::source_loader::{NoopSourceLoader, SourceLoader}; - -mod unit_builder; pub use self::unit_builder::LinkerError; pub(crate) use self::unit_builder::UnitBuilder; +pub(crate) use self::visibility::Visibility; +pub use self::with_span::{HasSpan, WithSpan}; -pub(crate) mod v1; +mod assembly; +pub(crate) mod attrs; -mod options; -pub use self::options::{Options, ParseOptionError}; +mod compile_visitor; +pub(crate) mod context; +pub(crate) mod context_error; +mod docs; +pub(crate) mod error; +pub(crate) mod ir; +pub(crate) mod meta_info; +mod prelude; -mod location; -pub(crate) use self::location::DynLocation; -pub use self::location::{Located, Location}; +mod source_loader; +mod unit_builder; +pub(crate) mod v1; +mod compile; +mod location; pub mod meta; -pub(crate) use self::meta::{Doc, ItemMeta}; -pub use self::meta::{MetaRef, SourceMeta}; - -mod pool; -pub(crate) use self::pool::{ItemId, ModId, ModMeta, Pool}; - mod named; -pub use self::named::Named; - mod names; -pub(crate) use self::names::Names; - +mod options; +mod pool; mod visibility; -pub(crate) use self::visibility::Visibility; - mod with_span; -pub use self::with_span::{HasSpan, WithSpan}; - -mod compile; -pub(crate) use self::compile::compile; /// Helper alias for compile results. pub type Result = ::core::result::Result; diff --git a/crates/rune/src/compile/options.rs b/crates/rune/src/compile/options.rs index e03e5737c..b70661ef8 100644 --- a/crates/rune/src/compile/options.rs +++ b/crates/rune/src/compile/options.rs @@ -1,16 +1,38 @@ -use core::fmt; - use ::rust_alloc::boxed::Box; +use core::fmt; /// Error raised when trying to parse an invalid option. #[derive(Debug, Clone)] -pub struct ParseOptionError { - option: Box, +pub enum ParseOptionError { + /// Error raised when trying to parse an invalid option. + UnsupportedOption { + /// The unsupported option. + option: Box, + }, + /// Error raised when trying to enable a option which is not enabled by a feature gate. + DisabledFeatureOptionError { + /// The disabled option. + option: Box, + /// The required feature to enable this option. + required_feature: &'static str, + }, } impl fmt::Display for ParseOptionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Unsupported compile option `{}`", self.option) + match self { + Self::UnsupportedOption { option } => { + write!(f, "Unsupported compile option `{}`", option) + } + Self::DisabledFeatureOptionError { + option, + required_feature, + } => write!( + f, + "Featured gated compile option `{}` requires feature `{}`", + option, required_feature + ), + } } } @@ -77,7 +99,7 @@ impl Options { self.function_body = it.next() == Some("true"); } _ => { - return Err(ParseOptionError { + return Err(ParseOptionError::UnsupportedOption { option: option.into(), }); } diff --git a/crates/rune/src/runtime/value.rs b/crates/rune/src/runtime/value.rs index 461369c19..234ab3aa7 100644 --- a/crates/rune/src/runtime/value.rs +++ b/crates/rune/src/runtime/value.rs @@ -1,5 +1,4 @@ -mod serde; - +use ::rust_alloc::sync::Arc; use core::any; use core::borrow::Borrow; use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; @@ -7,7 +6,7 @@ use core::fmt; use core::hash; use core::ptr; -use ::rust_alloc::sync::Arc; +use ::serde::{Deserialize, Serialize}; use crate::alloc::fmt::TryWrite; use crate::alloc::prelude::*; @@ -25,7 +24,7 @@ use crate::runtime::{ use crate::runtime::{Hasher, Tuple}; use crate::{Any, Hash}; -use ::serde::{Deserialize, Serialize}; +mod serde; // Small helper function to build errors. fn err(error: E) -> VmResult diff --git a/crates/rune/src/runtime/vm.rs b/crates/rune/src/runtime/vm.rs index 1bf2c5821..0a22e0c92 100644 --- a/crates/rune/src/runtime/vm.rs +++ b/crates/rune/src/runtime/vm.rs @@ -1,10 +1,9 @@ +use ::rust_alloc::sync::Arc; use core::cmp::Ordering; use core::mem::{replace, swap}; use core::ops; use core::slice; -use ::rust_alloc::sync::Arc; - use crate::alloc::prelude::*; use crate::alloc::{self, String}; use crate::hash::{Hash, IntoHash, ToTypeHash}; @@ -144,7 +143,7 @@ impl Vm { Arc::ptr_eq(&self.unit, unit) } - /// Set the current instruction pointer. + /// Set the current instruction pointer. #[inline] pub fn set_ip(&mut self, ip: usize) { self.ip = ip; @@ -966,20 +965,49 @@ impl Vm { } } target => { + let index = index.clone(); + + let CallResult::Unsupported(target) = + vm_try!(self.dynamic_field_get(target, &index)) + else { + let Some(value) = + vm_try!(>::from_value(vm_try!(self.stack.pop()))) + else { + return err(VmErrorKind::ObjectIndexMissing { slot: string_slot }); + }; + + return VmResult::Ok(CallResult::Ok(value)); + }; + let hash = index.hash(); - return VmResult::Ok( - match vm_try!(self.call_field_fn(Protocol::GET, target, hash, ())) { - CallResult::Ok(()) => CallResult::Ok(vm_try!(self.stack.pop())), - CallResult::Unsupported(target) => CallResult::Unsupported(target), - }, - ); + let CallResult::Unsupported(_) = + vm_try!(self.call_field_fn(Protocol::GET, target, hash, ())) + else { + return VmResult::Ok(CallResult::Ok(vm_try!(self.stack.pop()))); + }; } } err(VmErrorKind::ObjectIndexMissing { slot: string_slot }) } + #[cfg(not(feature = "dynamic_fields"))] + fn dynamic_field_get(&mut self, target: Value, _: &str) -> VmResult> { + VmResult::Ok(CallResult::Unsupported(target)) + } + + #[cfg(feature = "dynamic_fields")] + fn dynamic_field_get(&mut self, target: Value, field: &str) -> VmResult> { + if let CallResult::Unsupported(target) = + vm_try!(self.call_instance_fn(target.clone(), Protocol::DYNAMIC_FIELD_GET, (field,),)) + { + return VmResult::Ok(CallResult::Unsupported(target)); + } + + VmResult::Ok(CallResult::Ok(())) + } + fn try_object_slot_index_set( &mut self, target: Value, @@ -988,7 +1016,7 @@ impl Vm { ) -> VmResult> { let field = vm_try!(self.unit.lookup_string(string_slot)); - VmResult::Ok(match target { + match target { Value::Object(object) => { let mut object = vm_try!(object.borrow_mut()); let key = vm_try!(field.as_str().try_to_owned()); @@ -1024,17 +1052,58 @@ impl Vm { }); } target => { + let field = field.clone(); + + let CallResult::Unsupported(target) = + vm_try!(self.dynamic_field_set(target, &field, &value)) + else { + vm_try!(<()>::from_value(vm_try!(self.stack.pop()))); + return VmResult::Ok(CallResult::Ok(())); + }; + let hash = field.hash(); - match vm_try!(self.call_field_fn(Protocol::SET, target, hash, (value,))) { - CallResult::Ok(()) => { - vm_try!(<()>::from_value(vm_try!(self.stack.pop()))); - CallResult::Ok(()) - } - result => result, - } + let CallResult::Unsupported(target) = + vm_try!(self.call_field_fn(Protocol::SET, target, hash, (value,))) + else { + vm_try!(<()>::from_value(vm_try!(self.stack.pop()))); + return VmResult::Ok(CallResult::Ok(())); + }; + + return err(VmErrorKind::MissingField { + target: vm_try!(target.type_info()), + field: vm_try!(field.as_str().try_to_owned()), + }); } - }) + } + } + + #[cfg(not(feature = "dynamic_fields"))] + fn dynamic_field_set( + &mut self, + target: Value, + _: &str, + value: &Value, + ) -> VmResult> { + VmResult::Ok(CallResult::Unsupported(target)) + } + + #[cfg(feature = "dynamic_fields")] + fn dynamic_field_set( + &mut self, + target: Value, + field: &str, + value: &Value, + ) -> VmResult> { + if let CallResult::Unsupported(target) = vm_try!(self.call_instance_fn( + target.clone(), + Protocol::DYNAMIC_FIELD_SET, + (field, value.clone()), + )) { + return VmResult::Ok(CallResult::Unsupported(target)); + } + + VmResult::Ok(CallResult::Ok(())) } fn on_tuple(&mut self, ty: TypeCheck, value: &Value, f: F) -> VmResult> diff --git a/crates/rune/src/tests.rs b/crates/rune/src/tests.rs index d786fc62e..91b881ed4 100644 --- a/crates/rune/src/tests.rs +++ b/crates/rune/src/tests.rs @@ -3,7 +3,26 @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::approx_constant)] +use ::rust_alloc::string::String; +use ::rust_alloc::sync::Arc; +use core::fmt; + +use anyhow::{Context as _, Error, Result}; + +use crate::alloc; +use crate::compile::{IntoComponent, ItemBuf}; +use crate::runtime::{Args, VmError}; +use crate::{termcolor, BuildError, Context, Diagnostics, FromValue, Source, Sources, Unit, Vm}; + pub(crate) mod prelude { + pub(crate) use ::rust_alloc::borrow::ToOwned; + pub(crate) use ::rust_alloc::boxed::Box; + pub(crate) use ::rust_alloc::string::{String, ToString}; + pub(crate) use ::rust_alloc::sync::Arc; + pub(crate) use ::rust_alloc::vec::Vec; + + pub(crate) use futures_executor::block_on; + pub(crate) use crate as rune; pub(crate) use crate::alloc; pub(crate) use crate::alloc::prelude::*; @@ -24,27 +43,8 @@ pub(crate) mod prelude { from_value, prepare, sources, span, vm_try, Any, Context, ContextError, Diagnostics, FromValue, Hash, Module, Source, Sources, ToValue, Value, Vm, }; - pub(crate) use futures_executor::block_on; - - pub(crate) use ::rust_alloc::borrow::ToOwned; - pub(crate) use ::rust_alloc::boxed::Box; - pub(crate) use ::rust_alloc::string::{String, ToString}; - pub(crate) use ::rust_alloc::sync::Arc; - pub(crate) use ::rust_alloc::vec::Vec; } -use core::fmt; - -use ::rust_alloc::string::String; -use ::rust_alloc::sync::Arc; - -use anyhow::{Context as _, Error, Result}; - -use crate::alloc; -use crate::compile::{IntoComponent, ItemBuf}; -use crate::runtime::{Args, VmError}; -use crate::{termcolor, BuildError, Context, Diagnostics, FromValue, Source, Sources, Unit, Vm}; - /// An error that can be raised during testing. #[derive(Debug)] pub enum TestError { @@ -461,3 +461,6 @@ mod vm_tuples; mod vm_typed_tuple; mod vm_types; mod wildcard_imports; + +#[cfg(feature = "dynamic_fields")] +mod dynamic_fields; diff --git a/crates/rune/src/tests/bug_344.rs b/crates/rune/src/tests/bug_344.rs index aa8406019..9e13e0569 100644 --- a/crates/rune/src/tests/bug_344.rs +++ b/crates/rune/src/tests/bug_344.rs @@ -7,11 +7,11 @@ //! //! See: https://github.com/rune-rs/rune/issues/344 -prelude!(); - use std::cell::Cell; use std::rc::Rc; +prelude!(); + #[test] fn bug_344_function() -> Result<()> { let mut context = Context::new(); diff --git a/crates/rune/src/tests/dynamic_fields.rs b/crates/rune/src/tests/dynamic_fields.rs new file mode 100644 index 000000000..f174a501b --- /dev/null +++ b/crates/rune/src/tests/dynamic_fields.rs @@ -0,0 +1,194 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +prelude!(); + +const TEST_SCRIPT: &str = r#" + pub fn get_meta_field(value) { + value.b + } + + pub fn get_field(value) { + value.a + } + + pub fn set_meta_field(value, into) { + value.b = into; + value.b + } + "#; + +macro_rules! set_up_vm { + () => {{ + let mut context = Context::with_default_modules()?; + let mut module = Module::new(); + module.ty::()?; + module.function_meta(TestClass::get_meta_field)?; + module.function_meta(TestClass::set_meta_field)?; + context.install(module)?; + + let runtime = Arc::new(context.runtime()?); + + let mut sources = Sources::new(); + sources.insert(Source::new("script", TEST_SCRIPT)?)?; + + let result = prepare(&mut sources).with_context(&context).build(); + + let unit = result?; + Vm::new(runtime, Arc::new(unit)) + }}; +} + +macro_rules! register_type { + ($($protocols:ident),* $(,)?) => { + #[derive(Any, Clone)] + struct TestClass { + #[rune($($protocols),*)] + a: i64, + values: HashMap, + } + + impl TestClass { + #[rune::function(instance, protocol = DYNAMIC_FIELD_GET)] + fn get_meta_field(&self, key: &str) -> Option { + self.values.get(key).copied() + } + + #[rune::function(instance, protocol = DYNAMIC_FIELD_SET)] + fn set_meta_field(&mut self, key: String, val: i64) { + match self.values.entry(key) { + Entry::Occupied(entry) => { + *entry.into_mut() = val; + } + Entry::Vacant(entry) => { + entry.insert(val); + } + }; + } + } + }; +} + +#[test] +fn dynamic_fields_never() -> Result<()> { + register_type!(get, set); + let mut vm = set_up_vm!(); + + let input = TestClass { + a: 42, + values: { + let mut map = HashMap::with_capacity(1); + map.insert("b".into(), 69); + map + }, + }; + + let value = i64::from_value(vm.call(["get_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 42); + + let value = vm.call(["set_meta_field"], (input.clone(), 1337)); + assert!(value.is_err()); + + let value = vm.call(["get_meta_field"], (input,)); + assert!(value.is_err()); + + Ok(()) +} + +#[test] +fn dynamic_fields_first() -> Result<()> { + register_type!(get, set); + let mut vm = set_up_vm!(); + + let input = TestClass { + a: 69, + values: { + let mut map = HashMap::with_capacity(1); + map.insert("b".into(), 42); + map + }, + }; + + let value = i64::from_value(vm.call(["get_meta_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 42); + + let value = + i64::from_value(vm.call(["set_meta_field"], (input.clone(), 1337))?).into_result()?; + assert_eq!(value, 1337); + + let value = i64::from_value(vm.call(["get_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 69); + + let value = vm.call(["get_meta_field"], (input.clone(),)); + assert!(value.is_err()); + + let value = vm.call(["set_meta_field"], (input, 1337)); + assert!(value.is_err()); + + Ok(()) +} + +#[test] +fn dynamic_fields_last() -> Result<()> { + register_type!(get, set); + let mut vm = set_up_vm!(); + + let input = TestClass { + a: 69, + values: { + let mut map = HashMap::with_capacity(1); + map.insert("b".into(), 42); + map + }, + }; + + let value = i64::from_value(vm.call(["get_meta_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 42); + + let value = + i64::from_value(vm.call(["set_meta_field"], (input.clone(), 1337))?).into_result()?; + assert_eq!(value, 1337); + + let value = i64::from_value(vm.call(["get_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 69); + + let value = vm.call(["get_meta_field"], (input.clone(),)); + assert!(value.is_err()); + + let value = vm.call(["set_meta_field"], (input, 1337)); + assert!(value.is_err()); + + Ok(()) +} + +#[test] +fn dynamic_fields_only() -> Result<()> { + register_type!(get, set); + let mut vm = set_up_vm!(); + + let input = TestClass { + a: 69, + values: { + let mut map = HashMap::with_capacity(1); + map.insert("b".into(), 42); + map + }, + }; + + let value = i64::from_value(vm.call(["get_meta_field"], (input.clone(),))?).into_result()?; + assert_eq!(value, 42); + + let value = vm.call(["get_field"], (input.clone(),)); + assert!(value.is_err()); + + let value = vm.call(["get_meta_field"], (input.clone(),)); + assert!(value.is_err()); + + let value = vm.call(["set_meta_field"], (input.clone(), 1337)); + assert!(value.is_err()); + + let value = i64::from_value(vm.call(["get_field"], (input,))?).into_result()?; + assert_eq!(value, 69); + + Ok(()) +}