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: 98.9us
|
||||||
;; Benchmark-Repeat: 22
|
;; Benchmark-Repeat: 22
|
||||||
|
;; Skip: output
|
||||||
(do
|
(do
|
||||||
;; make-sma Factory (O(1))
|
;; make-sma Factory (O(1))
|
||||||
(def make-sma
|
(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);
|
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
|
// Any and Error are already handled by is_assignable_from — silently succeed
|
||||||
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
||||||
(StaticType::Error, _) | (_, StaticType::Error) => {}
|
(StaticType::Error, _) | (_, StaticType::Error) => {}
|
||||||
@@ -394,7 +402,7 @@ impl TypeChecker {
|
|||||||
// If multiple overloads match (e.g. `(* TypeVar Int)` matches both
|
// If multiple overloads match (e.g. `(* TypeVar Int)` matches both
|
||||||
// `(Int,Int)→Int` and `(Float,Float)→Float`), the choice is ambiguous
|
// `(Int,Int)→Int` and `(Float,Float)→Float`), the choice is ambiguous
|
||||||
// and binding the TypeVar to the first match would be incorrect.
|
// 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;
|
let mut unique_match: Option<&Signature> = None;
|
||||||
for sig in sigs {
|
for sig in sigs {
|
||||||
if !is_variadic(sig) && sig.params.is_assignable_from(args_ty) {
|
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) {
|
let mut ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||||
Some(ty) => ty,
|
Some(ty) => ty,
|
||||||
None => {
|
None => {
|
||||||
|
let callee_name = match &callee_typed.kind {
|
||||||
|
NodeKind::Identifier { symbol, .. } => format!("'{}'", symbol.name),
|
||||||
|
_ => "function".to_string(),
|
||||||
|
};
|
||||||
diag.push_error(
|
diag.push_error(
|
||||||
format!(
|
format!(
|
||||||
"Invalid arguments for function call. Expected {}, got {}",
|
"{}: no matching overload for ({})",
|
||||||
callee_typed.ty, args_typed.ty
|
callee_name, args_typed.ty.display_compact()
|
||||||
),
|
),
|
||||||
Some(node.identity.clone()),
|
Some(node.identity.clone()),
|
||||||
);
|
);
|
||||||
|
|||||||
+9
-9
@@ -43,10 +43,10 @@ fn register_arithmetic(env: &Environment) {
|
|||||||
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||||
ret: StaticType::DateTime,
|
ret: StaticType::DateTime,
|
||||||
},
|
},
|
||||||
// Variadic
|
// Variadic: any number of numeric args
|
||||||
Signature {
|
Signature {
|
||||||
params: StaticType::Any,
|
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||||
ret: StaticType::Any,
|
ret: StaticType::Float,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
env.register_native_fn("+", add_ty, Purity::Pure, |args| {
|
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]),
|
params: StaticType::Tuple(vec![StaticType::DateTime, StaticType::Int]),
|
||||||
ret: StaticType::DateTime,
|
ret: StaticType::DateTime,
|
||||||
},
|
},
|
||||||
// Variadic
|
// Variadic: any number of numeric args
|
||||||
Signature {
|
Signature {
|
||||||
params: StaticType::Any,
|
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||||
ret: StaticType::Any,
|
ret: StaticType::Float,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
env.register_native_fn("-", sub_ty, Purity::Pure, |args| {
|
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]),
|
params: StaticType::Tuple(vec![StaticType::Float, StaticType::Float]),
|
||||||
ret: StaticType::Float,
|
ret: StaticType::Float,
|
||||||
},
|
},
|
||||||
// Variadic
|
// Variadic: any number of numeric args
|
||||||
Signature {
|
Signature {
|
||||||
params: StaticType::Any,
|
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||||
ret: StaticType::Any,
|
ret: StaticType::Float,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
env.register_native_fn("*", mul_ty, Purity::Pure, |args| {
|
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.")
|
}).doc("Returns the absolute value of a number. Works for both int and float.")
|
||||||
.examples(&["(abs -5)", "(abs -3.14)"]);
|
.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 {
|
let variadic_ty = StaticType::Function(Box::new(Signature {
|
||||||
params: StaticType::Any,
|
params: StaticType::Variadic(Box::new(StaticType::Float)),
|
||||||
ret: StaticType::Any,
|
ret: StaticType::Float,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
env.register_native_fn("min", variadic_ty.clone(), Purity::Pure, |args| {
|
env.register_native_fn("min", variadic_ty.clone(), Purity::Pure, |args| {
|
||||||
|
|||||||
@@ -365,6 +365,10 @@ pub enum StaticType {
|
|||||||
FieldAccessor(Keyword),
|
FieldAccessor(Keyword),
|
||||||
Function(Box<Signature>),
|
Function(Box<Signature>),
|
||||||
FunctionOverloads(Vec<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),
|
Object(&'static str),
|
||||||
/// A polymorphic native function whose return type is computed from its argument types.
|
/// 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.
|
/// `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) => {
|
StaticType::FunctionOverloads(sigs) => {
|
||||||
write!(f, "overloads({} variants)", sigs.len())
|
write!(f, "overloads({} variants)", sigs.len())
|
||||||
}
|
}
|
||||||
|
StaticType::Variadic(inner) => write!(f, "variadic<{}>", inner),
|
||||||
StaticType::Object(name) => write!(f, "{}", name),
|
StaticType::Object(name) => write!(f, "{}", name),
|
||||||
StaticType::PolymorphicFn { .. } => write!(f, "<polymorphic-fn>"),
|
StaticType::PolymorphicFn { .. } => write!(f, "<polymorphic-fn>"),
|
||||||
StaticType::TypeVar(n) => write!(f, "?{}", n),
|
StaticType::TypeVar(n) => write!(f, "?{}", n),
|
||||||
@@ -488,6 +493,19 @@ impl StaticType {
|
|||||||
other => other.to_string(),
|
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 {
|
impl StaticType {
|
||||||
@@ -547,6 +565,12 @@ impl StaticType {
|
|||||||
inner.is_assignable_from(&StaticType::Matrix(m_inner.clone(), sub_shape))
|
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)
|
// Records are assignable if their layouts match (Structural identity via interning)
|
||||||
(StaticType::Record(a), StaticType::Record(b)) => std::sync::Arc::ptr_eq(a, b),
|
(StaticType::Record(a), StaticType::Record(b)) => std::sync::Arc::ptr_eq(a, b),
|
||||||
// Series are assignable if their inner types are assignable
|
// Series are assignable if their inner types are assignable
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fn test_call_argument_mismatch() {
|
|||||||
assert!(
|
assert!(
|
||||||
result
|
result
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.contains("Invalid arguments for function call")
|
.contains("no matching overload")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ fn test_error_recovery_type_checker() {
|
|||||||
assert!(
|
assert!(
|
||||||
error_msgs
|
error_msgs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|m| m.contains("Invalid arguments for function call")),
|
.any(|m| m.contains("no matching overload")),
|
||||||
"Expected invalid arguments error"
|
"Expected invalid arguments error"
|
||||||
);
|
);
|
||||||
assert!(result.ast.is_some(), "AST should be built with Error nodes");
|
assert!(result.ast.is_some(), "AST should be built with Error nodes");
|
||||||
@@ -157,6 +157,6 @@ fn test_error_recovery_multiple_errors() {
|
|||||||
assert!(
|
assert!(
|
||||||
error_msgs
|
error_msgs
|
||||||
.iter()
|
.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
|
// 1. Missing field
|
||||||
let res_missing = env.run_script("(.missing {:a 1})");
|
let res_missing = env.run_script("(.missing {:a 1})");
|
||||||
assert!(res_missing.is_err());
|
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
|
// 2. Not a record
|
||||||
let res_not_rec = env.run_script("(.name 123)");
|
let res_not_rec = env.run_script("(.name 123)");
|
||||||
assert!(res_not_rec.is_err());
|
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]
|
#[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!("{}", env1.run_script("(+ 1 2)").unwrap()), "3");
|
||||||
assert_eq!(format!("{}", env2.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