Continue implementing the VM
This commit is contained in:
parent
b48b5d4369
commit
26348fb82e
@ -16,7 +16,7 @@ pub enum Expression {
|
|||||||
FieldAccess(Node<Box<FieldAccess>>),
|
FieldAccess(Node<Box<FieldAccess>>),
|
||||||
Grouped(Node<Box<Expression>>),
|
Grouped(Node<Box<Expression>>),
|
||||||
Identifier(Node<Identifier>),
|
Identifier(Node<Identifier>),
|
||||||
If(Node<Box<If>>),
|
If(Node<Box<IfExpression>>),
|
||||||
List(Node<Box<ListExpression>>),
|
List(Node<Box<ListExpression>>),
|
||||||
ListIndex(Node<Box<ListIndex>>),
|
ListIndex(Node<Box<ListIndex>>),
|
||||||
Literal(Node<Box<LiteralExpression>>),
|
Literal(Node<Box<LiteralExpression>>),
|
||||||
@ -197,7 +197,7 @@ impl Expression {
|
|||||||
Self::ListIndex(Node::new(Box::new(list_index), position))
|
Self::ListIndex(Node::new(Box::new(list_index), position))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn r#if(r#if: If, position: Span) -> Self {
|
pub fn r#if(r#if: IfExpression, position: Span) -> Self {
|
||||||
Self::If(Node::new(Box::new(r#if), position))
|
Self::If(Node::new(Box::new(r#if), position))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,7 +543,7 @@ impl Display for LogicOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub enum If {
|
pub enum IfExpression {
|
||||||
If {
|
If {
|
||||||
condition: Expression,
|
condition: Expression,
|
||||||
if_block: Node<Block>,
|
if_block: Node<Block>,
|
||||||
@ -558,7 +558,7 @@ pub enum If {
|
|||||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub enum ElseExpression {
|
pub enum ElseExpression {
|
||||||
Block(Node<Block>),
|
Block(Node<Block>),
|
||||||
If(Box<If>),
|
If(Node<Box<IfExpression>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ElseExpression {
|
impl Display for ElseExpression {
|
||||||
@ -570,16 +570,16 @@ impl Display for ElseExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for If {
|
impl Display for IfExpression {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
If::If {
|
IfExpression::If {
|
||||||
condition,
|
condition,
|
||||||
if_block,
|
if_block,
|
||||||
} => {
|
} => {
|
||||||
write!(f, "if {} {}", condition, if_block)
|
write!(f, "if {} {}", condition, if_block)
|
||||||
}
|
}
|
||||||
If::IfElse {
|
IfExpression::IfElse {
|
||||||
condition,
|
condition,
|
||||||
if_block,
|
if_block,
|
||||||
r#else,
|
r#else,
|
||||||
|
@ -774,7 +774,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_if(&mut self) -> Result<If, ParseError> {
|
fn parse_if(&mut self) -> Result<IfExpression, ParseError> {
|
||||||
// Assume that the "if" token has already been consumed
|
// Assume that the "if" token has already been consumed
|
||||||
|
|
||||||
self.mode = ParserMode::Condition;
|
self.mode = ParserMode::Condition;
|
||||||
@ -788,27 +788,30 @@ impl<'src> Parser<'src> {
|
|||||||
if let Token::Else = self.current_token {
|
if let Token::Else = self.current_token {
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
|
let if_keyword_start = self.current_position.0;
|
||||||
|
|
||||||
if let Token::If = self.current_token {
|
if let Token::If = self.current_token {
|
||||||
self.next_token()?;
|
self.next_token()?;
|
||||||
|
|
||||||
let else_if = self.parse_if()?;
|
let if_expression = self.parse_if()?;
|
||||||
|
let position = (if_keyword_start, self.current_position.1);
|
||||||
|
|
||||||
Ok(If::IfElse {
|
Ok(IfExpression::IfElse {
|
||||||
condition,
|
condition,
|
||||||
if_block,
|
if_block,
|
||||||
r#else: ElseExpression::If(Box::new(else_if)),
|
r#else: ElseExpression::If(Node::new(Box::new(if_expression), position)),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let else_block = self.parse_block()?;
|
let else_block = self.parse_block()?;
|
||||||
|
|
||||||
Ok(If::IfElse {
|
Ok(IfExpression::IfElse {
|
||||||
condition,
|
condition,
|
||||||
if_block,
|
if_block,
|
||||||
r#else: ElseExpression::Block(else_block),
|
r#else: ElseExpression::Block(else_block),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(If::If {
|
Ok(IfExpression::If {
|
||||||
condition,
|
condition,
|
||||||
if_block,
|
if_block,
|
||||||
})
|
})
|
||||||
@ -1383,7 +1386,7 @@ mod tests {
|
|||||||
parse(source),
|
parse(source),
|
||||||
Ok(AbstractSyntaxTree::with_statements([
|
Ok(AbstractSyntaxTree::with_statements([
|
||||||
Statement::Expression(Expression::r#if(
|
Statement::Expression(Expression::r#if(
|
||||||
If::If {
|
IfExpression::If {
|
||||||
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
||||||
if_block: Node::new(
|
if_block: Node::new(
|
||||||
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
||||||
@ -1407,7 +1410,7 @@ mod tests {
|
|||||||
parse(source),
|
parse(source),
|
||||||
Ok(AbstractSyntaxTree::with_statements([
|
Ok(AbstractSyntaxTree::with_statements([
|
||||||
Statement::Expression(Expression::r#if(
|
Statement::Expression(Expression::r#if(
|
||||||
If::IfElse {
|
IfExpression::IfElse {
|
||||||
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
||||||
if_block: Node::new(
|
if_block: Node::new(
|
||||||
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
||||||
@ -1438,7 +1441,7 @@ mod tests {
|
|||||||
parse(source),
|
parse(source),
|
||||||
Ok(AbstractSyntaxTree::with_statements([
|
Ok(AbstractSyntaxTree::with_statements([
|
||||||
Statement::Expression(Expression::r#if(
|
Statement::Expression(Expression::r#if(
|
||||||
If::IfElse {
|
IfExpression::IfElse {
|
||||||
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
condition: Expression::identifier(Identifier::new("x"), (3, 4)),
|
||||||
if_block: Node::new(
|
if_block: Node::new(
|
||||||
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
||||||
@ -1447,23 +1450,24 @@ mod tests {
|
|||||||
))]),
|
))]),
|
||||||
(5, 10)
|
(5, 10)
|
||||||
),
|
),
|
||||||
r#else: ElseExpression::If(Box::new(If::IfElse {
|
r#else: ElseExpression::If(Node::new(
|
||||||
|
Box::new(IfExpression::IfElse {
|
||||||
condition: Expression::identifier(Identifier::new("z"), (19, 20)),
|
condition: Expression::identifier(Identifier::new("z"), (19, 20)),
|
||||||
if_block: Node::new(
|
if_block: Node::new(
|
||||||
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
Block::Sync(vec![Statement::Expression(
|
||||||
Identifier::new("a"),
|
Expression::identifier(Identifier::new("a"), (23, 24))
|
||||||
(23, 24)
|
)]),
|
||||||
))]),
|
|
||||||
(21, 26)
|
(21, 26)
|
||||||
),
|
),
|
||||||
r#else: ElseExpression::Block(Node::new(
|
r#else: ElseExpression::Block(Node::new(
|
||||||
Block::Sync(vec![Statement::Expression(Expression::identifier(
|
Block::Sync(vec![Statement::Expression(
|
||||||
Identifier::new("b"),
|
Expression::identifier(Identifier::new("b"), (34, 35))
|
||||||
(34, 35)
|
)]),
|
||||||
))]),
|
|
||||||
(32, 37)
|
(32, 37)
|
||||||
)),
|
)),
|
||||||
})),
|
}),
|
||||||
|
(16, 37)
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
(0, 37)
|
(0, 37)
|
||||||
))
|
))
|
||||||
|
@ -12,9 +12,12 @@ use std::{
|
|||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
abstract_tree::{AbstractSyntaxTree, Block, CallExpression, FieldAccess, Node, Statement},
|
abstract_tree::{
|
||||||
|
AbstractSyntaxTree, Block, CallExpression, ElseExpression, FieldAccess, IfExpression,
|
||||||
|
ListExpression, Node, Statement,
|
||||||
|
},
|
||||||
parse, Analyzer, BuiltInFunctionError, Context, DustError, Expression, Identifier, ParseError,
|
parse, Analyzer, BuiltInFunctionError, Context, DustError, Expression, Identifier, ParseError,
|
||||||
Span, Struct, StructType, Type, Value, ValueError,
|
Span, Value, ValueError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Run the source code and return the result.
|
/// Run the source code and return the result.
|
||||||
@ -104,7 +107,8 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_statement(&self, statement: Statement) -> Result<Option<Value>, VmError> {
|
fn run_statement(&self, statement: Statement) -> Result<Option<Value>, VmError> {
|
||||||
match statement {
|
let position = statement.position();
|
||||||
|
let result = match statement {
|
||||||
Statement::Expression(expression) => self.run_expression(expression),
|
Statement::Expression(expression) => self.run_expression(expression),
|
||||||
Statement::ExpressionNullified(expression) => {
|
Statement::ExpressionNullified(expression) => {
|
||||||
self.run_expression(expression.inner)?;
|
self.run_expression(expression.inner)?;
|
||||||
@ -113,37 +117,18 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
Statement::Let(_) => todo!(),
|
Statement::Let(_) => todo!(),
|
||||||
Statement::StructDefinition(_) => todo!(),
|
Statement::StructDefinition(_) => todo!(),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
result.map_err(|error| VmError::Trace {
|
||||||
|
error: Box::new(error),
|
||||||
|
position,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_expression(&self, expression: Expression) -> Result<Option<Value>, VmError> {
|
fn run_expression(&self, expression: Expression) -> Result<Option<Value>, VmError> {
|
||||||
match expression {
|
let position = expression.position();
|
||||||
Expression::Block(Node { inner, position }) => match *inner {
|
let result = match expression {
|
||||||
Block::Async(statements) => {
|
Expression::Block(Node { inner, .. }) => self.run_block(*inner),
|
||||||
let error_option = statements
|
|
||||||
.into_par_iter()
|
|
||||||
.find_map_any(|statement| self.run_statement(statement).err());
|
|
||||||
|
|
||||||
if let Some(error) = error_option {
|
|
||||||
Err(error)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Block::Sync(statements) => {
|
|
||||||
let mut previous_value = None;
|
|
||||||
|
|
||||||
for statement in statements {
|
|
||||||
let position = statement.position();
|
|
||||||
|
|
||||||
previous_value = self.run_statement(statement)?;
|
|
||||||
|
|
||||||
self.context.collect_garbage(position.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(previous_value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Expression::Call(Node { inner, .. }) => {
|
Expression::Call(Node { inner, .. }) => {
|
||||||
let CallExpression { invoker, arguments } = *inner;
|
let CallExpression { invoker, arguments } = *inner;
|
||||||
|
|
||||||
@ -195,10 +180,18 @@ impl Vm {
|
|||||||
|
|
||||||
Ok(container_value.get_field(&field.inner))
|
Ok(container_value.get_field(&field.inner))
|
||||||
}
|
}
|
||||||
Expression::Grouped(_) => todo!(),
|
Expression::Grouped(expression) => self.run_expression(*expression.inner),
|
||||||
Expression::Identifier(_) => todo!(),
|
Expression::Identifier(identifier) => {
|
||||||
Expression::If(_) => todo!(),
|
let value_option = self.context.get_value(&identifier.inner);
|
||||||
Expression::List(_) => todo!(),
|
|
||||||
|
if let Some(value) = value_option {
|
||||||
|
Ok(Some(value))
|
||||||
|
} else {
|
||||||
|
Err(VmError::UndefinedVariable { identifier })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expression::If(if_expression) => self.run_if(*if_expression.inner),
|
||||||
|
Expression::List(list_expression) => self.run_list(*list_expression.inner),
|
||||||
Expression::ListIndex(_) => todo!(),
|
Expression::ListIndex(_) => todo!(),
|
||||||
Expression::Literal(_) => todo!(),
|
Expression::Literal(_) => todo!(),
|
||||||
Expression::Loop(_) => todo!(),
|
Expression::Loop(_) => todo!(),
|
||||||
@ -206,6 +199,147 @@ impl Vm {
|
|||||||
Expression::Range(_) => todo!(),
|
Expression::Range(_) => todo!(),
|
||||||
Expression::Struct(_) => todo!(),
|
Expression::Struct(_) => todo!(),
|
||||||
Expression::TupleAccess(_) => todo!(),
|
Expression::TupleAccess(_) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
result.map_err(|error| VmError::Trace {
|
||||||
|
error: Box::new(error),
|
||||||
|
position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_list(&self, list_expression: ListExpression) -> Result<Option<Value>, VmError> {
|
||||||
|
match list_expression {
|
||||||
|
ListExpression::AutoFill {
|
||||||
|
repeat_operand,
|
||||||
|
length_operand,
|
||||||
|
} => {
|
||||||
|
let position = length_operand.position();
|
||||||
|
let length = if let Some(value) = self.run_expression(length_operand)? {
|
||||||
|
if let Some(length) = value.as_integer() {
|
||||||
|
length
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedInteger { position });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue { position });
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = repeat_operand.position();
|
||||||
|
let value = if let Some(value) = self.run_expression(repeat_operand)? {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue { position });
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Value::list(vec![value; length as usize])))
|
||||||
|
}
|
||||||
|
ListExpression::Ordered(expressions) => {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
|
||||||
|
for expression in expressions {
|
||||||
|
let position = expression.position();
|
||||||
|
|
||||||
|
if let Some(value) = self.run_expression(expression)? {
|
||||||
|
values.push(value);
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue { position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(Value::list(values)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_block(&self, block: Block) -> Result<Option<Value>, VmError> {
|
||||||
|
match block {
|
||||||
|
Block::Async(statements) => {
|
||||||
|
let error_option = statements
|
||||||
|
.into_par_iter()
|
||||||
|
.find_map_any(|statement| self.run_statement(statement).err());
|
||||||
|
|
||||||
|
if let Some(error) = error_option {
|
||||||
|
Err(error)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Block::Sync(statements) => {
|
||||||
|
let mut previous_value = None;
|
||||||
|
|
||||||
|
for statement in statements {
|
||||||
|
let position = statement.position();
|
||||||
|
|
||||||
|
previous_value = self.run_statement(statement)?;
|
||||||
|
|
||||||
|
self.context.collect_garbage(position.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(previous_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_if(&self, if_expression: IfExpression) -> Result<Option<Value>, VmError> {
|
||||||
|
match if_expression {
|
||||||
|
IfExpression::If {
|
||||||
|
condition,
|
||||||
|
if_block,
|
||||||
|
} => {
|
||||||
|
let condition_position = condition.position();
|
||||||
|
let condition_value = if let Some(value) = self.run_expression(condition)? {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue {
|
||||||
|
position: condition_position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(boolean) = condition_value.as_boolean() {
|
||||||
|
if boolean {
|
||||||
|
self.run_expression(Expression::block(if_block.inner, if_block.position))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedBoolean {
|
||||||
|
position: condition_position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
IfExpression::IfElse {
|
||||||
|
condition,
|
||||||
|
if_block,
|
||||||
|
r#else,
|
||||||
|
} => {
|
||||||
|
let condition_position = condition.position();
|
||||||
|
let condition_value = if let Some(value) = self.run_expression(condition)? {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedValue {
|
||||||
|
position: condition_position,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(boolean) = condition_value.as_boolean() {
|
||||||
|
if boolean {
|
||||||
|
self.run_expression(Expression::block(if_block.inner, if_block.position))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(VmError::ExpectedBoolean {
|
||||||
|
position: condition_position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match r#else {
|
||||||
|
ElseExpression::If(if_expression) => {
|
||||||
|
self.run_expression(Expression::If(if_expression))
|
||||||
|
}
|
||||||
|
ElseExpression::Block(block) => {
|
||||||
|
self.run_expression(Expression::block(block.inner, block.position))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,6 +347,10 @@ impl Vm {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum VmError {
|
pub enum VmError {
|
||||||
ParseError(ParseError),
|
ParseError(ParseError),
|
||||||
|
Trace {
|
||||||
|
error: Box<VmError>,
|
||||||
|
position: Span,
|
||||||
|
},
|
||||||
ValueError {
|
ValueError {
|
||||||
error: ValueError,
|
error: ValueError,
|
||||||
position: Span,
|
position: Span,
|
||||||
@ -274,6 +412,7 @@ impl VmError {
|
|||||||
pub fn position(&self) -> Span {
|
pub fn position(&self) -> Span {
|
||||||
match self {
|
match self {
|
||||||
Self::ParseError(parse_error) => parse_error.position(),
|
Self::ParseError(parse_error) => parse_error.position(),
|
||||||
|
Self::Trace { position, .. } => *position,
|
||||||
Self::ValueError { position, .. } => *position,
|
Self::ValueError { position, .. } => *position,
|
||||||
Self::CannotMutate { position, .. } => *position,
|
Self::CannotMutate { position, .. } => *position,
|
||||||
Self::BuiltInFunctionError { position, .. } => *position,
|
Self::BuiltInFunctionError { position, .. } => *position,
|
||||||
@ -305,6 +444,13 @@ impl Display for VmError {
|
|||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::ParseError(parse_error) => write!(f, "{}", parse_error),
|
Self::ParseError(parse_error) => write!(f, "{}", parse_error),
|
||||||
|
Self::Trace { error, position } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Error during execution at position: {:?}\n{}",
|
||||||
|
position, error
|
||||||
|
)
|
||||||
|
}
|
||||||
Self::ValueError { error, .. } => write!(f, "{}", error),
|
Self::ValueError { error, .. } => write!(f, "{}", error),
|
||||||
Self::CannotMutate { value, .. } => {
|
Self::CannotMutate { value, .. } => {
|
||||||
write!(f, "Cannot mutate immutable value {}", value)
|
write!(f, "Cannot mutate immutable value {}", value)
|
||||||
@ -492,13 +638,6 @@ mod tests {
|
|||||||
assert_eq!(run(input), Ok(Some(Value::integer(42))));
|
assert_eq!(run(input), Ok(Some(Value::integer(42))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn map_property_access_expression() {
|
|
||||||
let input = "{ foobar = 42 }.('foo' + 'bar')";
|
|
||||||
|
|
||||||
assert_eq!(run(input), Ok(Some(Value::integer(42))));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_index_range() {
|
fn list_index_range() {
|
||||||
let input = "[1, 2, 3, 4, 5][1..3]";
|
let input = "[1, 2, 3, 4, 5][1..3]";
|
||||||
|
Loading…
Reference in New Issue
Block a user