Skip to content

Commit

Permalink
Adds a new "flyweight" type - a lightweight object
Browse files Browse the repository at this point in the history
"Flyweights" are a bit of a fusion of anonymous objects and ColdMUD frobs.

They are composed of a sequence of "slots" which behave like
properties, a "delegate" to which verb and property (in case of slot
miss) delegate to, and a "contents" which is a sequence of values.

They are immutable and reference counted, and passed by value like MOO
lists and maps.

They will additionally (future) support a "sealing" function which
makes their contents opaque.

"this", and "caller" can be flyweights.

In the future object/room contents will permit flyweights. And command
matching will permit their return as well.
  • Loading branch information
rdaum committed Dec 8, 2024
1 parent 4f4d0da commit 5c9819c
Show file tree
Hide file tree
Showing 40 changed files with 1,016 additions and 210 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ pub use encode::{
};

pub use var::{
v_bool, v_empty_list, v_empty_map, v_empty_str, v_err, v_float, v_int, v_list, v_list_iter,
v_map, v_none, v_obj, v_objid, v_str, v_string, Associative, ErrorPack, IndexMode, List, Map,
Sequence, Str, Var, Variant, AMBIGUOUS, FAILED_MATCH, NOTHING, SYSTEM_OBJECT,
v_bool, v_empty_list, v_empty_map, v_empty_str, v_err, v_float, v_flyweight, v_int, v_list,
v_list_iter, v_map, v_map_iter, v_none, v_obj, v_objid, v_str, v_string, Associative,
ErrorPack, IndexMode, List, Map, Sequence, Str, Var, Variant, AMBIGUOUS, FAILED_MATCH, NOTHING,
SYSTEM_OBJECT,
};
pub use var::{Error, Obj, Symbol, VarType};

Expand Down
8 changes: 4 additions & 4 deletions crates/common/src/tasks/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// this program. If not, see <https://www.gnu.org/licenses/>.
//

use crate::{Obj, Symbol, Var};
use crate::{Symbol, Var};
use bincode::{Decode, Encode};
use std::time::SystemTime;

