Implement postfix parsing
This commit is contained in:
parent
60f8aab805
commit
82fbf796f3
@ -34,12 +34,6 @@ impl<T: Display> Display for Node<T> {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub enum Statement {
|
pub enum Statement {
|
||||||
// Variable assignment
|
|
||||||
Assignment {
|
|
||||||
identifier: Node<Identifier>,
|
|
||||||
value_node: Box<Node<Statement>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
// A sequence of statements
|
// A sequence of statements
|
||||||
Block(Vec<Node<Statement>>),
|
Block(Vec<Node<Statement>>),
|
||||||
|
|
||||||
@ -70,7 +64,7 @@ pub enum Statement {
|
|||||||
|
|
||||||
// Value collection expressions
|
// Value collection expressions
|
||||||
List(Vec<Node<Statement>>),
|
List(Vec<Node<Statement>>),
|
||||||
Map(Vec<(Node<Identifier>, Node<Statement>)>),
|
Map(Vec<(Node<Statement>, Node<Statement>)>),
|
||||||
|
|
||||||
// Hard-coded value
|
// Hard-coded value
|
||||||
Constant(Value),
|
Constant(Value),
|
||||||
@ -83,7 +77,6 @@ pub enum Statement {
|
|||||||
impl Statement {
|
impl Statement {
|
||||||
pub fn expected_type(&self, variables: &HashMap<Identifier, Value>) -> Option<Type> {
|
pub fn expected_type(&self, variables: &HashMap<Identifier, Value>) -> Option<Type> {
|
||||||
match self {
|
match self {
|
||||||
Statement::Assignment { .. } => None,
|
|
||||||
Statement::Block(nodes) => nodes.last().unwrap().inner.expected_type(variables),
|
Statement::Block(nodes) => nodes.last().unwrap().inner.expected_type(variables),
|
||||||
Statement::BinaryOperation { left, .. } => left.inner.expected_type(variables),
|
Statement::BinaryOperation { left, .. } => left.inner.expected_type(variables),
|
||||||
Statement::BuiltInFunctionCall { function, .. } => function.expected_return_type(),
|
Statement::BuiltInFunctionCall { function, .. } => function.expected_return_type(),
|
||||||
@ -104,10 +97,9 @@ impl Statement {
|
|||||||
let mut types = BTreeMap::new();
|
let mut types = BTreeMap::new();
|
||||||
|
|
||||||
for (identifier, item) in nodes {
|
for (identifier, item) in nodes {
|
||||||
types.insert(
|
if let Statement::Identifier(identifier) = &identifier.inner {
|
||||||
identifier.inner.clone(),
|
types.insert(identifier.clone(), item.inner.expected_type(variables)?);
|
||||||
item.inner.expected_type(variables)?,
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Type::Map(types))
|
Some(Type::Map(types))
|
||||||
@ -121,12 +113,6 @@ impl Statement {
|
|||||||
impl Display for Statement {
|
impl Display for Statement {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Statement::Assignment {
|
|
||||||
identifier,
|
|
||||||
value_node: value,
|
|
||||||
} => {
|
|
||||||
write!(f, "{identifier} = {value}")
|
|
||||||
}
|
|
||||||
Statement::Block(statements) => {
|
Statement::Block(statements) => {
|
||||||
write!(f, "{{ ")?;
|
write!(f, "{{ ")?;
|
||||||
|
|
||||||
@ -270,12 +256,18 @@ pub enum BinaryOperator {
|
|||||||
// Logic
|
// Logic
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
|
|
||||||
|
// Assignment
|
||||||
|
Assign,
|
||||||
|
AddAssign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for BinaryOperator {
|
impl Display for BinaryOperator {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
BinaryOperator::Add => write!(f, "+"),
|
BinaryOperator::Add => write!(f, "+"),
|
||||||
|
BinaryOperator::AddAssign => write!(f, "+="),
|
||||||
|
BinaryOperator::Assign => write!(f, "="),
|
||||||
BinaryOperator::And => write!(f, "&&"),
|
BinaryOperator::And => write!(f, "&&"),
|
||||||
BinaryOperator::Divide => write!(f, "/"),
|
BinaryOperator::Divide => write!(f, "/"),
|
||||||
BinaryOperator::Equal => write!(f, "=="),
|
BinaryOperator::Equal => write!(f, "=="),
|
||||||
|
@ -77,18 +77,6 @@ impl<'a> Analyzer<'a> {
|
|||||||
|
|
||||||
fn analyze_node(&self, node: &Node<Statement>) -> Result<(), AnalyzerError> {
|
fn analyze_node(&self, node: &Node<Statement>) -> Result<(), AnalyzerError> {
|
||||||
match &node.inner {
|
match &node.inner {
|
||||||
Statement::Assignment {
|
|
||||||
value_node: value, ..
|
|
||||||
} => {
|
|
||||||
self.analyze_node(value)?;
|
|
||||||
|
|
||||||
if value.inner.expected_type(self.variables).is_none() {
|
|
||||||
return Err(AnalyzerError::ExpectedValue {
|
|
||||||
actual: value.as_ref().clone(),
|
|
||||||
position: value.position,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Statement::BinaryOperation {
|
Statement::BinaryOperation {
|
||||||
left,
|
left,
|
||||||
operator,
|
operator,
|
||||||
@ -97,6 +85,12 @@ impl<'a> Analyzer<'a> {
|
|||||||
self.analyze_node(left)?;
|
self.analyze_node(left)?;
|
||||||
self.analyze_node(right)?;
|
self.analyze_node(right)?;
|
||||||
|
|
||||||
|
if let BinaryOperator::AddAssign | BinaryOperator::Assign = operator.inner {
|
||||||
|
if let Statement::Identifier(_) = left.inner {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let left_type = left.inner.expected_type(self.variables);
|
let left_type = left.inner.expected_type(self.variables);
|
||||||
let right_type = right.inner.expected_type(self.variables);
|
let right_type = right.inner.expected_type(self.variables);
|
||||||
|
|
||||||
@ -157,12 +151,13 @@ impl<'a> Analyzer<'a> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Statement::Identifier(_) => {
|
Statement::Identifier(identifier) => {
|
||||||
return Err(AnalyzerError::UnexpectedIdentifier {
|
if !self.variables.contains_key(identifier) {
|
||||||
|
return Err(AnalyzerError::UndefinedVariable {
|
||||||
identifier: node.clone(),
|
identifier: node.clone(),
|
||||||
position: node.position,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Statement::List(statements) => {
|
Statement::List(statements) => {
|
||||||
for statement in statements {
|
for statement in statements {
|
||||||
self.analyze_node(statement)?;
|
self.analyze_node(statement)?;
|
||||||
@ -251,6 +246,9 @@ pub enum AnalyzerError {
|
|||||||
actual: Node<Statement>,
|
actual: Node<Statement>,
|
||||||
position: Span,
|
position: Span,
|
||||||
},
|
},
|
||||||
|
UndefinedVariable {
|
||||||
|
identifier: Node<Statement>,
|
||||||
|
},
|
||||||
UnexpectedIdentifier {
|
UnexpectedIdentifier {
|
||||||
identifier: Node<Statement>,
|
identifier: Node<Statement>,
|
||||||
position: Span,
|
position: Span,
|
||||||
@ -274,6 +272,7 @@ impl AnalyzerError {
|
|||||||
AnalyzerError::ExpectedIntegerFloatOrString { position, .. } => *position,
|
AnalyzerError::ExpectedIntegerFloatOrString { position, .. } => *position,
|
||||||
AnalyzerError::ExpectedSameType { position, .. } => *position,
|
AnalyzerError::ExpectedSameType { position, .. } => *position,
|
||||||
AnalyzerError::ExpectedString { position, .. } => *position,
|
AnalyzerError::ExpectedString { position, .. } => *position,
|
||||||
|
AnalyzerError::UndefinedVariable { identifier } => identifier.position,
|
||||||
AnalyzerError::UnexpectedIdentifier { position, .. } => *position,
|
AnalyzerError::UnexpectedIdentifier { position, .. } => *position,
|
||||||
AnalyzerError::UnexectedString { position, .. } => *position,
|
AnalyzerError::UnexectedString { position, .. } => *position,
|
||||||
}
|
}
|
||||||
@ -315,6 +314,9 @@ impl Display for AnalyzerError {
|
|||||||
AnalyzerError::ExpectedValue { actual, .. } => {
|
AnalyzerError::ExpectedValue { actual, .. } => {
|
||||||
write!(f, "Expected value, found {}", actual)
|
write!(f, "Expected value, found {}", actual)
|
||||||
}
|
}
|
||||||
|
AnalyzerError::UndefinedVariable { identifier } => {
|
||||||
|
write!(f, "Undefined variable {}", identifier)
|
||||||
|
}
|
||||||
AnalyzerError::UnexpectedIdentifier { identifier, .. } => {
|
AnalyzerError::UnexpectedIdentifier { identifier, .. } => {
|
||||||
write!(f, "Unexpected identifier {}", identifier)
|
write!(f, "Unexpected identifier {}", identifier)
|
||||||
}
|
}
|
||||||
@ -443,7 +445,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unexpected_identifier() {
|
fn undefined_variable() {
|
||||||
let abstract_tree = AbstractSyntaxTree {
|
let abstract_tree = AbstractSyntaxTree {
|
||||||
nodes: [Node::new(
|
nodes: [Node::new(
|
||||||
Statement::Identifier(Identifier::new("x")),
|
Statement::Identifier(Identifier::new("x")),
|
||||||
@ -456,9 +458,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
analyzer.analyze(),
|
analyzer.analyze(),
|
||||||
Err(AnalyzerError::UnexpectedIdentifier {
|
Err(AnalyzerError::UndefinedVariable {
|
||||||
identifier: Node::new(Statement::Identifier(Identifier::new("x")), (0, 1)),
|
identifier: Node::new(Statement::Identifier(Identifier::new("x")), (0, 1))
|
||||||
position: (0, 1)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -122,10 +122,16 @@ impl Lexer {
|
|||||||
'"' => self.lex_string('"', source)?,
|
'"' => self.lex_string('"', source)?,
|
||||||
'\'' => self.lex_string('\'', source)?,
|
'\'' => self.lex_string('\'', source)?,
|
||||||
'+' => {
|
'+' => {
|
||||||
|
if let Some('=') = self.peek_second_char(source) {
|
||||||
|
self.position += 2;
|
||||||
|
|
||||||
|
(Token::PlusEqual, (self.position - 2, self.position))
|
||||||
|
} else {
|
||||||
self.position += 1;
|
self.position += 1;
|
||||||
|
|
||||||
(Token::Plus, (self.position - 1, self.position))
|
(Token::Plus, (self.position - 1, self.position))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
'*' => {
|
'*' => {
|
||||||
self.position += 1;
|
self.position += 1;
|
||||||
|
|
||||||
@ -452,6 +458,21 @@ impl Display for LexError {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_assign() {
|
||||||
|
let input = "x += 42";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
lex(input),
|
||||||
|
Ok(vec![
|
||||||
|
(Token::Identifier("x"), (0, 1)),
|
||||||
|
(Token::PlusEqual, (2, 4)),
|
||||||
|
(Token::Integer("42"), (5, 7)),
|
||||||
|
(Token::Eof, (7, 7)),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn or() {
|
fn or() {
|
||||||
let input = "true || false";
|
let input = "true || false";
|
||||||
|
@ -202,26 +202,11 @@ impl<'src> Parser<'src> {
|
|||||||
(Token::Identifier(text), position) => {
|
(Token::Identifier(text), position) => {
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
if let (Token::Equal, _) = self.current {
|
|
||||||
self.next_token()?;
|
|
||||||
|
|
||||||
let value_node = self.parse_node(0)?;
|
|
||||||
let right_end = value_node.position.1;
|
|
||||||
|
|
||||||
Ok(Node::new(
|
|
||||||
Statement::Assignment {
|
|
||||||
identifier: Node::new(Identifier::new(text), position),
|
|
||||||
value_node: Box::new(value_node),
|
|
||||||
},
|
|
||||||
(position.0, right_end),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(Node::new(
|
Ok(Node::new(
|
||||||
Statement::Identifier(Identifier::new(text)),
|
Statement::Identifier(Identifier::new(text)),
|
||||||
position,
|
position,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
(Token::String(string), position) => {
|
(Token::String(string), position) => {
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
@ -259,13 +244,17 @@ impl<'src> Parser<'src> {
|
|||||||
let next_node = self.parse_node(0)?;
|
let next_node = self.parse_node(0)?;
|
||||||
|
|
||||||
// If the next node is an assignment, this might be a map
|
// If the next node is an assignment, this might be a map
|
||||||
if let Statement::Assignment {
|
if let Statement::BinaryOperation {
|
||||||
identifier,
|
left,
|
||||||
value_node,
|
operator:
|
||||||
|
Node {
|
||||||
|
inner: BinaryOperator::Assign,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
right,
|
||||||
} = next_node.inner
|
} = next_node.inner
|
||||||
{
|
{
|
||||||
// If the current token is a comma, right curly brace, or the new
|
// If the current token is a comma, or the new statement is already a map
|
||||||
// statement is already a map
|
|
||||||
if self.current.0 == Token::Comma
|
if self.current.0 == Token::Comma
|
||||||
|| statement
|
|| statement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -281,38 +270,9 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the new property to the map
|
// Add the new property to the map
|
||||||
map_properties.push((identifier, *value_node));
|
map_properties.push((*left, *right));
|
||||||
}
|
}
|
||||||
// Otherwise, the new statement is a block
|
// Otherwise, the new statement is a block
|
||||||
} else if let Statement::Block(statements) =
|
|
||||||
statement.get_or_insert_with(|| Statement::Block(Vec::new()))
|
|
||||||
{
|
|
||||||
if self.current.0 == Token::Semicolon {
|
|
||||||
self.next_token()?;
|
|
||||||
|
|
||||||
statements.push(Node::new(
|
|
||||||
Statement::Nil(Box::new(Node::new(
|
|
||||||
Statement::Assignment {
|
|
||||||
identifier,
|
|
||||||
value_node,
|
|
||||||
},
|
|
||||||
next_node.position,
|
|
||||||
))),
|
|
||||||
(next_node.position.0, self.current.1 .1),
|
|
||||||
));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
statements.push(Node::new(
|
|
||||||
Statement::Assignment {
|
|
||||||
identifier,
|
|
||||||
value_node,
|
|
||||||
},
|
|
||||||
next_node.position,
|
|
||||||
));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if let Statement::Block(statements) =
|
} else if let Statement::Block(statements) =
|
||||||
statement.get_or_insert_with(|| Statement::Block(Vec::new()))
|
statement.get_or_insert_with(|| Statement::Block(Vec::new()))
|
||||||
@ -443,27 +403,42 @@ impl<'src> Parser<'src> {
|
|||||||
fn parse_infix(&mut self, left: Node<Statement>) -> Result<Node<Statement>, ParseError> {
|
fn parse_infix(&mut self, left: Node<Statement>) -> Result<Node<Statement>, ParseError> {
|
||||||
let left_start = left.position.0;
|
let left_start = left.position.0;
|
||||||
|
|
||||||
|
// Postfix operations
|
||||||
|
if let Token::Semicolon = &self.current.0 {
|
||||||
|
self.next_token()?;
|
||||||
|
|
||||||
|
let right_end = self.current.1 .1;
|
||||||
|
|
||||||
|
return Ok(Node::new(
|
||||||
|
Statement::Nil(Box::new(left)),
|
||||||
|
(left_start, right_end),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Infix operations
|
||||||
let binary_operator = match &self.current {
|
let binary_operator = match &self.current {
|
||||||
(Token::Dot, _) => {
|
(Token::Dot, _) => {
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
let right_node = self.parse_node(0)?;
|
let right = self.parse_node(0)?;
|
||||||
let right_end = right_node.position.1;
|
let right_end = right.position.1;
|
||||||
|
|
||||||
return Ok(Node::new(
|
return Ok(Node::new(
|
||||||
Statement::PropertyAccess(Box::new(left), Box::new(right_node)),
|
Statement::PropertyAccess(Box::new(left), Box::new(right)),
|
||||||
(left_start, right_end),
|
(left_start, right_end),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(Token::DoubleAmpersand, _) => Node::new(BinaryOperator::And, self.current.1),
|
(Token::DoubleAmpersand, _) => Node::new(BinaryOperator::And, self.current.1),
|
||||||
(Token::DoubleEqual, _) => Node::new(BinaryOperator::Equal, self.current.1),
|
(Token::DoubleEqual, _) => Node::new(BinaryOperator::Equal, self.current.1),
|
||||||
(Token::DoublePipe, _) => Node::new(BinaryOperator::Or, self.current.1),
|
(Token::DoublePipe, _) => Node::new(BinaryOperator::Or, self.current.1),
|
||||||
|
(Token::Equal, _) => Node::new(BinaryOperator::Assign, self.current.1),
|
||||||
(Token::Greater, _) => Node::new(BinaryOperator::Greater, self.current.1),
|
(Token::Greater, _) => Node::new(BinaryOperator::Greater, self.current.1),
|
||||||
(Token::GreaterEqual, _) => Node::new(BinaryOperator::GreaterOrEqual, self.current.1),
|
(Token::GreaterEqual, _) => Node::new(BinaryOperator::GreaterOrEqual, self.current.1),
|
||||||
(Token::Less, _) => Node::new(BinaryOperator::Less, self.current.1),
|
(Token::Less, _) => Node::new(BinaryOperator::Less, self.current.1),
|
||||||
(Token::LessEqual, _) => Node::new(BinaryOperator::LessOrEqual, self.current.1),
|
(Token::LessEqual, _) => Node::new(BinaryOperator::LessOrEqual, self.current.1),
|
||||||
(Token::Minus, _) => Node::new(BinaryOperator::Subtract, self.current.1),
|
(Token::Minus, _) => Node::new(BinaryOperator::Subtract, self.current.1),
|
||||||
(Token::Plus, _) => Node::new(BinaryOperator::Add, self.current.1),
|
(Token::Plus, _) => Node::new(BinaryOperator::Add, self.current.1),
|
||||||
|
(Token::PlusEqual, _) => Node::new(BinaryOperator::AddAssign, self.current.1),
|
||||||
(Token::Star, _) => Node::new(BinaryOperator::Multiply, self.current.1),
|
(Token::Star, _) => Node::new(BinaryOperator::Multiply, self.current.1),
|
||||||
(Token::Slash, _) => Node::new(BinaryOperator::Divide, self.current.1),
|
(Token::Slash, _) => Node::new(BinaryOperator::Divide, self.current.1),
|
||||||
(Token::Percent, _) => Node::new(BinaryOperator::Modulo, self.current.1),
|
(Token::Percent, _) => Node::new(BinaryOperator::Modulo, self.current.1),
|
||||||
@ -479,6 +454,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
|
let left_start = left.position.0;
|
||||||
let right = self.parse_node(0)?;
|
let right = self.parse_node(0)?;
|
||||||
let right_end = right.position.1;
|
let right_end = right.position.1;
|
||||||
|
|
||||||
@ -494,6 +470,8 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
fn current_precedence(&self) -> u8 {
|
fn current_precedence(&self) -> u8 {
|
||||||
match self.current.0 {
|
match self.current.0 {
|
||||||
|
Token::Semicolon => 10,
|
||||||
|
Token::Equal | Token::PlusEqual => 8,
|
||||||
Token::DoubleEqual => 7,
|
Token::DoubleEqual => 7,
|
||||||
Token::DoubleAmpersand | Token::DoublePipe => 6,
|
Token::DoubleAmpersand | Token::DoublePipe => 6,
|
||||||
Token::Greater | Token::GreaterEqual | Token::Less | Token::LessEqual => 5,
|
Token::Greater | Token::GreaterEqual | Token::Less | Token::LessEqual => 5,
|
||||||
@ -506,6 +484,17 @@ impl<'src> Parser<'src> {
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn peek_token(&mut self) -> Token {
|
||||||
|
self.lexer
|
||||||
|
.peek_token(self.source)
|
||||||
|
.map(|(token, _)| token)
|
||||||
|
.unwrap_or(Token::Eof)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_is_postfix(&mut self) -> bool {
|
||||||
|
matches!(self.peek_token(), Token::Semicolon)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
@ -588,6 +577,29 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_assign() {
|
||||||
|
let input = "a += 1";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse(input),
|
||||||
|
Ok(AbstractSyntaxTree {
|
||||||
|
nodes: [Node::new(
|
||||||
|
Statement::BinaryOperation {
|
||||||
|
left: Box::new(Node::new(
|
||||||
|
Statement::Identifier(Identifier::new("a")),
|
||||||
|
(0, 1)
|
||||||
|
)),
|
||||||
|
operator: Node::new(BinaryOperator::AddAssign, (2, 4)),
|
||||||
|
right: Box::new(Node::new(Statement::Constant(Value::integer(1)), (5, 6))),
|
||||||
|
},
|
||||||
|
(0, 6)
|
||||||
|
)]
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn or() {
|
fn or() {
|
||||||
let input = "true || false";
|
let input = "true || false";
|
||||||
@ -667,37 +679,49 @@ mod tests {
|
|||||||
Statement::Block(vec![
|
Statement::Block(vec![
|
||||||
Node::new(
|
Node::new(
|
||||||
Statement::Nil(Box::new(Node::new(
|
Statement::Nil(Box::new(Node::new(
|
||||||
Statement::Assignment {
|
Statement::BinaryOperation {
|
||||||
identifier: Node::new(Identifier::new("foo"), (2, 5)),
|
left: Box::new(Node::new(
|
||||||
value_node: Box::new(Node::new(
|
Statement::Identifier(Identifier::new("foo")),
|
||||||
|
(2, 5)
|
||||||
|
)),
|
||||||
|
operator: Node::new(BinaryOperator::Assign, (6, 8)),
|
||||||
|
right: Box::new(Node::new(
|
||||||
Statement::Constant(Value::integer(42)),
|
Statement::Constant(Value::integer(42)),
|
||||||
(8, 10)
|
(9, 11)
|
||||||
))
|
)),
|
||||||
},
|
},
|
||||||
(2, 10)
|
(2, 11)
|
||||||
),)),
|
),)),
|
||||||
(2, 15)
|
(2, 15)
|
||||||
),
|
),
|
||||||
Node::new(
|
Node::new(
|
||||||
Statement::Nil(Box::new(Node::new(
|
Statement::Nil(Box::new(Node::new(
|
||||||
Statement::Assignment {
|
Statement::BinaryOperation {
|
||||||
identifier: Node::new(Identifier::new("bar"), (12, 15)),
|
left: Box::new(Node::new(
|
||||||
value_node: Box::new(Node::new(
|
Statement::Identifier(Identifier::new("bar")),
|
||||||
|
(16, 19)
|
||||||
|
)),
|
||||||
|
operator: Node::new(BinaryOperator::Assign, (20, 22)),
|
||||||
|
right: Box::new(Node::new(
|
||||||
Statement::Constant(Value::integer(42)),
|
Statement::Constant(Value::integer(42)),
|
||||||
(18, 20)
|
(23, 25)
|
||||||
))
|
)),
|
||||||
},
|
},
|
||||||
(12, 20)
|
(12, 20)
|
||||||
),)),
|
),)),
|
||||||
(12, 25)
|
(12, 25)
|
||||||
),
|
),
|
||||||
Node::new(
|
Node::new(
|
||||||
Statement::Assignment {
|
Statement::BinaryOperation {
|
||||||
identifier: Node::new(Identifier::new("baz"), (22, 25)),
|
left: Box::new(Node::new(
|
||||||
value_node: Box::new(Node::new(
|
Statement::Identifier(Identifier::new("baz")),
|
||||||
|
(26, 29)
|
||||||
|
)),
|
||||||
|
operator: Node::new(BinaryOperator::Assign, (30, 32)),
|
||||||
|
right: Box::new(Node::new(
|
||||||
Statement::Constant(Value::string("42")),
|
Statement::Constant(Value::string("42")),
|
||||||
(28, 32)
|
(22, 32)
|
||||||
))
|
)),
|
||||||
},
|
},
|
||||||
(22, 32)
|
(22, 32)
|
||||||
)
|
)
|
||||||
@ -731,15 +755,15 @@ mod tests {
|
|||||||
nodes: [Node::new(
|
nodes: [Node::new(
|
||||||
Statement::Map(vec![
|
Statement::Map(vec![
|
||||||
(
|
(
|
||||||
Node::new(Identifier::new("foo"), (2, 5)),
|
Node::new(Statement::Identifier(Identifier::new("foo")), (2, 5)),
|
||||||
Node::new(Statement::Constant(Value::integer(42)), (8, 10))
|
Node::new(Statement::Constant(Value::integer(42)), (8, 10))
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Node::new(Identifier::new("bar"), (12, 15)),
|
Node::new(Statement::Identifier(Identifier::new("bar")), (12, 15)),
|
||||||
Node::new(Statement::Constant(Value::integer(42)), (18, 20))
|
Node::new(Statement::Constant(Value::integer(42)), (18, 20))
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Node::new(Identifier::new("baz"), (22, 25)),
|
Node::new(Statement::Identifier(Identifier::new("baz")), (22, 25)),
|
||||||
Node::new(Statement::Constant(Value::string("42")), (28, 32))
|
Node::new(Statement::Constant(Value::string("42")), (28, 32))
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@ -760,11 +784,11 @@ mod tests {
|
|||||||
nodes: [Node::new(
|
nodes: [Node::new(
|
||||||
Statement::Map(vec![
|
Statement::Map(vec![
|
||||||
(
|
(
|
||||||
Node::new(Identifier::new("x"), (2, 3)),
|
Node::new(Statement::Identifier(Identifier::new("x")), (2, 3)),
|
||||||
Node::new(Statement::Constant(Value::integer(42)), (6, 8))
|
Node::new(Statement::Constant(Value::integer(42)), (6, 8))
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Node::new(Identifier::new("y"), (10, 11)),
|
Node::new(Statement::Identifier(Identifier::new("y")), (10, 11)),
|
||||||
Node::new(Statement::Constant(Value::string("foobar")), (14, 22))
|
Node::new(Statement::Constant(Value::string("foobar")), (14, 22))
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
@ -784,7 +808,7 @@ mod tests {
|
|||||||
Ok(AbstractSyntaxTree {
|
Ok(AbstractSyntaxTree {
|
||||||
nodes: [Node::new(
|
nodes: [Node::new(
|
||||||
Statement::Map(vec![(
|
Statement::Map(vec![(
|
||||||
Node::new(Identifier::new("x"), (2, 3)),
|
Node::new(Statement::Identifier(Identifier::new("x")), (2, 3)),
|
||||||
Node::new(Statement::Constant(Value::integer(42)), (6, 8))
|
Node::new(Statement::Constant(Value::integer(42)), (6, 8))
|
||||||
)]),
|
)]),
|
||||||
(0, 11)
|
(0, 11)
|
||||||
@ -854,19 +878,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn malformed_assignment() {
|
|
||||||
let input = "false = 1";
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse(input),
|
|
||||||
Err(ParseError::UnexpectedToken {
|
|
||||||
actual: TokenOwned::Equal,
|
|
||||||
position: (6, 7)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn less_than() {
|
fn less_than() {
|
||||||
let input = "1 < 2";
|
let input = "1 < 2";
|
||||||
@ -1278,9 +1289,13 @@ mod tests {
|
|||||||
parse(input),
|
parse(input),
|
||||||
Ok(AbstractSyntaxTree {
|
Ok(AbstractSyntaxTree {
|
||||||
nodes: [Node::new(
|
nodes: [Node::new(
|
||||||
Statement::Assignment {
|
Statement::BinaryOperation {
|
||||||
identifier: Node::new(Identifier::new("a"), (0, 1)),
|
left: Box::new(Node::new(
|
||||||
value_node: Box::new(Node::new(
|
Statement::Identifier(Identifier::new("a")),
|
||||||
|
(0, 1)
|
||||||
|
)),
|
||||||
|
operator: Node::new(BinaryOperator::Assign, (2, 3)),
|
||||||
|
right: Box::new(Node::new(
|
||||||
Statement::BinaryOperation {
|
Statement::BinaryOperation {
|
||||||
left: Box::new(Node::new(
|
left: Box::new(Node::new(
|
||||||
Statement::Constant(Value::integer(1)),
|
Statement::Constant(Value::integer(1)),
|
||||||
|
@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// Source code token.
|
/// Source code token.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum Token<'src> {
|
pub enum Token<'src> {
|
||||||
|
// End of file
|
||||||
Eof,
|
Eof,
|
||||||
|
|
||||||
Identifier(&'src str),
|
|
||||||
|
|
||||||
// Hard-coded values
|
// Hard-coded values
|
||||||
Boolean(&'src str),
|
Boolean(&'src str),
|
||||||
Float(&'src str),
|
Float(&'src str),
|
||||||
|
Identifier(&'src str),
|
||||||
Integer(&'src str),
|
Integer(&'src str),
|
||||||
String(&'src str),
|
String(&'src str),
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ pub enum Token<'src> {
|
|||||||
Minus,
|
Minus,
|
||||||
Percent,
|
Percent,
|
||||||
Plus,
|
Plus,
|
||||||
|
PlusEqual,
|
||||||
RightCurlyBrace,
|
RightCurlyBrace,
|
||||||
RightParenthesis,
|
RightParenthesis,
|
||||||
RightSquareBrace,
|
RightSquareBrace,
|
||||||
@ -75,6 +76,7 @@ impl<'src> Token<'src> {
|
|||||||
Token::Minus => TokenOwned::Minus,
|
Token::Minus => TokenOwned::Minus,
|
||||||
Token::Percent => TokenOwned::Percent,
|
Token::Percent => TokenOwned::Percent,
|
||||||
Token::Plus => TokenOwned::Plus,
|
Token::Plus => TokenOwned::Plus,
|
||||||
|
Token::PlusEqual => TokenOwned::PlusEqual,
|
||||||
Token::ReadLine => TokenOwned::ReadLine,
|
Token::ReadLine => TokenOwned::ReadLine,
|
||||||
Token::RightCurlyBrace => TokenOwned::RightCurlyBrace,
|
Token::RightCurlyBrace => TokenOwned::RightCurlyBrace,
|
||||||
Token::RightParenthesis => TokenOwned::RightParenthesis,
|
Token::RightParenthesis => TokenOwned::RightParenthesis,
|
||||||
@ -113,6 +115,7 @@ impl<'src> Token<'src> {
|
|||||||
Token::Minus => "-",
|
Token::Minus => "-",
|
||||||
Token::Percent => "%",
|
Token::Percent => "%",
|
||||||
Token::Plus => "+",
|
Token::Plus => "+",
|
||||||
|
Token::PlusEqual => "+=",
|
||||||
Token::ReadLine => "read_line",
|
Token::ReadLine => "read_line",
|
||||||
Token::RightCurlyBrace => "}",
|
Token::RightCurlyBrace => "}",
|
||||||
Token::RightParenthesis => ")",
|
Token::RightParenthesis => ")",
|
||||||
@ -159,6 +162,7 @@ impl<'src> PartialEq for Token<'src> {
|
|||||||
(Token::Minus, Token::Minus) => true,
|
(Token::Minus, Token::Minus) => true,
|
||||||
(Token::Percent, Token::Percent) => true,
|
(Token::Percent, Token::Percent) => true,
|
||||||
(Token::Plus, Token::Plus) => true,
|
(Token::Plus, Token::Plus) => true,
|
||||||
|
(Token::PlusEqual, Token::PlusEqual) => true,
|
||||||
(Token::ReadLine, Token::ReadLine) => true,
|
(Token::ReadLine, Token::ReadLine) => true,
|
||||||
(Token::RightCurlyBrace, Token::RightCurlyBrace) => true,
|
(Token::RightCurlyBrace, Token::RightCurlyBrace) => true,
|
||||||
(Token::RightParenthesis, Token::RightParenthesis) => true,
|
(Token::RightParenthesis, Token::RightParenthesis) => true,
|
||||||
@ -212,6 +216,7 @@ pub enum TokenOwned {
|
|||||||
Minus,
|
Minus,
|
||||||
Percent,
|
Percent,
|
||||||
Plus,
|
Plus,
|
||||||
|
PlusEqual,
|
||||||
RightCurlyBrace,
|
RightCurlyBrace,
|
||||||
RightParenthesis,
|
RightParenthesis,
|
||||||
RightSquareBrace,
|
RightSquareBrace,
|
||||||
@ -247,6 +252,7 @@ impl Display for TokenOwned {
|
|||||||
TokenOwned::Minus => Token::Minus.fmt(f),
|
TokenOwned::Minus => Token::Minus.fmt(f),
|
||||||
TokenOwned::Percent => Token::Percent.fmt(f),
|
TokenOwned::Percent => Token::Percent.fmt(f),
|
||||||
TokenOwned::Plus => Token::Plus.fmt(f),
|
TokenOwned::Plus => Token::Plus.fmt(f),
|
||||||
|
TokenOwned::PlusEqual => Token::PlusEqual.fmt(f),
|
||||||
TokenOwned::ReadLine => Token::ReadLine.fmt(f),
|
TokenOwned::ReadLine => Token::ReadLine.fmt(f),
|
||||||
TokenOwned::RightCurlyBrace => Token::RightCurlyBrace.fmt(f),
|
TokenOwned::RightCurlyBrace => Token::RightCurlyBrace.fmt(f),
|
||||||
TokenOwned::RightParenthesis => Token::RightParenthesis.fmt(f),
|
TokenOwned::RightParenthesis => Token::RightParenthesis.fmt(f),
|
||||||
|
@ -52,28 +52,74 @@ impl Vm {
|
|||||||
variables: &mut HashMap<Identifier, Value>,
|
variables: &mut HashMap<Identifier, Value>,
|
||||||
) -> Result<Option<Value>, VmError> {
|
) -> Result<Option<Value>, VmError> {
|
||||||
match node.inner {
|
match node.inner {
|
||||||
Statement::Assignment {
|
|
||||||
identifier,
|
|
||||||
value_node,
|
|
||||||
} => {
|
|
||||||
let value_node_position = value_node.position;
|
|
||||||
let value = if let Some(value) = self.run_node(*value_node, variables)? {
|
|
||||||
value
|
|
||||||
} else {
|
|
||||||
return Err(VmError::ExpectedValue {
|
|
||||||
position: value_node_position,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
variables.insert(identifier.inner, value);
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
Statement::BinaryOperation {
|
Statement::BinaryOperation {
|
||||||
left,
|
left,
|
||||||
operator,
|
operator,
|
||||||
right,
|
right,
|
||||||
} => {
|
} => {
|
||||||
|
let right_position = right.position;
|
||||||
|
|
||||||
|
if let BinaryOperator::Assign = operator.inner {
|
||||||
|
let identifier = if let Statement::Identifier(identifier) = left.inner {
|
||||||
|
identifier
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedIdentifier {
|
||||||
|
position: left.position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if let Some(value) = self.run_node(*right, variables)? {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue {
|
||||||
|
position: right_position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
variables.insert(identifier, value.clone());
|
||||||
|
|
||||||
|
return Ok(Some(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let BinaryOperator::AddAssign = operator.inner {
|
||||||
|
let identifier = if let Statement::Identifier(identifier) = left.inner {
|
||||||
|
identifier
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedIdentifier {
|
||||||
|
position: left.position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let right_value = if let Some(value) = self.run_node(*right, variables)? {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue {
|
||||||
|
position: right_position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let left_value =
|
||||||
|
variables
|
||||||
|
.get(&identifier)
|
||||||
|
.ok_or_else(|| VmError::UndefinedVariable {
|
||||||
|
identifier: Node::new(
|
||||||
|
Statement::Identifier(identifier.clone()),
|
||||||
|
left.position,
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let new_value = left_value.add(&right_value).map_err(|value_error| {
|
||||||
|
VmError::ValueError {
|
||||||
|
error: value_error,
|
||||||
|
position: right_position,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
variables.insert(identifier, new_value.clone());
|
||||||
|
|
||||||
|
return Ok(Some(new_value));
|
||||||
|
}
|
||||||
|
|
||||||
let left_position = left.position;
|
let left_position = left.position;
|
||||||
let left_value = if let Some(value) = self.run_node(*left, variables)? {
|
let left_value = if let Some(value) = self.run_node(*left, variables)? {
|
||||||
value
|
value
|
||||||
@ -83,7 +129,6 @@ impl Vm {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let right_position = right.position;
|
|
||||||
let right_value = if let Some(value) = self.run_node(*right, variables)? {
|
let right_value = if let Some(value) = self.run_node(*right, variables)? {
|
||||||
value
|
value
|
||||||
} else {
|
} else {
|
||||||
@ -92,7 +137,7 @@ impl Vm {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = match operator.inner {
|
match operator.inner {
|
||||||
BinaryOperator::Add => left_value.add(&right_value),
|
BinaryOperator::Add => left_value.add(&right_value),
|
||||||
BinaryOperator::And => left_value.and(&right_value),
|
BinaryOperator::And => left_value.and(&right_value),
|
||||||
BinaryOperator::Divide => left_value.divide(&right_value),
|
BinaryOperator::Divide => left_value.divide(&right_value),
|
||||||
@ -107,13 +152,13 @@ impl Vm {
|
|||||||
BinaryOperator::Multiply => left_value.multiply(&right_value),
|
BinaryOperator::Multiply => left_value.multiply(&right_value),
|
||||||
BinaryOperator::Or => left_value.or(&right_value),
|
BinaryOperator::Or => left_value.or(&right_value),
|
||||||
BinaryOperator::Subtract => left_value.subtract(&right_value),
|
BinaryOperator::Subtract => left_value.subtract(&right_value),
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
.map(Some)
|
||||||
.map_err(|value_error| VmError::ValueError {
|
.map_err(|value_error| VmError::ValueError {
|
||||||
error: value_error,
|
error: value_error,
|
||||||
position: node.position,
|
position: node.position,
|
||||||
})?;
|
})
|
||||||
|
|
||||||
Ok(Some(result))
|
|
||||||
}
|
}
|
||||||
Statement::Block(statements) => {
|
Statement::Block(statements) => {
|
||||||
let mut previous_value = None;
|
let mut previous_value = None;
|
||||||
@ -206,9 +251,8 @@ impl Vm {
|
|||||||
if let Some(value) = variables.get(&identifier) {
|
if let Some(value) = variables.get(&identifier) {
|
||||||
Ok(Some(value.clone()))
|
Ok(Some(value.clone()))
|
||||||
} else {
|
} else {
|
||||||
Err(VmError::UndefinedIdentifier {
|
Err(VmError::UndefinedVariable {
|
||||||
identifier,
|
identifier: Node::new(Statement::Identifier(identifier), node.position),
|
||||||
position: node.position,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,6 +275,13 @@ impl Vm {
|
|||||||
let mut values = BTreeMap::new();
|
let mut values = BTreeMap::new();
|
||||||
|
|
||||||
for (identifier, value_node) in nodes {
|
for (identifier, value_node) in nodes {
|
||||||
|
let identifier = if let Statement::Identifier(identifier) = identifier.inner {
|
||||||
|
identifier
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedIdentifier {
|
||||||
|
position: identifier.position,
|
||||||
|
});
|
||||||
|
};
|
||||||
let position = value_node.position;
|
let position = value_node.position;
|
||||||
let value = if let Some(value) = self.run_node(value_node, variables)? {
|
let value = if let Some(value) = self.run_node(value_node, variables)? {
|
||||||
value
|
value
|
||||||
@ -238,7 +289,7 @@ impl Vm {
|
|||||||
return Err(VmError::ExpectedValue { position });
|
return Err(VmError::ExpectedValue { position });
|
||||||
};
|
};
|
||||||
|
|
||||||
values.insert(identifier.inner, value);
|
values.insert(identifier, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(Value::map(values)))
|
Ok(Some(Value::map(values)))
|
||||||
@ -347,9 +398,8 @@ pub enum VmError {
|
|||||||
ExpectedValue {
|
ExpectedValue {
|
||||||
position: Span,
|
position: Span,
|
||||||
},
|
},
|
||||||
UndefinedIdentifier {
|
UndefinedVariable {
|
||||||
identifier: Identifier,
|
identifier: Node<Statement>,
|
||||||
position: Span,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +416,7 @@ impl VmError {
|
|||||||
Self::ExpectedFunction { position, .. } => *position,
|
Self::ExpectedFunction { position, .. } => *position,
|
||||||
Self::ExpectedList { position } => *position,
|
Self::ExpectedList { position } => *position,
|
||||||
Self::ExpectedValue { position } => *position,
|
Self::ExpectedValue { position } => *position,
|
||||||
Self::UndefinedIdentifier { position, .. } => *position,
|
Self::UndefinedVariable { identifier } => identifier.position,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,15 +480,8 @@ impl Display for VmError {
|
|||||||
Self::ExpectedValue { position } => {
|
Self::ExpectedValue { position } => {
|
||||||
write!(f, "Expected a value at position: {:?}", position)
|
write!(f, "Expected a value at position: {:?}", position)
|
||||||
}
|
}
|
||||||
Self::UndefinedIdentifier {
|
Self::UndefinedVariable { identifier } => {
|
||||||
identifier,
|
write!(f, "Undefined identifier: {}", identifier)
|
||||||
position,
|
|
||||||
} => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Undefined identifier: {} at position: {:?}",
|
|
||||||
identifier, position
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -448,6 +491,13 @@ impl Display for VmError {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_assign() {
|
||||||
|
let input = "x = 1; x += 1; x";
|
||||||
|
|
||||||
|
assert_eq!(run(input, &mut HashMap::new()), Ok(Some(Value::integer(2))));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn or() {
|
fn or() {
|
||||||
let input = "true || false";
|
let input = "true || false";
|
||||||
|
Loading…
Reference in New Issue
Block a user