/* * test_cs_lsp_bench.c — parity-tracking benchmark for cs_lsp. * * The fixture is a synthetic but representative ~260-line C# file that * exercises the parity features cs_lsp implements: * - using / using static / using alias / global using * - file-scoped namespace * - class / record / interface / struct / enum * - inheritance + interface implementation * - generics: List, Dictionary, generic methods * - async / await with Task / ValueTask unwrap * - LINQ method syntax (Where / Select / First / Count / ToList ...) * - properties (auto + expression-bodied), fields, indexers * - primary constructors (records + C# 12 classes) * - method chaining via return-type propagation * - this / base dispatch * - var inference, foreach element typing * - common BCL stdlib calls (Console, string, StringBuilder, Math) * * This is NOT a Roslyn parity proof — it measures the resolution rate * (resolved_calls vs textual calls) on idiomatic code, which is the * subset cs_lsp targets. See the honest scope notes in cs_lsp.c. * * Reports: total textual calls, resolved calls (raw), high-confidence * resolutions (>= 0.90), and the resolution ratio. Asserts a floor of * 45% under sanitizers (idiomatic-code production expectation is higher; * the gap is dominated by ctor-synthetic 0.50 markers and BCL long tail). */ #include "test_framework.h" #include "cbm.h" #include "lsp/cs_lsp.h" #include #include static const char *bench_source = "global using System;\n" "using System.Collections.Generic;\n" "using System.Linq;\n" "using System.Text;\n" "using System.Threading.Tasks;\n" "using static System.Math;\n" "using Ints = System.Collections.Generic.List;\n" "\n" "namespace Bench.App;\n" "\n" "public enum Status { Active, Disabled, Pending }\n" "\n" "public interface IRepository\n" "{\n" " User Find(int id);\n" " Task FindAsync(int id);\n" "}\n" "\n" "public record Address(string Street, string City)\n" "{\n" " public string Full() { return Street + \", \" + City; }\n" "}\n" "\n" "public class User\n" "{\n" " public int Id { get; set; }\n" " public string Name { get; set; }\n" " public Address Home { get; set; }\n" " public Status State { get; set; }\n" " public User(int id, string name) { Id = id; Name = name; }\n" " public string Display() { return Name.ToUpper(); }\n" " public string Greeting() => \"Hi \" + Name.Trim();\n" " public string City() { return Home.City; }\n" "}\n" "\n" "public class BaseService\n" "{\n" " protected readonly StringBuilder _log = new StringBuilder();\n" " public virtual string Tag() { return \"base\"; }\n" " public void Note(string m) { _log.Append(m).Append(';'); }\n" " public string Dump() { return _log.ToString(); }\n" "}\n" "\n" "public class UserService : BaseService, IRepository\n" "{\n" " private readonly Dictionary _users = new Dictionary();\n" " private readonly List _all = new List();\n" "\n" " public override string Tag() { return \"users\"; }\n" "\n" " public void Add(User u)\n" " {\n" " _users.Add(u.Id, u);\n" " _all.Add(u);\n" " Note(u.Display());\n" " }\n" "\n" " public User Find(int id)\n" " {\n" " if (_users.ContainsKey(id))\n" " {\n" " var u = _users[id];\n" " u.Display();\n" " return u;\n" " }\n" " return null;\n" " }\n" "\n" " public async Task FindAsync(int id)\n" " {\n" " await Task.Delay(1);\n" " var u = Find(id);\n" " return u;\n" " }\n" "\n" " public List ActiveNames()\n" " {\n" " return _all\n" " .Where(x => x.State == Status.Active)\n" " .Select(x => x.Display())\n" " .ToList();\n" " }\n" "\n" " public int Count() { return _all.Count(); }\n" "\n" " public string FirstCity()\n" " {\n" " var first = _all.First();\n" " return first.City();\n" " }\n" "\n" " public void Each()\n" " {\n" " foreach (var u in _all)\n" " {\n" " u.Greeting();\n" " this.Note(u.Name);\n" " }\n" " }\n" "}\n" "\n" "public class Calculator\n" "{\n" " public double Hypotenuse(double a, double b)\n" " {\n" " return Sqrt(Pow(a, 2) + Pow(b, 2));\n" " }\n" " public T Echo(T value) { return value; }\n" "}\n" "\n" "public class Program\n" "{\n" " public static string Render(UserService svc)\n" " {\n" " var sb = new StringBuilder();\n" " var names = svc.ActiveNames();\n" " foreach (var n in names)\n" " {\n" " sb.Append(n).Append('\\n');\n" " }\n" " return sb.ToString();\n" " }\n" "\n" " public static void Main()\n" " {\n" " var svc = new UserService();\n" " var a = new Address(\"1 St\", \"Town\");\n" " var u = new User(1, \"alice\");\n" " u.Home = a;\n" " svc.Add(u);\n" " var found = svc.Find(1);\n" " found.Display();\n" " found.City();\n" " var c = new Calculator();\n" " var h = c.Hypotenuse(3.0, 4.0);\n" " Console.WriteLine(h.ToString());\n" " var rendered = Render(svc);\n" " Console.WriteLine(rendered.Trim());\n" " var addr = a.Full();\n" " Console.WriteLine(addr);\n" " svc.Tag();\n" " svc.Dump();\n" " Ints xs = new Ints();\n" " xs.Add(svc.Count());\n" " }\n" "}\n"; static double elapsed_ms(struct timespec t0, struct timespec t1) { double s = (double)(t1.tv_sec - t0.tv_sec); double ns = (double)(t1.tv_nsec - t0.tv_nsec); return s * 1000.0 + ns / 1000000.0; } TEST(cslsp_bench_resolution_ratio) { /* Perf benchmark: time-budgeted. Under ASan+UBSan the budget is scaled * (see the sanitizer-aware time-budget assert below); the benchmark always * runs so regressions surface in every configuration. */ int slen = (int)strlen(bench_source); struct timespec t0; struct timespec t1; clock_gettime(CLOCK_MONOTONIC, &t0); CBMFileResult *r = cbm_extract_file(bench_source, slen, CBM_LANG_CSHARP, "test", "bench.cs", 0, NULL, NULL); clock_gettime(CLOCK_MONOTONIC, &t1); ASSERT_NOT_NULL(r); double ms = elapsed_ms(t0, t1); int calls = r->calls.count; int resolved = r->resolved_calls.count; int high_conf = 0; for (int i = 0; i < r->resolved_calls.count; i++) { if (r->resolved_calls.items[i].confidence >= 0.90f) high_conf++; } int loc = 0; for (const char *p = bench_source; *p; p++) { if (*p == '\n') loc++; } double ratio = calls > 0 ? (double)resolved / (double)calls : 0.0; double hi_ratio = calls > 0 ? (double)high_conf / (double)calls : 0.0; printf(" cs bench: %d lines, %d calls, %d resolved (%.0f%%), " "%d high-conf (%.0f%%), %.2f ms\n", loc, calls, resolved, ratio * 100.0, high_conf, hi_ratio * 100.0, ms); /* Free the result BEFORE asserting so a budget miss doesn't leak. */ cbm_free_result(r); ASSERT_GTE(calls, 1); ASSERT_GTE(resolved, 1); /* Floor at 45% under sanitizers. The gap to 100% is dominated by: * - ctor-synthetic 0.50 markers (counted as resolved but low-conf) * - BCL long-tail calls not in the curated stdlib * - LINQ lambda-body inference (out of scope, documented) * This benchmark exists to track regressions, not to claim Roslyn * parity. */ if (calls >= 20) { ASSERT_GTE(resolved * 100, calls * 45); } /* Time budget. ASan+UBSan instrumentation slows the parse ~5-10×, so * scale the budget when a sanitizer is active. Native: 200 ms for a * ~260-line fixture; sanitized: 2000 ms. */ #ifdef __SANITIZE_ADDRESS__ ASSERT(ms < 2000.0); #else ASSERT(ms < 200.0); #endif PASS(); } SUITE(cs_lsp_bench) { RUN_TEST(cslsp_bench_resolution_ratio); }