Add anaylsis to check for valid fields and indexes
This commit is contained in:
parent
40a71da3a5
commit
2b8dda14e3
@ -147,7 +147,7 @@ impl Statement {
|
|||||||
BinaryOperator::ListIndex => {
|
BinaryOperator::ListIndex => {
|
||||||
let left_type = left.inner.expected_type(context)?;
|
let left_type = left.inner.expected_type(context)?;
|
||||||
|
|
||||||
if let Type::List { item_type } = left_type {
|
if let Type::List { item_type, .. } = left_type {
|
||||||
Some(*item_type)
|
Some(*item_type)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -167,6 +167,7 @@ impl Statement {
|
|||||||
|
|
||||||
Some(Type::List {
|
Some(Type::List {
|
||||||
item_type: Box::new(item_type),
|
item_type: Box::new(item_type),
|
||||||
|
length: nodes.len(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Statement::Map(nodes) => {
|
Statement::Map(nodes) => {
|
||||||
|
@ -11,7 +11,7 @@ use std::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
abstract_tree::{BinaryOperator, UnaryOperator},
|
abstract_tree::{BinaryOperator, UnaryOperator},
|
||||||
parse, AbstractSyntaxTree, Context, DustError, Node, Span, Statement, Type,
|
parse, AbstractSyntaxTree, Context, DustError, Identifier, Node, Span, Statement, Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Analyzes the abstract syntax tree for errors.
|
/// Analyzes the abstract syntax tree for errors.
|
||||||
@ -105,8 +105,11 @@ impl<'a> Analyzer<'a> {
|
|||||||
self.analyze_statement(right)?;
|
self.analyze_statement(right)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Type::Map { .. }) = left.inner.expected_type(self.context) {
|
let left_type = left.inner.expected_type(self.context);
|
||||||
if let Some(Type::String) = right.inner.expected_type(self.context) {
|
let right_type = right.inner.expected_type(self.context);
|
||||||
|
|
||||||
|
if let Some(Type::Map { .. }) = left_type {
|
||||||
|
if let Some(Type::String) = right_type {
|
||||||
// Allow indexing maps with strings
|
// Allow indexing maps with strings
|
||||||
} else if let Statement::Identifier(_) = right.inner {
|
} else if let Statement::Identifier(_) = right.inner {
|
||||||
// Allow indexing maps with identifiers
|
// Allow indexing maps with identifiers
|
||||||
@ -121,6 +124,31 @@ impl<'a> Analyzer<'a> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the accessor is an identifier, check if it is a valid field
|
||||||
|
if let Statement::Identifier(identifier) = &right.inner {
|
||||||
|
if let Some(Type::Map(fields)) = &left_type {
|
||||||
|
if !fields.contains_key(identifier) {
|
||||||
|
return Err(AnalyzerError::UndefinedField {
|
||||||
|
identifier: right.as_ref().clone(),
|
||||||
|
map: left.as_ref().clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the accessor is a constant, check if it is a valid field
|
||||||
|
if let Statement::Constant(value) = &right.inner {
|
||||||
|
if let Some(field_name) = value.as_string() {
|
||||||
|
if let Some(Type::Map(fields)) = left_type {
|
||||||
|
if !fields.contains_key(&Identifier::new(field_name)) {
|
||||||
|
return Err(AnalyzerError::UndefinedField {
|
||||||
|
identifier: right.as_ref().clone(),
|
||||||
|
map: left.as_ref().clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +156,8 @@ impl<'a> Analyzer<'a> {
|
|||||||
self.analyze_statement(left)?;
|
self.analyze_statement(left)?;
|
||||||
self.analyze_statement(right)?;
|
self.analyze_statement(right)?;
|
||||||
|
|
||||||
if let Some(Type::List { .. }) = left.inner.expected_type(self.context) {
|
if let Some(Type::List { length, .. }) = left.inner.expected_type(self.context)
|
||||||
|
{
|
||||||
let index_type = right.inner.expected_type(self.context);
|
let index_type = right.inner.expected_type(self.context);
|
||||||
|
|
||||||
if let Some(Type::Integer | Type::Range) = index_type {
|
if let Some(Type::Integer | Type::Range) = index_type {
|
||||||
@ -138,6 +167,22 @@ impl<'a> Analyzer<'a> {
|
|||||||
actual: right.as_ref().clone(),
|
actual: right.as_ref().clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the index is a constant, check if it is out of bounds
|
||||||
|
if let Statement::Constant(value) = &right.inner {
|
||||||
|
if let Some(index_value) = value.as_integer() {
|
||||||
|
let index_value = index_value as usize;
|
||||||
|
|
||||||
|
if index_value >= length {
|
||||||
|
return Err(AnalyzerError::IndexOutOfBounds {
|
||||||
|
list: left.as_ref().clone(),
|
||||||
|
index: right.as_ref().clone(),
|
||||||
|
index_value,
|
||||||
|
length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(AnalyzerError::ExpectedList {
|
return Err(AnalyzerError::ExpectedList {
|
||||||
actual: left.as_ref().clone(),
|
actual: left.as_ref().clone(),
|
||||||
@ -459,6 +504,10 @@ pub enum AnalyzerError {
|
|||||||
UndefinedVariable {
|
UndefinedVariable {
|
||||||
identifier: Node<Statement>,
|
identifier: Node<Statement>,
|
||||||
},
|
},
|
||||||
|
UndefinedField {
|
||||||
|
identifier: Node<Statement>,
|
||||||
|
map: Node<Statement>,
|
||||||
|
},
|
||||||
UnexpectedIdentifier {
|
UnexpectedIdentifier {
|
||||||
identifier: Node<Statement>,
|
identifier: Node<Statement>,
|
||||||
},
|
},
|
||||||
@ -484,6 +533,7 @@ impl AnalyzerError {
|
|||||||
AnalyzerError::TypeConflict {
|
AnalyzerError::TypeConflict {
|
||||||
actual_statement, ..
|
actual_statement, ..
|
||||||
} => actual_statement.position,
|
} => actual_statement.position,
|
||||||
|
AnalyzerError::UndefinedField { identifier, .. } => identifier.position,
|
||||||
AnalyzerError::UndefinedVariable { identifier } => identifier.position,
|
AnalyzerError::UndefinedVariable { identifier } => identifier.position,
|
||||||
AnalyzerError::UnexpectedIdentifier { identifier } => identifier.position,
|
AnalyzerError::UnexpectedIdentifier { identifier } => identifier.position,
|
||||||
AnalyzerError::UnexectedString { actual } => actual.position,
|
AnalyzerError::UnexectedString { actual } => actual.position,
|
||||||
@ -518,9 +568,9 @@ impl Display for AnalyzerError {
|
|||||||
} => write!(f, "Expected {} value arguments, found {}", expected, actual),
|
} => write!(f, "Expected {} value arguments, found {}", expected, actual),
|
||||||
AnalyzerError::IndexOutOfBounds {
|
AnalyzerError::IndexOutOfBounds {
|
||||||
list,
|
list,
|
||||||
index,
|
|
||||||
index_value,
|
index_value,
|
||||||
length,
|
length,
|
||||||
|
..
|
||||||
} => write!(
|
} => write!(
|
||||||
f,
|
f,
|
||||||
"Index {} out of bounds for list {} with length {}",
|
"Index {} out of bounds for list {} with length {}",
|
||||||
@ -537,6 +587,9 @@ impl Display for AnalyzerError {
|
|||||||
expected, actual_statement, actual_type
|
expected, actual_statement, actual_type
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AnalyzerError::UndefinedField { identifier, map } => {
|
||||||
|
write!(f, "Undefined field {} in map {}", identifier, map)
|
||||||
|
}
|
||||||
AnalyzerError::UndefinedVariable { identifier } => {
|
AnalyzerError::UndefinedVariable { identifier } => {
|
||||||
write!(f, "Undefined variable {}", identifier)
|
write!(f, "Undefined variable {}", identifier)
|
||||||
}
|
}
|
||||||
@ -582,14 +635,43 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nonexistant_field() {
|
fn nonexistant_field_identifier() {
|
||||||
let source = "{ x = 1 }.y";
|
let source = "{ x = 1 }.y";
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
analyze(source),
|
analyze(source),
|
||||||
Err(DustError::AnalyzerError {
|
Err(DustError::AnalyzerError {
|
||||||
analyzer_error: AnalyzerError::UndefinedVariable {
|
analyzer_error: AnalyzerError::UndefinedField {
|
||||||
identifier: Node::new(Statement::Identifier(Identifier::new("y")), (10, 11)),
|
identifier: Node::new(Statement::Identifier(Identifier::new("y")), (10, 11)),
|
||||||
|
map: Node::new(
|
||||||
|
Statement::Map(vec![(
|
||||||
|
Node::new(Statement::Identifier(Identifier::new("x")), (2, 3)),
|
||||||
|
Node::new(Statement::Constant(Value::integer(1)), (6, 7))
|
||||||
|
)]),
|
||||||
|
(0, 9)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
source
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonexistant_field_string() {
|
||||||
|
let source = "{ x = 1 }.'y'";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
analyze(source),
|
||||||
|
Err(DustError::AnalyzerError {
|
||||||
|
analyzer_error: AnalyzerError::UndefinedField {
|
||||||
|
identifier: Node::new(Statement::Constant(Value::string("y")), (10, 13)),
|
||||||
|
map: Node::new(
|
||||||
|
Statement::Map(vec![(
|
||||||
|
Node::new(Statement::Identifier(Identifier::new("x")), (2, 3)),
|
||||||
|
Node::new(Statement::Constant(Value::integer(1)), (6, 7))
|
||||||
|
)]),
|
||||||
|
(0, 9)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
source
|
source
|
||||||
})
|
})
|
||||||
|
@ -47,7 +47,7 @@ impl BuiltInFunction {
|
|||||||
BuiltInFunction::Length => {
|
BuiltInFunction::Length => {
|
||||||
vec![(
|
vec![(
|
||||||
"value".into(),
|
"value".into(),
|
||||||
Type::List {
|
Type::ListOf {
|
||||||
item_type: Box::new(Type::Any),
|
item_type: Box::new(Type::Any),
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
|
@ -50,6 +50,10 @@ pub enum Type {
|
|||||||
Integer,
|
Integer,
|
||||||
List {
|
List {
|
||||||
item_type: Box<Type>,
|
item_type: Box<Type>,
|
||||||
|
length: usize,
|
||||||
|
},
|
||||||
|
ListOf {
|
||||||
|
item_type: Box<Type>,
|
||||||
},
|
},
|
||||||
Map(BTreeMap<Identifier, Type>),
|
Map(BTreeMap<Identifier, Type>),
|
||||||
Number,
|
Number,
|
||||||
@ -139,9 +143,35 @@ impl Type {
|
|||||||
(
|
(
|
||||||
Type::List {
|
Type::List {
|
||||||
item_type: left_type,
|
item_type: left_type,
|
||||||
|
length: left_length,
|
||||||
},
|
},
|
||||||
Type::List {
|
Type::List {
|
||||||
item_type: right_type,
|
item_type: right_type,
|
||||||
|
length: right_length,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if left_length != right_length {
|
||||||
|
return Err(TypeConflict {
|
||||||
|
actual: other.clone(),
|
||||||
|
expected: self.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if left_type.check(right_type).is_err() {
|
||||||
|
return Err(TypeConflict {
|
||||||
|
actual: other.clone(),
|
||||||
|
expected: self.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Type::ListOf {
|
||||||
|
item_type: left_type,
|
||||||
|
},
|
||||||
|
Type::ListOf {
|
||||||
|
item_type: right_type,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if left_type.check(right_type).is_err() {
|
if left_type.check(right_type).is_err() {
|
||||||
@ -150,9 +180,37 @@ impl Type {
|
|||||||
expected: self.clone(),
|
expected: self.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Type::List {
|
||||||
|
item_type: list_item_type,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
Type::ListOf {
|
||||||
|
item_type: list_of_item_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
Type::ListOf {
|
||||||
|
item_type: list_of_item_type,
|
||||||
|
},
|
||||||
|
Type::List {
|
||||||
|
item_type: list_item_type,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
// TODO: This is a hack, remove it.
|
||||||
|
if let Type::Any = **list_of_item_type {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if list_item_type.check(list_of_item_type).is_err() {
|
||||||
|
return Err(TypeConflict {
|
||||||
|
actual: other.clone(),
|
||||||
|
expected: self.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
(
|
(
|
||||||
Type::Function {
|
Type::Function {
|
||||||
type_parameters: left_type_parameters,
|
type_parameters: left_type_parameters,
|
||||||
@ -248,7 +306,8 @@ impl Display for Type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::Integer => write!(f, "int"),
|
Type::Integer => write!(f, "int"),
|
||||||
Type::List { item_type } => write!(f, "[{item_type}]"),
|
Type::List { item_type, length } => write!(f, "[{item_type}; {length}]"),
|
||||||
|
Type::ListOf { item_type } => write!(f, "list_of({item_type})"),
|
||||||
Type::Map(map) => {
|
Type::Map(map) => {
|
||||||
write!(f, "{{ ")?;
|
write!(f, "{{ ")?;
|
||||||
|
|
||||||
@ -316,9 +375,11 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Type::List {
|
Type::List {
|
||||||
item_type: Box::new(Type::Boolean),
|
item_type: Box::new(Type::Boolean),
|
||||||
|
length: 42
|
||||||
}
|
}
|
||||||
.check(&Type::List {
|
.check(&Type::List {
|
||||||
item_type: Box::new(Type::Boolean),
|
item_type: Box::new(Type::Boolean),
|
||||||
|
length: 42
|
||||||
}),
|
}),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
@ -360,6 +421,7 @@ mod tests {
|
|||||||
Type::Integer,
|
Type::Integer,
|
||||||
Type::List {
|
Type::List {
|
||||||
item_type: Box::new(Type::Integer),
|
item_type: Box::new(Type::Integer),
|
||||||
|
length: 42,
|
||||||
},
|
},
|
||||||
Type::Map(BTreeMap::new()),
|
Type::Map(BTreeMap::new()),
|
||||||
Type::Range,
|
Type::Range,
|
||||||
|
@ -679,6 +679,7 @@ impl ValueInner {
|
|||||||
|
|
||||||
Type::List {
|
Type::List {
|
||||||
item_type: Box::new(item_type),
|
item_type: Box::new(item_type),
|
||||||
|
length: values.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ValueInner::Map(value_map) => {
|
ValueInner::Map(value_map) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user