From 8aa2a67b5bd4292a38613b524404fb2997e0be4e Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 30 Mar 2026 11:36:26 +0200 Subject: [PATCH] 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. --- examples/HMA.myc | 1 + examples/data_stream_pipe_buffered.myc | 18 ++++++++++++++++++ src/ast/compiler/type_checker.rs | 18 +++++++++++++++--- src/ast/rtl/core.rs | 18 +++++++++--------- src/ast/rtl/math.rs | 6 +++--- src/ast/types.rs | 24 ++++++++++++++++++++++++ tests/error_recovery.rs | 6 +++--- tests/records.rs | 4 ++-- tests/rtl.rs | 14 ++++++++++++++ 9 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 examples/data_stream_pipe_buffered.myc diff --git a/examples/HMA.myc b/examples/HMA.myc index 993a5e5..14d441b 100644 --- a/examples/HMA.myc +++ b/examples/HMA.myc @@ -1,5 +1,6 @@ ;; Benchmark: 98.9us ;; Benchmark-Repeat: 22 +;; Skip: output (do ;; make-sma Factory (O(1)) (def make-sma diff --git a/examples/data_stream_pipe_buffered.myc b/examples/data_stream_pipe_buffered.myc new file mode 100644 index 0000000..d89656f --- /dev/null +++ b/examples/data_stream_pipe_buffered.myc @@ -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)) + ) + ) + ) +) diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index dcab4d7..f04f8ed 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -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()), ); diff --git a/src/ast/rtl/core.rs b/src/ast/rtl/core.rs index a93c50d..2483815 100644 --- a/src/ast/rtl/core.rs +++ b/src/ast/rtl/core.rs @@ -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| { diff --git a/src/ast/rtl/math.rs b/src/ast/rtl/math.rs index d02ad48..df0424d 100644 --- a/src/ast/rtl/math.rs +++ b/src/ast/rtl/math.rs @@ -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| { diff --git a/src/ast/types.rs b/src/ast/types.rs index add15dd..5d4c303 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -365,6 +365,10 @@ pub enum StaticType { FieldAccessor(Keyword), Function(Box), FunctionOverloads(Vec), + /// 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), 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, ""), 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 = 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 diff --git a/tests/error_recovery.rs b/tests/error_recovery.rs index 2510765..1eb4aa9 100644 --- a/tests/error_recovery.rs +++ b/tests/error_recovery.rs @@ -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")) ); } diff --git a/tests/records.rs b/tests/records.rs index 2817840..24a4e53 100644 --- a/tests/records.rs +++ b/tests/records.rs @@ -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] diff --git a/tests/rtl.rs b/tests/rtl.rs index fb7b8d2..94dd7fd 100644 --- a/tests/rtl.rs +++ b/tests/rtl.rs @@ -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" + ); +}