Introduce StaticType::Variadic for variadic parameters
This commit introduces `StaticType::Variadic` to represent variadic parameter types in function signatures. This allows for more precise type checking of functions that accept a variable number of arguments, such as arithmetic operators. Previously, variadic functions were handled using `StaticType::Any`, which was too permissive and could lead to runtime errors. The new `Variadic` type enforces that all arguments must conform to a specified inner type. Changes include: - Adding `StaticType::Variadic` to `src/ast/types.rs`. - Updating the type checker to handle `Variadic` types correctly when unifying with tuples or single arguments. - Modifying built-in functions like arithmetic operators to use `Variadic` for their parameter types. - Improving error messages for function call mismatches to be more specific. - Adding a new test case to ensure arithmetic type mismatches are caught at compile time. - A new example `data_stream_pipe_buffered.myc` is added. - The `HMA.myc` example now has a `Skip: output` directive.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
;; Benchmark: 98.9us
|
||||
;; Benchmark-Repeat: 22
|
||||
;; Skip: output
|
||||
(do
|
||||
;; make-sma Factory (O(1))
|
||||
(def make-sma
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
;; Skip: data output to stdout
|
||||
(do
|
||||
(def EURUSD (create-m1-stream "EURUSD" (date "2020-01-03 09:30:00") (date "2020-01-03 10:00:00")))
|
||||
(def GER40 (create-m1-stream "GER40" (date "2020-01-03 09:30:00") (date "2020-01-03 10:00:00")))
|
||||
|
||||
(def cnt 1)
|
||||
|
||||
(pipe-buffered 2 [EURUSD GER40]
|
||||
(fn [eu ge]
|
||||
(do
|
||||
(def dax (.close (ge 0)))
|
||||
(def eur (- dax (ge 1)))
|
||||
(print cnt ": dax=" eur " (" (/ eur (.close (eu 0))) "$)" )
|
||||
(assign cnt (+ cnt 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -352,6 +352,14 @@ impl TypeChecker {
|
||||
self.unify(e, (*inner).clone(), diag);
|
||||
}
|
||||
}
|
||||
// Variadic(T) unifies with a Tuple by unifying each element with T.
|
||||
(StaticType::Variadic(inner), StaticType::Tuple(elems))
|
||||
| (StaticType::Tuple(elems), StaticType::Variadic(inner)) => {
|
||||
drop(subst);
|
||||
for e in elems {
|
||||
self.unify(e, (*inner).clone(), diag);
|
||||
}
|
||||
}
|
||||
// Any and Error are already handled by is_assignable_from — silently succeed
|
||||
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
||||
(StaticType::Error, _) | (_, StaticType::Error) => {}
|
||||
@@ -394,7 +402,7 @@ impl TypeChecker {
|
||||
// If multiple overloads match (e.g. `(* TypeVar Int)` matches both
|
||||
// `(Int,Int)→Int` and `(Float,Float)→Float`), the choice is ambiguous
|
||||
// and binding the TypeVar to the first match would be incorrect.
|
||||
let is_variadic = |sig: &Signature| matches!(sig.params, StaticType::Any);
|
||||
let is_variadic = |sig: &Signature| matches!(sig.params, StaticType::Any | StaticType::Variadic(_));
|
||||
let mut unique_match: Option<&Signature> = None;
|
||||
for sig in sigs {
|
||||
if !is_variadic(sig) && sig.params.is_assignable_from(args_ty) {
|
||||
@@ -1397,10 +1405,14 @@ impl TypeChecker {
|
||||
let mut ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
let callee_name = match &callee_typed.kind {
|
||||
NodeKind::Identifier { symbol, .. } => format!("'{}'", symbol.name),
|
||||
_ => "function".to_string(),
|
||||
};
|
||||
diag.push_error(
|
||||
format!(
|
||||
"Invalid arguments for function call. Expected {}, got {}",
|
||||
callee_typed.ty, args_typed.ty
|
||||
"{}: no matching overload for ({})",
|
||||
callee_name, args_typed.ty.display_compact()
|
||||
),
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
|
||||
+9
-9
@@ -43,10 +43,10 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||
ret: StaticType::DateTime,
|
||||
},
|
||||
// Variadic
|
||||
// Variadic: any number of numeric args
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||
ret: StaticType::Float,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("+", add_ty, Purity::Pure, |args| {
|
||||
@@ -110,10 +110,10 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||
ret: StaticType::DateTime,
|
||||
},
|
||||
// Variadic
|
||||
// Variadic: any number of numeric args
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||
ret: StaticType::Float,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("-", sub_ty, Purity::Pure, |args| {
|
||||
@@ -169,10 +169,10 @@ fn register_arithmetic(env: &Environment) {
|
||||
params: StaticType::Tuple(vec![StaticType::Float, StaticType::Float]),
|
||||
ret: StaticType::Float,
|
||||
},
|
||||
// Variadic
|
||||
// Variadic: any number of numeric args
|
||||
Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||
ret: StaticType::Float,
|
||||
},
|
||||
]);
|
||||
env.register_native_fn("*", mul_ty, Purity::Pure, |args| {
|
||||
|
||||
+3
-3
@@ -104,10 +104,10 @@ pub fn register(env: &Environment) {
|
||||
}).doc("Returns the absolute value of a number. Works for both int and float.")
|
||||
.examples(&["(abs -5)", "(abs -3.14)"]);
|
||||
|
||||
// Variadic min / max
|
||||
// Variadic min / max — each argument must be numeric
|
||||
let variadic_ty = StaticType::Function(Box::new(Signature {
|
||||
params: StaticType::Any,
|
||||
ret: StaticType::Any,
|
||||
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||
ret: StaticType::Float,
|
||||
}));
|
||||
|
||||
env.register_native_fn("min", variadic_ty.clone(), Purity::Pure, |args| {
|
||||
|
||||
@@ -365,6 +365,10 @@ pub enum StaticType {
|
||||
FieldAccessor(Keyword),
|
||||
Function(Box<Signature>),
|
||||
FunctionOverloads(Vec<Signature>),
|
||||
/// Variadic parameter constraint: accepts any number of arguments,
|
||||
/// but each must be assignable to the inner type.
|
||||
/// Used for arithmetic operators like `(+ 1 2 3)` where all args must be numeric.
|
||||
Variadic(Box<StaticType>),
|
||||
Object(&'static str),
|
||||
/// A polymorphic native function whose return type is computed from its argument types.
|
||||
/// `resolve_return` receives the full argument type and returns the resolved return type.
|
||||
@@ -440,6 +444,7 @@ impl fmt::Display for StaticType {
|
||||
StaticType::FunctionOverloads(sigs) => {
|
||||
write!(f, "overloads({} variants)", sigs.len())
|
||||
}
|
||||
StaticType::Variadic(inner) => write!(f, "variadic<{}>", inner),
|
||||
StaticType::Object(name) => write!(f, "{}", name),
|
||||
StaticType::PolymorphicFn { .. } => write!(f, "<polymorphic-fn>"),
|
||||
StaticType::TypeVar(n) => write!(f, "?{}", n),
|
||||
@@ -488,6 +493,19 @@ impl StaticType {
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a compact type representation suitable for error messages.
|
||||
/// Abbreviates verbose types like records to keep diagnostics readable.
|
||||
pub fn display_compact(&self) -> String {
|
||||
match self {
|
||||
StaticType::Record(_) => "record".to_string(),
|
||||
StaticType::Tuple(elements) => {
|
||||
let inner: Vec<String> = elements.iter().map(|e| e.display_compact()).collect();
|
||||
format!("[{}]", inner.join(" "))
|
||||
}
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticType {
|
||||
@@ -547,6 +565,12 @@ impl StaticType {
|
||||
inner.is_assignable_from(&StaticType::Matrix(m_inner.clone(), sub_shape))
|
||||
}
|
||||
}
|
||||
// Variadic(T) accepts a Tuple where every element is assignable to T,
|
||||
// or a single value assignable to T.
|
||||
(StaticType::Variadic(inner), StaticType::Tuple(elements)) => {
|
||||
elements.iter().all(|e| inner.is_assignable_from(e))
|
||||
}
|
||||
(StaticType::Variadic(inner), other) => inner.is_assignable_from(other),
|
||||
// Records are assignable if their layouts match (Structural identity via interning)
|
||||
(StaticType::Record(a), StaticType::Record(b)) => std::sync::Arc::ptr_eq(a, b),
|
||||
// Series are assignable if their inner types are assignable
|
||||
|
||||
@@ -34,7 +34,7 @@ fn test_call_argument_mismatch() {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.contains("Invalid arguments for function call")
|
||||
.contains("no matching overload")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ fn test_error_recovery_type_checker() {
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Invalid arguments for function call")),
|
||||
.any(|m| m.contains("no matching overload")),
|
||||
"Expected invalid arguments error"
|
||||
);
|
||||
assert!(result.ast.is_some(), "AST should be built with Error nodes");
|
||||
@@ -157,6 +157,6 @@ fn test_error_recovery_multiple_errors() {
|
||||
assert!(
|
||||
error_msgs
|
||||
.iter()
|
||||
.any(|m| m.contains("Invalid arguments for function call"))
|
||||
.any(|m| m.contains("no matching overload"))
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -100,12 +100,12 @@ fn test_record_errors() {
|
||||
// 1. Missing field
|
||||
let res_missing = env.run_script("(.missing {:a 1})");
|
||||
assert!(res_missing.is_err());
|
||||
assert!(res_missing.unwrap_err().contains("Invalid arguments"));
|
||||
assert!(res_missing.unwrap_err().contains("no matching overload"));
|
||||
|
||||
// 2. Not a record
|
||||
let res_not_rec = env.run_script("(.name 123)");
|
||||
assert!(res_not_rec.is_err());
|
||||
assert!(res_not_rec.unwrap_err().contains("Invalid arguments"));
|
||||
assert!(res_not_rec.unwrap_err().contains("no matching overload"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -137,3 +137,17 @@ fn test_environments_from_shared_rtl_are_independent() {
|
||||
assert_eq!(format!("{}", env1.run_script("(+ 1 2)").unwrap()), "3");
|
||||
assert_eq!(format!("{}", env2.run_script("(+ 1 2)").unwrap()), "3");
|
||||
}
|
||||
|
||||
/// Arithmetic operators with incompatible types (e.g. Float - Record)
|
||||
/// must be caught at compile time, not silently return Void at runtime.
|
||||
#[test]
|
||||
fn test_arithmetic_type_mismatch_is_compile_error() {
|
||||
let env = Environment::new();
|
||||
|
||||
// Float - Record must fail
|
||||
let res = env.run_script("(do (def r {:price 42.0}) (- 10.0 r))");
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Float - Record should be a compile error, not silently return Void"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user