Expand All @@ -23,7 +23,7 @@ pub struct NarrativeEvent {
/// When the event happened, in the server's system time.
pub timestamp: SystemTime,
/// The object that authored or caused the event.
pub author: Obj,
pub author: Var,
/// The event itself.
pub event: Event,
}
Expand All @@ -41,7 +41,7 @@ pub enum Event {

impl NarrativeEvent {
#[must_use]
pub fn notify(author: Obj, value: Var, content_type: Option<Symbol>) -> Self {
pub fn notify(author: Var, value: Var, content_type: Option<Symbol>) -> Self {
Self {
timestamp: SystemTime::now(),
author,
Expand All @@ -54,7 +54,7 @@ impl NarrativeEvent {
self.timestamp
}
#[must_use]
pub fn author(&self) -> &Obj {
pub fn author(&self) -> &Var {
&self.author
}
#[must_use]
Expand Down
295 changes: 295 additions & 0 deletions crates/common/src/var/flyweight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// Copyright (C) 2024 Ryan Daum <[email protected]>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, version 3.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.
//

//! A "flyweight" is a lightweight object type which consists only of a delegate, a set of
//! "slots" (symbol -> var pairs), a single "contents" value (a list), and an optional sealed/signed
//! state which makes it opaque.
//!
//! It is a reference counted, immutable bucket of slots.
//! Verbs called on it dispatch to the delegate. `this`, `caller`, perms etc all resolve to the
//! actual flyweight.
//! Properties are resolved in the slots, then the delegate.
//! Verbs are resolved in the delegate.
//!
//! Type protocol for an unsealed flyweight is a sequence. It behaves like a list around the
//! contents portion. The slots are accessed with a property access notation.
//!
//! The delegate is visible via the `.delegate` property access.
//! The slots can be listed with a `.slots` property access.
//! It is therefore illegal for a slot to have the name `slots` or `delegate`.
//!
//! So appending, etc can be done like:
//! `< x.delegate, x.slots, {@x, y} >`
//!
//! Literal syntax is:
//!
//! `< delegate, [ slot -> value, ... ], contents >`
//!
//! Setting the secret is done with the `seal(priv-key, secret)` function, which signs the flyweight with a
//! private key. The flyweight then becomes opaque without calling `unseal(pub-key, secret)` with the
//! right public key and the correct secret.
//!
//! When a flyweight is sealed, `.slots`, and `.delegate` (and literal output) will not
//! be available without calling `unseal()` with the correct secret.
//!
//! The purpose is to support two kinds of scenarios:
//! "Lightweight" non-persistent patterns where a full object would be overkill.
//! "Capability" style security patterns where the flyweight is a capability to a full object.
//!
//!
//! The structure of the flyweight also resembles an XML/HTML node, with a delegate as the tag name,
//! slots as the attributes, and contents as the inner text/nodes.
use crate::Error::E_TYPE;
use crate::{Error, List, Obj, Sequence, Symbol, Var, Variant};
use bincode::de::{BorrowDecoder, Decoder};
use bincode::enc::Encoder;
use bincode::error::{DecodeError, EncodeError};
use bincode::{BorrowDecode, Decode, Encode};
use std::fmt::{Debug, Formatter};
use std::hash::Hash;
use std::sync::Arc;

#[derive(Clone, Encode, Decode, PartialEq, Eq, PartialOrd, Ord)]
pub struct Flyweight(Arc<Inner>);

#[derive(Clone, Hash, PartialOrd, Ord, Eq)]

Check failure on line 67 in crates/common/src/var/flyweight.rs

View workflow job for this annotation

GitHub Actions / clippy

you are deriving `Hash` but have implemented `PartialEq` explicitly

error: you are deriving `Hash` but have implemented `PartialEq` explicitly --> crates/common/src/var/flyweight.rs:67:17 | 67 | #[derive(Clone, Hash, PartialOrd, Ord, Eq)] | ^^^^ | note: `PartialEq` implemented here --> crates/common/src/var/flyweight.rs:78:1 | 78 | impl PartialEq for Inner { | ^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#derived_hash_with_manual_eq = note: `#[deny(clippy::derived_hash_with_manual_eq)]` on by default = note: this error originates in the derive macro `Hash` (in Nightly builds, run with -Z macro-backtrace for more info)
struct Inner {
delegate: Obj,
slots: im::Vector<(Symbol, Var)>,
contents: List,
/// If `secret` is present it's a string signed with a key-pair that can be used to unseal
/// the flyweight.
/// The meaning of the key is up to the application.
seal: Option<String>,
}

impl PartialEq for Inner {
fn eq(&self, other: &Self) -> bool {
// Two flyweights where there are 'secrets' involved are never eq.
// To avoid leaking information about the secret.
if self.seal.is_some() || other.seal.is_some() {
return false;
}
self.delegate == other.delegate
&& self.slots == other.slots
&& self.contents == other.contents
}
}

impl Hash for Flyweight {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.delegate.hash(state);
self.0.slots.hash(state);
self.0.contents.hash(state);
self.0.seal.hash(state);
}
}

impl Encode for Inner {
fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
self.delegate.encode(encoder)?;
self.slots.len().encode(encoder)?;
for (k, v) in &self.slots {
k.encode(encoder)?;
v.encode(encoder)?;
}
self.contents.encode(encoder)?;
self.seal.encode(encoder)
}
}

impl Decode for Inner {
fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
let delegate = Obj::decode(decoder)?;
let len = usize::decode(decoder)?;
let mut slots = im::Vector::new();
for _ in 0..len {
let k = Symbol::decode(decoder)?;
let v = Var::decode(decoder)?;
slots.push_back((k, v));
}
let contents = List::decode(decoder)?;
let seal = Option::<String>::decode(decoder)?;
Ok(Self {
delegate,
slots,
contents,
seal,
})
}
}

impl<'a> BorrowDecode<'a> for Inner {
fn borrow_decode<D: BorrowDecoder<'a>>(decoder: &mut D) -> Result<Self, DecodeError> {
let delegate = Obj::borrow_decode(decoder)?;
let len = usize::borrow_decode(decoder)?;
let mut slots = im::Vector::new();
for _ in 0..len {
let k = Symbol::borrow_decode(decoder)?;
let v = Var::borrow_decode(decoder)?;
slots.push_back((k, v));
}
let contents = List::borrow_decode(decoder)?;
let seal = Option::<String>::borrow_decode(decoder)?;
Ok(Self {
delegate,
slots,
contents,
seal,
})
}
}
impl Debug for Flyweight {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.0.seal.is_some() {
write!(f, "<sealed flyweight>")
} else {
write!(
f,
"<{:?}, {:?}, {:?}>",
self.0.delegate, self.0.slots, self.0.contents
)
}
}
}

impl Flyweight {
pub fn mk_flyweight(
delegate: Obj,
slots: &[(Symbol, Var)],
contents: List,
seal: Option<String>,
) -> Self {
Self(Arc::new(Inner {
delegate,
slots: slots.into(),
contents,
seal,
}))

Check warning on line 180 in crates/common/src/var/flyweight.rs

View workflow job for this annotation

GitHub Actions / clippy

usage of an `Arc` that is not `Send` and `Sync`

warning: usage of an `Arc` that is not `Send` and `Sync` --> crates/common/src/var/flyweight.rs:175:14 | 175 | Self(Arc::new(Inner { | ______________^ 176 | | delegate, 177 | | slots: slots.into(), 178 | | contents, 179 | | seal, 180 | | })) | |__________^ | = note: `Arc<Inner>` is not `Send` and `Sync` as `Inner` is neither `Send` nor `Sync` = help: if the `Arc` will not used be across threads replace it with an `Rc` = help: otherwise make `Inner` `Send` and `Sync` or consider a wrapper type such as `Mutex` = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#arc_with_non_send_sync = note: `#[warn(clippy::arc_with_non_send_sync)]` on by default
}
}

impl Flyweight {
/// Return the slot with the given key, if it exists.
pub fn get_slot(&self, key: &Symbol) -> Option<&Var> {
self.0.slots.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}

pub fn slots(&self) -> &im::Vector<(Symbol, Var)> {
&self.0.slots
}

pub fn delegate(&self) -> &Obj {
&self.0.delegate
}

pub fn seal(&self) -> Option<&String> {
self.0.seal.as_ref()
}

pub fn contents(&self) -> &List {
&self.0.contents
}

pub fn is_sealed(&self) -> bool {
self.0.seal.is_some()
}

pub fn with_new_contents(&self, new_contents: List) -> Var {
let fi = Inner {
delegate: self.0.delegate.clone(),
slots: self.0.slots.clone(),
contents: new_contents,
seal: self.0.seal.clone(),
};
let fl = Flyweight(Arc::new(fi));

Check warning on line 217 in crates/common/src/var/flyweight.rs

View workflow job for this annotation

GitHub Actions / clippy

usage of an `Arc` that is not `Send` and `Sync`

warning: usage of an `Arc` that is not `Send` and `Sync` --> crates/common/src/var/flyweight.rs:217:28 | 217 | let fl = Flyweight(Arc::new(fi)); | ^^^^^^^^^^^^ | = note: `Arc<Inner>` is not `Send` and `Sync` as `Inner` is neither `Send` nor `Sync` = help: if the `Arc` will not used be across threads replace it with an `Rc` = help: otherwise make `Inner` `Send` and `Sync` or consider a wrapper type such as `Mutex` = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#arc_with_non_send_sync
let variant = Variant::Flyweight(fl);
Var::from_variant(variant)
}
}

impl Sequence for Flyweight {
fn is_empty(&self) -> bool {
self.0.contents.is_empty()
}

fn len(&self) -> usize {
self.0.contents.len()
}

fn index_in(&self, value: &Var, case_sensitive: bool) -> Result<Option<usize>, Error> {
self.0.contents.index_in(value, case_sensitive)
}

fn contains(&self, value: &Var, case_sensitive: bool) -> Result<bool, Error> {
self.0.contents.contains(value, case_sensitive)
}

fn index(&self, index: usize) -> Result<Var, Error> {
self.0.contents.index(index)
}

fn index_set(&self, index: usize, value: &Var) -> Result<Var, Error> {
let new_contents = self.0.contents.index_set(index, value)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}

fn push(&self, value: &Var) -> Result<Var, Error> {
let new_contents = self.0.contents.push(value)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}

fn insert(&self, index: usize, value: &Var) -> Result<Var, Error> {
let new_contents = self.0.contents.insert(index, value)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}

fn range(&self, from: isize, to: isize) -> Result<Var, Error> {
self.0.contents.range(from, to)
}

fn range_set(&self, from: isize, to: isize, with: &Var) -> Result<Var, Error> {
let new_contents = self.0.contents.range_set(from, to, with)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}

fn append(&self, other: &Var) -> Result<Var, Error> {
let new_contents = self.0.contents.append(other)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}

fn remove_at(&self, index: usize) -> Result<Var, Error> {
let new_contents = self.0.contents.remove_at(index)?;
let Variant::List(new_contents_as_list) = new_contents.variant() else {
return Err(E_TYPE);
};
Ok(self.with_new_contents(new_contents_as_list.clone()))
}
}
11 changes: 11 additions & 0 deletions crates/common/src/var/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use bincode::error::{DecodeError, EncodeError};
use bincode::{BorrowDecode, Decode, Encode};
use num_traits::ToPrimitive;
use std::cmp::max;
use std::fmt::{Debug, Formatter};
use std::hash::Hash;

#[derive(Clone)]
Expand All @@ -35,6 +36,11 @@ impl List {
Var::from_variant(Variant::List(List(Box::new(l))))
}

pub fn mk_list(values: &[Var]) -> List {
let l = im::Vector::from(values.to_vec());
List(Box::new(l))
}

pub fn iter(&self) -> impl Iterator<Item = Var> + '_ {
(0..self.len()).map(move |i| self.index(i).unwrap())
}
Expand Down Expand Up @@ -73,6 +79,11 @@ impl List {
}
}

impl Debug for List {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Sequence for List {
fn is_empty(&self) -> bool {
self.0.is_empty()
Expand Down
Loading

0 comments on commit 5c9819c

Please sign in to comment.