feat: Improve destructuring and function call type checking
Refine the type checker to correctly handle destructuring of various collection types (tuples, vectors, matrices, lists, records). Additionally, prevent implicit type coercion for function arguments, ensuring that only tuples are passed to functions expecting tuple arguments. This avoids unexpected behavior where vectors or matrices might be treated as tuples. Add a new example file demonstrating pattern matching and destructuring rules.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
(do
|
||||
(def val1 4) ; korrekt
|
||||
(def val2 [4 5]) ; korrekt
|
||||
(def [val3] 5) ; falsch
|
||||
(def [val4] [5]) ; korrekt
|
||||
|
||||
(def [[x1 [y1 y3]] z1] [[1 [2 3]] 4]) ; korrekt
|
||||
(def [[x2 [y2 y4]] z2] [1 2 3 4]) ; falsch
|
||||
;(def [[x [y1 y2]] z] [1] [2 3] 4) ; falsch
|
||||
;(def [[x [y1 y2]] z] [1 [2 3]] 4) ; falsch
|
||||
|
||||
(def f (fn [[x [y1 y2]] z] (+ x y1 y2 z)))
|
||||
|
||||
(f [1 [2 3]] 4) ; korrekt
|
||||
(f [1 2 3 4]) ; falsch
|
||||
(f [1 2 [3] 4]) ; falsch
|
||||
(f 1 2 3 4) ; falsch
|
||||
(f 1 [2 [3]] 4) ; falsch
|
||||
(f [[1 2] [3 4]]) ; falsch
|
||||
)
|
||||
@@ -184,6 +184,21 @@ impl TypeChecker {
|
||||
)
|
||||
}
|
||||
BoundKind::Tuple { elements } => {
|
||||
match specialized_ty {
|
||||
StaticType::Any
|
||||
| StaticType::Tuple(_)
|
||||
| StaticType::Vector(_, _)
|
||||
| StaticType::Matrix(_, _)
|
||||
| StaticType::List(_)
|
||||
| StaticType::Record(_) => {}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Cannot destructure type {} as a tuple/vector",
|
||||
specialized_ty
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut typed_elements = Vec::new();
|
||||
let mut elem_types = Vec::new();
|
||||
|
||||
@@ -191,6 +206,11 @@ impl TypeChecker {
|
||||
let sub_ty = match specialized_ty {
|
||||
StaticType::Tuple(t) => t.get(i).cloned().unwrap_or(StaticType::Any),
|
||||
StaticType::Vector(inner, _) => (**inner).clone(),
|
||||
StaticType::Matrix(inner, _) => (**inner).clone(), // Matrix flat iteration or row? Wait. Matrix destructuring logic is tricky. But for now let's focus on Record.
|
||||
StaticType::List(inner) => (**inner).clone(),
|
||||
StaticType::Record(fields) => {
|
||||
fields.get(i).map(|(_, ty)| ty.clone()).unwrap_or(StaticType::Any)
|
||||
}
|
||||
_ => StaticType::Any,
|
||||
};
|
||||
let t = self.check_params(el, &sub_ty, ctx)?;
|
||||
@@ -405,12 +425,37 @@ impl TypeChecker {
|
||||
|
||||
BoundKind::Call { callee, args } => {
|
||||
let callee_typed = self.check_node(*callee, ctx)?;
|
||||
let args_typed = self.check_node(*args, ctx)?;
|
||||
|
||||
let ret_ty = callee_typed
|
||||
.ty
|
||||
.resolve_call(&args_typed.ty)
|
||||
.unwrap_or(StaticType::Any);
|
||||
// Manually check args (Tuple) to prevent Vector/Matrix promotion
|
||||
let args_typed = if let BoundKind::Tuple { elements } = args.kind {
|
||||
let mut typed_elements = Vec::new();
|
||||
let mut elem_types = Vec::new();
|
||||
for e in elements {
|
||||
let t = self.check_node(e, ctx)?;
|
||||
elem_types.push(t.ty.clone());
|
||||
typed_elements.push(t);
|
||||
}
|
||||
Node {
|
||||
identity: args.identity,
|
||||
kind: BoundKind::Tuple {
|
||||
elements: typed_elements,
|
||||
},
|
||||
ty: StaticType::Tuple(elem_types),
|
||||
}
|
||||
} else {
|
||||
// Should not happen as parser wraps args in Tuple, but fallback safely
|
||||
self.check_node(*args, ctx)?
|
||||
};
|
||||
|
||||
let ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
return Err(format!(
|
||||
"Invalid arguments for function call. Expected {}, got {}",
|
||||
callee_typed.ty, args_typed.ty
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
BoundKind::Call {
|
||||
@@ -422,7 +467,25 @@ impl TypeChecker {
|
||||
}
|
||||
|
||||
BoundKind::Again { args } => {
|
||||
let args_typed = self.check_node(*args, ctx)?;
|
||||
// Manually check args (Tuple) to prevent Vector/Matrix promotion
|
||||
let args_typed = if let BoundKind::Tuple { elements } = args.kind {
|
||||
let mut typed_elements = Vec::new();
|
||||
let mut elem_types = Vec::new();
|
||||
for e in elements {
|
||||
let t = self.check_node(e, ctx)?;
|
||||
elem_types.push(t.ty.clone());
|
||||
typed_elements.push(t);
|
||||
}
|
||||
Node {
|
||||
identity: args.identity,
|
||||
kind: BoundKind::Tuple {
|
||||
elements: typed_elements,
|
||||
},
|
||||
ty: StaticType::Tuple(elem_types),
|
||||
}
|
||||
} else {
|
||||
self.check_node(*args, ctx)?
|
||||
};
|
||||
|
||||
if let Some(expected_ty) = &ctx.current_params_ty
|
||||
&& !expected_ty.is_assignable_from(&args_typed.ty)
|
||||
@@ -542,6 +605,44 @@ mod tests {
|
||||
env.compile(source).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_scalar_error() {
|
||||
let env = crate::ast::environment::Environment::new();
|
||||
let result = env.compile("(def [x] 5)");
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Cannot destructure type int as a tuple/vector"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_argument_mismatch() {
|
||||
let env = crate::ast::environment::Environment::new();
|
||||
// fn takes 1 arg, called with 0
|
||||
let result = env.compile("(do (def f (fn [x] x)) (f))");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Invalid arguments for function call"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_vector_args() {
|
||||
let env = crate::ast::environment::Environment::new();
|
||||
// ((fn [[x y]] (+ x y)) [10 20]) -> 30
|
||||
let result = env.compile("((fn [[x y]] (+ x y)) [10 20])");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_destructuring_record_args() {
|
||||
let env = crate::ast::environment::Environment::new();
|
||||
// ((fn [[x y]] (+ x y)) {:a 5 :b 7}) -> 12
|
||||
let result = env.compile("((fn [[x y]] (+ x y)) {:a 5 :b 7})");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(get_ret_type(&result.unwrap()), StaticType::Int);
|
||||
}
|
||||
|
||||
fn get_ret_type(node: &TypedNode) -> StaticType {
|
||||
if let StaticType::Function(sig) = &node.ty {
|
||||
sig.ret.clone()
|
||||
|
||||
@@ -38,6 +38,11 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||
ret: StaticType::DateTime,
|
||||
},
|
||||
// Variadic
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("+", add_ty, Purity::Pure, |args| {
|
||||
if args.len() == 2 {
|
||||
@@ -98,6 +103,11 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||
ret: StaticType::DateTime,
|
||||
},
|
||||
// Variadic
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("-", sub_ty, Purity::Pure, |args| {
|
||||
if args.is_empty() {
|
||||
@@ -151,6 +161,11 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::Float, StaticType::Float]),
|
||||
ret: StaticType::Float,
|
||||
},
|
||||
// Variadic
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("*", mul_ty, Purity::Pure, |args| {
|
||||
if args.len() == 2 {
|
||||
|
||||
@@ -259,6 +259,16 @@ impl StaticType {
|
||||
.zip(other_elements.iter())
|
||||
.all(|(e, o)| e.is_assignable_from(o))
|
||||
}
|
||||
// Record to Tuple (Destructuring by values)
|
||||
(StaticType::Tuple(elements), StaticType::Record(fields)) => {
|
||||
if elements.len() != fields.len() {
|
||||
return false;
|
||||
}
|
||||
elements
|
||||
.iter()
|
||||
.zip(fields.iter().map(|(_, ty)| ty))
|
||||
.all(|(e, o)| e.is_assignable_from(o))
|
||||
}
|
||||
// A Matrix is a Vector (of Vectors/Matrices)
|
||||
(StaticType::Vector(inner, len), StaticType::Matrix(m_inner, shape)) => {
|
||||
if shape.is_empty() || shape[0] != *len {
|
||||
|
||||
Reference in New Issue
Block a user