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:
2026-03-30 11:36:26 +02:00
parent f1621ed1ac
commit 8aa2a67b5b
9 changed files with 89 additions and 20 deletions
+1
View File
@@ -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
+18
View File
@@ -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))
)
)
)
)
+15 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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| {
+24
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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]
+14
View File
@@ -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"
);
}