Add new means of reporting type check errors
This commit is contained in:
parent
d4487117eb
commit
bb53331b65
@ -1,7 +1,9 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tree_sitter::Node;
|
|
||||||
|
|
||||||
use crate::{AbstractTree, Error, Identifier, Map, Result, Statement, Type, TypeDefinition, Value};
|
use crate::{
|
||||||
|
AbstractTree, Error, Identifier, Map, Result, Statement, SyntaxNode, SyntaxPosition, Type,
|
||||||
|
TypeDefinition, Value,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
pub struct Assignment {
|
pub struct Assignment {
|
||||||
@ -9,6 +11,7 @@ pub struct Assignment {
|
|||||||
type_definition: Option<TypeDefinition>,
|
type_definition: Option<TypeDefinition>,
|
||||||
operator: AssignmentOperator,
|
operator: AssignmentOperator,
|
||||||
statement: Statement,
|
statement: Statement,
|
||||||
|
syntax_position: SyntaxPosition,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
@ -19,15 +22,15 @@ pub enum AssignmentOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AbstractTree for Assignment {
|
impl AbstractTree for Assignment {
|
||||||
fn from_syntax_node(source: &str, node: Node, context: &Map) -> Result<Self> {
|
fn from_syntax_node(source: &str, syntax_node: SyntaxNode, context: &Map) -> Result<Self> {
|
||||||
Error::expect_syntax_node(source, "assignment", node)?;
|
Error::expect_syntax_node(source, "assignment", syntax_node)?;
|
||||||
|
|
||||||
let child_count = node.child_count();
|
let child_count = syntax_node.child_count();
|
||||||
|
|
||||||
let identifier_node = node.child(0).unwrap();
|
let identifier_node = syntax_node.child(0).unwrap();
|
||||||
let identifier = Identifier::from_syntax_node(source, identifier_node, context)?;
|
let identifier = Identifier::from_syntax_node(source, identifier_node, context)?;
|
||||||
|
|
||||||
let type_node = node.child(1).unwrap();
|
let type_node = syntax_node.child(1).unwrap();
|
||||||
let type_definition = if type_node.kind() == "type_definition" {
|
let type_definition = if type_node.kind() == "type_definition" {
|
||||||
Some(TypeDefinition::from_syntax_node(
|
Some(TypeDefinition::from_syntax_node(
|
||||||
source, type_node, context,
|
source, type_node, context,
|
||||||
@ -36,7 +39,11 @@ impl AbstractTree for Assignment {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let operator_node = node.child(child_count - 2).unwrap().child(0).unwrap();
|
let operator_node = syntax_node
|
||||||
|
.child(child_count - 2)
|
||||||
|
.unwrap()
|
||||||
|
.child(0)
|
||||||
|
.unwrap();
|
||||||
let operator = match operator_node.kind() {
|
let operator = match operator_node.kind() {
|
||||||
"=" => AssignmentOperator::Equal,
|
"=" => AssignmentOperator::Equal,
|
||||||
"+=" => AssignmentOperator::PlusEqual,
|
"+=" => AssignmentOperator::PlusEqual,
|
||||||
@ -51,7 +58,7 @@ impl AbstractTree for Assignment {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let statement_node = node.child(child_count - 1).unwrap();
|
let statement_node = syntax_node.child(child_count - 1).unwrap();
|
||||||
let statement = Statement::from_syntax_node(source, statement_node, context)?;
|
let statement = Statement::from_syntax_node(source, statement_node, context)?;
|
||||||
let statement_type = statement.expected_type(context)?;
|
let statement_type = statement.expected_type(context)?;
|
||||||
|
|
||||||
@ -71,16 +78,20 @@ impl AbstractTree for Assignment {
|
|||||||
type_definition,
|
type_definition,
|
||||||
operator,
|
operator,
|
||||||
statement,
|
statement,
|
||||||
|
syntax_position: syntax_node.range().into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, context: &Map) -> Result<()> {
|
fn check_type(&self, source: &str, context: &Map) -> Result<()> {
|
||||||
let statement_type = self.statement.expected_type(context)?;
|
let statement_type = self.statement.expected_type(context)?;
|
||||||
|
|
||||||
if let Some(type_definition) = &self.type_definition {
|
if let Some(type_definition) = &self.type_definition {
|
||||||
match self.operator {
|
match self.operator {
|
||||||
AssignmentOperator::Equal => {
|
AssignmentOperator::Equal => {
|
||||||
type_definition.inner().check(&statement_type)?;
|
type_definition
|
||||||
|
.inner()
|
||||||
|
.check(&statement_type)
|
||||||
|
.map_err(|error| error.at_source_position(source, self.syntax_position))?;
|
||||||
}
|
}
|
||||||
AssignmentOperator::PlusEqual => {
|
AssignmentOperator::PlusEqual => {
|
||||||
if let Type::List(item_type) = type_definition.inner() {
|
if let Type::List(item_type) = type_definition.inner() {
|
||||||
@ -88,7 +99,10 @@ impl AbstractTree for Assignment {
|
|||||||
} else {
|
} else {
|
||||||
type_definition
|
type_definition
|
||||||
.inner()
|
.inner()
|
||||||
.check(&self.identifier.expected_type(context)?)?;
|
.check(&self.identifier.expected_type(context)?)
|
||||||
|
.map_err(|error| {
|
||||||
|
error.at_source_position(source, self.syntax_position)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssignmentOperator::MinusEqual => todo!(),
|
AssignmentOperator::MinusEqual => todo!(),
|
||||||
@ -98,14 +112,16 @@ impl AbstractTree for Assignment {
|
|||||||
AssignmentOperator::Equal => {}
|
AssignmentOperator::Equal => {}
|
||||||
AssignmentOperator::PlusEqual => {
|
AssignmentOperator::PlusEqual => {
|
||||||
if let Type::List(item_type) = self.identifier.expected_type(context)? {
|
if let Type::List(item_type) = self.identifier.expected_type(context)? {
|
||||||
item_type.check(&statement_type)?;
|
item_type.check(&statement_type).map_err(|error| {
|
||||||
|
error.at_source_position(source, self.syntax_position)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssignmentOperator::MinusEqual => todo!(),
|
AssignmentOperator::MinusEqual => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.statement.check_type(context)?;
|
self.statement.check_type(source, context)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -50,12 +50,12 @@ impl AbstractTree for Block {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
for statement in &self.statements {
|
for statement in &self.statements {
|
||||||
if let Statement::Return(inner_statement) = statement {
|
if let Statement::Return(inner_statement) = statement {
|
||||||
return inner_statement.check_type(_context);
|
return inner_statement.check_type(_source, _context);
|
||||||
} else {
|
} else {
|
||||||
statement.check_type(_context)?;
|
statement.check_type(_source, _context)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,15 +65,15 @@ impl AbstractTree for Expression {
|
|||||||
Ok(expression)
|
Ok(expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Expression::Value(value_node) => value_node.check_type(_context),
|
Expression::Value(value_node) => value_node.check_type(_source, _context),
|
||||||
Expression::Identifier(identifier) => identifier.check_type(_context),
|
Expression::Identifier(identifier) => identifier.check_type(_source, _context),
|
||||||
Expression::Math(math) => math.check_type(_context),
|
Expression::Math(math) => math.check_type(_source, _context),
|
||||||
Expression::Logic(logic) => logic.check_type(_context),
|
Expression::Logic(logic) => logic.check_type(_source, _context),
|
||||||
Expression::FunctionCall(function_call) => function_call.check_type(_context),
|
Expression::FunctionCall(function_call) => function_call.check_type(_source, _context),
|
||||||
Expression::Index(index) => index.check_type(_context),
|
Expression::Index(index) => index.check_type(_source, _context),
|
||||||
Expression::Yield(r#yield) => r#yield.check_type(_context),
|
Expression::Yield(r#yield) => r#yield.check_type(_source, _context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ impl AbstractTree for FunctionCall {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, context: &Map) -> Result<()> {
|
||||||
let function_expression_type = self.function_expression.expected_type(context)?;
|
let function_expression_type = self.function_expression.expected_type(context)?;
|
||||||
|
|
||||||
let parameter_types = match function_expression_type {
|
let parameter_types = match function_expression_type {
|
||||||
|
@ -2,7 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tree_sitter::Node;
|
use tree_sitter::Node;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AbstractTree, Block, Error, Function, Identifier, Map, Result, Type, TypeDefinition, Value,
|
AbstractTree, Block, Error, Function, Identifier, Map, Result, SyntaxPosition, Type,
|
||||||
|
TypeDefinition, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -10,14 +11,21 @@ pub struct FunctionNode {
|
|||||||
parameters: Vec<Identifier>,
|
parameters: Vec<Identifier>,
|
||||||
body: Block,
|
body: Block,
|
||||||
r#type: Type,
|
r#type: Type,
|
||||||
|
syntax_position: SyntaxPosition,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FunctionNode {
|
impl FunctionNode {
|
||||||
pub fn new(parameters: Vec<Identifier>, body: Block, r#type: Type) -> Self {
|
pub fn new(
|
||||||
|
parameters: Vec<Identifier>,
|
||||||
|
body: Block,
|
||||||
|
r#type: Type,
|
||||||
|
syntax_position: SyntaxPosition,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
parameters,
|
parameters,
|
||||||
body,
|
body,
|
||||||
r#type,
|
r#type,
|
||||||
|
syntax_position,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +122,15 @@ impl AbstractTree for FunctionNode {
|
|||||||
let body = Block::from_syntax_node(source, body_node, &function_context)?;
|
let body = Block::from_syntax_node(source, body_node, &function_context)?;
|
||||||
|
|
||||||
let r#type = Type::function(parameter_types, return_type.take_inner());
|
let r#type = Type::function(parameter_types, return_type.take_inner());
|
||||||
|
let syntax_position = node.range().into();
|
||||||
|
|
||||||
Ok(FunctionNode::new(parameters, body, r#type))
|
Ok(FunctionNode::new(parameters, body, r#type, syntax_position))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, context: &Map) -> Result<()> {
|
fn check_type(&self, source: &str, context: &Map) -> Result<()> {
|
||||||
self.return_type()
|
self.return_type()
|
||||||
.check(&self.body.expected_type(context)?)?;
|
.check(&self.body.expected_type(context)?)
|
||||||
|
.map_err(|error| error.at_source_position(source, self.syntax_position))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,34 @@ pub use {
|
|||||||
r#match::*, r#while::*, r#yield::*, statement::*, type_definition::*, value_node::*,
|
r#match::*, r#while::*, r#yield::*, statement::*, type_definition::*, value_node::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tree_sitter::Node;
|
use tree_sitter::Node;
|
||||||
|
|
||||||
use crate::{Error, Map, Result, Value};
|
use crate::{Error, Map, Result, Value};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub struct SyntaxPosition {
|
||||||
|
pub start_byte: usize,
|
||||||
|
pub end_byte: usize,
|
||||||
|
pub start_row: usize,
|
||||||
|
pub start_column: usize,
|
||||||
|
pub end_row: usize,
|
||||||
|
pub end_column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tree_sitter::Range> for SyntaxPosition {
|
||||||
|
fn from(range: tree_sitter::Range) -> Self {
|
||||||
|
SyntaxPosition {
|
||||||
|
start_byte: range.start_byte,
|
||||||
|
end_byte: range.end_byte,
|
||||||
|
start_row: range.start_point.row,
|
||||||
|
start_column: range.start_point.column,
|
||||||
|
end_row: range.end_point.row,
|
||||||
|
end_column: range.end_point.column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Root {
|
pub struct Root {
|
||||||
statements: Vec<Statement>,
|
statements: Vec<Statement>,
|
||||||
}
|
}
|
||||||
@ -60,12 +84,12 @@ impl AbstractTree for Root {
|
|||||||
Ok(Root { statements })
|
Ok(Root { statements })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
for statement in &self.statements {
|
for statement in &self.statements {
|
||||||
if let Statement::Return(inner_statement) = statement {
|
if let Statement::Return(inner_statement) = statement {
|
||||||
return inner_statement.check_type(_context);
|
return inner_statement.check_type(_source, _context);
|
||||||
} else {
|
} else {
|
||||||
statement.check_type(_context)?;
|
statement.check_type(_source, _context)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +130,7 @@ pub trait AbstractTree: Sized {
|
|||||||
fn from_syntax_node(source: &str, node: Node, context: &Map) -> Result<Self>;
|
fn from_syntax_node(source: &str, node: Node, context: &Map) -> Result<Self>;
|
||||||
|
|
||||||
/// Verify the type integrity of the node.
|
/// Verify the type integrity of the node.
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,17 +66,19 @@ impl AbstractTree for Statement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Statement::Assignment(assignment) => assignment.check_type(_context),
|
Statement::Assignment(assignment) => assignment.check_type(_source, _context),
|
||||||
Statement::Expression(expression) => expression.check_type(_context),
|
Statement::Expression(expression) => expression.check_type(_source, _context),
|
||||||
Statement::IfElse(if_else) => if_else.check_type(_context),
|
Statement::IfElse(if_else) => if_else.check_type(_source, _context),
|
||||||
Statement::Match(r#match) => r#match.check_type(_context),
|
Statement::Match(r#match) => r#match.check_type(_source, _context),
|
||||||
Statement::While(r#while) => r#while.check_type(_context),
|
Statement::While(r#while) => r#while.check_type(_source, _context),
|
||||||
Statement::Block(block) => block.check_type(_context),
|
Statement::Block(block) => block.check_type(_source, _context),
|
||||||
Statement::For(r#for) => r#for.check_type(_context),
|
Statement::For(r#for) => r#for.check_type(_source, _context),
|
||||||
Statement::IndexAssignment(index_assignment) => index_assignment.check_type(_context),
|
Statement::IndexAssignment(index_assignment) => {
|
||||||
Statement::Return(statement) => statement.check_type(_context),
|
index_assignment.check_type(_source, _context)
|
||||||
|
}
|
||||||
|
Statement::Return(statement) => statement.check_type(_source, _context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
39
src/error.rs
39
src/error.rs
@ -6,7 +6,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tree_sitter::{LanguageError, Node, Point};
|
use tree_sitter::{LanguageError, Node, Point};
|
||||||
|
|
||||||
use crate::{value::Value, Type};
|
use crate::{value::Value, SyntaxPosition, Type};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Formatter},
|
fmt::{self, Formatter},
|
||||||
@ -21,11 +21,13 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
WithContext {
|
AtSourcePosition {
|
||||||
error: Box<Error>,
|
error: Box<Error>,
|
||||||
#[serde(skip)]
|
|
||||||
location: Point,
|
|
||||||
source: String,
|
source: String,
|
||||||
|
start_row: usize,
|
||||||
|
start_column: usize,
|
||||||
|
end_row: usize,
|
||||||
|
end_column: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
UnexpectedSyntaxNode {
|
UnexpectedSyntaxNode {
|
||||||
@ -181,11 +183,16 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn at_node(self, node: Node, source: &str) -> Self {
|
pub fn at_source_position(self, source: &str, position: SyntaxPosition) -> Self {
|
||||||
Error::WithContext {
|
let byte_range = position.start_byte..position.end_byte;
|
||||||
|
|
||||||
|
Error::AtSourcePosition {
|
||||||
error: Box::new(self),
|
error: Box::new(self),
|
||||||
location: node.start_position(),
|
source: source[byte_range].to_string(),
|
||||||
source: source[node.byte_range()].to_string(),
|
start_row: position.start_row,
|
||||||
|
start_column: position.start_column,
|
||||||
|
end_row: position.end_row,
|
||||||
|
end_column: position.end_column,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +232,7 @@ impl Error {
|
|||||||
|
|
||||||
pub fn is_type_check_error(&self, other: &Error) -> bool {
|
pub fn is_type_check_error(&self, other: &Error) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Error::WithContext { error, .. } => error.as_ref() == other,
|
Error::AtSourcePosition { error, .. } => error.as_ref() == other,
|
||||||
_ => self == other,
|
_ => self == other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,14 +434,18 @@ impl fmt::Display for Error {
|
|||||||
TypeCheckExpectedFunction { actual } => {
|
TypeCheckExpectedFunction { actual } => {
|
||||||
write!(f, "Type check error. Expected a function but got {actual}.")
|
write!(f, "Type check error. Expected a function but got {actual}.")
|
||||||
}
|
}
|
||||||
WithContext {
|
AtSourcePosition {
|
||||||
error,
|
error,
|
||||||
location,
|
|
||||||
source,
|
source,
|
||||||
|
start_row,
|
||||||
|
start_column,
|
||||||
|
end_row,
|
||||||
|
end_column,
|
||||||
} => {
|
} => {
|
||||||
let location = get_position(location);
|
write!(
|
||||||
|
f,
|
||||||
write!(f, "{error} Occured at {location}: \"{source}\"")
|
"{error} Occured at ({start_row}, {start_column}) to ({end_row}, {end_column}). Source: {source}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SerdeJson(message) => write!(f, "JSON processing error: {message}"),
|
SerdeJson(message) => write!(f, "JSON processing error: {message}"),
|
||||||
ParserCancelled => write!(
|
ParserCancelled => write!(
|
||||||
|
@ -115,7 +115,7 @@ impl Interpreter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(abstract_tree) = &self.abstract_tree {
|
if let Some(abstract_tree) = &self.abstract_tree {
|
||||||
abstract_tree.check_type(&self.context)?;
|
abstract_tree.check_type(source, &self.context)?;
|
||||||
abstract_tree.run(source, &self.context)
|
abstract_tree.run(source, &self.context)
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::none())
|
Ok(Value::none())
|
||||||
|
@ -11,6 +11,8 @@ pub use crate::{
|
|||||||
value::{function::Function, list::List, map::Map, Value},
|
value::{function::Function, list::List, map::Map, Value},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use tree_sitter::Node as SyntaxNode;
|
||||||
|
|
||||||
mod abstract_tree;
|
mod abstract_tree;
|
||||||
pub mod built_in_functions;
|
pub mod built_in_functions;
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -57,10 +57,12 @@ impl AbstractTree for Function {
|
|||||||
Ok(Function::ContextDefined(inner_function))
|
Ok(Function::ContextDefined(inner_function))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_type(&self, _context: &Map) -> Result<()> {
|
fn check_type(&self, _source: &str, _context: &Map) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Function::BuiltIn(_) => Ok(()),
|
Function::BuiltIn(_) => Ok(()),
|
||||||
Function::ContextDefined(defined_function) => defined_function.check_type(_context),
|
Function::ContextDefined(defined_function) => {
|
||||||
|
defined_function.check_type(_source, _context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user