Add a variable assignment statement
This commit is contained in:
parent
a8c164819a
commit
746d567554
19 changed files with 195 additions and 84 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -150,6 +150,7 @@ dependencies = [
|
|||
"color-eyre",
|
||||
"dotenv",
|
||||
"from_variants",
|
||||
"itertools",
|
||||
"match_any",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
@ -196,6 +197,12 @@ version = "0.15.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.7"
|
||||
|
@ -282,6 +289,15 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
|
|
@ -8,6 +8,7 @@ clap = { version = "3.1.9", features = ["derive"] }
|
|||
color-eyre = "0.6.1"
|
||||
dotenv = "0.15.0"
|
||||
from_variants = "1.0.0"
|
||||
itertools = "0.10.3"
|
||||
match_any = "1.0.1"
|
||||
tracing = "0.1.32"
|
||||
tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }
|
||||
|
|
37
src/ast/ast_parser_iter.rs
Normal file
37
src/ast/ast_parser_iter.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use itertools::{peek_nth, PeekNth, PeekingNext};
|
||||
|
||||
pub struct ParserIter<T: Iterator>(PeekNth<T>);
|
||||
|
||||
impl<T: Iterator> ParserIter<T> {
|
||||
pub fn new(iter: T) -> ParserIter<T> {
|
||||
ParserIter(peek_nth(iter))
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> T::Item {
|
||||
self.0.next().unwrap()
|
||||
}
|
||||
|
||||
pub fn peek_nth(&mut self, n: usize) -> &T::Item {
|
||||
self.0.peek_nth(n).unwrap()
|
||||
}
|
||||
pub fn peek(&mut self) -> &T::Item {
|
||||
self.peek_nth(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Iterator> Iterator for ParserIter<T> {
|
||||
type Item = T::Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
Some(self.next())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Iterator> PeekingNext for ParserIter<T> {
|
||||
fn peeking_next<F>(&mut self, accept: F) -> Option<Self::Item>
|
||||
where
|
||||
F: FnOnce(&Self::Item) -> bool,
|
||||
{
|
||||
self.0.peeking_next(accept)
|
||||
}
|
||||
}
|
|
@ -120,7 +120,7 @@ pub enum Literal {
|
|||
Int(i32),
|
||||
Float(f32),
|
||||
Bool(bool),
|
||||
Nil
|
||||
Nil,
|
||||
}
|
||||
|
||||
pub struct BinaryExpr {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use itertools::PeekingNext;
|
||||
|
||||
use crate::lexer::token::{self, TokenType};
|
||||
|
||||
use super::super::parser::{InnerASTParsingError, Parser, Result};
|
||||
|
@ -12,7 +14,7 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
let mut node = self.comparison()?;
|
||||
while let Some(o) = self
|
||||
.token_iter
|
||||
.next_if(|t| matches!(t.token_type, TokenType::EqualEqual | TokenType::BangEqual))
|
||||
.peeking_next(|t| matches!(t.token_type, TokenType::EqualEqual | TokenType::BangEqual))
|
||||
{
|
||||
node = BinaryExpr::new(
|
||||
Box::new(node),
|
||||
|
@ -27,7 +29,7 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
fn comparison(&mut self) -> Result<ExpressionNode> {
|
||||
let mut node = self.term()?;
|
||||
|
||||
while let Some(o) = self.token_iter.next_if(|t| {
|
||||
while let Some(o) = self.token_iter.peeking_next(|t| {
|
||||
matches!(
|
||||
t.token_type,
|
||||
TokenType::Greater
|
||||
|
@ -50,7 +52,7 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
|
||||
while let Some(o) = self
|
||||
.token_iter
|
||||
.next_if(|t| matches!(t.token_type, TokenType::Minus | TokenType::Plus))
|
||||
.peeking_next(|t| matches!(t.token_type, TokenType::Minus | TokenType::Plus))
|
||||
{
|
||||
node = BinaryExpr::new(
|
||||
Box::new(node),
|
||||
|
@ -67,7 +69,7 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
|
||||
while let Some(o) = self
|
||||
.token_iter
|
||||
.next_if(|t| matches!(t.token_type, TokenType::Star | TokenType::Slash))
|
||||
.peeking_next(|t| matches!(t.token_type, TokenType::Star | TokenType::Slash))
|
||||
{
|
||||
node = BinaryExpr::new(
|
||||
Box::new(node),
|
||||
|
@ -82,7 +84,7 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
fn unary(&mut self) -> Result<ExpressionNode> {
|
||||
if let Some(op) = self
|
||||
.token_iter
|
||||
.next_if(|t| matches!(t.token_type, TokenType::Bang | TokenType::Minus))
|
||||
.peeking_next(|t| matches!(t.token_type, TokenType::Bang | TokenType::Minus))
|
||||
{
|
||||
let right = Box::new(self.unary()?);
|
||||
Ok(ExpressionNode::UnaryExpr(UnaryExpr::new(
|
||||
|
@ -95,30 +97,26 @@ impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
|||
}
|
||||
|
||||
fn primary(&mut self) -> Result<ExpressionNode> {
|
||||
let node = match self.token_iter.next() {
|
||||
Some(token) => match token.token_type {
|
||||
TokenType::False => ExpressionNode::Literal(Literal::Bool(false)),
|
||||
TokenType::True => ExpressionNode::Literal(Literal::Bool(true)),
|
||||
TokenType::Int(i) => ExpressionNode::Literal(Literal::Int(i)),
|
||||
TokenType::String(i) => ExpressionNode::Literal(Literal::String(i)),
|
||||
TokenType::Float(f) => ExpressionNode::Literal(Literal::Float(f)),
|
||||
TokenType::Nil => ExpressionNode::Literal(Literal::Nil),
|
||||
TokenType::LeftParen => {
|
||||
let expr = self.expression()?;
|
||||
let group = GroupingExpr::new(Box::new(expr));
|
||||
match self
|
||||
.token_iter
|
||||
.next_if(|v| matches!(v.token_type, TokenType::RightParen))
|
||||
{
|
||||
Some(_) => return Ok(group.into()),
|
||||
None => {
|
||||
return Err(token.location.wrap(InnerASTParsingError::UnmatchedBrace))
|
||||
}
|
||||
}
|
||||
let token = self.token_iter.next();
|
||||
let node = match token.token_type {
|
||||
TokenType::False => ExpressionNode::Literal(Literal::Bool(false)),
|
||||
TokenType::True => ExpressionNode::Literal(Literal::Bool(true)),
|
||||
TokenType::Int(i) => ExpressionNode::Literal(Literal::Int(i)),
|
||||
TokenType::String(i) => ExpressionNode::Literal(Literal::String(i)),
|
||||
TokenType::Float(f) => ExpressionNode::Literal(Literal::Float(f)),
|
||||
TokenType::Nil => ExpressionNode::Literal(Literal::Nil),
|
||||
TokenType::LeftParen => {
|
||||
let expr = self.expression()?;
|
||||
let group = GroupingExpr::new(Box::new(expr));
|
||||
match self
|
||||
.token_iter
|
||||
.peeking_next(|v| matches!(v.token_type, TokenType::RightParen))
|
||||
{
|
||||
Some(_) => return Ok(group.into()),
|
||||
None => return Err(token.location.wrap(InnerASTParsingError::UnmatchedBrace)),
|
||||
}
|
||||
a => return Err(token.location.wrap(InnerASTParsingError::IncorrectToken(a))),
|
||||
},
|
||||
None => todo!(),
|
||||
}
|
||||
a => return Err(token.location.wrap(InnerASTParsingError::IncorrectToken(a))),
|
||||
};
|
||||
Ok(node)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod ast_parser_iter;
|
||||
pub mod expression;
|
||||
pub mod statement;
|
||||
pub mod parser;
|
||||
pub mod statement;
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
use super::expression::expression_node;
|
||||
use super::ast_parser_iter::ParserIter;
|
||||
use super::statement::statement_node;
|
||||
use crate::error::ErrorLocationWrapper;
|
||||
use crate::lexer::{token, token::TokenType};
|
||||
|
||||
use std::iter;
|
||||
use std::result::Result as StdResult;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InnerASTParsingError {
|
||||
IncorrectToken(TokenType),
|
||||
UnmatchedBrace,
|
||||
ExpectedSemi,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InnerASTParsingError {
|
||||
|
@ -17,6 +16,7 @@ impl std::fmt::Display for InnerASTParsingError {
|
|||
match *self {
|
||||
Self::UnmatchedBrace => write!(f, "Unmatched brace"),
|
||||
Self::IncorrectToken(ref token) => write!(f, "Incorrect token {:?}", token),
|
||||
Self::ExpectedSemi => write!(f, "Expected semicolon"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ pub type ASTParsingError = ErrorLocationWrapper<InnerASTParsingError>;
|
|||
pub(super) type Result<T> = StdResult<T, ASTParsingError>;
|
||||
|
||||
pub struct Parser<'a, T: Iterator<Item = token::Token<'a>>> {
|
||||
pub(super) token_iter: iter::Peekable<T>,
|
||||
pub(super) token_iter: super::ast_parser_iter::ParserIter<T>,
|
||||
}
|
||||
|
||||
pub type ParseAllResult = StdResult<Vec<statement_node::Statement>, Vec<ASTParsingError>>;
|
||||
|
@ -34,13 +34,13 @@ pub type ParseAllResult = StdResult<Vec<statement_node::Statement>, Vec<ASTParsi
|
|||
impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
||||
pub fn new(iter: T) -> Parser<'a, T> {
|
||||
Parser {
|
||||
token_iter: iter.peekable(),
|
||||
token_iter: ParserIter::new(iter),
|
||||
}
|
||||
}
|
||||
pub fn parse_all(&mut self) -> ParseAllResult {
|
||||
let mut res = Ok(Vec::new());
|
||||
|
||||
while !matches!(self.token_iter.peek().unwrap().token_type, token::TokenType::Eof) {
|
||||
while !matches!(self.token_iter.peek().token_type, token::TokenType::Eof) {
|
||||
match self.statement() {
|
||||
Ok(s) => {
|
||||
if let Ok(ref mut v) = res {
|
||||
|
|
|
@ -5,6 +5,7 @@ use from_variants::FromVariants;
|
|||
pub enum Statement {
|
||||
Expression(ExpressionStatement),
|
||||
Print(PrintStatement),
|
||||
VariableAssignment(VariableAssignmentStatement),
|
||||
}
|
||||
|
||||
macro_rules! all_variants {
|
||||
|
@ -12,7 +13,7 @@ macro_rules! all_variants {
|
|||
{
|
||||
use match_any::match_any;
|
||||
use $crate::ast::statement::statement_node::*;
|
||||
match_any!($expr, Statement::Expression($val_name) | Statement::Print($val_name) => $expr_arm)
|
||||
match_any!($expr, Statement::Expression($val_name) | Statement::Print($val_name) | Statement::VariableAssignment($val_name) => $expr_arm)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -33,3 +34,15 @@ impl PrintStatement {
|
|||
Self(expr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VariableAssignmentStatement {
|
||||
pub var_name: String,
|
||||
pub node: ExpressionNode,
|
||||
}
|
||||
|
||||
impl VariableAssignmentStatement {
|
||||
pub fn new(var_name: String, node: ExpressionNode) -> Self {
|
||||
Self { var_name, node }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,70 @@
|
|||
use super::statement_node::{ExpressionStatement, Statement, PrintStatement};
|
||||
use itertools::PeekingNext;
|
||||
|
||||
use super::statement_node::{ExpressionStatement, PrintStatement, Statement};
|
||||
use crate::{
|
||||
ast::parser::{ASTParsingError, Parser, Result},
|
||||
lexer::token,
|
||||
ast::{
|
||||
parser::{InnerASTParsingError, Parser, Result},
|
||||
statement::statement_node::VariableAssignmentStatement,
|
||||
},
|
||||
lexer::token::{self, Token, TokenType},
|
||||
};
|
||||
|
||||
impl<'a, T: Iterator<Item = token::Token<'a>>> Parser<'a, T> {
|
||||
pub fn statement(&mut self) -> Result<Statement> {
|
||||
if let Some(_) = self
|
||||
self.print_statement()
|
||||
}
|
||||
|
||||
fn print_statement(&mut self) -> Result<Statement> {
|
||||
if let Some(Token { location: loc, .. }) = self
|
||||
.token_iter
|
||||
.next_if(|t| matches!(t.token_type, token::TokenType::Print))
|
||||
.peeking_next(|t| matches!(t.token_type, token::TokenType::Print))
|
||||
{
|
||||
self.print_statement()
|
||||
let expr = self.expression()?;
|
||||
if let token::TokenType::Semicolon = self.token_iter.peek().token_type {
|
||||
self.token_iter.next();
|
||||
Ok(PrintStatement::new(expr).into())
|
||||
} else {
|
||||
Err(loc.wrap(InnerASTParsingError::ExpectedSemi))
|
||||
}
|
||||
} else {
|
||||
self.expression_statement()
|
||||
self.variable_assignment_statement()
|
||||
}
|
||||
}
|
||||
fn print_statement(&mut self) -> Result<Statement> {
|
||||
let expr = self.expression()?;
|
||||
if let token::TokenType::Semicolon = self.token_iter.peek().unwrap().token_type {
|
||||
|
||||
fn variable_assignment_statement(&mut self) -> Result<Statement> {
|
||||
if matches!(
|
||||
self.token_iter.peek_nth(0).token_type,
|
||||
TokenType::Identifier(_)
|
||||
) && matches!(self.token_iter.peek_nth(1).token_type, TokenType::Equal)
|
||||
{
|
||||
let ident = if let TokenType::Identifier(ident) = self.token_iter.next().token_type {
|
||||
ident
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
self.token_iter.next();
|
||||
Ok(PrintStatement::new(expr).into())
|
||||
let expr = self.expression()?;
|
||||
|
||||
let token = self.token_iter.peek();
|
||||
if let token::TokenType::Semicolon = token.token_type {
|
||||
self.token_iter.next();
|
||||
Ok(VariableAssignmentStatement::new(ident, expr).into())
|
||||
} else {
|
||||
Err(token.location.wrap(InnerASTParsingError::ExpectedSemi))
|
||||
}
|
||||
} else {
|
||||
panic!();
|
||||
self.expression_statement()
|
||||
}
|
||||
}
|
||||
|
||||
fn expression_statement(&mut self) -> Result<Statement> {
|
||||
let expr = self.expression()?;
|
||||
if let token::TokenType::Semicolon = self.token_iter.peek().unwrap().token_type {
|
||||
let token = self.token_iter.peek();
|
||||
if let TokenType::Semicolon = token.token_type {
|
||||
self.token_iter.next();
|
||||
Ok(ExpressionStatement::new(expr).into())
|
||||
} else {
|
||||
panic!();
|
||||
Err(token.location.wrap(InnerASTParsingError::ExpectedSemi))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use super::{RuntimeError, types::Value};
|
||||
use super::Interpret;
|
||||
use super::{types::Value, RuntimeError};
|
||||
use crate::ast::expression::expression_node;
|
||||
|
||||
|
||||
|
||||
impl Interpret for expression_node::ExpressionNode {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
expression_node::all_variants!(self, n => n.interpret())
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
mod expression_interpreter;
|
||||
mod statement_interpreter;
|
||||
pub use super::{error::RuntimeError, types};
|
||||
use super::types::Value;
|
||||
pub use super::{error::RuntimeError, types};
|
||||
|
||||
pub trait Interpret {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError>;
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
use super::{RuntimeError, types::Value};
|
||||
use super::Interpret;
|
||||
use super::{types::Value, RuntimeError};
|
||||
use crate::ast::statement::statement_node;
|
||||
|
||||
|
||||
|
||||
impl Interpret for statement_node::Statement {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
statement_node::all_variants!(self, n => n.interpret())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Interpret for statement_node::PrintStatement {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
let res = self.0.interpret()?;
|
||||
println!("{:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interpret for statement_node::ExpressionStatement {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
self.0.interpret()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interpret for statement_node::VariableAssignmentStatement {
|
||||
fn interpret(&self) -> Result<Value, RuntimeError> {
|
||||
let expr_val = self.node.interpret()?;
|
||||
Ok(expr_val)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
struct Environment {
|
||||
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
pub mod ast_walker;
|
||||
pub mod types;
|
||||
pub mod error;
|
||||
mod environment;
|
||||
pub mod types;
|
||||
|
|
|
@ -42,17 +42,24 @@ impl<'a> LexerIter<'a> {
|
|||
|
||||
pub fn as_str_while<F: FnMut(char) -> bool>(&mut self, mut predicate: F) -> &'a str {
|
||||
let str = self.inner.as_str();
|
||||
let mut end_indice = 0;
|
||||
for (i, c) in str.char_indices() {
|
||||
end_indice = i;
|
||||
if !predicate(c) {
|
||||
break;
|
||||
|
||||
let mut iter = str.char_indices();
|
||||
let end_indice = loop {
|
||||
match iter.next() {
|
||||
Some((i, c)) => {
|
||||
if !predicate(c) {
|
||||
break i;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
break iter.offset();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
unsafe {
|
||||
self.inner = str.get_unchecked(end_indice..).chars();
|
||||
let res = str.get_unchecked(0..end_indice);
|
||||
res
|
||||
|
||||
str.get_unchecked(0..end_indice) as _
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ impl<'a, 'b> Lexer<'a, 'b> {
|
|||
.map(|v| self.get_token(token::TokenType::Int(v)))
|
||||
.map_err(|_| LexingErrorKind::IntPrimitiveTooBig)
|
||||
};
|
||||
return res.map(|v| Some(v)).map_err(|e| self.get_error(e))
|
||||
return res.map(Some).map_err(|e| self.get_error(e));
|
||||
/*
|
||||
|
||||
Err(IntErrorKind::PosOverflow) | Err(IntErrorKind::NegOverflow) => return Err(self.get_error(LexingErrorKind::IntPrimitiveTooBig)),
|
||||
|
@ -191,7 +191,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_int_literal_too_large() {
|
||||
let mut lexer = Lexer::new("2222222222222222222", None);
|
||||
let mut lexer = Lexer::new("2222222222222222222223", None);
|
||||
let errors = lexer.scan_tokens().unwrap_err();
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert!(matches!(
|
||||
|
|
|
@ -6,6 +6,12 @@ pub struct Token<'a> {
|
|||
pub location: Location<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Token<'a> {
|
||||
pub fn as_tuple<'b>(&'b self) -> (&'b TokenType, Location<'a>) {
|
||||
(&self.token_type, self.location)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TokenType {
|
||||
LeftParen,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![feature(char_indices_offset)]
|
||||
pub mod ast;
|
||||
pub mod error;
|
||||
pub mod interpreter;
|
||||
|
|
|
@ -2,12 +2,12 @@ use crftng_intrprtrs::run;
|
|||
|
||||
#[test]
|
||||
fn test_one_equality() {
|
||||
run_check_result_eq_bool("1 == 1", true);
|
||||
run_check_result_eq_bool("1 >= 1", true);
|
||||
run_check_result_eq_bool("1 <= 1", true);
|
||||
run_check_result_eq_bool("1 != 1", false);
|
||||
run_check_result_eq_bool("1 > 1", false);
|
||||
run_check_result_eq_bool("1 < 1", false);
|
||||
run_check_result_eq_bool("1 == 1;", true);
|
||||
run_check_result_eq_bool("1 >= 1;", true);
|
||||
run_check_result_eq_bool("1 <= 1;", true);
|
||||
run_check_result_eq_bool("1 != 1;", false);
|
||||
run_check_result_eq_bool("1 > 1;", false);
|
||||
run_check_result_eq_bool("1 < 1;", false);
|
||||
}
|
||||
|
||||
fn run_check_result_eq_bool(code: &str, value: bool) {
|
||||
|
|
Loading…
Reference in a new issue