use yog_ids::script_problem;
use yog_script_analyze::ast::DeclKind;
use yog_script_analyze::{check, system};
fn codes(src: &str) -> Vec<String> {
check("A.ys", src)
.problems
.into_iter()
.filter_map(|d| d.code.map(|c| c.to_string()))
.collect()
}
#[test]
fn the_librarys_sources_check() {
assert!(!system::sources().is_empty());
for (path, src) in system::sources() {
let checked = check(path, src);
assert!(
checked.problems.is_empty(),
"{path}: {:#?}",
checked.problems
);
let folders = path
.rsplit_once(yog_script_ids::system::FOLDER_SEP)
.map_or("", |(f, _)| f);
let declared = checked.file.decls.iter().find_map(|d| match &d.kind {
DeclKind::Namespace { path, .. } => Some(
path.iter()
.map(|n| n.text.as_str())
.collect::<Vec<_>>()
.join("."),
),
_ => None,
});
assert_eq!(
declared.as_deref(),
Some(
folders
.replace(
yog_script_ids::system::FOLDER_SEP,
yog_script_ids::system::NAMESPACE_SEP
)
.as_str()
),
"{path}"
);
}
}
#[test]
fn results_are_rusts() {
let ok = "Result<i32, Error> Parse(string s) => Ok(4);\n\
Result<i32, Error> Twice(string s) { let v = try Parse(s); return Ok(v * 2); }\n\
i32 Max() => Math.Max(1, 2);\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "Result<i32, Error> Parse(string s) => Ok(\"x\");\n\
string Twice(string s) { string v = try Parse(s); return v; }\n";
assert_eq!(
codes(bad),
[script_problem::TYPE_MISMATCH, script_problem::TYPE_MISMATCH]
);
}
#[test]
fn attributes_are_attribute_classes() {
let ok = "[Obsolete(\"use G\")]\nvoid F() {}\nclass Range(i32 min, i32 max) : Attribute {}\n[Range(0, 10)]\nvoid H() {}\n[Derive(Clone)]\nclass Box {}\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "class NotOne {}\n[NotOne]\nvoid F() {}\n[Obsolete(3)]\nvoid G() {}\n";
assert_eq!(
codes(bad),
[script_problem::ATTRIBUTE, script_problem::TYPE_MISMATCH]
);
}
#[test]
fn rust_is_reached_on_the_bus() {
let ok = "using Yog.Native;\n[Service(\"charge:simulate\")]\nstatic extern i64 Simulate(i64 nodes);\ni64 F() => Simulate(3);\n\
[Provide(\"charge:double\")]\nstatic i64 Double(i64 x) => x * 2;\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
assert_eq!(
codes("static extern i64 Simulate(i64 nodes);\n"),
[script_problem::ATTRIBUTE]
);
}
#[test]
fn time_spans_add_and_compare_by_their_operators() {
let ok = "TimeSpan Wait() => TimeSpan.FromSeconds(2) + TimeSpan.FromMilliseconds(500);\n\
bool Long(TimeSpan t) => t * 2.0 >= TimeSpan.FromMinutes(1) && -t < TimeSpan.Zero;\n\
f64 Halves(TimeSpan t) => t / TimeSpan.FromSeconds(0.5);\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "TimeSpan F(TimeSpan t) => t * t;\nf64 G(TimeSpan t) => t;\n";
assert_eq!(
codes(bad),
[script_problem::TYPE_MISMATCH, script_problem::TYPE_MISMATCH]
);
}
#[test]
fn vectors_are_numerics_by_their_operators() {
let ok = "using System.Numerics;\n\
Vector3 Step(Vector3 from, Vector3 to, f32 speed) => from + Vector3.Normalize(to - from) * speed;\n\
bool Still(Vector2 v) => -v == Vector2.Zero;\n\
f32 Far(Vector3 a, Vector3 b) => Math.Max(Vector3.Distance(a, b), 1);\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "using System.Numerics;\nf32 F(Vector3 v) => v + 1;\n";
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH]);
assert_eq!(
codes("Vector3 F() => default;\n"),
[script_problem::UNKNOWN_TYPE]
);
let full = "System.Numerics.Vector3 F(System.Numerics.Vector3 v) =>\n\
System.Numerics.Vector3.Normalize(v) + v;\n";
assert!(codes(full).is_empty(), "{:?}", codes(full));
}
#[test]
fn matrices_and_quaternions_move_vectors() {
let ok = "using System.Numerics;\n\
Vector3 Moved(Vector3 v, Quaternion q) =>\n\
Vector3.Transform(v, Matrix4x4.CreateFromQuaternion(q) * Matrix4x4.CreateTranslation(Vector3.One));\n\
Vector3 Turned(Vector3 v) => Vector3.Transform(v, Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1));\n\
Matrix4x4? Back(Matrix4x4 m) => Matrix4x4.Invert(Matrix4x4.Transpose(m));\n\
Vector2 Flat(Vector2 v) => Vector2.Transform(v, Matrix3x2.CreateRotation(1) * Matrix3x2.Identity);\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "using System.Numerics;\nVector3 F(Vector3 v) => v + Matrix4x4.Identity;\n";
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH]);
}
#[test]
fn read_only_collections_are_read_through_in() {
let ok = "i32 Total(in IReadOnlyDictionary<string, i32> ages) {\n\
\x20 let mut total = 0;\n foreach (let k in ages.Keys) {\n total += ages[k];\n }\n\
\x20 return ages.ContainsKey(\"a\") ? total : ages.TryGetValue(\"b\") ?? 0;\n}\n\
bool Inside(in IReadOnlySet<i32> a, in IReadOnlySet<i32> b) => a.IsProperSubsetOf(b) && a.Contains(1);\n\
i32 Use() {\n let d = new Dictionary<string, i32>();\n let s = new HashSet<i32>();\n\
\x20 let t = new SortedSet<i32>();\n return Inside(in s, in t) ? Total(in d) : 0;\n}\n";
assert!(codes(ok).is_empty(), "{:?}", codes(ok));
let bad = "void F(in IReadOnlyDictionary<string, i32> ages) {\n ages[\"a\"] = 1;\n}\n";
assert!(!codes(bad).is_empty());
}
#[test]
fn types_are_values() {
let ok = "class Box<T> {\n public T Item;\n}\n\
bool Same(object o) => o.GetType() == typeof(Box<i32>) || typeof(List<string>).IsGenericType;\n\
string Name(Box<string> b) => b.GetType().Name + typeof(i32[]).FullName + typeof(i32).Namespace;\n\
bool Kinds(Type t) => t.IsAssignableFrom(typeof(string)) && t.GetGenericArguments().Length == 1;\n\
object? Call(object box) => box.GetType().GetMethod(\"Area\")!.Invoke(box, null);\n\
i32 Count(Type t) => t.GetFields().Length + t.GetProperties().Length + t.GetConstructors()[0].GetParameters().Length;\n\
bool Old(Type t) => t.IsDefined(typeof(Obsolete), true) || t.GetCustomAttributes(false).Length > 0;\n\
string Of(Box<i32> b) {\n let x = move b;\n return x.GetType().Name + x.ToString();\n}\n\
object? Call(System.Reflection.MethodInfo m, object box) => m.Invoke(in box, [\"a\", 3]);\n\
i32 Hidden(Type t) => t.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Length;\n";
let problems = check("A.ys", ok).problems;
assert!(problems.is_empty(), "{problems:#?}");
assert_eq!(
codes("string F() => typeof(i32);\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn lambdas_are_quoted_as_expression_trees() {
let ok = "using System.Linq.Expressions;\n\
Expression<Func<i32, bool>> Big() => x => x > 10;\n\
bool Run() => Big().Compile()(12) && Big().Body.NodeType == ExpressionType.GreaterThan;\n\
Expression<Func<i32, i32>> Made() {\n let x = Expression.Parameter(typeof(i32), \"x\");\n\
\x20 return Expression.Lambda<Func<i32, i32>>(Expression.Add(x, Expression.Constant(1)), [x]);\n}\n";
let problems = check("A.ys", ok).problems;
assert!(problems.is_empty(), "{problems:#?}");
let block = "using System.Linq.Expressions;\nExpression<Func<i32, i32>> F() => x => {\n return x;\n};\n";
assert_eq!(codes(block), [script_problem::TYPE_MISMATCH]);
}
#[test]
fn enums_are_numbers_their_cases_data() {
let ok = "[Flags]\nenum Access : u8 { None = 0, Read = 1, Write = 2 }\n\
enum Maybe<T> { Nothing, Just(T Value) }\n\
Access Rw() => Access.Read | Access.Write;\n\
bool Has(Access a) => a.HasFlag(Access.Read) && (a & Access.Write) != Access.None && ~a != a;\n\
i32 Num(Access a) => (i32)a;\n\
Access[] All() => Enum.GetValues<Access>();\n\
Maybe<i32> None() => Maybe<i32>.Nothing;\n\
i32 Or(Maybe<i32> m) => m switch {\n Maybe.Just(let v) => v + 1,\n Maybe<i32>.Nothing => 0,\n};\n";
let problems = check("A.ys", ok).problems;
assert!(problems.is_empty(), "{problems:#?}");
}
#[test]
fn a_stray_brace_ends_its_declaration() {
let problems = check("A.ys", "i32 F() => 1;\n}\ni32 G() => 2;\n").problems;
assert!(!problems.is_empty());
}
#[test]
fn collections_are_the_librarys_own() {
let ok = "i32 Sum(List<i32> xs) {\n let mut total = 0;\n foreach (let x in xs) {\n total += x;\n }\n return total + xs.Count + xs[0];\n}\n\
bool Has(Dictionary<string, i32> ages, string name) => ages.ContainsKey(name) && ages[name] > 3;\n\
i32? Age(Dictionary<string, i32> ages) => ages.TryGetValue(\"steve\");\n\
void Fill(List<string> names) {\n names.Add(\"a\");\n names.Insert(0, \"b\");\n names.RemoveAll(n => n == \"a\");\n names.Sort((a, b) => a.Length - b.Length);\n}\n\
i32 Firsts(in IEnumerable<i32> xs) {\n foreach (let x in xs) {\n return x;\n }\n return 0;\n}\n\
i32 Pass(List<i32> xs) => Firsts(in xs);\n\
List<i32> Made() => [1, 2, 3];\n\
HashSet<string> Set() => [\"a\", \"b\"];\n\
Dictionary<string, i32> Map() => [\"a\" => 1, \"b\" => 2];\n\
i32 Pairs(Dictionary<string, i32> d) {\n let mut n = 0;\n foreach (let kv in d) {\n n += kv.Value;\n }\n return n;\n}\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"string F(List<i32> xs) => xs[0];\n",
"void G(List<i32> xs) {\n xs.Add(\"no\");\n}\n",
"HashSet<i32> H() => [\"a\"];\n",
"bool K(Dictionary<string, i32> d) => d.ContainsKey(3);\n",
"void L(List<i32> xs) {\n xs.RemoveAll(x => x + 1);\n}\n",
] {
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH], "{bad}");
}
assert_eq!(
codes("i32 C(List<i32> xs) => xs.Length;\n"),
[script_problem::UNKNOWN_MEMBER]
);
}
#[test]
fn strings_have_strings_members() {
let ok = "string F(string s) => s.Trim().ToUpper().Substring(1, s.Length - 2);\n\
char G(string s) => s[0];\n\
bool H(string? s) => String.IsNullOrEmpty(s) || s!.StartsWith(\"a\");\n\
string J(List<string> parts) => String.Join(\", \", in parts);\n\
i32 K(string s) => s.Split(',').Length + s.IndexOf('x');\n\
String L(string s) => s;\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("i32 F(string s) => s.Size;\n"),
[script_problem::UNKNOWN_MEMBER]
);
assert_eq!(
codes("i32 F(string s) => s.ToUpper();\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn arrays_are_csharps() {
let ok = "i32 Sum(i32[] xs) {\n let mut total = 0;\n foreach (let x in xs) {\n total += x;\n }\n return total + xs.Length + xs[0];\n}\n\
i32 Grid() {\n let g = new i32[3, 4];\n g[1, 2] = 5;\n return g.GetLength(1) + g.Rank + g[1, 2];\n}\n\
i32[][] Jagged() => new i32[3][];\n\
string[] Names() => new[] { \"a\", \"b\" };\n\
i32[] Made() => [1, 2, 3];\n\
i32[,] Square() => new i32[,] { { 1, 2 }, { 3, 4 } };\n\
void Sorted(i32[] xs) {\n let mut ys = move xs;\n Array.Sort(ref ys);\n let i = Array.IndexOf(in ys, 3);\n}\n\
i32 Count(in IEnumerable<i32> xs) => 0;\n\
i32 Viewed(i32[] xs) => Count(in xs);\n\
object[] Covariant(string[] names) => names;\n\
string[] Parts(string s) => s.Split(',');\n\
i32[] Back(List<i32> xs) => xs.ToArray();\n\
void Grown(i32[] xs) {\n let mut ys = move xs;\n Array.Resize(ref ys, 10);\n}\n\
i32[] None() => Array.Empty<i32>();\n\
string[] Long(string[] names) => Array.FindAll(in names, n => n.Length > 2);\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"i32 F(i32[] xs) => xs[\"a\"];\n",
"i64[] G(i32[] xs) => xs;\n",
"i32[] H() => new i32[] { \"a\" };\n",
"i32[,] K() => new i32[,] { 1, 2 };\n",
] {
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH], "{bad}");
}
assert_eq!(
codes("i32 F(i32[,] g) => g[1];\n"),
[script_problem::WRONG_ARGS]
);
assert_eq!(
codes("i32 F(i32[] xs) => xs.Count;\n"),
[script_problem::UNKNOWN_MEMBER]
);
}
#[test]
fn variance_is_csharps() {
let ok = "IEnumerable<object> Out(List<string> xs) => xs;\n\
IReadOnlyList<object> Read(List<string> xs) => xs;\n\
Predicate<string> In(Predicate<object> p) => p;\n\
Func<string, object> Both(Func<object, string> f) => f;\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"List<object> F(List<string> xs) => xs;\n",
"IEnumerable<object> G(List<i32> xs) => xs;\n",
"Predicate<object> H(Predicate<string> p) => p;\n",
] {
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH], "{bad}");
}
}
#[test]
fn every_type_is_an_object() {
let ok = "class Tag(string name) {\n public string Name = name;\n\
public override string ToString() => Name;\n public override i32 GetHashCode() => 7;\n}\n\
struct Pos(i32 x) {\n public i32 X = x;\n}\n\
string F(Tag t, Pos p) => t.ToString() + p.ToString() + 5.ToString();\n\
bool G(Pos a, Pos b) => a.Equals(b) && a.GetHashCode() == b.GetHashCode();\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("class Tag {\n public override string Name() => \"\";\n}\n"),
[script_problem::INHERIT]
);
assert_eq!(
codes("class Bag : IEnumerable<i32> {}\n"),
[script_problem::INHERIT]
);
}
#[test]
fn explicit_implementations_are_csharps() {
let ok = "public interface IShape {\n f64 Area();\n string Name { get; }\n}\n\
public class Square(f64 side) : IShape {\n public f64 Side = side;\n\
f64 IShape.Area() => Side * Side;\n string IShape.Name => \"square\";\n}\n\
f64 A(IShape s) => s.Area();\n\
bool Added(HashSet<string> set) => set.Add(\"a\");\n\
bool Pair(ICollection<KeyValuePair<string, i32>> pairs, Dictionary<string, i32> d) =>\n\
pairs.Remove(new KeyValuePair<string, i32>(\"a\", 1)) && d.Remove(\"a\");\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
let shape = "public interface IShape {\n f64 Area();\n}\n";
for (body, code) in [
(
"class A {\n f64 IShape.Area() => 1;\n}\n",
script_problem::INHERIT,
),
(
"class B : IShape {\n f64 IShape.Area() => 1;\n f64 IShape.Size() => 1;\n}\n",
script_problem::INHERIT,
),
(
"class C : IShape {\n public f64 IShape.Area() => 1;\n}\n",
script_problem::INHERIT,
),
(
"class D : IShape {\n f64 IShape.Area() => 1;\n}\nf64 F(D d) => d.Area();\n",
script_problem::UNKNOWN_MEMBER,
),
] {
assert_eq!(codes(&format!("{shape}{body}")), [code], "{body}");
}
}
#[test]
fn linq_is_csharps() {
let ok = "record Player(string Name, i32 Level);\n\
List<string> High(List<Player> ps) => ps.Where(p => p.Level > 10).OrderBy(p => p.Name).Select(p => p.Name).ToList();\n\
i32 Levels(List<Player> ps) => ps.Sum(p => p.Level) + ps.Count(p => p.Level > 3) + ps.Max(p => p.Level);\n\
f64 Mean(i32[] xs) => xs.Average();\n\
bool Any(i32[] xs) => xs.Any(x => x > 3) && xs.Contains(4);\n\
Dictionary<string, i32> ByName(List<Player> ps) => ps.ToDictionary(p => p.Name, p => p.Level);\n\
i32 Groups(List<Player> ps) {\n let mut n = 0;\n foreach (let g in ps.GroupBy(p => p.Level)) {\n n += g.Key + g.Count();\n }\n return n;\n}\n\
string? First(List<Player> ps) => ps.FirstOrDefault(p => p.Level > 99)?.Name;\n\
i32 Squares() => Enumerable.Range(1, 10).Select(x => x * x).Where(x => x % 2 == 0).Sum();\n\
i32 Total(List<i32> xs) => xs.Aggregate(0, (acc, x) => acc + x);\n\
string[] Names(List<Player> ps) => ps.Select(p => p.Name).Distinct().ToArray();\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"record P(i32 Level);\ni32 F(List<P> ps) => ps.Select(p => p.Level).Sum(x => x.Level);\n",
"i32 G(List<i32> xs) => xs.Where(x => x + 1).Count();\n",
] {
assert!(!codes(bad).is_empty(), "{bad}");
}
}
#[test]
fn the_other_collections_are_dotnets() {
let ok = "i32 Q() {\n let mut q = new Queue<i32>();\n q.Enqueue(1);\n let first = q.Dequeue();\n return first + (q.TryPeek() ?? 0) + q.Count;\n}\n\
string S() {\n let mut s = new Stack<string>();\n s.Push(\"a\");\n return s.Pop();\n}\n\
i32 Sorted() {\n let mut set = new SortedSet<i32>();\n let added = set.Add(3);\n return set.Min + set.Max + set.GetViewBetween(1, 2).Count;\n}\n\
string? Ranked(SortedDictionary<i32, string> d) => d.TryGetValue(1);\n\
string Next() {\n let mut pq = new PriorityQueue<string, i32>();\n pq.Enqueue(\"low\", 5);\n pq.Enqueue(\"high\", 1);\n return pq.Dequeue();\n}\n\
i32 Linked() {\n let mut list = new LinkedList<i32>();\n let node = list.AddLast(2);\n list.AddBefore(node, 1);\n return list.First!.Value + (list.Find(2)?.Next?.Value ?? 0);\n}\n\
i32 Order(i32 a, i32 b) => Comparer<i32>.Default.Compare(a, b);\n\
IComparer<string> ByLength() => Comparer<string>.Create((x, y) => x.Length - y.Length);\n\
SortedSet<string> Named() => new SortedSet<string>(Comparer<string>.Create((x, y) => x.Length - y.Length));\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"string F(Queue<i32> q) => q.Dequeue();\n",
"void G(Stack<i32> s) {\n s.Push(\"a\");\n}\n",
"string H(PriorityQueue<string, i32> pq) {\n pq.Enqueue(\"a\", \"b\");\n return pq.Peek();\n}\n",
] {
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH], "{bad}");
}
}
#[test]
fn of_type_and_chunk_know_their_types() {
let ok = "List<string> Texts(List<object> things) => things.OfType<string>().ToList();\n\
i32 Firsts(List<i32> xs) {\n let mut n = 0;\n foreach (let c in xs.Chunk(3)) {\n n += c[0] + c.Length;\n }\n return n;\n}\n\
IEnumerable<string> All(List<object> things) => things.Cast<string>();\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
}
#[test]
fn numbers_and_chars_have_their_members() {
let ok = "i32 F(string s) => i32.Parse(s) + (i32.TryParse(\"x\") ?? 0) + Int32.MaxValue;\n\
bool G(char c) => char.IsDigit(c) || Char.IsLetter(c) && char.ToUpper(c) == 'A';\n\
bool H(f64 x) => f64.IsNaN(x) || x < f64.Epsilon || x > Double.Pi;\n\
i32 K(i64 a, i64 b) => a.CompareTo(b) + u8.MaxValue;\n\
string L(bool b, i32 n) => b.ToString() + n.ToString() + bool.TrueString;\n\
decimal M(decimal d) => decimal.Round(d) + Decimal.One;\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("string F(string s) => i32.Parse(s);\n"),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(
codes("i32 G() => i32.Size;\n"),
[script_problem::UNKNOWN_MEMBER]
);
}
#[test]
fn random_text_and_lazy_are_dotnets() {
let ok = "i32 Roll(Random rng) => rng.Next(1, 7) + Random.Shared.Next(10);\n\
f64 Unit() => new Random(42).NextDouble();\n\
void Mix(Random rng, i32[] xs) {\n let mut ys = move xs;\n rng.Shuffle(ref ys);\n}\n\
string Text(List<string> names) {\n let mut b = new StringBuilder();\n foreach (let n in names) {\n b.Append(n).Append(',').Append(3);\n }\n b.Length = b.Length - 1;\n b[0] = 'X';\n return b.AppendLine().ToString();\n}\n\
i32 Later() {\n let lazy = new Lazy<i32>(() => 42);\n return lazy.IsValueCreated ? 0 : lazy.Value;\n}\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("string F(Random r) => r.NextDouble();\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn a_constructors_arguments_are_checked() {
let ok = "class Box(i32 size) {\n public i32 Size = size;\n}\nBox F() => new Box(3);\nLazy<string> G() => new Lazy<string>(() => \"x\");\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for bad in [
"class Box(i32 size) {\n public i32 Size = size;\n}\nBox F() => new Box(\"no\");\n",
"Lazy<i32> G() => new Lazy<i32>(() => \"x\");\n",
"Random H() => new Random(\"seed\");\n",
] {
assert_eq!(codes(bad), [script_problem::TYPE_MISMATCH], "{bad}");
}
}
#[test]
fn date_times_are_dotnets() {
let ok = "TimeSpan Age(DateTime born) => DateTime.Now - born;\n\
DateTime Later(DateTime d) => d + TimeSpan.FromDays(3) - TimeSpan.FromHours(1);\n\
bool Weekend(DateTime d) => d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday;\n\
string Day(DateTime d) => d.AddMonths(1).ToString(\"yyyy-MM-dd\") + d.Year;\n\
bool Before(DateTime a, DateTime b) => a < b && DateTime.IsLeapYear(a.Year);\n\
DateTime New() => new DateTime(2024, 3, 1);\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("DateTime F(DateTime d) => d + 3;\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn void_is_system_void() {
assert!(codes("Void F() {}\nvoid G() {\n F();\n}\n").is_empty());
assert_eq!(
codes("Void F() {}\ni32 G() => F();\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn tuples_are_csharps() {
let ok = "(i32 Min, i32 Max) Bounds(in List<i32> xs) => (xs.Min(), xs.Max());\n\
i32 Width(List<i32> xs) {\n let b = Bounds(xs);\n let (lo, hi) = b;\n return b.Max - b.Item1 + hi - lo;\n}\n\
(string, i64) Named() => (\"a\", 1);\n\
string Inferred(string name, i32 age) {\n let t = (name, age);\n return t.name + t.age;\n}\n\
(string Key, i32 N) Explicit() => (Key: \"k\", N: 2);\n\
ValueTuple<i32, i32> Spelled() => (1, 2);\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
for (bad, code) in [
(
"(i32, i32) F() => (1, \"a\");\n",
script_problem::TYPE_MISMATCH,
),
(
"i32 G((i32 A, i32 B) t) => t.C;\n",
script_problem::UNKNOWN_MEMBER,
),
] {
assert_eq!(codes(bad), [code], "{bad}");
}
}
#[test]
fn queries_are_linqs_calls() {
let ok = "record Player(string Name, i32 Level, string Team);\nrecord Team(string Name, i32 Rank);\n\
List<string> High(List<Player> ps) => (from p in ps where p.Level > 10 orderby p.Level descending, p.Name select p.Name).ToList();\n\
IEnumerable<string> Teams(List<Player> ps, List<Team> ts) =>\n\
from p in ps join t in ts on p.Team equals t.Name where t.Rank < 3 select p.Name + t.Name;\n\
IEnumerable<i32> Pairs(i32[] xs, i32[] ys) => from x in xs from y in ys let s = x + y where s > 2 select s * x;\n\
IEnumerable<IGrouping<i32, string>> ByLevel(List<Player> ps) => from p in ps group p.Name by p.Level;\n\
IEnumerable<i32> Sizes(List<Player> ps) => from p in ps group p by p.Team into g select g.Count();\n\
IEnumerable<i32> Counts(List<Player> ps, List<Team> ts) => from t in ts join p in ps on t.Name equals p.Team into members select members.Count();\n\
i32 Name(i32 from) => from + 1;\n";
assert!(codes(ok).is_empty(), "{:?}", check("A.ys", ok).problems);
assert_eq!(
codes("IEnumerable<i32> F(List<string> xs) => from x in xs select x.Length + x;\n"),
[script_problem::TYPE_MISMATCH]
);
}