Implement automatic value dropping
This commit is contained in:
parent
d05b5a8628
commit
4f5ad1e4aa
@ -7,8 +7,9 @@ use serde::{de::Visitor, Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
built_in_identifiers::all_built_in_identifiers,
|
built_in_identifiers::all_built_in_identifiers,
|
||||||
|
built_in_values::all_built_in_values,
|
||||||
error::{RuntimeError, SyntaxError, ValidationError},
|
error::{RuntimeError, SyntaxError, ValidationError},
|
||||||
AbstractTree, Context, Format, SyntaxNode, Type, Value,
|
AbstractTree, Context, Format, SyntaxNode, Type, Value, ValueData,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A string by which a variable is known to a context.
|
/// A string by which a variable is known to a context.
|
||||||
@ -60,9 +61,17 @@ impl AbstractTree for Identifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
|
fn validate(&self, _source: &str, context: &Context) -> Result<(), ValidationError> {
|
||||||
if let Some(_) = context.get_type(self)? {
|
if let Some((_, counter)) = context.get_data_and_counter(self)? {
|
||||||
|
counter.add_allowance()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
for built_in_value in all_built_in_values() {
|
||||||
|
if built_in_value.name() == self.inner().as_ref() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Err(ValidationError::VariableIdentifierNotFound(self.clone()))
|
Err(ValidationError::VariableIdentifierNotFound(self.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,18 +80,38 @@ impl AbstractTree for Identifier {
|
|||||||
if let Some(r#type) = context.get_type(self)? {
|
if let Some(r#type) = context.get_type(self)? {
|
||||||
Ok(r#type)
|
Ok(r#type)
|
||||||
} else {
|
} else {
|
||||||
|
for built_in_value in all_built_in_values() {
|
||||||
|
if built_in_value.name() == self.inner().as_ref() {
|
||||||
|
return Ok(built_in_value.get().r#type()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Err(ValidationError::VariableIdentifierNotFound(self.clone()))
|
Err(ValidationError::VariableIdentifierNotFound(self.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, _source: &str, context: &Context) -> Result<Value, RuntimeError> {
|
fn run(&self, _source: &str, context: &Context) -> Result<Value, RuntimeError> {
|
||||||
if let Some(value) = context.get_value(self)? {
|
if let Some((value_data, counter)) = context.get_data_and_counter(self)? {
|
||||||
Ok(value.clone())
|
if let ValueData::Value(value) = value_data {
|
||||||
} else {
|
counter.add_runtime_use()?;
|
||||||
return Err(RuntimeError::ValidationFailure(
|
|
||||||
ValidationError::VariableIdentifierNotFound(self.clone()),
|
if counter.runtime_uses() == counter.allowances() {
|
||||||
));
|
context.unset(self)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Ok(value.clone());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for built_in_value in all_built_in_values() {
|
||||||
|
if built_in_value.name() == self.inner().as_ref() {
|
||||||
|
return Ok(built_in_value.get().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RuntimeError::ValidationFailure(
|
||||||
|
ValidationError::VariableIdentifierNotFound(self.clone()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,12 @@
|
|||||||
//! has been explicitly set. If nothing is found, it will then check the built-
|
//! has been explicitly set. If nothing is found, it will then check the built-
|
||||||
//! in values and type definitions for a match. This means that the user can
|
//! in values and type definitions for a match. This means that the user can
|
||||||
//! override the built-ins.
|
//! override the built-ins.
|
||||||
|
mod usage_counter;
|
||||||
|
mod value_data;
|
||||||
|
|
||||||
|
pub use usage_counter::UsageCounter;
|
||||||
|
pub use value_data::ValueData;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
@ -45,7 +51,7 @@ use crate::{
|
|||||||
/// See the [module-level docs][self] for more info.
|
/// See the [module-level docs][self] for more info.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
inner: Arc<RwLock<BTreeMap<Identifier, ValueData>>>,
|
inner: Arc<RwLock<BTreeMap<Identifier, (ValueData, UsageCounter)>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
@ -57,7 +63,9 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return a lock guard to the inner BTreeMap.
|
/// Return a lock guard to the inner BTreeMap.
|
||||||
pub fn inner(&self) -> Result<RwLockReadGuard<BTreeMap<Identifier, ValueData>>, RwLockError> {
|
pub fn inner(
|
||||||
|
&self,
|
||||||
|
) -> Result<RwLockReadGuard<BTreeMap<Identifier, (ValueData, UsageCounter)>>, RwLockError> {
|
||||||
Ok(self.inner.read()?)
|
Ok(self.inner.read()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,21 +104,30 @@ impl Context {
|
|||||||
pub fn inherit_from(&self, other: &Context) -> Result<(), RwLockError> {
|
pub fn inherit_from(&self, other: &Context) -> Result<(), RwLockError> {
|
||||||
let mut self_variables = self.inner.write()?;
|
let mut self_variables = self.inner.write()?;
|
||||||
|
|
||||||
for (identifier, value_data) in other.inner.read()?.iter() {
|
for (identifier, (value_data, _counter)) in other.inner.read()?.iter() {
|
||||||
if let ValueData::Value { inner, .. } = value_data {
|
if let ValueData::Value(value) = value_data {
|
||||||
if inner.is_function() {
|
if value.is_function() {
|
||||||
self_variables.insert(identifier.clone(), value_data.clone());
|
self_variables.insert(
|
||||||
|
identifier.clone(),
|
||||||
|
(value_data.clone(), UsageCounter::new()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ValueData::TypeHint { inner } = value_data {
|
if let ValueData::TypeHint(r#type) = value_data {
|
||||||
if inner.is_function() {
|
if r#type.is_function() {
|
||||||
self_variables.insert(identifier.clone(), value_data.clone());
|
self_variables.insert(
|
||||||
|
identifier.clone(),
|
||||||
|
(value_data.clone(), UsageCounter::new()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ValueData::TypeDefinition(_) = value_data {
|
if let ValueData::TypeDefinition(_) = value_data {
|
||||||
self_variables.insert(identifier.clone(), value_data.clone());
|
self_variables.insert(
|
||||||
|
identifier.clone(),
|
||||||
|
(value_data.clone(), UsageCounter::new()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,21 +156,33 @@ impl Context {
|
|||||||
pub fn inherit_all_from(&self, other: &Context) -> Result<(), RwLockError> {
|
pub fn inherit_all_from(&self, other: &Context) -> Result<(), RwLockError> {
|
||||||
let mut self_variables = self.inner.write()?;
|
let mut self_variables = self.inner.write()?;
|
||||||
|
|
||||||
for (identifier, value_data) in other.inner.read()?.iter() {
|
for (identifier, (value_data, _counter)) in other.inner.read()?.iter() {
|
||||||
self_variables.insert(identifier.clone(), value_data.clone());
|
self_variables.insert(
|
||||||
|
identifier.clone(),
|
||||||
|
(value_data.clone(), UsageCounter::new()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a value from the context.
|
/// Get a [Value] and its [UsageCounter] from the context.
|
||||||
///
|
pub fn get_data_and_counter(
|
||||||
/// This will also return a built-in value if one matches the key. See the
|
&self,
|
||||||
/// [module-level docs][self] for more info.
|
identifier: &Identifier,
|
||||||
|
) -> Result<Option<(ValueData, UsageCounter)>, RwLockError> {
|
||||||
|
if let Some((value_data, counter)) = self.inner.read()?.get(identifier) {
|
||||||
|
return Ok(Some((value_data.clone(), counter.clone())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a [Value] from the context.
|
||||||
pub fn get_value(&self, identifier: &Identifier) -> Result<Option<Value>, RwLockError> {
|
pub fn get_value(&self, identifier: &Identifier) -> Result<Option<Value>, RwLockError> {
|
||||||
if let Some(value_data) = self.inner.read()?.get(identifier) {
|
if let Some((value_data, _counter)) = self.inner.read()?.get(identifier) {
|
||||||
if let ValueData::Value { inner, .. } = value_data {
|
if let ValueData::Value(value) = value_data {
|
||||||
return Ok(Some(inner.clone()));
|
return Ok(Some(value.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,15 +195,15 @@ impl Context {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a type from the context.
|
/// Get a [Type] from the context.
|
||||||
///
|
///
|
||||||
/// If the key matches a stored value, its type will be returned. It if
|
/// If the key matches a stored [Value], its type will be returned. It if
|
||||||
/// matches a type hint, the type hint will be returned.
|
/// matches a type hint, the type hint will be returned.
|
||||||
pub fn get_type(&self, identifier: &Identifier) -> Result<Option<Type>, RwLockError> {
|
pub fn get_type(&self, identifier: &Identifier) -> Result<Option<Type>, RwLockError> {
|
||||||
if let Some(value_data) = self.inner.read()?.get(identifier) {
|
if let Some((value_data, _counter)) = self.inner.read()?.get(identifier) {
|
||||||
match value_data {
|
match value_data {
|
||||||
ValueData::Value { inner, .. } => return Ok(Some(inner.r#type()?)),
|
ValueData::Value(value) => return Ok(Some(value.r#type()?)),
|
||||||
ValueData::TypeHint { inner, .. } => return Ok(Some(inner.clone())),
|
ValueData::TypeHint(r#type) => return Ok(Some(r#type.clone())),
|
||||||
ValueData::TypeDefinition(_) => todo!(),
|
ValueData::TypeDefinition(_) => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,7 +217,7 @@ impl Context {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a type definition from the context.
|
/// Get a [TypeDefinition] from the context.
|
||||||
///
|
///
|
||||||
/// This will also return a built-in type definition if one matches the key.
|
/// This will also return a built-in type definition if one matches the key.
|
||||||
/// See the [module-level docs][self] for more info.
|
/// See the [module-level docs][self] for more info.
|
||||||
@ -196,7 +225,7 @@ impl Context {
|
|||||||
&self,
|
&self,
|
||||||
identifier: &Identifier,
|
identifier: &Identifier,
|
||||||
) -> Result<Option<TypeDefinition>, RwLockError> {
|
) -> Result<Option<TypeDefinition>, RwLockError> {
|
||||||
if let Some(value_data) = self.inner.read()?.get(identifier) {
|
if let Some((value_data, _counter)) = self.inner.read()?.get(identifier) {
|
||||||
if let ValueData::TypeDefinition(definition) = value_data {
|
if let ValueData::TypeDefinition(definition) = value_data {
|
||||||
return Ok(Some(definition.clone()));
|
return Ok(Some(definition.clone()));
|
||||||
}
|
}
|
||||||
@ -215,13 +244,14 @@ impl Context {
|
|||||||
pub fn set_value(&self, key: Identifier, value: Value) -> Result<(), RwLockError> {
|
pub fn set_value(&self, key: Identifier, value: Value) -> Result<(), RwLockError> {
|
||||||
log::info!("Setting value: {key} = {value}");
|
log::info!("Setting value: {key} = {value}");
|
||||||
|
|
||||||
self.inner.write()?.insert(
|
let mut map = self.inner.write()?;
|
||||||
key,
|
let old_data = map.remove(&key);
|
||||||
ValueData::Value {
|
|
||||||
inner: value,
|
if let Some((_, old_counter)) = old_data {
|
||||||
runtime_uses: Arc::new(RwLock::new(0)),
|
map.insert(key, (ValueData::Value(value), old_counter.clone()));
|
||||||
},
|
} else {
|
||||||
);
|
map.insert(key, (ValueData::Value(value), UsageCounter::new()));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -235,7 +265,7 @@ impl Context {
|
|||||||
|
|
||||||
self.inner
|
self.inner
|
||||||
.write()?
|
.write()?
|
||||||
.insert(key, ValueData::TypeHint { inner: r#type });
|
.insert(key, (ValueData::TypeHint(r#type), UsageCounter::new()));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -249,15 +279,18 @@ impl Context {
|
|||||||
key: Identifier,
|
key: Identifier,
|
||||||
definition: TypeDefinition,
|
definition: TypeDefinition,
|
||||||
) -> Result<(), RwLockError> {
|
) -> Result<(), RwLockError> {
|
||||||
self.inner
|
self.inner.write()?.insert(
|
||||||
.write()?
|
key,
|
||||||
.insert(key, ValueData::TypeDefinition(definition));
|
(ValueData::TypeDefinition(definition), UsageCounter::new()),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a key-value pair.
|
/// Remove a key-value pair.
|
||||||
pub fn unset(&self, key: &Identifier) -> Result<(), RwLockError> {
|
pub fn unset(&self, key: &Identifier) -> Result<(), RwLockError> {
|
||||||
|
log::debug!("Dropping variable {key}.");
|
||||||
|
|
||||||
self.inner.write()?.remove(key);
|
self.inner.write()?.remove(key);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -294,7 +327,7 @@ impl PartialEq for Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Context {
|
impl PartialOrd for Context {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,79 +341,6 @@ impl Ord for Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ValueData {
|
|
||||||
Value {
|
|
||||||
inner: Value,
|
|
||||||
runtime_uses: Arc<RwLock<u16>>,
|
|
||||||
},
|
|
||||||
TypeHint {
|
|
||||||
inner: Type,
|
|
||||||
},
|
|
||||||
TypeDefinition(TypeDefinition),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for ValueData {}
|
|
||||||
|
|
||||||
impl PartialEq for ValueData {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(
|
|
||||||
ValueData::Value {
|
|
||||||
inner: left_inner,
|
|
||||||
runtime_uses: left_runtime_uses,
|
|
||||||
},
|
|
||||||
ValueData::Value {
|
|
||||||
inner: right_inner,
|
|
||||||
runtime_uses: right_runtime_uses,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if left_inner != right_inner {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
*left_runtime_uses.read().unwrap() == *right_runtime_uses.read().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(
|
|
||||||
ValueData::TypeHint { inner: left_inner },
|
|
||||||
ValueData::TypeHint { inner: right_inner },
|
|
||||||
) => left_inner == right_inner,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for ValueData {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for ValueData {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
use Ordering::*;
|
|
||||||
|
|
||||||
match (self, other) {
|
|
||||||
(
|
|
||||||
ValueData::Value {
|
|
||||||
inner: inner_left, ..
|
|
||||||
},
|
|
||||||
ValueData::Value {
|
|
||||||
inner: inner_right, ..
|
|
||||||
},
|
|
||||||
) => inner_left.cmp(inner_right),
|
|
||||||
(ValueData::Value { .. }, _) => Greater,
|
|
||||||
(
|
|
||||||
ValueData::TypeHint { inner: inner_left },
|
|
||||||
ValueData::TypeHint { inner: inner_right },
|
|
||||||
) => inner_left.cmp(inner_right),
|
|
||||||
(ValueData::TypeDefinition(left), ValueData::TypeDefinition(right)) => left.cmp(right),
|
|
||||||
(ValueData::TypeDefinition(_), _) => Greater,
|
|
||||||
(ValueData::TypeHint { .. }, _) => Less,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Context {
|
impl Display for Context {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
writeln!(f, "{{")?;
|
writeln!(f, "{{")?;
|
70
src/context/usage_counter.rs
Normal file
70
src/context/usage_counter.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::rw_lock_error::RwLockError;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct UsageCounter(Arc<RwLock<UsageCounterInner>>);
|
||||||
|
|
||||||
|
impl UsageCounter {
|
||||||
|
pub fn new() -> UsageCounter {
|
||||||
|
UsageCounter(Arc::new(RwLock::new(UsageCounterInner {
|
||||||
|
allowances: 0,
|
||||||
|
runtime_uses: 0,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allowances(&self) -> Result<usize, RwLockError> {
|
||||||
|
Ok(self.0.read()?.allowances)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime_uses(&self) -> Result<usize, RwLockError> {
|
||||||
|
Ok(self.0.read()?.runtime_uses)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_allowance(&self) -> Result<(), RwLockError> {
|
||||||
|
self.0.write()?.allowances += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_runtime_use(&self) -> Result<(), RwLockError> {
|
||||||
|
self.0.write()?.runtime_uses += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for UsageCounter {}
|
||||||
|
|
||||||
|
impl PartialEq for UsageCounter {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
let left = self.0.read().unwrap();
|
||||||
|
let right = other.0.read().unwrap();
|
||||||
|
|
||||||
|
*left == *right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for UsageCounter {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for UsageCounter {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let left = self.0.read().unwrap();
|
||||||
|
let right = other.0.read().unwrap();
|
||||||
|
|
||||||
|
left.cmp(&right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
struct UsageCounterInner {
|
||||||
|
pub allowances: usize,
|
||||||
|
pub runtime_uses: usize,
|
||||||
|
}
|
8
src/context/value_data.rs
Normal file
8
src/context/value_data.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use crate::{Type, TypeDefinition, Value};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub enum ValueData {
|
||||||
|
Value(Value),
|
||||||
|
TypeHint(Type),
|
||||||
|
TypeDefinition(TypeDefinition),
|
||||||
|
}
|
@ -308,10 +308,10 @@ impl Completer for DustCompleter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, value_data) in self.context.inner().unwrap().iter() {
|
for (key, (value_data, _counter)) in self.context.inner().unwrap().iter() {
|
||||||
let value = match value_data {
|
let value = match value_data {
|
||||||
ValueData::Value { inner, .. } => inner,
|
ValueData::Value(value) => value,
|
||||||
ValueData::TypeHint { .. } => continue,
|
ValueData::TypeHint(_) => continue,
|
||||||
ValueData::TypeDefinition(_) => continue,
|
ValueData::TypeDefinition(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user