Expand modules and function built-ins
This commit is contained in:
parent
18b8fd6681
commit
b7ae0f1b52
54
Cargo.lock
generated
54
Cargo.lock
generated
@ -205,6 +205,7 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"stanza",
|
"stanza",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -231,6 +232,17 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@ -300,6 +312,12 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
@ -327,6 +345,36 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.3"
|
version = "1.10.3"
|
||||||
@ -453,6 +501,12 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -19,4 +19,5 @@ clap = { version = "4.5.2", features = ["derive"] }
|
|||||||
colored = "2.1.0"
|
colored = "2.1.0"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
rand = "0.8.5"
|
||||||
stanza = "0.5.1"
|
stanza = "0.5.1"
|
||||||
|
@ -6,7 +6,7 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
abstract_tree::{Identifier, Type},
|
abstract_tree::{Identifier, Type},
|
||||||
error::RwLockPoisonError,
|
error::RwLockPoisonError,
|
||||||
value::{BuiltInFunction, BuiltInValue},
|
value::{BUILT_IN_FUNCTIONS, BUILT_IN_MODULES},
|
||||||
Value,
|
Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,10 +56,19 @@ impl Context {
|
|||||||
if self.inner.read()?.contains_key(identifier) {
|
if self.inner.read()?.contains_key(identifier) {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
match identifier.as_str() {
|
for module in BUILT_IN_MODULES {
|
||||||
"io" | "output" => Ok(true),
|
if identifier.as_str() == module.name() {
|
||||||
_ => Ok(false),
|
return Ok(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for function in BUILT_IN_FUNCTIONS {
|
||||||
|
if identifier.as_str() == function.name() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,26 +82,38 @@ impl Context {
|
|||||||
return Ok(Some(r#type.clone()));
|
return Ok(Some(r#type.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let r#type = match identifier.as_str() {
|
for module in BUILT_IN_MODULES {
|
||||||
"io" => BuiltInValue::Io.r#type(),
|
if identifier.as_str() == module.name() {
|
||||||
"output" => BuiltInFunction::Output.r#type(),
|
return Ok(Some(module.r#type()));
|
||||||
_ => return Ok(None),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(Some(r#type))
|
for function in BUILT_IN_MODULES {
|
||||||
|
if identifier.as_str() == function.name() {
|
||||||
|
return Ok(Some(function.r#type()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_value(&self, identifier: &Identifier) -> Result<Option<Value>, RwLockPoisonError> {
|
pub fn get_value(&self, identifier: &Identifier) -> Result<Option<Value>, RwLockPoisonError> {
|
||||||
if let Some(ValueData::Value(value)) = self.inner.read()?.get(identifier) {
|
if let Some(ValueData::Value(value)) = self.inner.read()?.get(identifier) {
|
||||||
Ok(Some(value.clone()))
|
Ok(Some(value.clone()))
|
||||||
} else {
|
} else {
|
||||||
let value = match identifier.as_str() {
|
for module in BUILT_IN_MODULES {
|
||||||
"io" => BuiltInValue::Io.value(),
|
if identifier.as_str() == module.name() {
|
||||||
"output" => Value::built_in_function(BuiltInFunction::Output),
|
return Ok(Some(module.value()));
|
||||||
_ => return Ok(None),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(Some(value))
|
for function in BUILT_IN_MODULES {
|
||||||
|
if identifier.as_str() == function.name() {
|
||||||
|
return Ok(Some(function.value()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@ impl Error {
|
|||||||
ValidationError::ExpectedFunction { .. } => todo!(),
|
ValidationError::ExpectedFunction { .. } => todo!(),
|
||||||
ValidationError::ExpectedValue(_) => todo!(),
|
ValidationError::ExpectedValue(_) => todo!(),
|
||||||
ValidationError::PropertyNotFound { .. } => todo!(),
|
ValidationError::PropertyNotFound { .. } => todo!(),
|
||||||
|
ValidationError::WrongArguments { .. } => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,6 +274,10 @@ pub enum ValidationError {
|
|||||||
/// The position of the item that gave the "expected" type.
|
/// The position of the item that gave the "expected" type.
|
||||||
expected_position: SourcePosition,
|
expected_position: SourcePosition,
|
||||||
},
|
},
|
||||||
|
WrongArguments {
|
||||||
|
expected: Vec<Type>,
|
||||||
|
actual: Vec<Type>,
|
||||||
|
},
|
||||||
VariableNotFound(Identifier),
|
VariableNotFound(Identifier),
|
||||||
PropertyNotFound {
|
PropertyNotFound {
|
||||||
identifier: Identifier,
|
identifier: Identifier,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
use chumsky::prelude::*;
|
use chumsky::{prelude::*, text::whitespace};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
@ -228,10 +228,11 @@ pub fn lexer<'src>() -> impl Parser<
|
|||||||
just("loop").padded(),
|
just("loop").padded(),
|
||||||
just("while").padded(),
|
just("while").padded(),
|
||||||
))
|
))
|
||||||
|
.delimited_by(whitespace(), whitespace())
|
||||||
.map(Token::Keyword);
|
.map(Token::Keyword);
|
||||||
|
|
||||||
choice((
|
choice((
|
||||||
boolean, float, integer, string, keyword, identifier, control, operator,
|
boolean, float, integer, string, identifier, keyword, control, operator,
|
||||||
))
|
))
|
||||||
.map_with(|token, state| (token, state.span()))
|
.map_with(|token, state| (token, state.span()))
|
||||||
.padded()
|
.padded()
|
||||||
|
151
src/value.rs
151
src/value.rs
@ -3,10 +3,12 @@ use std::{
|
|||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter},
|
||||||
io::stdin,
|
io::stdin,
|
||||||
|
num::ParseIntError,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
use stanza::{
|
use stanza::{
|
||||||
renderer::{console::Console, Renderer},
|
renderer::{console::Console, Renderer},
|
||||||
style::{HAlign, MinWidth, Styles},
|
style::{HAlign, MinWidth, Styles},
|
||||||
@ -16,7 +18,7 @@ use stanza::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
abstract_tree::{AbstractTree, Action, Block, Identifier, Type, WithPosition},
|
abstract_tree::{AbstractTree, Action, Block, Identifier, Type, WithPosition},
|
||||||
context::Context,
|
context::Context,
|
||||||
error::RuntimeError,
|
error::{RuntimeError, ValidationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@ -278,48 +280,104 @@ pub struct ParsedFunction {
|
|||||||
body: WithPosition<Block>,
|
body: WithPosition<Block>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static INT_PARSE: OnceLock<Value> = OnceLock::new();
|
||||||
|
static INT_RANDOM_RANGE: OnceLock<Value> = OnceLock::new();
|
||||||
|
static READ_LINE: OnceLock<Value> = OnceLock::new();
|
||||||
|
static WRITE_LINE: OnceLock<Value> = OnceLock::new();
|
||||||
|
|
||||||
|
pub const BUILT_IN_FUNCTIONS: [BuiltInFunction; 4] = [
|
||||||
|
BuiltInFunction::IntParse,
|
||||||
|
BuiltInFunction::IntRandomRange,
|
||||||
|
BuiltInFunction::ReadLine,
|
||||||
|
BuiltInFunction::WriteLine,
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
pub enum BuiltInFunction {
|
pub enum BuiltInFunction {
|
||||||
Output,
|
IntParse,
|
||||||
|
IntRandomRange,
|
||||||
ReadLine,
|
ReadLine,
|
||||||
|
WriteLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuiltInFunction {
|
impl BuiltInFunction {
|
||||||
pub fn output() -> Value {
|
pub fn name(&self) -> &'static str {
|
||||||
static OUTPUT: OnceLock<Value> = OnceLock::new();
|
match self {
|
||||||
|
BuiltInFunction::IntParse => "parse",
|
||||||
OUTPUT
|
BuiltInFunction::IntRandomRange => "random_range",
|
||||||
.get_or_init(|| Value::built_in_function(BuiltInFunction::Output))
|
BuiltInFunction::ReadLine => "read_line",
|
||||||
.clone()
|
BuiltInFunction::WriteLine => "write_line",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_line() -> Value {
|
pub fn value(&self) -> Value {
|
||||||
static READ_LINE: OnceLock<Value> = OnceLock::new();
|
match self {
|
||||||
|
BuiltInFunction::IntParse => {
|
||||||
READ_LINE
|
INT_PARSE.get_or_init(|| Value::built_in_function(BuiltInFunction::IntParse))
|
||||||
.get_or_init(|| Value::built_in_function(BuiltInFunction::ReadLine))
|
}
|
||||||
.clone()
|
BuiltInFunction::IntRandomRange => INT_RANDOM_RANGE
|
||||||
|
.get_or_init(|| Value::built_in_function(BuiltInFunction::IntRandomRange)),
|
||||||
|
BuiltInFunction::ReadLine => {
|
||||||
|
READ_LINE.get_or_init(|| Value::built_in_function(BuiltInFunction::ReadLine))
|
||||||
|
}
|
||||||
|
BuiltInFunction::WriteLine => {
|
||||||
|
WRITE_LINE.get_or_init(|| Value::built_in_function(BuiltInFunction::WriteLine))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn r#type(&self) -> Type {
|
pub fn r#type(&self) -> Type {
|
||||||
match self {
|
match self {
|
||||||
BuiltInFunction::Output => Type::Function {
|
BuiltInFunction::IntParse => Type::Function {
|
||||||
parameter_types: vec![Type::Any],
|
parameter_types: vec![Type::String],
|
||||||
return_type: Box::new(Type::None),
|
return_type: Box::new(Type::Integer),
|
||||||
|
},
|
||||||
|
BuiltInFunction::IntRandomRange => Type::Function {
|
||||||
|
parameter_types: vec![Type::Range],
|
||||||
|
return_type: Box::new(Type::Integer),
|
||||||
},
|
},
|
||||||
BuiltInFunction::ReadLine => Type::Function {
|
BuiltInFunction::ReadLine => Type::Function {
|
||||||
parameter_types: Vec::with_capacity(0),
|
parameter_types: Vec::with_capacity(0),
|
||||||
return_type: Box::new(Type::String),
|
return_type: Box::new(Type::String),
|
||||||
},
|
},
|
||||||
|
BuiltInFunction::WriteLine => Type::Function {
|
||||||
|
parameter_types: vec![Type::Any],
|
||||||
|
return_type: Box::new(Type::None),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call(&self, arguments: Vec<Value>, _context: &Context) -> Result<Action, RuntimeError> {
|
pub fn call(&self, arguments: Vec<Value>, _context: &Context) -> Result<Action, RuntimeError> {
|
||||||
match self {
|
match self {
|
||||||
BuiltInFunction::Output => {
|
BuiltInFunction::IntParse => {
|
||||||
println!("{}", arguments[0]);
|
let string = arguments.get(0).unwrap();
|
||||||
|
|
||||||
Ok(Action::None)
|
if let ValueInner::String(string) = string.inner().as_ref() {
|
||||||
|
// let integer = string.parse();
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
|
||||||
|
// Ok(Action::Return(Value::integer(integer)))
|
||||||
|
} else {
|
||||||
|
Err(RuntimeError::ValidationFailure(
|
||||||
|
ValidationError::WrongArguments {
|
||||||
|
expected: vec![Type::String],
|
||||||
|
actual: arguments.iter().map(|value| value.r#type()).collect(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuiltInFunction::IntRandomRange => {
|
||||||
|
let range = arguments.get(0).unwrap();
|
||||||
|
|
||||||
|
if let ValueInner::Range(range) = range.inner().as_ref() {
|
||||||
|
let random = thread_rng().gen_range(range.clone());
|
||||||
|
|
||||||
|
Ok(Action::Return(Value::integer(random)))
|
||||||
|
} else {
|
||||||
|
panic!("Built-in function cannot have a non-function type.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BuiltInFunction::ReadLine => {
|
BuiltInFunction::ReadLine => {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
@ -328,6 +386,11 @@ impl BuiltInFunction {
|
|||||||
|
|
||||||
Ok(Action::Return(Value::string(input)))
|
Ok(Action::Return(Value::string(input)))
|
||||||
}
|
}
|
||||||
|
BuiltInFunction::WriteLine => {
|
||||||
|
println!("{}", arguments[0]);
|
||||||
|
|
||||||
|
Ok(Action::None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,25 +398,59 @@ impl BuiltInFunction {
|
|||||||
impl Display for BuiltInFunction {
|
impl Display for BuiltInFunction {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
BuiltInFunction::Output => write!(f, "(to_output : any) : none {{ *MAGIC* }}"),
|
BuiltInFunction::IntParse => write!(f, "(input : int) : str {{ *MAGIC* }}"),
|
||||||
|
BuiltInFunction::IntRandomRange => write!(f, "(input: range) : int {{ *MAGIC* }}"),
|
||||||
BuiltInFunction::ReadLine => write!(f, "() : str {{ *MAGIC* }}"),
|
BuiltInFunction::ReadLine => write!(f, "() : str {{ *MAGIC* }}"),
|
||||||
|
BuiltInFunction::WriteLine => write!(f, "(to_output : any) : none {{ *MAGIC* }}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static INT: OnceLock<Value> = OnceLock::new();
|
||||||
static IO: OnceLock<Value> = OnceLock::new();
|
static IO: OnceLock<Value> = OnceLock::new();
|
||||||
|
|
||||||
pub enum BuiltInValue {
|
pub const BUILT_IN_MODULES: [BuiltInModule; 2] = [BuiltInModule::Integer, BuiltInModule::Io];
|
||||||
|
|
||||||
|
pub enum BuiltInModule {
|
||||||
|
Integer,
|
||||||
Io,
|
Io,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuiltInValue {
|
impl BuiltInModule {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
BuiltInModule::Integer => "int",
|
||||||
|
BuiltInModule::Io => "io",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn value(self) -> Value {
|
pub fn value(self) -> Value {
|
||||||
match self {
|
match self {
|
||||||
BuiltInValue::Io => {
|
BuiltInModule::Integer => {
|
||||||
let mut properties = BTreeMap::new();
|
let mut properties = BTreeMap::new();
|
||||||
|
|
||||||
properties.insert(Identifier::new("read_line"), BuiltInFunction::read_line());
|
properties.insert(
|
||||||
|
Identifier::new("parse"),
|
||||||
|
Value::built_in_function(BuiltInFunction::IntParse),
|
||||||
|
);
|
||||||
|
properties.insert(
|
||||||
|
Identifier::new("random_range"),
|
||||||
|
Value::built_in_function(BuiltInFunction::IntRandomRange),
|
||||||
|
);
|
||||||
|
|
||||||
|
INT.get_or_init(|| Value::map(properties)).clone()
|
||||||
|
}
|
||||||
|
BuiltInModule::Io => {
|
||||||
|
let mut properties = BTreeMap::new();
|
||||||
|
|
||||||
|
properties.insert(
|
||||||
|
Identifier::new("read_line"),
|
||||||
|
Value::built_in_function(BuiltInFunction::ReadLine),
|
||||||
|
);
|
||||||
|
properties.insert(
|
||||||
|
Identifier::new("write_line"),
|
||||||
|
Value::built_in_function(BuiltInFunction::WriteLine),
|
||||||
|
);
|
||||||
|
|
||||||
IO.get_or_init(|| Value::map(properties)).clone()
|
IO.get_or_init(|| Value::map(properties)).clone()
|
||||||
}
|
}
|
||||||
@ -361,8 +458,6 @@ impl BuiltInValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn r#type(self) -> Type {
|
pub fn r#type(self) -> Type {
|
||||||
match self {
|
Type::Map
|
||||||
BuiltInValue::Io => Type::Map,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user