From cb87c1244a899facf3d55d5b025be644c21f4319 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 15:31:34 -0400 Subject: [PATCH 01/10] Add fixture-based test harness system Canonical path for multi-file type system testing, execution verification, and benchmarking. Fixtures are directories under testdata/fixtures/ with .lua files and optional manifest.json. Inline expect-error/expect-warning annotations in source files for diagnostic assertions. Includes mini-require runtime for multi-module fixture execution, golden file comparison with FIXTURE_UPDATE=1 support, and recursive discovery with go test -run filtering by category path. --- fixture_harness_test.go | 502 ++++++++++++++++++ fixture_test.go | 43 ++ testdata/fixtures/basic/arithmetic/main.lua | 11 + .../fixtures/basic/arithmetic/output.golden | 3 + testdata/fixtures/bench/fibonacci/main.lua | 6 + .../fixtures/bench/fibonacci/manifest.json | 4 + .../fixtures/bench/fibonacci/output.golden | 1 + .../fixtures/errors/type-mismatch/main.lua | 3 + .../fixtures/modules/simple-export/main.lua | 3 + .../modules/simple-export/manifest.json | 3 + .../modules/simple-export/mathlib.lua | 11 + .../modules/simple-export/output.golden | 1 + .../fixtures/narrowing/typeof-guard/main.lua | 9 + 13 files changed, 600 insertions(+) create mode 100644 fixture_harness_test.go create mode 100644 fixture_test.go create mode 100644 testdata/fixtures/basic/arithmetic/main.lua create mode 100644 testdata/fixtures/basic/arithmetic/output.golden create mode 100644 testdata/fixtures/bench/fibonacci/main.lua create mode 100644 testdata/fixtures/bench/fibonacci/manifest.json create mode 100644 testdata/fixtures/bench/fibonacci/output.golden create mode 100644 testdata/fixtures/errors/type-mismatch/main.lua create mode 100644 testdata/fixtures/modules/simple-export/main.lua create mode 100644 testdata/fixtures/modules/simple-export/manifest.json create mode 100644 testdata/fixtures/modules/simple-export/mathlib.lua create mode 100644 testdata/fixtures/modules/simple-export/output.golden create mode 100644 testdata/fixtures/narrowing/typeof-guard/main.lua diff --git a/fixture_harness_test.go b/fixture_harness_test.go new file mode 100644 index 00000000..d0724ad3 --- /dev/null +++ b/fixture_harness_test.go @@ -0,0 +1,502 @@ +package lua + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/wippyai/go-lua/compiler/check/tests/testutil" + "github.com/wippyai/go-lua/types/diag" +) + +// Suite describes a fixture suite loaded from manifest.json. +type fixtureSuite struct { + Description string `json:"description,omitempty"` + Files []string `json:"files,omitempty"` + Stdlib *bool `json:"stdlib,omitempty"` + Check *fixtureCheck `json:"check,omitempty"` + Run *fixtureRun `json:"run,omitempty"` + Bench *fixtureBench `json:"bench,omitempty"` + Skip string `json:"skip,omitempty"` +} + +type fixtureCheck struct { + Errors *int `json:"errors,omitempty"` + Skip string `json:"skip,omitempty"` +} + +type fixtureRun struct { + Golden string `json:"golden,omitempty"` + Error bool `json:"error,omitempty"` + ErrorContains string `json:"error_contains,omitempty"` + Skip string `json:"skip,omitempty"` +} + +type fixtureBench struct { + Skip string `json:"skip,omitempty"` +} + +type namedSuite struct { + Name string // path-based name for t.Run (e.g. "narrowing/typeof-guard") + Dir string // absolute directory path + Suite fixtureSuite +} + +type inlineExpectation struct { + File string + Line int + Severity string // "error" or "warning" + Contains string +} + +var expectRe = regexp.MustCompile(`--\s*expect-(error|warning)(?::\s*(.+?))?\s*$`) + +// discoverFixtures recursively walks root and finds directories containing .lua files. +func discoverFixtures(root string) ([]namedSuite, error) { + var suites []namedSuite + root, err := filepath.Abs(root) + if err != nil { + return nil, err + } + + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + luaFiles, _ := filepath.Glob(filepath.Join(path, "*.lua")) + if len(luaFiles) == 0 { + return nil + } + + rel, _ := filepath.Rel(root, path) + name := filepath.ToSlash(rel) + + s := fixtureSuite{} + manifestPath := filepath.Join(path, "manifest.json") + if data, err := os.ReadFile(manifestPath); err == nil { + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("bad manifest in %s: %w", name, err) + } + } + + suites = append(suites, namedSuite{Name: name, Dir: path, Suite: s}) + return nil + }) + if err != nil { + return nil, err + } + + sort.Slice(suites, func(i, j int) bool { return suites[i].Name < suites[j].Name }) + return suites, nil +} + +// resolveFiles returns the ordered file list for the suite. +func resolveFiles(s namedSuite) []string { + if len(s.Suite.Files) > 0 { + return s.Suite.Files + } + entries, err := os.ReadDir(s.Dir) + if err != nil { + return []string{"main.lua"} + } + var modules []string + hasMain := false + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") { + continue + } + if e.Name() == "main.lua" { + hasMain = true + continue + } + modules = append(modules, e.Name()) + } + sort.Strings(modules) + if hasMain { + return append(modules, "main.lua") + } + if len(modules) > 0 { + return modules + } + return []string{"main.lua"} +} + +func resolveStdlib(s namedSuite) bool { + if s.Suite.Stdlib != nil { + return *s.Suite.Stdlib + } + return true +} + +func readFixtureFile(dir, name string) string { + data, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil { + panic(fmt.Sprintf("fixture file %s/%s: %v", dir, name, err)) + } + return string(data) +} + +// parseExpectations scans source lines for expect-error/expect-warning comments. +func parseExpectations(filename, source string) []inlineExpectation { + var expectations []inlineExpectation + for i, line := range strings.Split(source, "\n") { + m := expectRe.FindStringSubmatch(line) + if m == nil { + continue + } + expectations = append(expectations, inlineExpectation{ + File: filename, + Line: i + 1, + Severity: m[1], + Contains: strings.TrimSpace(m[2]), + }) + } + return expectations +} + +// runCheckPhase type-checks the fixture and verifies diagnostics. +func runCheckPhase(t *testing.T, s namedSuite) { + t.Helper() + if s.Suite.Check != nil && s.Suite.Check.Skip != "" { + t.Skip(s.Suite.Check.Skip) + } + + files := resolveFiles(s) + stdlib := resolveStdlib(s) + + var baseOpts []testutil.Option + if stdlib { + baseOpts = append(baseOpts, testutil.WithStdlib()) + } + + // Collect all sources and their expectations + sources := make(map[string]string) + var allExpectations []inlineExpectation + for _, f := range files { + src := readFixtureFile(s.Dir, f) + sources[f] = src + allExpectations = append(allExpectations, parseExpectations(f, src)...) + } + + // Check and export dependency modules (all except entry) + modules := make(map[string]*testutil.ModuleResult) + var allDiagnostics []diag.Diagnostic + for _, f := range files[:len(files)-1] { + modOpts := append([]testutil.Option{}, baseOpts...) + for depName, depMod := range modules { + modOpts = append(modOpts, testutil.WithModule(depName, depMod)) + } + name := strings.TrimSuffix(f, ".lua") + mod := testutil.CheckAndExport(sources[f], name, modOpts...) + modules[name] = mod + allDiagnostics = append(allDiagnostics, mod.Errors...) + } + + // Check entry point + entryOpts := append([]testutil.Option{}, baseOpts...) + for name, mod := range modules { + entryOpts = append(entryOpts, testutil.WithModule(name, mod)) + } + entryFile := files[len(files)-1] + result := testutil.Check(sources[entryFile], entryOpts...) + allDiagnostics = append(allDiagnostics, result.Diagnostics...) + + // Verify expectations + if len(allExpectations) > 0 { + verifyInlineExpectations(t, allExpectations, allDiagnostics, entryFile) + } else if s.Suite.Check != nil && s.Suite.Check.Errors != nil { + verifyErrorCount(t, *s.Suite.Check.Errors, allDiagnostics) + } else { + verifyClean(t, allDiagnostics) + } +} + +func verifyInlineExpectations(t *testing.T, expectations []inlineExpectation, diagnostics []diag.Diagnostic, entryFile string) { + t.Helper() + matched := make([]bool, len(diagnostics)) + failed := false + + for _, exp := range expectations { + found := false + for i, d := range diagnostics { + if !matchesExpectation(exp, d, entryFile) { + continue + } + found = true + matched[i] = true + break + } + if !found { + failed = true + if exp.Contains != "" { + t.Errorf("expected %s at %s:%d not found: %q", exp.Severity, exp.File, exp.Line, exp.Contains) + } else { + t.Errorf("expected %s at %s:%d not found", exp.Severity, exp.File, exp.Line) + } + } + } + + for i, d := range diagnostics { + if matched[i] || d.Severity == diag.SeverityHint { + continue + } + failed = true + t.Errorf("unexpected %s at %s:%d: %s (%s)", + d.Severity, d.Position.File, d.Position.Line, d.Message, d.Code.Name()) + } + + if failed { + dumpDiagnostics(t, diagnostics) + } +} + +func matchesExpectation(exp inlineExpectation, d diag.Diagnostic, entryFile string) bool { + expFile := exp.File + // Match diagnostic file: d.Position.File is set by the checker (e.g. "test.lua" or module name) + if !strings.HasSuffix(d.Position.File, strings.TrimSuffix(expFile, ".lua")) && + !(expFile == entryFile && d.Position.File == "test.lua") { + return false + } + if d.Position.Line != exp.Line { + return false + } + wantSeverity := diag.SeverityError + if exp.Severity == "warning" { + wantSeverity = diag.SeverityWarning + } + if d.Severity != wantSeverity { + return false + } + if exp.Contains != "" && !strings.Contains(d.Message, exp.Contains) { + return false + } + return true +} + +func verifyErrorCount(t *testing.T, want int, diagnostics []diag.Diagnostic) { + t.Helper() + var errors []diag.Diagnostic + for _, d := range diagnostics { + if d.Severity == diag.SeverityError { + errors = append(errors, d) + } + } + if len(errors) != want { + t.Errorf("expected %d errors, got %d", want, len(errors)) + dumpDiagnostics(t, diagnostics) + } +} + +func verifyClean(t *testing.T, diagnostics []diag.Diagnostic) { + t.Helper() + var errors []diag.Diagnostic + for _, d := range diagnostics { + if d.Severity == diag.SeverityError { + errors = append(errors, d) + } + } + if len(errors) > 0 { + t.Errorf("expected clean check, got %d errors", len(errors)) + dumpDiagnostics(t, diagnostics) + } +} + +func dumpDiagnostics(t *testing.T, diagnostics []diag.Diagnostic) { + t.Helper() + t.Log("--- all diagnostics ---") + for _, d := range diagnostics { + t.Logf(" %s:%d:%d [%s] %s: %s", + d.Position.File, d.Position.Line, d.Position.Column, + d.Severity, d.Code.Name(), d.Message) + } +} + +// runExecPhase executes the fixture and verifies output. +func runExecPhase(t *testing.T, s namedSuite) { + t.Helper() + if s.Suite.Run == nil { + // Auto-enable if output.golden exists + goldenPath := filepath.Join(s.Dir, "output.golden") + if _, err := os.Stat(goldenPath); err != nil { + return + } + } else if s.Suite.Run.Skip != "" { + t.Skip(s.Suite.Run.Skip) + } + + files := resolveFiles(s) + + L := NewState() + defer L.Close() + OpenBase(L) + OpenString(L) + OpenTable(L) + OpenMath(L) + + // Build module source map and install require + moduleSources := make(map[string]string) + for _, f := range files[:len(files)-1] { + moduleSources[strings.TrimSuffix(f, ".lua")] = readFixtureFile(s.Dir, f) + } + installRequire(L, moduleSources) + + // Capture print output + var buf bytes.Buffer + capturePrint(L, &buf) + + // Execute entry point + entrySrc := readFixtureFile(s.Dir, files[len(files)-1]) + err := L.DoString(entrySrc) + + runCfg := s.Suite.Run + if runCfg != nil && runCfg.Error { + if err == nil { + t.Error("expected runtime error, got none") + } else if runCfg.ErrorContains != "" && !strings.Contains(err.Error(), runCfg.ErrorContains) { + t.Errorf("error %q does not contain %q", err.Error(), runCfg.ErrorContains) + } + return + } + if err != nil { + t.Fatalf("runtime error: %v", err) + } + + verifyGoldenOutput(t, s, &buf) +} + +// installRequire sets up a require() global that loads modules from the given source map. +// Modules are compiled, executed, cached, and returned — matching standard Lua require semantics. +func installRequire(L *LState, sources map[string]string) { + loaded := L.NewTable() + L.SetGlobal("require", L.NewFunction(func(L *LState) int { + name := L.CheckString(1) + // Return cached module + if cached := loaded.RawGetString(name); cached != LNil { + L.Push(cached) + return 1 + } + src, ok := sources[name] + if !ok { + L.RaiseError("module '%s' not found", name) + return 0 + } + fn, err := L.LoadString(src) + if err != nil { + L.RaiseError("module '%s': %s", name, err.Error()) + return 0 + } + L.Push(fn) + L.Call(0, 1) + result := L.Get(-1) + if result == LNil { + result = LTrue + } + loaded.RawSetString(name, result) + return 1 + })) +} + +func capturePrint(L *LState, buf *bytes.Buffer) { + L.SetGlobal("print", L.NewFunction(func(L *LState) int { + top := L.GetTop() + for i := 1; i <= top; i++ { + if i > 1 { + buf.WriteByte('\t') + } + buf.WriteString(L.ToStringMeta(L.Get(i)).String()) + } + buf.WriteByte('\n') + return 0 + })) +} + +func verifyGoldenOutput(t *testing.T, s namedSuite, buf *bytes.Buffer) { + t.Helper() + goldenName := "output.golden" + if s.Suite.Run != nil && s.Suite.Run.Golden != "" { + goldenName = s.Suite.Run.Golden + } + goldenPath := filepath.Join(s.Dir, goldenName) + + if os.Getenv("FIXTURE_UPDATE") != "" { + got := buf.String() + if got != "" { + if err := os.WriteFile(goldenPath, []byte(got), 0644); err != nil { + t.Fatalf("updating golden file: %v", err) + } + t.Logf("updated %s", goldenPath) + } + return + } + + golden, err := os.ReadFile(goldenPath) + if err != nil { + if os.IsNotExist(err) && buf.Len() == 0 { + return // no golden file and no output, that's fine + } + if os.IsNotExist(err) { + t.Fatalf("output produced but no golden file at %s (run with FIXTURE_UPDATE=1 to create)", goldenPath) + } + t.Fatalf("reading golden file: %v", err) + } + + got := buf.String() + want := string(golden) + if got != want { + t.Errorf("output mismatch:\n--- want ---\n%s--- got ---\n%s", want, got) + } +} + +// runBenchPhase benchmarks the fixture. +func runBenchPhase(b *testing.B, s namedSuite) { + b.Helper() + if s.Suite.Bench == nil { + b.Skip("no bench config") + } + if s.Suite.Bench.Skip != "" { + b.Skip(s.Suite.Bench.Skip) + } + + files := resolveFiles(s) + + L := NewState() + defer L.Close() + OpenBase(L) + OpenString(L) + OpenTable(L) + OpenMath(L) + + moduleSources := make(map[string]string) + for _, f := range files[:len(files)-1] { + moduleSources[strings.TrimSuffix(f, ".lua")] = readFixtureFile(s.Dir, f) + } + installRequire(L, moduleSources) + + // Silence print + L.SetGlobal("print", L.NewFunction(func(L *LState) int { return 0 })) + + entrySrc := readFixtureFile(s.Dir, files[len(files)-1]) + fn, err := L.LoadString(entrySrc) + if err != nil { + b.Fatalf("compile error: %v", err) + } + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + L.Push(fn) + if err := L.PCall(0, MultRet, nil); err != nil { + b.Fatalf("runtime error: %v", err) + } + L.SetTop(0) + } +} diff --git a/fixture_test.go b/fixture_test.go new file mode 100644 index 00000000..43db7e2c --- /dev/null +++ b/fixture_test.go @@ -0,0 +1,43 @@ +package lua + +import "testing" + +func TestFixtures(t *testing.T) { + suites, err := discoverFixtures("testdata/fixtures") + if err != nil { + t.Fatalf("discovering fixtures: %v", err) + } + if len(suites) == 0 { + t.Fatal("no fixture suites found") + } + for _, s := range suites { + s := s + t.Run(s.Name, func(t *testing.T) { + if s.Suite.Skip != "" { + t.Skip(s.Suite.Skip) + } + t.Run("check", func(t *testing.T) { + runCheckPhase(t, s) + }) + t.Run("run", func(t *testing.T) { + runExecPhase(t, s) + }) + }) + } +} + +func BenchmarkFixtures(b *testing.B) { + suites, err := discoverFixtures("testdata/fixtures") + if err != nil { + b.Fatalf("discovering fixtures: %v", err) + } + for _, s := range suites { + if s.Suite.Bench == nil { + continue + } + s := s + b.Run(s.Name, func(b *testing.B) { + runBenchPhase(b, s) + }) + } +} diff --git a/testdata/fixtures/basic/arithmetic/main.lua b/testdata/fixtures/basic/arithmetic/main.lua new file mode 100644 index 00000000..91c1b8b3 --- /dev/null +++ b/testdata/fixtures/basic/arithmetic/main.lua @@ -0,0 +1,11 @@ +local function add(a: number, b: number): number + return a + b +end + +local function mul(a: number, b: number): number + return a * b +end + +print(add(10, 20)) +print(mul(3, 4)) +print(add(1, mul(2, 3))) diff --git a/testdata/fixtures/basic/arithmetic/output.golden b/testdata/fixtures/basic/arithmetic/output.golden new file mode 100644 index 00000000..557fae14 --- /dev/null +++ b/testdata/fixtures/basic/arithmetic/output.golden @@ -0,0 +1,3 @@ +30 +12 +7 diff --git a/testdata/fixtures/bench/fibonacci/main.lua b/testdata/fixtures/bench/fibonacci/main.lua new file mode 100644 index 00000000..a1256724 --- /dev/null +++ b/testdata/fixtures/bench/fibonacci/main.lua @@ -0,0 +1,6 @@ +local function fib(n: number): number + if n < 2 then return n end + return fib(n - 1) + fib(n - 2) +end + +print(fib(10)) diff --git a/testdata/fixtures/bench/fibonacci/manifest.json b/testdata/fixtures/bench/fibonacci/manifest.json new file mode 100644 index 00000000..77088d4a --- /dev/null +++ b/testdata/fixtures/bench/fibonacci/manifest.json @@ -0,0 +1,4 @@ +{ + "run": {}, + "bench": {} +} diff --git a/testdata/fixtures/bench/fibonacci/output.golden b/testdata/fixtures/bench/fibonacci/output.golden new file mode 100644 index 00000000..c3f407c0 --- /dev/null +++ b/testdata/fixtures/bench/fibonacci/output.golden @@ -0,0 +1 @@ +55 diff --git a/testdata/fixtures/errors/type-mismatch/main.lua b/testdata/fixtures/errors/type-mismatch/main.lua new file mode 100644 index 00000000..ffc3a383 --- /dev/null +++ b/testdata/fixtures/errors/type-mismatch/main.lua @@ -0,0 +1,3 @@ +local x: number = "not a number" -- expect-error: cannot assign +local y: string = 42 -- expect-error: cannot assign +local z: boolean = "true" -- expect-error: cannot assign diff --git a/testdata/fixtures/modules/simple-export/main.lua b/testdata/fixtures/modules/simple-export/main.lua new file mode 100644 index 00000000..7cf2d861 --- /dev/null +++ b/testdata/fixtures/modules/simple-export/main.lua @@ -0,0 +1,3 @@ +local mathlib = require("mathlib") +local result = mathlib.add(10, mathlib.double(5)) +print(result) diff --git a/testdata/fixtures/modules/simple-export/manifest.json b/testdata/fixtures/modules/simple-export/manifest.json new file mode 100644 index 00000000..c3c42420 --- /dev/null +++ b/testdata/fixtures/modules/simple-export/manifest.json @@ -0,0 +1,3 @@ +{ + "files": ["mathlib.lua", "main.lua"] +} diff --git a/testdata/fixtures/modules/simple-export/mathlib.lua b/testdata/fixtures/modules/simple-export/mathlib.lua new file mode 100644 index 00000000..814208db --- /dev/null +++ b/testdata/fixtures/modules/simple-export/mathlib.lua @@ -0,0 +1,11 @@ +local M = {} + +function M.add(a: number, b: number): number + return a + b +end + +function M.double(x: number): number + return x * 2 +end + +return M diff --git a/testdata/fixtures/modules/simple-export/output.golden b/testdata/fixtures/modules/simple-export/output.golden new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/testdata/fixtures/modules/simple-export/output.golden @@ -0,0 +1 @@ +20 diff --git a/testdata/fixtures/narrowing/typeof-guard/main.lua b/testdata/fixtures/narrowing/typeof-guard/main.lua new file mode 100644 index 00000000..995223aa --- /dev/null +++ b/testdata/fixtures/narrowing/typeof-guard/main.lua @@ -0,0 +1,9 @@ +local x: string | number = "hello" +if type(x) == "string" then + local s: string = x +end + +local y: string | number = 42 +if type(y) == "string" then + local n: number = y -- expect-error: cannot assign +end From 41c98004ca842e951172814bac07c19d58b9b041 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 15:43:21 -0400 Subject: [PATCH 02/10] Retrofit narrowing and typedef tests to fixture harness Migrate narrowing_test.go (18 cases) and minimal_narrowing_test.go (3 cases) to testdata/fixtures/narrowing/. Migrate typedef_test.go (22 cases) to testdata/fixtures/types/. Remove original Go test files. Also fixes BenchFixtures -> BenchmarkFixtures naming and replaces PreloadModule approach with installRequire mini-runtime for proper require() support in multi-module fixtures. --- .../tests/narrowing/minimal_narrowing_test.go | 69 ---- .../check/tests/narrowing/narrowing_test.go | 325 --------------- compiler/check/tests/types/typedef_test.go | 381 ------------------ .../fixtures/narrowing/assert-truthy/main.lua | 3 + .../narrowing/assert-with-condition/main.lua | 3 + .../narrowing/boolean-discriminant/main.lua | 9 + .../discriminator-tagged-union/main.lua | 11 + .../discriminator-wrong-method/main.lua | 9 + .../narrowing/else-branch-wrong-type/main.lua | 8 + .../narrowing/equality-discriminant/main.lua | 9 + .../narrowing/field-existence/main.lua | 15 + .../narrowing/nested-field-channel/main.lua | 16 + .../narrowing/nil-check-else/main.lua | 6 + .../narrowing/nil-check-optional/main.lua | 4 + .../optional-deeply-nested-method/main.lua | 17 + .../optional-multiple-methods/main.lua | 12 + .../optional-nested-method-call/main.lua | 8 + .../optional-nested-preserves-method/main.lua | 11 + .../optional-nested-preserves/main.lua | 11 + .../narrowing/optional-nested-simple/main.lua | 8 + .../narrowing/truthiness-narrows/main.lua | 4 + .../narrowing/typeof-excludes-other/main.lua | 4 + .../fixtures/narrowing/typeof-number/main.lua | 4 + .../fixtures/narrowing/typeof-string/main.lua | 4 + testdata/fixtures/types/array/main.lua | 2 + .../types/chained-references/main.lua | 4 + .../fixtures/types/function-type/main.lua | 4 + testdata/fixtures/types/in-do-block/main.lua | 4 + testdata/fixtures/types/in-for-loop/main.lua | 4 + .../types/in-nested-function/main.lua | 6 + .../fixtures/types/in-while-loop/main.lua | 6 + .../fixtures/types/inside-if-block/main.lua | 4 + testdata/fixtures/types/map/main.lua | 2 + .../fixtures/types/missing-field/main.lua | 2 + testdata/fixtures/types/multiple/main.lua | 4 + .../fixtures/types/nested-record/main.lua | 6 + .../types/not-visible-outside-block/main.lua | 4 + testdata/fixtures/types/optional/main.lua | 3 + .../types/record-optional-field/main.lua | 3 + .../types/references-another/main.lua | 4 + testdata/fixtures/types/shadowing/main.lua | 7 + .../fixtures/types/simple-record/main.lua | 2 + .../fixtures/types/union-mismatch/main.lua | 2 + testdata/fixtures/types/union/main.lua | 3 + .../types/used-before-definition/main.lua | 2 + .../fixtures/types/wrong-field-type/main.lua | 2 + 46 files changed, 256 insertions(+), 775 deletions(-) delete mode 100644 compiler/check/tests/narrowing/minimal_narrowing_test.go delete mode 100644 compiler/check/tests/narrowing/narrowing_test.go delete mode 100644 compiler/check/tests/types/typedef_test.go create mode 100644 testdata/fixtures/narrowing/assert-truthy/main.lua create mode 100644 testdata/fixtures/narrowing/assert-with-condition/main.lua create mode 100644 testdata/fixtures/narrowing/boolean-discriminant/main.lua create mode 100644 testdata/fixtures/narrowing/discriminator-tagged-union/main.lua create mode 100644 testdata/fixtures/narrowing/discriminator-wrong-method/main.lua create mode 100644 testdata/fixtures/narrowing/else-branch-wrong-type/main.lua create mode 100644 testdata/fixtures/narrowing/equality-discriminant/main.lua create mode 100644 testdata/fixtures/narrowing/field-existence/main.lua create mode 100644 testdata/fixtures/narrowing/nested-field-channel/main.lua create mode 100644 testdata/fixtures/narrowing/nil-check-else/main.lua create mode 100644 testdata/fixtures/narrowing/nil-check-optional/main.lua create mode 100644 testdata/fixtures/narrowing/optional-deeply-nested-method/main.lua create mode 100644 testdata/fixtures/narrowing/optional-multiple-methods/main.lua create mode 100644 testdata/fixtures/narrowing/optional-nested-method-call/main.lua create mode 100644 testdata/fixtures/narrowing/optional-nested-preserves-method/main.lua create mode 100644 testdata/fixtures/narrowing/optional-nested-preserves/main.lua create mode 100644 testdata/fixtures/narrowing/optional-nested-simple/main.lua create mode 100644 testdata/fixtures/narrowing/truthiness-narrows/main.lua create mode 100644 testdata/fixtures/narrowing/typeof-excludes-other/main.lua create mode 100644 testdata/fixtures/narrowing/typeof-number/main.lua create mode 100644 testdata/fixtures/narrowing/typeof-string/main.lua create mode 100644 testdata/fixtures/types/array/main.lua create mode 100644 testdata/fixtures/types/chained-references/main.lua create mode 100644 testdata/fixtures/types/function-type/main.lua create mode 100644 testdata/fixtures/types/in-do-block/main.lua create mode 100644 testdata/fixtures/types/in-for-loop/main.lua create mode 100644 testdata/fixtures/types/in-nested-function/main.lua create mode 100644 testdata/fixtures/types/in-while-loop/main.lua create mode 100644 testdata/fixtures/types/inside-if-block/main.lua create mode 100644 testdata/fixtures/types/map/main.lua create mode 100644 testdata/fixtures/types/missing-field/main.lua create mode 100644 testdata/fixtures/types/multiple/main.lua create mode 100644 testdata/fixtures/types/nested-record/main.lua create mode 100644 testdata/fixtures/types/not-visible-outside-block/main.lua create mode 100644 testdata/fixtures/types/optional/main.lua create mode 100644 testdata/fixtures/types/record-optional-field/main.lua create mode 100644 testdata/fixtures/types/references-another/main.lua create mode 100644 testdata/fixtures/types/shadowing/main.lua create mode 100644 testdata/fixtures/types/simple-record/main.lua create mode 100644 testdata/fixtures/types/union-mismatch/main.lua create mode 100644 testdata/fixtures/types/union/main.lua create mode 100644 testdata/fixtures/types/used-before-definition/main.lua create mode 100644 testdata/fixtures/types/wrong-field-type/main.lua diff --git a/compiler/check/tests/narrowing/minimal_narrowing_test.go b/compiler/check/tests/narrowing/minimal_narrowing_test.go deleted file mode 100644 index b3e3005e..00000000 --- a/compiler/check/tests/narrowing/minimal_narrowing_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package narrowing - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -func TestMinimalNarrowing_Equality(t *testing.T) { - source := ` - type A = {tag: "a", value: string} - type B = {tag: "b", value: number} - local r: A | B = {tag="a", value="x"} - - if r.tag == "a" then - local s: string = r.value - else - local n: number = r.value - end - ` - result := testutil.Check(source, testutil.WithStdlib()) - for _, d := range result.Diagnostics { - t.Logf("diagnostic: %s", d.Message) - } - if result.HasError() { - t.Errorf("expected no errors, got: %v", testutil.ErrorMessages(result.Diagnostics)) - } -} - -func TestMinimalNarrowing_ElseBranchWrongType(t *testing.T) { - source := ` - type A = {tag: "a", value: string} - type B = {tag: "b", value: number} - local r: A | B = {tag="a", value="x"} - - if r.tag == "a" then - else - local s: string = r.value - end - ` - result := testutil.Check(source, testutil.WithStdlib()) - for _, d := range result.Diagnostics { - t.Logf("diagnostic: %s", d.Message) - } - if !result.HasError() { - t.Errorf("expected error (assigning number to string), got none") - } -} - -func TestMinimalNarrowing_BooleanDiscriminant(t *testing.T) { - source := ` - type OK = {ok: true, value: string} - type ERR = {ok: false, value: number} - local r: OK | ERR = {ok=true, value="x"} - - if r.ok then - local s: string = r.value - else - local n: number = r.value - end - ` - result := testutil.Check(source, testutil.WithStdlib()) - for _, d := range result.Diagnostics { - t.Logf("diagnostic: %s", d.Message) - } - if result.HasError() { - t.Errorf("expected no errors, got: %v", testutil.ErrorMessages(result.Diagnostics)) - } -} diff --git a/compiler/check/tests/narrowing/narrowing_test.go b/compiler/check/tests/narrowing/narrowing_test.go deleted file mode 100644 index 38388029..00000000 --- a/compiler/check/tests/narrowing/narrowing_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package narrowing - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -func TestNarrowing_TypeofGuard(t *testing.T) { - tests := []testutil.Case{ - { - Name: "typeof narrowing string", - Code: ` - local x: string | number = "hello" - if type(x) == "string" then - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "typeof narrowing number", - Code: ` - local x: string | number = 42 - if type(x) == "number" then - local n: number = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "typeof narrowing excludes other", - Code: ` - local x: string | number = 42 - if type(x) == "string" then - local n: number = x - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_NilCheck(t *testing.T) { - tests := []testutil.Case{ - { - Name: "nil check narrows optional", - Code: ` - local x: string? = nil - if x ~= nil then - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nil check in else branch", - Code: ` - local x: string? = nil - if x == nil then - local s: nil = x - else - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "truthiness narrows nil", - Code: ` - local x: string? = "test" - if x then - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_Discriminator(t *testing.T) { - tests := []testutil.Case{ - { - Name: "tagged union discriminator", - Code: ` - type Dog = {kind: "dog", bark: () -> ()} - type Cat = {kind: "cat", meow: () -> ()} - type Animal = Dog | Cat - - local function speak(a: Animal) - if a.kind == "dog" then - a.bark() - else - a.meow() - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "discriminator wrong method", - Code: ` - type Dog = {kind: "dog", bark: () -> ()} - type Cat = {kind: "cat", meow: () -> ()} - type Animal = Dog | Cat - - local function speak(a: Animal) - if a.kind == "dog" then - a.meow() - end - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_NestedField(t *testing.T) { - tests := []testutil.Case{ - { - Name: "narrowing union by channel field", - Code: ` - type ChanInt = {__tag: "int"} - type ChanStr = {__tag: "str"} - type SelResult = - {channel: ChanInt, value: {error: string}, ok: boolean} | - {channel: ChanStr, value: {data: number}, ok: boolean} - - local function get_result(a: ChanInt, b: ChanStr): SelResult - return {channel = a, value = {error = "oops"}, ok = true} - end - - local function f(ch1: ChanInt, ch2: ChanStr) - local result = get_result(ch1, ch2) - if result.channel == ch1 then - local e: string = result.value.error - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_Assert(t *testing.T) { - tests := []testutil.Case{ - { - Name: "assert narrows to truthy", - Code: ` - local x: string? = "test" - assert(x) - local s: string = x - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert with condition", - Code: ` - local x: string | number = 42 - assert(type(x) == "number") - local n: number = x - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_FieldExistence(t *testing.T) { - tests := []testutil.Case{ - { - Name: "field existence narrows union", - Code: ` - type Event = {kind: string, error: string?} - type Message = {topic: string, payload: any} - type Timer = {elapsed: number} - type SelectResult = Event | Message | Timer - - local function get_result(): SelectResult - return {kind = "exit", error = nil} - end - - local function f() - local result = get_result() - if result.kind then - local k: string = result.kind - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestNarrowing_OptionalNestedIf(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple if narrows optional record", - Code: ` - type Error = {kind: string, message: string} - local function test(): Error? - return {kind = "test", message = "msg"} - end - local err = test() - if err then - local msg = err.message - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested if preserves narrowing record", - Code: ` - type Error = {kind: string, message: string} - local function test(): Error? - return {kind = "test", message = "msg"} - end - local err = test() - local flag = true - if err then - if flag then - local msg = err.message - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "simple if narrows optional for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} - local function test(): Error? - return nil - end - local err = test() - if err then - local msg = err:message() - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested if preserves narrowing for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} - local function test(): Error? - return nil - end - local err = test() - local flag = true - if err then - if flag then - local msg = err:message() - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "deeply nested if preserves narrowing for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} - local function test(): Error? - return nil - end - local err = test() - local a, b, c = true, true, true - if err then - if a then - if b then - if c then - local k = err:kind() - local m = err:message() - local r = err:retryable() - end - end - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "multiple method calls after nil check", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} - local function test(): Error? - return nil - end - local err = test() - if err then - local kind = err:kind() - if kind == "network" then - local retryable = err:retryable() - local message = err:message() - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/types/typedef_test.go b/compiler/check/tests/types/typedef_test.go deleted file mode 100644 index ed5ebb1b..00000000 --- a/compiler/check/tests/types/typedef_test.go +++ /dev/null @@ -1,381 +0,0 @@ -package types - -import ( - "strings" - "testing" - - "github.com/wippyai/go-lua/compiler/check" - "github.com/wippyai/go-lua/compiler/check/hooks" - "github.com/wippyai/go-lua/types/db" - "github.com/wippyai/go-lua/types/query/core" -) - -func TestTypeDef_SimpleRecord(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Point = {x: number, y: number} - local p: Point = {x = 10, y = 20} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_UsedBeforeDefinition(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - local p: Point = {x = 10, y = 20} - type Point = {x: number, y: number} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - // Should have error - Point used before defined - if len(sess.Diagnostics) == 0 { - t.Error("expected diagnostic for undefined type") - } -} - -func TestTypeDef_ReferencesAnother(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Point = {x: number, y: number} - type MaybePoint = Point? - local p: MaybePoint = {x = 1, y = 2} - local q: MaybePoint = nil - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_Union(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type StringOrNumber = string | number - local a: StringOrNumber = "hello" - local b: StringOrNumber = 42 - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_UnionMismatch(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type StringOrNumber = string | number - local a: StringOrNumber = true - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - if len(sess.Diagnostics) == 0 { - t.Error("expected diagnostic for type mismatch") - } -} - -func TestTypeDef_Array(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Numbers = {number} - local arr: Numbers = {1, 2, 3} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_NestedRecord(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Point = {x: number, y: number} - type Line = {start: Point, finish: Point} - local line: Line = { - start = {x = 0, y = 0}, - finish = {x = 10, y = 10} - } - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_InsideIfBlock(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - if true then - type LocalPoint = {x: number, y: number} - local p: LocalPoint = {x = 1, y = 2} - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_NotVisibleOutsideBlock(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - if true then - type LocalPoint = {x: number, y: number} - end - local p: LocalPoint = {x = 1, y = 2} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - // Should have error - LocalPoint not visible outside if block - if len(sess.Diagnostics) == 0 { - t.Error("expected diagnostic for type not in scope") - } -} - -func TestTypeDef_Shadowing(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Value = number - local a: Value = 10 - if true then - type Value = string - local b: Value = "hello" - end - local c: Value = 20 - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_Multiple(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Name = string - type Age = number - type Person = {name: Name, age: Age} - local p: Person = {name = "Alice", age = 30} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_Function(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Callback = (x: number) -> string - local cb: Callback = function(x: number): string - return tostring(x) - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_Map(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type StringMap = {[string]: number} - local m: StringMap = {a = 1, b = 2} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_Optional(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type MaybeNumber = number? - local a: MaybeNumber = 10 - local b: MaybeNumber = nil - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_RecordWithOptionalField(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Config = {name: string, port?: number} - local c1: Config = {name = "server"} - local c2: Config = {name = "server", port = 8080} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_WrongFieldType(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Point = {x: number, y: number} - local p: Point = {x = "wrong", y = 20} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - if len(sess.Diagnostics) == 0 { - t.Error("expected diagnostic for wrong field type") - } -} - -func TestTypeDef_MissingField(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type Point = {x: number, y: number} - local p: Point = {x = 10} - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - if len(sess.Diagnostics) == 0 { - t.Error("expected diagnostic for missing field") - } -} - -func TestTypeDef_InWhileLoop(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - local i = 0 - while i < 1 do - type Counter = {value: number} - local c: Counter = {value = i} - i = i + 1 - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_InForLoop(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - for i = 1, 3 do - type Index = number - local idx: Index = i - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_InDoBlock(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - do - type Inner = {value: number} - local x: Inner = {value = 42} - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_ChainedReferences(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - type A = number - type B = A - type C = B - local x: C = 42 - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - t.Errorf("unexpected diagnostic: %s", d.Message) - } -} - -func TestTypeDef_InNestedFunction(t *testing.T) { - c := check.NewChecker(db.New(), check.Deps{Types: core.NewEngine()}, hooks.WithAssign()) - sess := c.Check(` - local function outer() - type LocalType = {x: number} - local function inner() - local v: LocalType = {x = 1} - end - end - `, "test.lua") - - if sess == nil { - t.Fatal("Check returned nil") - } - for _, d := range sess.Diagnostics { - if !strings.Contains(d.Message, "LocalType") { - t.Errorf("unexpected diagnostic: %s", d.Message) - } - } -} diff --git a/testdata/fixtures/narrowing/assert-truthy/main.lua b/testdata/fixtures/narrowing/assert-truthy/main.lua new file mode 100644 index 00000000..2ec461ee --- /dev/null +++ b/testdata/fixtures/narrowing/assert-truthy/main.lua @@ -0,0 +1,3 @@ +local x: string? = "test" +assert(x) +local s: string = x diff --git a/testdata/fixtures/narrowing/assert-with-condition/main.lua b/testdata/fixtures/narrowing/assert-with-condition/main.lua new file mode 100644 index 00000000..8bfeaa30 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-with-condition/main.lua @@ -0,0 +1,3 @@ +local x: string | number = 42 +assert(type(x) == "number") +local n: number = x diff --git a/testdata/fixtures/narrowing/boolean-discriminant/main.lua b/testdata/fixtures/narrowing/boolean-discriminant/main.lua new file mode 100644 index 00000000..06a16b10 --- /dev/null +++ b/testdata/fixtures/narrowing/boolean-discriminant/main.lua @@ -0,0 +1,9 @@ +type OK = {ok: true, value: string} +type ERR = {ok: false, value: number} +local r: OK | ERR = {ok=true, value="x"} + +if r.ok then + local s: string = r.value +else + local n: number = r.value +end diff --git a/testdata/fixtures/narrowing/discriminator-tagged-union/main.lua b/testdata/fixtures/narrowing/discriminator-tagged-union/main.lua new file mode 100644 index 00000000..c43f6595 --- /dev/null +++ b/testdata/fixtures/narrowing/discriminator-tagged-union/main.lua @@ -0,0 +1,11 @@ +type Dog = {kind: "dog", bark: () -> ()} +type Cat = {kind: "cat", meow: () -> ()} +type Animal = Dog | Cat + +local function speak(a: Animal) + if a.kind == "dog" then + a.bark() + else + a.meow() + end +end diff --git a/testdata/fixtures/narrowing/discriminator-wrong-method/main.lua b/testdata/fixtures/narrowing/discriminator-wrong-method/main.lua new file mode 100644 index 00000000..214dc57f --- /dev/null +++ b/testdata/fixtures/narrowing/discriminator-wrong-method/main.lua @@ -0,0 +1,9 @@ +type Dog = {kind: "dog", bark: () -> ()} +type Cat = {kind: "cat", meow: () -> ()} +type Animal = Dog | Cat + +local function speak(a: Animal) + if a.kind == "dog" then + a.meow() -- expect-error + end +end diff --git a/testdata/fixtures/narrowing/else-branch-wrong-type/main.lua b/testdata/fixtures/narrowing/else-branch-wrong-type/main.lua new file mode 100644 index 00000000..55d4587b --- /dev/null +++ b/testdata/fixtures/narrowing/else-branch-wrong-type/main.lua @@ -0,0 +1,8 @@ +type A = {tag: "a", value: string} +type B = {tag: "b", value: number} +local r: A | B = {tag="a", value="x"} + +if r.tag == "a" then +else + local s: string = r.value -- expect-error: cannot assign +end diff --git a/testdata/fixtures/narrowing/equality-discriminant/main.lua b/testdata/fixtures/narrowing/equality-discriminant/main.lua new file mode 100644 index 00000000..2ad33306 --- /dev/null +++ b/testdata/fixtures/narrowing/equality-discriminant/main.lua @@ -0,0 +1,9 @@ +type A = {tag: "a", value: string} +type B = {tag: "b", value: number} +local r: A | B = {tag="a", value="x"} + +if r.tag == "a" then + local s: string = r.value +else + local n: number = r.value +end diff --git a/testdata/fixtures/narrowing/field-existence/main.lua b/testdata/fixtures/narrowing/field-existence/main.lua new file mode 100644 index 00000000..53339bca --- /dev/null +++ b/testdata/fixtures/narrowing/field-existence/main.lua @@ -0,0 +1,15 @@ +type Event = {kind: string, error: string?} +type Message = {topic: string, payload: any} +type Timer = {elapsed: number} +type SelectResult = Event | Message | Timer + +local function get_result(): SelectResult + return {kind = "exit", error = nil} +end + +local function f() + local result = get_result() + if result.kind then + local k: string = result.kind + end +end diff --git a/testdata/fixtures/narrowing/nested-field-channel/main.lua b/testdata/fixtures/narrowing/nested-field-channel/main.lua new file mode 100644 index 00000000..4dbced76 --- /dev/null +++ b/testdata/fixtures/narrowing/nested-field-channel/main.lua @@ -0,0 +1,16 @@ +type ChanInt = {__tag: "int"} +type ChanStr = {__tag: "str"} +type SelResult = + {channel: ChanInt, value: {error: string}, ok: boolean} | + {channel: ChanStr, value: {data: number}, ok: boolean} + +local function get_result(a: ChanInt, b: ChanStr): SelResult + return {channel = a, value = {error = "oops"}, ok = true} +end + +local function f(ch1: ChanInt, ch2: ChanStr) + local result = get_result(ch1, ch2) + if result.channel == ch1 then + local e: string = result.value.error + end +end diff --git a/testdata/fixtures/narrowing/nil-check-else/main.lua b/testdata/fixtures/narrowing/nil-check-else/main.lua new file mode 100644 index 00000000..28550d16 --- /dev/null +++ b/testdata/fixtures/narrowing/nil-check-else/main.lua @@ -0,0 +1,6 @@ +local x: string? = nil +if x == nil then + local s: nil = x +else + local s: string = x +end diff --git a/testdata/fixtures/narrowing/nil-check-optional/main.lua b/testdata/fixtures/narrowing/nil-check-optional/main.lua new file mode 100644 index 00000000..150de358 --- /dev/null +++ b/testdata/fixtures/narrowing/nil-check-optional/main.lua @@ -0,0 +1,4 @@ +local x: string? = nil +if x ~= nil then + local s: string = x +end diff --git a/testdata/fixtures/narrowing/optional-deeply-nested-method/main.lua b/testdata/fixtures/narrowing/optional-deeply-nested-method/main.lua new file mode 100644 index 00000000..64493224 --- /dev/null +++ b/testdata/fixtures/narrowing/optional-deeply-nested-method/main.lua @@ -0,0 +1,17 @@ +type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} +local function test(): Error? + return nil +end +local err = test() +local a, b, c = true, true, true +if err then + if a then + if b then + if c then + local k = err:kind() + local m = err:message() + local r = err:retryable() + end + end + end +end diff --git a/testdata/fixtures/narrowing/optional-multiple-methods/main.lua b/testdata/fixtures/narrowing/optional-multiple-methods/main.lua new file mode 100644 index 00000000..f9afab4a --- /dev/null +++ b/testdata/fixtures/narrowing/optional-multiple-methods/main.lua @@ -0,0 +1,12 @@ +type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} +local function test(): Error? + return nil +end +local err = test() +if err then + local kind = err:kind() + if kind == "network" then + local retryable = err:retryable() + local message = err:message() + end +end diff --git a/testdata/fixtures/narrowing/optional-nested-method-call/main.lua b/testdata/fixtures/narrowing/optional-nested-method-call/main.lua new file mode 100644 index 00000000..8fa442e9 --- /dev/null +++ b/testdata/fixtures/narrowing/optional-nested-method-call/main.lua @@ -0,0 +1,8 @@ +type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} +local function test(): Error? + return nil +end +local err = test() +if err then + local msg = err:message() +end diff --git a/testdata/fixtures/narrowing/optional-nested-preserves-method/main.lua b/testdata/fixtures/narrowing/optional-nested-preserves-method/main.lua new file mode 100644 index 00000000..be579399 --- /dev/null +++ b/testdata/fixtures/narrowing/optional-nested-preserves-method/main.lua @@ -0,0 +1,11 @@ +type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} +local function test(): Error? + return nil +end +local err = test() +local flag = true +if err then + if flag then + local msg = err:message() + end +end diff --git a/testdata/fixtures/narrowing/optional-nested-preserves/main.lua b/testdata/fixtures/narrowing/optional-nested-preserves/main.lua new file mode 100644 index 00000000..c9b910b8 --- /dev/null +++ b/testdata/fixtures/narrowing/optional-nested-preserves/main.lua @@ -0,0 +1,11 @@ +type Error = {kind: string, message: string} +local function test(): Error? + return {kind = "test", message = "msg"} +end +local err = test() +local flag = true +if err then + if flag then + local msg = err.message + end +end diff --git a/testdata/fixtures/narrowing/optional-nested-simple/main.lua b/testdata/fixtures/narrowing/optional-nested-simple/main.lua new file mode 100644 index 00000000..edc35a2b --- /dev/null +++ b/testdata/fixtures/narrowing/optional-nested-simple/main.lua @@ -0,0 +1,8 @@ +type Error = {kind: string, message: string} +local function test(): Error? + return {kind = "test", message = "msg"} +end +local err = test() +if err then + local msg = err.message +end diff --git a/testdata/fixtures/narrowing/truthiness-narrows/main.lua b/testdata/fixtures/narrowing/truthiness-narrows/main.lua new file mode 100644 index 00000000..34f705d8 --- /dev/null +++ b/testdata/fixtures/narrowing/truthiness-narrows/main.lua @@ -0,0 +1,4 @@ +local x: string? = "test" +if x then + local s: string = x +end diff --git a/testdata/fixtures/narrowing/typeof-excludes-other/main.lua b/testdata/fixtures/narrowing/typeof-excludes-other/main.lua new file mode 100644 index 00000000..e2e5b48c --- /dev/null +++ b/testdata/fixtures/narrowing/typeof-excludes-other/main.lua @@ -0,0 +1,4 @@ +local x: string | number = 42 +if type(x) == "string" then + local n: number = x -- expect-error: cannot assign +end diff --git a/testdata/fixtures/narrowing/typeof-number/main.lua b/testdata/fixtures/narrowing/typeof-number/main.lua new file mode 100644 index 00000000..60791a85 --- /dev/null +++ b/testdata/fixtures/narrowing/typeof-number/main.lua @@ -0,0 +1,4 @@ +local x: string | number = 42 +if type(x) == "number" then + local n: number = x +end diff --git a/testdata/fixtures/narrowing/typeof-string/main.lua b/testdata/fixtures/narrowing/typeof-string/main.lua new file mode 100644 index 00000000..c99dcaba --- /dev/null +++ b/testdata/fixtures/narrowing/typeof-string/main.lua @@ -0,0 +1,4 @@ +local x: string | number = "hello" +if type(x) == "string" then + local s: string = x +end diff --git a/testdata/fixtures/types/array/main.lua b/testdata/fixtures/types/array/main.lua new file mode 100644 index 00000000..b3910ecd --- /dev/null +++ b/testdata/fixtures/types/array/main.lua @@ -0,0 +1,2 @@ +type Numbers = {number} +local arr: Numbers = {1, 2, 3} diff --git a/testdata/fixtures/types/chained-references/main.lua b/testdata/fixtures/types/chained-references/main.lua new file mode 100644 index 00000000..5ddd66a2 --- /dev/null +++ b/testdata/fixtures/types/chained-references/main.lua @@ -0,0 +1,4 @@ +type A = number +type B = A +type C = B +local x: C = 42 diff --git a/testdata/fixtures/types/function-type/main.lua b/testdata/fixtures/types/function-type/main.lua new file mode 100644 index 00000000..26911f10 --- /dev/null +++ b/testdata/fixtures/types/function-type/main.lua @@ -0,0 +1,4 @@ +type Callback = (x: number) -> string +local cb: Callback = function(x: number): string + return tostring(x) +end diff --git a/testdata/fixtures/types/in-do-block/main.lua b/testdata/fixtures/types/in-do-block/main.lua new file mode 100644 index 00000000..bdae8a31 --- /dev/null +++ b/testdata/fixtures/types/in-do-block/main.lua @@ -0,0 +1,4 @@ +do + type Inner = {value: number} + local x: Inner = {value = 42} +end diff --git a/testdata/fixtures/types/in-for-loop/main.lua b/testdata/fixtures/types/in-for-loop/main.lua new file mode 100644 index 00000000..ee750b7d --- /dev/null +++ b/testdata/fixtures/types/in-for-loop/main.lua @@ -0,0 +1,4 @@ +for i = 1, 3 do + type Index = number + local idx: Index = i +end diff --git a/testdata/fixtures/types/in-nested-function/main.lua b/testdata/fixtures/types/in-nested-function/main.lua new file mode 100644 index 00000000..434cea98 --- /dev/null +++ b/testdata/fixtures/types/in-nested-function/main.lua @@ -0,0 +1,6 @@ +local function outer() + type LocalType = {x: number} + local function inner() + local v: LocalType = {x = 1} + end +end diff --git a/testdata/fixtures/types/in-while-loop/main.lua b/testdata/fixtures/types/in-while-loop/main.lua new file mode 100644 index 00000000..3a0715d9 --- /dev/null +++ b/testdata/fixtures/types/in-while-loop/main.lua @@ -0,0 +1,6 @@ +local i = 0 +while i < 1 do + type Counter = {value: number} + local c: Counter = {value = i} + i = i + 1 +end diff --git a/testdata/fixtures/types/inside-if-block/main.lua b/testdata/fixtures/types/inside-if-block/main.lua new file mode 100644 index 00000000..7d85d772 --- /dev/null +++ b/testdata/fixtures/types/inside-if-block/main.lua @@ -0,0 +1,4 @@ +if true then + type LocalPoint = {x: number, y: number} + local p: LocalPoint = {x = 1, y = 2} +end diff --git a/testdata/fixtures/types/map/main.lua b/testdata/fixtures/types/map/main.lua new file mode 100644 index 00000000..be7013ef --- /dev/null +++ b/testdata/fixtures/types/map/main.lua @@ -0,0 +1,2 @@ +type StringMap = {[string]: number} +local m: StringMap = {a = 1, b = 2} diff --git a/testdata/fixtures/types/missing-field/main.lua b/testdata/fixtures/types/missing-field/main.lua new file mode 100644 index 00000000..6efe5f35 --- /dev/null +++ b/testdata/fixtures/types/missing-field/main.lua @@ -0,0 +1,2 @@ +type Point = {x: number, y: number} +local p: Point = {x = 10} -- expect-error diff --git a/testdata/fixtures/types/multiple/main.lua b/testdata/fixtures/types/multiple/main.lua new file mode 100644 index 00000000..5d16c2c2 --- /dev/null +++ b/testdata/fixtures/types/multiple/main.lua @@ -0,0 +1,4 @@ +type Name = string +type Age = number +type Person = {name: Name, age: Age} +local p: Person = {name = "Alice", age = 30} diff --git a/testdata/fixtures/types/nested-record/main.lua b/testdata/fixtures/types/nested-record/main.lua new file mode 100644 index 00000000..1fd06970 --- /dev/null +++ b/testdata/fixtures/types/nested-record/main.lua @@ -0,0 +1,6 @@ +type Point = {x: number, y: number} +type Line = {start: Point, finish: Point} +local line: Line = { + start = {x = 0, y = 0}, + finish = {x = 10, y = 10} +} diff --git a/testdata/fixtures/types/not-visible-outside-block/main.lua b/testdata/fixtures/types/not-visible-outside-block/main.lua new file mode 100644 index 00000000..0ad58c79 --- /dev/null +++ b/testdata/fixtures/types/not-visible-outside-block/main.lua @@ -0,0 +1,4 @@ +if true then + type LocalPoint = {x: number, y: number} +end +local p: LocalPoint = {x = 1, y = 2} -- expect-error diff --git a/testdata/fixtures/types/optional/main.lua b/testdata/fixtures/types/optional/main.lua new file mode 100644 index 00000000..90ac4ccf --- /dev/null +++ b/testdata/fixtures/types/optional/main.lua @@ -0,0 +1,3 @@ +type MaybeNumber = number? +local a: MaybeNumber = 10 +local b: MaybeNumber = nil diff --git a/testdata/fixtures/types/record-optional-field/main.lua b/testdata/fixtures/types/record-optional-field/main.lua new file mode 100644 index 00000000..90552aa3 --- /dev/null +++ b/testdata/fixtures/types/record-optional-field/main.lua @@ -0,0 +1,3 @@ +type Config = {name: string, port?: number} +local c1: Config = {name = "server"} +local c2: Config = {name = "server", port = 8080} diff --git a/testdata/fixtures/types/references-another/main.lua b/testdata/fixtures/types/references-another/main.lua new file mode 100644 index 00000000..15fbb9ed --- /dev/null +++ b/testdata/fixtures/types/references-another/main.lua @@ -0,0 +1,4 @@ +type Point = {x: number, y: number} +type MaybePoint = Point? +local p: MaybePoint = {x = 1, y = 2} +local q: MaybePoint = nil diff --git a/testdata/fixtures/types/shadowing/main.lua b/testdata/fixtures/types/shadowing/main.lua new file mode 100644 index 00000000..8b745f67 --- /dev/null +++ b/testdata/fixtures/types/shadowing/main.lua @@ -0,0 +1,7 @@ +type Value = number +local a: Value = 10 +if true then + type Value = string + local b: Value = "hello" +end +local c: Value = 20 diff --git a/testdata/fixtures/types/simple-record/main.lua b/testdata/fixtures/types/simple-record/main.lua new file mode 100644 index 00000000..23213dc9 --- /dev/null +++ b/testdata/fixtures/types/simple-record/main.lua @@ -0,0 +1,2 @@ +type Point = {x: number, y: number} +local p: Point = {x = 10, y = 20} diff --git a/testdata/fixtures/types/union-mismatch/main.lua b/testdata/fixtures/types/union-mismatch/main.lua new file mode 100644 index 00000000..b2589981 --- /dev/null +++ b/testdata/fixtures/types/union-mismatch/main.lua @@ -0,0 +1,2 @@ +type StringOrNumber = string | number +local a: StringOrNumber = true -- expect-error diff --git a/testdata/fixtures/types/union/main.lua b/testdata/fixtures/types/union/main.lua new file mode 100644 index 00000000..49f306b2 --- /dev/null +++ b/testdata/fixtures/types/union/main.lua @@ -0,0 +1,3 @@ +type StringOrNumber = string | number +local a: StringOrNumber = "hello" +local b: StringOrNumber = 42 diff --git a/testdata/fixtures/types/used-before-definition/main.lua b/testdata/fixtures/types/used-before-definition/main.lua new file mode 100644 index 00000000..d78a587f --- /dev/null +++ b/testdata/fixtures/types/used-before-definition/main.lua @@ -0,0 +1,2 @@ +local p: Point = {x = 10, y = 20} -- expect-error +type Point = {x: number, y: number} diff --git a/testdata/fixtures/types/wrong-field-type/main.lua b/testdata/fixtures/types/wrong-field-type/main.lua new file mode 100644 index 00000000..9a4f8696 --- /dev/null +++ b/testdata/fixtures/types/wrong-field-type/main.lua @@ -0,0 +1,2 @@ +type Point = {x: number, y: number} +local p: Point = {x = "wrong", y = 20} -- expect-error From 75f5cc1fb5580e69cfa9a7b0f4503bc706f2b153 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 15:48:00 -0400 Subject: [PATCH 03/10] Retrofit generics, functions, flow, and regression tests to fixtures Migrate generics_test.go (19 cases), functions_test.go (26 cases), control_flow_test.go (17 cases), and wippy_suite_test.go easy cases (21 cases) to fixture harness. Remove original Go files. Fix inline expect-error to absorb all diagnostics on annotated line, not just the first match. Keeps manifest-based regression tests (TestModuleGenericInstantiation, TestRegistryTypeLoss) in place. --- .../check/tests/flow/control_flow_test.go | 243 ------------- .../check/tests/functions/functions_test.go | 322 ------------------ .../check/tests/generics/generics_test.go | 266 --------------- .../tests/regression/wippy_suite_test.go | 314 ----------------- fixture_harness_test.go | 2 +- testdata/fixtures/flow/break-in-for/main.lua | 3 + .../fixtures/flow/break-in-while/main.lua | 5 + .../flow/callback-preserves-type/main.lua | 9 + .../callback-result-preserves-type/main.lua | 6 + .../flow/closure-captures-type/main.lua | 7 + .../fixtures/flow/do-block-nested/main.lua | 9 + .../fixtures/flow/do-block-scope/main.lua | 4 + .../flow/error-return-optional/main.lua | 7 + .../flow/higher-order-function-types/main.lua | 12 + testdata/fixtures/flow/if-else/main.lua | 1 + .../fixtures/flow/if-elseif-else/main.lua | 1 + .../fixtures/flow/if-scope-isolation/main.lua | 4 + testdata/fixtures/flow/if-simple/main.lua | 1 + .../flow/return-correct-type/main.lua | 3 + .../fixtures/flow/return-early-in-if/main.lua | 6 + .../flow/return-multiple-values/main.lua | 4 + .../fixtures/flow/return-wrong-type/main.lua | 3 + .../functions/call-correct-types/main.lua | 4 + .../functions/call-non-callable/main.lua | 2 + .../functions/call-result-wrong-type/main.lua | 4 + .../functions/call-statement-correct/main.lua | 2 + .../call-statement-wrong-type/main.lua | 2 + .../functions/call-too-few-arguments/main.lua | 4 + .../functions/call-wrong-argument/main.lua | 4 + .../functions/closure-captures-outer/main.lua | 5 + .../functions/closure-modifies-outer/main.lua | 6 + .../explicit-nil-check-optional/main.lua | 6 + .../function-with-parameters/main.lua | 1 + .../function-with-return-type/main.lua | 1 + .../generic-function-identity/main.lua | 5 + .../functions/local-function/main.lua | 1 + .../method-call-with-params/main.lua | 12 + .../functions/method-call-with-self/main.lua | 12 + .../functions/multiple-or-defaults/main.lua | 8 + .../functions/multiple-returns/main.lua | 1 + .../functions/or-default-optional/main.lua | 5 + .../functions/return-call-wrong-arg/main.lua | 7 + .../functions/simple-function/main.lua | 1 + .../functions/typed-optional-param/main.lua | 6 + .../functions/variadic-correct/main.lua | 4 + .../functions/variadic-function/main.lua | 1 + .../functions/variadic-wrong-type/main.lua | 4 + .../functions/wrong-return-type/main.lua | 1 + .../generics/bounded-array-in-bounds/main.lua | 2 + .../bounded-array-last-element/main.lua | 2 + .../bounded-array-out-of-bounds-fail/main.lua | 2 + .../bounded-array-out-of-bounds-nil/main.lua | 2 + .../constraint-on-type-param/main.lua | 4 + .../generics/constraint-satisfied/main.lua | 6 + .../generics/constraint-violation/main.lua | 5 + .../generic-array-literal-index/main.lua | 3 + .../generics/generic-array-type/main.lua | 3 + .../generics/generic-optional-type/main.lua | 3 + .../generics/generic-record-type/main.lua | 3 + .../generics/generic-result-type/main.lua | 2 + .../generics/identity-function/main.lua | 5 + .../generics/identity-wrong-type/main.lua | 4 + .../generics/inferred-instantiation/main.lua | 4 + .../method-returns-type-param/main.lua | 11 + .../nested-generic-instantiation/main.lua | 4 + .../fixtures/generics/pair-function/main.lua | 6 + .../fixtures/generics/swap-function/main.lua | 6 + .../callback-correct-param-type/main.lua | 7 + .../callback-nested-preserves-types/main.lua | 6 + .../callback-preserves-return-type/main.lua | 6 + .../field-access-after-boolean-check/main.lua | 10 + .../generic-instantiate-concrete/main.lua | 4 + .../generic-multiple-type-params/main.lua | 10 + .../generic-nested-instantiate/main.lua | 4 + .../generic-record-with-method/main.lua | 11 + .../generic-type-alias-instantiate/main.lua | 3 + .../method-call-after-narrowing/main.lua | 10 + .../nil-narrow-and-operator/main.lua | 6 + .../regression/nil-narrow-condition/main.lua | 6 + .../nil-narrow-early-return/main.lua | 6 + .../nil-narrow-record-field/main.lua | 7 + .../regression/nil-narrow-to-non-nil/main.lua | 6 + .../regression/type-alias-chain/main.lua | 5 + .../type-alias-equivalence/main.lua | 3 + .../type-alias-function-param/main.lua | 5 + .../type-alias-function-return/main.lua | 6 + .../regression/type-alias-optional/main.lua | 3 + .../type-alias-record-field/main.lua | 4 + 88 files changed, 400 insertions(+), 1146 deletions(-) delete mode 100644 compiler/check/tests/flow/control_flow_test.go delete mode 100644 compiler/check/tests/functions/functions_test.go delete mode 100644 compiler/check/tests/generics/generics_test.go create mode 100644 testdata/fixtures/flow/break-in-for/main.lua create mode 100644 testdata/fixtures/flow/break-in-while/main.lua create mode 100644 testdata/fixtures/flow/callback-preserves-type/main.lua create mode 100644 testdata/fixtures/flow/callback-result-preserves-type/main.lua create mode 100644 testdata/fixtures/flow/closure-captures-type/main.lua create mode 100644 testdata/fixtures/flow/do-block-nested/main.lua create mode 100644 testdata/fixtures/flow/do-block-scope/main.lua create mode 100644 testdata/fixtures/flow/error-return-optional/main.lua create mode 100644 testdata/fixtures/flow/higher-order-function-types/main.lua create mode 100644 testdata/fixtures/flow/if-else/main.lua create mode 100644 testdata/fixtures/flow/if-elseif-else/main.lua create mode 100644 testdata/fixtures/flow/if-scope-isolation/main.lua create mode 100644 testdata/fixtures/flow/if-simple/main.lua create mode 100644 testdata/fixtures/flow/return-correct-type/main.lua create mode 100644 testdata/fixtures/flow/return-early-in-if/main.lua create mode 100644 testdata/fixtures/flow/return-multiple-values/main.lua create mode 100644 testdata/fixtures/flow/return-wrong-type/main.lua create mode 100644 testdata/fixtures/functions/call-correct-types/main.lua create mode 100644 testdata/fixtures/functions/call-non-callable/main.lua create mode 100644 testdata/fixtures/functions/call-result-wrong-type/main.lua create mode 100644 testdata/fixtures/functions/call-statement-correct/main.lua create mode 100644 testdata/fixtures/functions/call-statement-wrong-type/main.lua create mode 100644 testdata/fixtures/functions/call-too-few-arguments/main.lua create mode 100644 testdata/fixtures/functions/call-wrong-argument/main.lua create mode 100644 testdata/fixtures/functions/closure-captures-outer/main.lua create mode 100644 testdata/fixtures/functions/closure-modifies-outer/main.lua create mode 100644 testdata/fixtures/functions/explicit-nil-check-optional/main.lua create mode 100644 testdata/fixtures/functions/function-with-parameters/main.lua create mode 100644 testdata/fixtures/functions/function-with-return-type/main.lua create mode 100644 testdata/fixtures/functions/generic-function-identity/main.lua create mode 100644 testdata/fixtures/functions/local-function/main.lua create mode 100644 testdata/fixtures/functions/method-call-with-params/main.lua create mode 100644 testdata/fixtures/functions/method-call-with-self/main.lua create mode 100644 testdata/fixtures/functions/multiple-or-defaults/main.lua create mode 100644 testdata/fixtures/functions/multiple-returns/main.lua create mode 100644 testdata/fixtures/functions/or-default-optional/main.lua create mode 100644 testdata/fixtures/functions/return-call-wrong-arg/main.lua create mode 100644 testdata/fixtures/functions/simple-function/main.lua create mode 100644 testdata/fixtures/functions/typed-optional-param/main.lua create mode 100644 testdata/fixtures/functions/variadic-correct/main.lua create mode 100644 testdata/fixtures/functions/variadic-function/main.lua create mode 100644 testdata/fixtures/functions/variadic-wrong-type/main.lua create mode 100644 testdata/fixtures/functions/wrong-return-type/main.lua create mode 100644 testdata/fixtures/generics/bounded-array-in-bounds/main.lua create mode 100644 testdata/fixtures/generics/bounded-array-last-element/main.lua create mode 100644 testdata/fixtures/generics/bounded-array-out-of-bounds-fail/main.lua create mode 100644 testdata/fixtures/generics/bounded-array-out-of-bounds-nil/main.lua create mode 100644 testdata/fixtures/generics/constraint-on-type-param/main.lua create mode 100644 testdata/fixtures/generics/constraint-satisfied/main.lua create mode 100644 testdata/fixtures/generics/constraint-violation/main.lua create mode 100644 testdata/fixtures/generics/generic-array-literal-index/main.lua create mode 100644 testdata/fixtures/generics/generic-array-type/main.lua create mode 100644 testdata/fixtures/generics/generic-optional-type/main.lua create mode 100644 testdata/fixtures/generics/generic-record-type/main.lua create mode 100644 testdata/fixtures/generics/generic-result-type/main.lua create mode 100644 testdata/fixtures/generics/identity-function/main.lua create mode 100644 testdata/fixtures/generics/identity-wrong-type/main.lua create mode 100644 testdata/fixtures/generics/inferred-instantiation/main.lua create mode 100644 testdata/fixtures/generics/method-returns-type-param/main.lua create mode 100644 testdata/fixtures/generics/nested-generic-instantiation/main.lua create mode 100644 testdata/fixtures/generics/pair-function/main.lua create mode 100644 testdata/fixtures/generics/swap-function/main.lua create mode 100644 testdata/fixtures/regression/callback-correct-param-type/main.lua create mode 100644 testdata/fixtures/regression/callback-nested-preserves-types/main.lua create mode 100644 testdata/fixtures/regression/callback-preserves-return-type/main.lua create mode 100644 testdata/fixtures/regression/field-access-after-boolean-check/main.lua create mode 100644 testdata/fixtures/regression/generic-instantiate-concrete/main.lua create mode 100644 testdata/fixtures/regression/generic-multiple-type-params/main.lua create mode 100644 testdata/fixtures/regression/generic-nested-instantiate/main.lua create mode 100644 testdata/fixtures/regression/generic-record-with-method/main.lua create mode 100644 testdata/fixtures/regression/generic-type-alias-instantiate/main.lua create mode 100644 testdata/fixtures/regression/method-call-after-narrowing/main.lua create mode 100644 testdata/fixtures/regression/nil-narrow-and-operator/main.lua create mode 100644 testdata/fixtures/regression/nil-narrow-condition/main.lua create mode 100644 testdata/fixtures/regression/nil-narrow-early-return/main.lua create mode 100644 testdata/fixtures/regression/nil-narrow-record-field/main.lua create mode 100644 testdata/fixtures/regression/nil-narrow-to-non-nil/main.lua create mode 100644 testdata/fixtures/regression/type-alias-chain/main.lua create mode 100644 testdata/fixtures/regression/type-alias-equivalence/main.lua create mode 100644 testdata/fixtures/regression/type-alias-function-param/main.lua create mode 100644 testdata/fixtures/regression/type-alias-function-return/main.lua create mode 100644 testdata/fixtures/regression/type-alias-optional/main.lua create mode 100644 testdata/fixtures/regression/type-alias-record-field/main.lua diff --git a/compiler/check/tests/flow/control_flow_test.go b/compiler/check/tests/flow/control_flow_test.go deleted file mode 100644 index 4d54d3a2..00000000 --- a/compiler/check/tests/flow/control_flow_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package flow - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -func TestControlFlow_If(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple if", - Code: `if true then end`, - WantError: false, - Stdlib: true, - }, - { - Name: "if else", - Code: `if true then local x = 1 else local x = 2 end`, - WantError: false, - Stdlib: true, - }, - { - Name: "if elseif else", - Code: `if true then local x = 1 elseif false then local x = 2 else local x = 3 end`, - WantError: false, - Stdlib: true, - }, - { - Name: "if local scope isolation", - Code: ` - if true then - local x = 1 - end - local y: number = x - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestControlFlow_DoBlock(t *testing.T) { - tests := []testutil.Case{ - { - Name: "do block creates scope", - Code: ` - do - local x = 1 - end - local y: number = x - `, - WantError: true, - Stdlib: true, - }, - { - Name: "nested do blocks", - Code: ` - local x = 0 - do - local y = 1 - do - local z = 2 - x = z - end - end - local result: number = x - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestControlFlow_Return(t *testing.T) { - tests := []testutil.Case{ - { - Name: "return correct type", - Code: ` - local function f(): number - return 42 - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "return wrong type", - Code: ` - local function f(): number - return "wrong" - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "early return in if", - Code: ` - local function f(x: number): number - if x < 0 then - return 0 - end - return x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "multiple return values", - Code: ` - local function f(): (number, string) - return 1, "ok" - end - local a: number, b: string = f() - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestControlFlow_Break(t *testing.T) { - tests := []testutil.Case{ - { - Name: "break in while", - Code: ` - local i = 0 - while true do - i = i + 1 - if i > 10 then break end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "break in for", - Code: ` - for i = 1, 100 do - if i > 10 then break end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestControlFlow_ErrorReturn(t *testing.T) { - tests := []testutil.Case{ - { - Name: "optional error return", - Code: ` - local function div(a: number, b: number): (number?, string?) - if b == 0 then - return nil, "division by zero" - end - return a / b, nil - end - local result, err = div(10, 2) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestControlFlow_TypePreservation tests that types are preserved through -// callbacks and higher-order functions. -func TestControlFlow_TypePreservation(t *testing.T) { - tests := []testutil.Case{ - { - Name: "callback_preserves_type", - Code: ` - type Handler = fun(data: string): nil - local function process(items: {string}, handler: Handler) - for _, item in ipairs(items) do - handler(item) - end - end - process({"a", "b"}, function(s: string) - local upper: string = s:upper() - end) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "higher_order_function_types", - Code: ` - type Mapper = fun(x: T): U - local function map(arr: {T}, f: Mapper): {U} - local result: {U} = {} - for i, v in ipairs(arr) do - result[i] = f(v) - end - return result - end - local nums = map({"1", "2", "3"}, function(s: string): number - return tonumber(s) or 0 - end) - local n: number = nums[1] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "closure_captures_type", - Code: ` - local function make_adder(n: number): fun(x: number): number - return function(x: number): number - return x + n - end - end - local add5 = make_adder(5) - local result: number = add5(10) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "callback_result_preserves_type", - Code: ` - local function apply(value: T, fn: fun(x: T): U): U - return fn(value) - end - local result: string = apply(42, function(n: number): string - return tostring(n) - end) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/functions/functions_test.go b/compiler/check/tests/functions/functions_test.go deleted file mode 100644 index a9ef1cf2..00000000 --- a/compiler/check/tests/functions/functions_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package functions - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -func TestFunction_Definition(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple function", - Code: `function f() end`, - WantError: false, - Stdlib: true, - }, - { - Name: "function with parameters", - Code: `function f(x: number, y: string) end`, - WantError: false, - Stdlib: true, - }, - { - Name: "function with return type", - Code: `function f(): number return 1 end`, - WantError: false, - Stdlib: true, - }, - { - Name: "wrong return type", - Code: `function f(): number return "hello" end`, - WantError: true, - Stdlib: true, - }, - { - Name: "multiple returns", - Code: `function f(): (number, string) return 1, "a" end`, - WantError: false, - Stdlib: true, - }, - { - Name: "variadic function", - Code: `function f(...: number) end`, - WantError: false, - Stdlib: true, - }, - { - Name: "local function", - Code: `local function f() end`, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestFunction_Call(t *testing.T) { - tests := []testutil.Case{ - { - Name: "call with correct types", - Code: ` - local function add(a: number, b: number): number - return a + b - end - local x: number = add(1, 2) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "call with wrong argument type", - Code: ` - local function add(a: number, b: number): number - return a + b - end - local x = add(1, "wrong") - `, - WantError: true, - Stdlib: true, - }, - { - Name: "call result assigned to wrong type", - Code: ` - local function add(a: number, b: number): number - return a + b - end - local x: string = add(1, 2) - `, - WantError: true, - Stdlib: true, - }, - { - Name: "too few arguments", - Code: ` - local function add(a: number, b: number): number - return a + b - end - local x = add(1) - `, - WantError: true, - Stdlib: true, - }, - { - Name: "call non-callable", - Code: ` - local x: number = 42 - local y = x() - `, - WantError: true, - Stdlib: true, - }, - { - Name: "variadic function correct", - Code: ` - local function sum(...: number): number - return 0 - end - local x: number = sum(1, 2, 3, 4, 5) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "variadic function wrong type", - Code: ` - local function sum(...: number): number - return 0 - end - local x = sum(1, 2, "three") - `, - WantError: true, - Stdlib: true, - }, - { - Name: "call statement correct", - Code: ` - local function log(msg: string) end - log("hello") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "call statement wrong type", - Code: ` - local function log(msg: string) end - log(123) - `, - WantError: true, - Stdlib: true, - }, - { - Name: "return call wrong argument type", - Code: ` - local function add(a: number, b: number): number - return a + b - end - local function f(): number - return add("bad", 2) - end - local x = f() - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestFunction_Method(t *testing.T) { - tests := []testutil.Case{ - { - Name: "method call with self", - Code: ` - type Counter = { - count: number, - increment: (self: Counter) -> number - } - local c: Counter = { - count = 0, - increment = function(self: Counter): number - self.count = self.count + 1 - return self.count - end - } - local n: number = c:increment() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "method call with parameters", - Code: ` - type Adder = { - value: number, - add: (self: Adder, n: number) -> number - } - local a: Adder = { - value = 0, - add = function(self: Adder, n: number): number - self.value = self.value + n - return self.value - end - } - local r: number = a:add(5) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestFunction_Closure(t *testing.T) { - tests := []testutil.Case{ - { - Name: "closure captures outer variable", - Code: ` - local x: number = 10 - local function inner(): number - return x - end - local y: number = inner() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "closure modifies outer variable", - Code: ` - local x: number = 0 - local function increment() - x = x + 1 - end - increment() - local y: number = x - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestFunction_Generic(t *testing.T) { - tests := []testutil.Case{ - { - Name: "generic function identity", - Code: ` - local function identity(x: T): T - return x - end - local n: number = identity(42) - local s: string = identity("hello") - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestFunction_OptionalParameterInference tests that parameters with or-default -// patterns are inferred as optional. -func TestFunction_OptionalParameterInference(t *testing.T) { - tests := []testutil.Case{ - { - Name: "or_default_marks_param_optional", - Code: ` - local function greet(name, greeting) - local msg = greeting or "Hello" - return msg .. ", " .. name - end - greet("World") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "multiple_or_defaults", - Code: ` - local function format(value, prefix, suffix) - local p = prefix or "[" - local s = suffix or "]" - return p .. tostring(value) .. s - end - format(42) - format(42, "<") - format(42, "<", ">") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "explicit_nil_check_optional", - Code: ` - local function process(data, callback) - if callback ~= nil then - callback(data) - end - end - process("test") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "typed_optional_param", - Code: ` - local function log(msg: string, level: string?) - local lvl = level or "INFO" - print(lvl .. ": " .. msg) - end - log("hello") - log("hello", "DEBUG") - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/generics/generics_test.go b/compiler/check/tests/generics/generics_test.go deleted file mode 100644 index c2c1354c..00000000 --- a/compiler/check/tests/generics/generics_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package generics - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestGenerics_BasicIdentity tests basic generic function identity. -func TestGenerics_BasicIdentity(t *testing.T) { - tests := []testutil.Case{ - { - Name: "identity function", - Code: ` - local function identity(x: T): T - return x - end - local n: number = identity(42) - local s: string = identity("hello") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "identity wrong type fails", - Code: ` - local function identity(x: T): T - return x - end - local n: number = identity("hello") - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenerics_MultipleTypeParams tests functions with multiple type parameters. -func TestGenerics_MultipleTypeParams(t *testing.T) { - tests := []testutil.Case{ - { - Name: "pair function", - Code: ` - local function pair(a: A, b: B): (A, B) - return a, b - end - local n, s = pair(42, "hello") - local x: number = n - local y: string = s - `, - WantError: false, - Stdlib: true, - }, - { - Name: "swap function", - Code: ` - local function swap(a: A, b: B): (B, A) - return b, a - end - local s, n = swap(42, "hello") - local x: string = s - local y: number = n - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenerics_GenericTypes tests generic type definitions. -func TestGenerics_GenericTypes(t *testing.T) { - tests := []testutil.Case{ - { - Name: "generic array type", - Code: ` - type Array = {[integer]: T} - local arr: Array = {1, 2, 3} - local n: number? = arr[1] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic optional type", - Code: ` - type Maybe = T | nil - local m: Maybe = 42 - local n: Maybe = nil - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic record type", - Code: ` - type Box = {value: T} - local b: Box = {value = 42} - local n: number = b.value - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic result type", - Code: ` - type Result = {ok: true, value: T} | {ok: false, error: E} - local r: Result = {ok = true, value = 42} - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenerics_Constraints tests generic type constraints. -func TestGenerics_Constraints(t *testing.T) { - tests := []testutil.Case{ - { - Name: "constraint on type param", - Code: ` - type Printable = {tostring: (self: Printable) -> string} - local function print_it(x: T): string - return x:tostring() - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "constraint violation at call site", - Code: ` - type HasName = {name: string} - local function wrap(x: T): T - return x - end - local n: number = wrap(42) - `, - WantError: true, - Stdlib: true, - }, - { - Name: "constraint satisfied at call site", - Code: ` - type HasName = {name: string} - local function wrap(x: T): T - return x - end - local r = wrap({name = "Alice"}) - local s: string = r.name - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenerics_Instantiation tests generic type instantiation. -func TestGenerics_Instantiation(t *testing.T) { - tests := []testutil.Case{ - { - Name: "inferred instantiation", - Code: ` - local function identity(x: T): T - return x - end - local n: number = identity(42) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested generic instantiation", - Code: ` - type Box = {value: T} - type DoubleBox = Box> - local db: DoubleBox = {value = {value = 42}} - local n: number = db.value.value - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenerics_MethodsOnGenericTypes tests methods on generic types. -func TestGenerics_MethodsOnGenericTypes(t *testing.T) { - tests := []testutil.Case{ - { - Name: "method returns type parameter", - Code: ` - type Container = { - _value: T, - get: (self: Container) -> T - } - local c: Container = { - _value = 42, - get = function(self: Container): number - return self._value - end - } - local n: number = c:get() - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestBoundedArrayLiteral tests that array literal indexing is sound. -func TestBoundedArrayLiteral(t *testing.T) { - tests := []testutil.Case{ - { - Name: "literal index in bounds is non-optional", - Code: ` - local arr = {1, 2, 3} - local n: number = arr[1] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "literal index at last element", - Code: ` - local arr = {1, 2, 3} - local n: number = arr[3] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "literal index out of bounds is nil", - Code: ` - local arr = {1, 2, 3} - local n: nil = arr[4] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assigning out of bounds to non-optional fails", - Code: ` - local arr = {1, 2, 3} - local n: number = arr[4] - `, - WantError: true, - Stdlib: true, - }, - { - Name: "generic array type with literal index", - Code: ` - type Array = {[integer]: T} - local arr: Array = {1, 2, 3} - local n: number? = arr[1] - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/regression/wippy_suite_test.go b/compiler/check/tests/regression/wippy_suite_test.go index ba00f560..bbf79540 100644 --- a/compiler/check/tests/regression/wippy_suite_test.go +++ b/compiler/check/tests/regression/wippy_suite_test.go @@ -8,231 +8,6 @@ import ( "github.com/wippyai/go-lua/types/typ" ) -// TestTypeAliasEquivalence tests that type aliases are equivalent to their underlying types. -// False positive: assigning a value to a type alias of the same underlying type fails. -func TestTypeAliasEquivalence(t *testing.T) { - tests := []testutil.Case{ - { - Name: "type alias is equivalent to underlying type", - Code: ` - type UserID = string - local id: UserID = "user-123" - local s: string = id - `, - WantError: false, - Stdlib: true, - }, - { - Name: "function accepting type alias works with underlying type", - Code: ` - type Amount = number - local function process(a: Amount): number - return a * 2 - end - local result = process(100) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested type alias chain resolves correctly", - Code: ` - type ID = string - type UserID = ID - type AdminID = UserID - local id: AdminID = "admin-123" - local s: string = id - `, - WantError: false, - Stdlib: true, - }, - { - Name: "type alias in record field", - Code: ` - type Name = string - type Person = {name: Name, age: number} - local p: Person = {name = "Alice", age = 30} - local n: string = p.name - `, - WantError: false, - Stdlib: true, - }, - { - Name: "type alias in function return", - Code: ` - type Result = {ok: boolean, data: any} - local function fetch(): Result - return {ok = true, data = "hello"} - end - local r = fetch() - local ok: boolean = r.ok - `, - WantError: false, - Stdlib: true, - }, - { - Name: "type alias with optional", - Code: ` - type MaybeID = string? - local id: MaybeID = "123" - local id2: MaybeID = nil - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestNilNarrowing tests that nil checks properly narrow optional types. -// False positive: accessing field after nil check still produces nil-related errors. -func TestNilNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "nil check narrows optional to non-nil", - Code: ` - local function process(x: string?): string - if x ~= nil then - return x - end - return "default" - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nil check in condition narrows for then block", - Code: ` - local function get_length(s: string?): number - if s ~= nil then - return #s - end - return 0 - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nil check narrows record field", - Code: ` - type Config = {name: string, port?: number} - local function get_port(c: Config): number - if c.port ~= nil then - return c.port - end - return 8080 - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "early return on nil narrows rest of function", - Code: ` - local function require_value(x: string?): string - if x == nil then - return "missing" - end - return x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nil check with and operator", - Code: ` - local function safe_concat(a: string?, b: string): string - if a ~= nil then - return a .. b - end - return b - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGenericInstantiation tests that generic types instantiate correctly. -// False positive: using a method on an instantiated generic type fails. -func TestGenericInstantiation(t *testing.T) { - tests := []testutil.Case{ - { - Name: "generic function instantiates with concrete type", - Code: ` - local function first(arr: {T}): T? - return arr[1] - end - local n = first({1, 2, 3}) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic type alias instantiates correctly", - Code: ` - type Box = {value: T} - local b: Box = {value = 42} - local n: number = b.value - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested generic instantiation", - Code: ` - type Wrapper = {inner: T} - type DoubleWrap = Wrapper> - local dw: DoubleWrap = {inner = {inner = "hello"}} - local s: string = dw.inner.inner - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic function with multiple type params", - Code: ` - local function map(arr: {T}, fn: (T) -> U): {U} - local result: {U} = {} - for i, v in ipairs(arr) do - result[i] = fn(v) - end - return result - end - local nums = map({"a", "bb", "ccc"}, function(s: string): number - return #s - end) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic record with method", - Code: ` - type Container = { - value: T, - get: (self: Container) -> T - } - local c: Container = { - value = "hello", - get = function(self: Container): string - return self.value - end - } - local s: string = c:get() - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - // TestModuleGenericInstantiation tests generics from module manifests. // False positive: using generic type from module fails to instantiate. func TestModuleGenericInstantiation(t *testing.T) { @@ -379,92 +154,3 @@ func TestRegistryTypeLoss(t *testing.T) { } } -// TestCallbackTypePreservation tests that callback types are preserved through function calls. -// False positive: callback parameter type is lost when passed to higher-order function. -func TestCallbackTypePreservation(t *testing.T) { - tests := []testutil.Case{ - { - Name: "callback receives correct parameter type", - Code: ` - type User = {name: string, age: number} - local function process_user(u: User, callback: (User) -> nil) - callback(u) - end - process_user({name = "Alice", age = 30}, function(u: User) - local n: string = u.name - end) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "async callback preserves return type", - Code: ` - local function fetch(url: string, on_done: (string) -> nil) - on_done("response") - end - fetch("http://example.com", function(data: string) - local s: string = data - end) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested callback preserves types", - Code: ` - local function outer(f: (number) -> number) - return f(10) - end - local result: number = outer(function(x: number): number - return x * 2 - end) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestMethodCallOnUnion tests method calls on union types after narrowing. -// False positive: method call fails even after narrowing union to single variant. -func TestMethodCallOnUnion(t *testing.T) { - tests := []testutil.Case{ - { - Name: "method call after type narrowing", - Code: ` - type A = {kind: "a", get_a: (self: A) -> string} - type B = {kind: "b", get_b: (self: B) -> number} - type AB = A | B - - local function process(x: AB): string - if x.kind == "a" then - return x:get_a() - end - return tostring(x:get_b()) - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "field access after boolean check", - Code: ` - type Success = {ok: true, value: string} - type Failure = {ok: false, error: string} - type Result = Success | Failure - - local function get_value(r: Result): string - if r.ok then - return r.value - end - return r.error - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/fixture_harness_test.go b/fixture_harness_test.go index d0724ad3..23cb21df 100644 --- a/fixture_harness_test.go +++ b/fixture_harness_test.go @@ -227,13 +227,13 @@ func verifyInlineExpectations(t *testing.T, expectations []inlineExpectation, di for _, exp := range expectations { found := false + // An expect-error annotation absorbs ALL matching diagnostics on that line for i, d := range diagnostics { if !matchesExpectation(exp, d, entryFile) { continue } found = true matched[i] = true - break } if !found { failed = true diff --git a/testdata/fixtures/flow/break-in-for/main.lua b/testdata/fixtures/flow/break-in-for/main.lua new file mode 100644 index 00000000..c1d2a4fc --- /dev/null +++ b/testdata/fixtures/flow/break-in-for/main.lua @@ -0,0 +1,3 @@ +for i = 1, 100 do + if i > 10 then break end +end diff --git a/testdata/fixtures/flow/break-in-while/main.lua b/testdata/fixtures/flow/break-in-while/main.lua new file mode 100644 index 00000000..9c0bd137 --- /dev/null +++ b/testdata/fixtures/flow/break-in-while/main.lua @@ -0,0 +1,5 @@ +local i = 0 +while true do + i = i + 1 + if i > 10 then break end +end diff --git a/testdata/fixtures/flow/callback-preserves-type/main.lua b/testdata/fixtures/flow/callback-preserves-type/main.lua new file mode 100644 index 00000000..8dea40f7 --- /dev/null +++ b/testdata/fixtures/flow/callback-preserves-type/main.lua @@ -0,0 +1,9 @@ +type Handler = fun(data: string): nil +local function process(items: {string}, handler: Handler) + for _, item in ipairs(items) do + handler(item) + end +end +process({"a", "b"}, function(s: string) + local upper: string = s:upper() +end) diff --git a/testdata/fixtures/flow/callback-result-preserves-type/main.lua b/testdata/fixtures/flow/callback-result-preserves-type/main.lua new file mode 100644 index 00000000..e4de2b48 --- /dev/null +++ b/testdata/fixtures/flow/callback-result-preserves-type/main.lua @@ -0,0 +1,6 @@ +local function apply(value: T, fn: fun(x: T): U): U + return fn(value) +end +local result: string = apply(42, function(n: number): string + return tostring(n) +end) diff --git a/testdata/fixtures/flow/closure-captures-type/main.lua b/testdata/fixtures/flow/closure-captures-type/main.lua new file mode 100644 index 00000000..c7ab9bda --- /dev/null +++ b/testdata/fixtures/flow/closure-captures-type/main.lua @@ -0,0 +1,7 @@ +local function make_adder(n: number): fun(x: number): number + return function(x: number): number + return x + n + end +end +local add5 = make_adder(5) +local result: number = add5(10) diff --git a/testdata/fixtures/flow/do-block-nested/main.lua b/testdata/fixtures/flow/do-block-nested/main.lua new file mode 100644 index 00000000..b9155a4e --- /dev/null +++ b/testdata/fixtures/flow/do-block-nested/main.lua @@ -0,0 +1,9 @@ +local x = 0 +do + local y = 1 + do + local z = 2 + x = z + end +end +local result: number = x diff --git a/testdata/fixtures/flow/do-block-scope/main.lua b/testdata/fixtures/flow/do-block-scope/main.lua new file mode 100644 index 00000000..e8f23a3c --- /dev/null +++ b/testdata/fixtures/flow/do-block-scope/main.lua @@ -0,0 +1,4 @@ +do + local x = 1 +end +local y: number = x -- expect-error diff --git a/testdata/fixtures/flow/error-return-optional/main.lua b/testdata/fixtures/flow/error-return-optional/main.lua new file mode 100644 index 00000000..65e4d329 --- /dev/null +++ b/testdata/fixtures/flow/error-return-optional/main.lua @@ -0,0 +1,7 @@ +local function div(a: number, b: number): (number?, string?) + if b == 0 then + return nil, "division by zero" + end + return a / b, nil +end +local result, err = div(10, 2) diff --git a/testdata/fixtures/flow/higher-order-function-types/main.lua b/testdata/fixtures/flow/higher-order-function-types/main.lua new file mode 100644 index 00000000..5fd6f669 --- /dev/null +++ b/testdata/fixtures/flow/higher-order-function-types/main.lua @@ -0,0 +1,12 @@ +type Mapper = fun(x: T): U +local function map(arr: {T}, f: Mapper): {U} + local result: {U} = {} + for i, v in ipairs(arr) do + result[i] = f(v) + end + return result +end +local nums = map({"1", "2", "3"}, function(s: string): number + return tonumber(s) or 0 +end) +local n: number = nums[1] diff --git a/testdata/fixtures/flow/if-else/main.lua b/testdata/fixtures/flow/if-else/main.lua new file mode 100644 index 00000000..ef4a4c5e --- /dev/null +++ b/testdata/fixtures/flow/if-else/main.lua @@ -0,0 +1 @@ +if true then local x = 1 else local x = 2 end diff --git a/testdata/fixtures/flow/if-elseif-else/main.lua b/testdata/fixtures/flow/if-elseif-else/main.lua new file mode 100644 index 00000000..e51f73c9 --- /dev/null +++ b/testdata/fixtures/flow/if-elseif-else/main.lua @@ -0,0 +1 @@ +if true then local x = 1 elseif false then local x = 2 else local x = 3 end diff --git a/testdata/fixtures/flow/if-scope-isolation/main.lua b/testdata/fixtures/flow/if-scope-isolation/main.lua new file mode 100644 index 00000000..27390e2c --- /dev/null +++ b/testdata/fixtures/flow/if-scope-isolation/main.lua @@ -0,0 +1,4 @@ +if true then + local x = 1 +end +local y: number = x -- expect-error diff --git a/testdata/fixtures/flow/if-simple/main.lua b/testdata/fixtures/flow/if-simple/main.lua new file mode 100644 index 00000000..3a2642fd --- /dev/null +++ b/testdata/fixtures/flow/if-simple/main.lua @@ -0,0 +1 @@ +if true then end diff --git a/testdata/fixtures/flow/return-correct-type/main.lua b/testdata/fixtures/flow/return-correct-type/main.lua new file mode 100644 index 00000000..80457fa2 --- /dev/null +++ b/testdata/fixtures/flow/return-correct-type/main.lua @@ -0,0 +1,3 @@ +local function f(): number + return 42 +end diff --git a/testdata/fixtures/flow/return-early-in-if/main.lua b/testdata/fixtures/flow/return-early-in-if/main.lua new file mode 100644 index 00000000..459f4906 --- /dev/null +++ b/testdata/fixtures/flow/return-early-in-if/main.lua @@ -0,0 +1,6 @@ +local function f(x: number): number + if x < 0 then + return 0 + end + return x +end diff --git a/testdata/fixtures/flow/return-multiple-values/main.lua b/testdata/fixtures/flow/return-multiple-values/main.lua new file mode 100644 index 00000000..fb9472db --- /dev/null +++ b/testdata/fixtures/flow/return-multiple-values/main.lua @@ -0,0 +1,4 @@ +local function f(): (number, string) + return 1, "ok" +end +local a: number, b: string = f() diff --git a/testdata/fixtures/flow/return-wrong-type/main.lua b/testdata/fixtures/flow/return-wrong-type/main.lua new file mode 100644 index 00000000..014bdbe5 --- /dev/null +++ b/testdata/fixtures/flow/return-wrong-type/main.lua @@ -0,0 +1,3 @@ +local function f(): number + return "wrong" -- expect-error +end diff --git a/testdata/fixtures/functions/call-correct-types/main.lua b/testdata/fixtures/functions/call-correct-types/main.lua new file mode 100644 index 00000000..03e22ed5 --- /dev/null +++ b/testdata/fixtures/functions/call-correct-types/main.lua @@ -0,0 +1,4 @@ +local function add(a: number, b: number): number + return a + b +end +local x: number = add(1, 2) diff --git a/testdata/fixtures/functions/call-non-callable/main.lua b/testdata/fixtures/functions/call-non-callable/main.lua new file mode 100644 index 00000000..212c1879 --- /dev/null +++ b/testdata/fixtures/functions/call-non-callable/main.lua @@ -0,0 +1,2 @@ +local x: number = 42 +local y = x() -- expect-error diff --git a/testdata/fixtures/functions/call-result-wrong-type/main.lua b/testdata/fixtures/functions/call-result-wrong-type/main.lua new file mode 100644 index 00000000..c527a554 --- /dev/null +++ b/testdata/fixtures/functions/call-result-wrong-type/main.lua @@ -0,0 +1,4 @@ +local function add(a: number, b: number): number + return a + b +end +local x: string = add(1, 2) -- expect-error diff --git a/testdata/fixtures/functions/call-statement-correct/main.lua b/testdata/fixtures/functions/call-statement-correct/main.lua new file mode 100644 index 00000000..925e5995 --- /dev/null +++ b/testdata/fixtures/functions/call-statement-correct/main.lua @@ -0,0 +1,2 @@ +local function log(msg: string) end +log("hello") diff --git a/testdata/fixtures/functions/call-statement-wrong-type/main.lua b/testdata/fixtures/functions/call-statement-wrong-type/main.lua new file mode 100644 index 00000000..b1696c25 --- /dev/null +++ b/testdata/fixtures/functions/call-statement-wrong-type/main.lua @@ -0,0 +1,2 @@ +local function log(msg: string) end +log(123) -- expect-error diff --git a/testdata/fixtures/functions/call-too-few-arguments/main.lua b/testdata/fixtures/functions/call-too-few-arguments/main.lua new file mode 100644 index 00000000..275306b5 --- /dev/null +++ b/testdata/fixtures/functions/call-too-few-arguments/main.lua @@ -0,0 +1,4 @@ +local function add(a: number, b: number): number + return a + b +end +local x = add(1) -- expect-error diff --git a/testdata/fixtures/functions/call-wrong-argument/main.lua b/testdata/fixtures/functions/call-wrong-argument/main.lua new file mode 100644 index 00000000..8ff114c0 --- /dev/null +++ b/testdata/fixtures/functions/call-wrong-argument/main.lua @@ -0,0 +1,4 @@ +local function add(a: number, b: number): number + return a + b +end +local x = add(1, "wrong") -- expect-error diff --git a/testdata/fixtures/functions/closure-captures-outer/main.lua b/testdata/fixtures/functions/closure-captures-outer/main.lua new file mode 100644 index 00000000..f0b3ddfe --- /dev/null +++ b/testdata/fixtures/functions/closure-captures-outer/main.lua @@ -0,0 +1,5 @@ +local x: number = 10 +local function inner(): number + return x +end +local y: number = inner() diff --git a/testdata/fixtures/functions/closure-modifies-outer/main.lua b/testdata/fixtures/functions/closure-modifies-outer/main.lua new file mode 100644 index 00000000..d15fee79 --- /dev/null +++ b/testdata/fixtures/functions/closure-modifies-outer/main.lua @@ -0,0 +1,6 @@ +local x: number = 0 +local function increment() + x = x + 1 +end +increment() +local y: number = x diff --git a/testdata/fixtures/functions/explicit-nil-check-optional/main.lua b/testdata/fixtures/functions/explicit-nil-check-optional/main.lua new file mode 100644 index 00000000..6e56eeab --- /dev/null +++ b/testdata/fixtures/functions/explicit-nil-check-optional/main.lua @@ -0,0 +1,6 @@ +local function process(data, callback) + if callback ~= nil then + callback(data) + end +end +process("test") diff --git a/testdata/fixtures/functions/function-with-parameters/main.lua b/testdata/fixtures/functions/function-with-parameters/main.lua new file mode 100644 index 00000000..22e03bfb --- /dev/null +++ b/testdata/fixtures/functions/function-with-parameters/main.lua @@ -0,0 +1 @@ +function f(x: number, y: string) end diff --git a/testdata/fixtures/functions/function-with-return-type/main.lua b/testdata/fixtures/functions/function-with-return-type/main.lua new file mode 100644 index 00000000..2b05c663 --- /dev/null +++ b/testdata/fixtures/functions/function-with-return-type/main.lua @@ -0,0 +1 @@ +function f(): number return 1 end diff --git a/testdata/fixtures/functions/generic-function-identity/main.lua b/testdata/fixtures/functions/generic-function-identity/main.lua new file mode 100644 index 00000000..8a8c321f --- /dev/null +++ b/testdata/fixtures/functions/generic-function-identity/main.lua @@ -0,0 +1,5 @@ +local function identity(x: T): T + return x +end +local n: number = identity(42) +local s: string = identity("hello") diff --git a/testdata/fixtures/functions/local-function/main.lua b/testdata/fixtures/functions/local-function/main.lua new file mode 100644 index 00000000..f73be2d1 --- /dev/null +++ b/testdata/fixtures/functions/local-function/main.lua @@ -0,0 +1 @@ +local function f() end diff --git a/testdata/fixtures/functions/method-call-with-params/main.lua b/testdata/fixtures/functions/method-call-with-params/main.lua new file mode 100644 index 00000000..b018883f --- /dev/null +++ b/testdata/fixtures/functions/method-call-with-params/main.lua @@ -0,0 +1,12 @@ +type Adder = { + value: number, + add: (self: Adder, n: number) -> number +} +local a: Adder = { + value = 0, + add = function(self: Adder, n: number): number + self.value = self.value + n + return self.value + end +} +local r: number = a:add(5) diff --git a/testdata/fixtures/functions/method-call-with-self/main.lua b/testdata/fixtures/functions/method-call-with-self/main.lua new file mode 100644 index 00000000..c4064af3 --- /dev/null +++ b/testdata/fixtures/functions/method-call-with-self/main.lua @@ -0,0 +1,12 @@ +type Counter = { + count: number, + increment: (self: Counter) -> number +} +local c: Counter = { + count = 0, + increment = function(self: Counter): number + self.count = self.count + 1 + return self.count + end +} +local n: number = c:increment() diff --git a/testdata/fixtures/functions/multiple-or-defaults/main.lua b/testdata/fixtures/functions/multiple-or-defaults/main.lua new file mode 100644 index 00000000..cc9523d3 --- /dev/null +++ b/testdata/fixtures/functions/multiple-or-defaults/main.lua @@ -0,0 +1,8 @@ +local function format(value, prefix, suffix) + local p = prefix or "[" + local s = suffix or "]" + return p .. tostring(value) .. s +end +format(42) +format(42, "<") +format(42, "<", ">") diff --git a/testdata/fixtures/functions/multiple-returns/main.lua b/testdata/fixtures/functions/multiple-returns/main.lua new file mode 100644 index 00000000..17429a82 --- /dev/null +++ b/testdata/fixtures/functions/multiple-returns/main.lua @@ -0,0 +1 @@ +function f(): (number, string) return 1, "a" end diff --git a/testdata/fixtures/functions/or-default-optional/main.lua b/testdata/fixtures/functions/or-default-optional/main.lua new file mode 100644 index 00000000..eea4b859 --- /dev/null +++ b/testdata/fixtures/functions/or-default-optional/main.lua @@ -0,0 +1,5 @@ +local function greet(name, greeting) + local msg = greeting or "Hello" + return msg .. ", " .. name +end +greet("World") diff --git a/testdata/fixtures/functions/return-call-wrong-arg/main.lua b/testdata/fixtures/functions/return-call-wrong-arg/main.lua new file mode 100644 index 00000000..69b9e4a8 --- /dev/null +++ b/testdata/fixtures/functions/return-call-wrong-arg/main.lua @@ -0,0 +1,7 @@ +local function add(a: number, b: number): number + return a + b +end +local function f(): number + return add("bad", 2) -- expect-error +end +local x = f() diff --git a/testdata/fixtures/functions/simple-function/main.lua b/testdata/fixtures/functions/simple-function/main.lua new file mode 100644 index 00000000..d17fb12b --- /dev/null +++ b/testdata/fixtures/functions/simple-function/main.lua @@ -0,0 +1 @@ +function f() end diff --git a/testdata/fixtures/functions/typed-optional-param/main.lua b/testdata/fixtures/functions/typed-optional-param/main.lua new file mode 100644 index 00000000..3dcc9c9c --- /dev/null +++ b/testdata/fixtures/functions/typed-optional-param/main.lua @@ -0,0 +1,6 @@ +local function log(msg: string, level: string?) + local lvl = level or "INFO" + print(lvl .. ": " .. msg) +end +log("hello") +log("hello", "DEBUG") diff --git a/testdata/fixtures/functions/variadic-correct/main.lua b/testdata/fixtures/functions/variadic-correct/main.lua new file mode 100644 index 00000000..78c2dafd --- /dev/null +++ b/testdata/fixtures/functions/variadic-correct/main.lua @@ -0,0 +1,4 @@ +local function sum(...: number): number + return 0 +end +local x: number = sum(1, 2, 3, 4, 5) diff --git a/testdata/fixtures/functions/variadic-function/main.lua b/testdata/fixtures/functions/variadic-function/main.lua new file mode 100644 index 00000000..570d6fd6 --- /dev/null +++ b/testdata/fixtures/functions/variadic-function/main.lua @@ -0,0 +1 @@ +function f(...: number) end diff --git a/testdata/fixtures/functions/variadic-wrong-type/main.lua b/testdata/fixtures/functions/variadic-wrong-type/main.lua new file mode 100644 index 00000000..6bee9915 --- /dev/null +++ b/testdata/fixtures/functions/variadic-wrong-type/main.lua @@ -0,0 +1,4 @@ +local function sum(...: number): number + return 0 +end +local x = sum(1, 2, "three") -- expect-error diff --git a/testdata/fixtures/functions/wrong-return-type/main.lua b/testdata/fixtures/functions/wrong-return-type/main.lua new file mode 100644 index 00000000..219fd1e2 --- /dev/null +++ b/testdata/fixtures/functions/wrong-return-type/main.lua @@ -0,0 +1 @@ +function f(): number return "hello" end -- expect-error diff --git a/testdata/fixtures/generics/bounded-array-in-bounds/main.lua b/testdata/fixtures/generics/bounded-array-in-bounds/main.lua new file mode 100644 index 00000000..c5d81e1f --- /dev/null +++ b/testdata/fixtures/generics/bounded-array-in-bounds/main.lua @@ -0,0 +1,2 @@ +local arr = {1, 2, 3} +local n: number = arr[1] diff --git a/testdata/fixtures/generics/bounded-array-last-element/main.lua b/testdata/fixtures/generics/bounded-array-last-element/main.lua new file mode 100644 index 00000000..361078ee --- /dev/null +++ b/testdata/fixtures/generics/bounded-array-last-element/main.lua @@ -0,0 +1,2 @@ +local arr = {1, 2, 3} +local n: number = arr[3] diff --git a/testdata/fixtures/generics/bounded-array-out-of-bounds-fail/main.lua b/testdata/fixtures/generics/bounded-array-out-of-bounds-fail/main.lua new file mode 100644 index 00000000..af4163fa --- /dev/null +++ b/testdata/fixtures/generics/bounded-array-out-of-bounds-fail/main.lua @@ -0,0 +1,2 @@ +local arr = {1, 2, 3} +local n: number = arr[4] -- expect-error diff --git a/testdata/fixtures/generics/bounded-array-out-of-bounds-nil/main.lua b/testdata/fixtures/generics/bounded-array-out-of-bounds-nil/main.lua new file mode 100644 index 00000000..2ec35baf --- /dev/null +++ b/testdata/fixtures/generics/bounded-array-out-of-bounds-nil/main.lua @@ -0,0 +1,2 @@ +local arr = {1, 2, 3} +local n: nil = arr[4] diff --git a/testdata/fixtures/generics/constraint-on-type-param/main.lua b/testdata/fixtures/generics/constraint-on-type-param/main.lua new file mode 100644 index 00000000..344b413f --- /dev/null +++ b/testdata/fixtures/generics/constraint-on-type-param/main.lua @@ -0,0 +1,4 @@ +type Printable = {tostring: (self: Printable) -> string} +local function print_it(x: T): string + return x:tostring() +end diff --git a/testdata/fixtures/generics/constraint-satisfied/main.lua b/testdata/fixtures/generics/constraint-satisfied/main.lua new file mode 100644 index 00000000..a8e2cd20 --- /dev/null +++ b/testdata/fixtures/generics/constraint-satisfied/main.lua @@ -0,0 +1,6 @@ +type HasName = {name: string} +local function wrap(x: T): T + return x +end +local r = wrap({name = "Alice"}) +local s: string = r.name diff --git a/testdata/fixtures/generics/constraint-violation/main.lua b/testdata/fixtures/generics/constraint-violation/main.lua new file mode 100644 index 00000000..8cee3f71 --- /dev/null +++ b/testdata/fixtures/generics/constraint-violation/main.lua @@ -0,0 +1,5 @@ +type HasName = {name: string} +local function wrap(x: T): T + return x +end +local n: number = wrap(42) -- expect-error diff --git a/testdata/fixtures/generics/generic-array-literal-index/main.lua b/testdata/fixtures/generics/generic-array-literal-index/main.lua new file mode 100644 index 00000000..71a064ff --- /dev/null +++ b/testdata/fixtures/generics/generic-array-literal-index/main.lua @@ -0,0 +1,3 @@ +type Array = {[integer]: T} +local arr: Array = {1, 2, 3} +local n: number? = arr[1] diff --git a/testdata/fixtures/generics/generic-array-type/main.lua b/testdata/fixtures/generics/generic-array-type/main.lua new file mode 100644 index 00000000..71a064ff --- /dev/null +++ b/testdata/fixtures/generics/generic-array-type/main.lua @@ -0,0 +1,3 @@ +type Array = {[integer]: T} +local arr: Array = {1, 2, 3} +local n: number? = arr[1] diff --git a/testdata/fixtures/generics/generic-optional-type/main.lua b/testdata/fixtures/generics/generic-optional-type/main.lua new file mode 100644 index 00000000..854ed1e1 --- /dev/null +++ b/testdata/fixtures/generics/generic-optional-type/main.lua @@ -0,0 +1,3 @@ +type Maybe = T | nil +local m: Maybe = 42 +local n: Maybe = nil diff --git a/testdata/fixtures/generics/generic-record-type/main.lua b/testdata/fixtures/generics/generic-record-type/main.lua new file mode 100644 index 00000000..82e39cde --- /dev/null +++ b/testdata/fixtures/generics/generic-record-type/main.lua @@ -0,0 +1,3 @@ +type Box = {value: T} +local b: Box = {value = 42} +local n: number = b.value diff --git a/testdata/fixtures/generics/generic-result-type/main.lua b/testdata/fixtures/generics/generic-result-type/main.lua new file mode 100644 index 00000000..fd2d2712 --- /dev/null +++ b/testdata/fixtures/generics/generic-result-type/main.lua @@ -0,0 +1,2 @@ +type Result = {ok: true, value: T} | {ok: false, error: E} +local r: Result = {ok = true, value = 42} diff --git a/testdata/fixtures/generics/identity-function/main.lua b/testdata/fixtures/generics/identity-function/main.lua new file mode 100644 index 00000000..8a8c321f --- /dev/null +++ b/testdata/fixtures/generics/identity-function/main.lua @@ -0,0 +1,5 @@ +local function identity(x: T): T + return x +end +local n: number = identity(42) +local s: string = identity("hello") diff --git a/testdata/fixtures/generics/identity-wrong-type/main.lua b/testdata/fixtures/generics/identity-wrong-type/main.lua new file mode 100644 index 00000000..dd3a69ea --- /dev/null +++ b/testdata/fixtures/generics/identity-wrong-type/main.lua @@ -0,0 +1,4 @@ +local function identity(x: T): T + return x +end +local n: number = identity("hello") -- expect-error diff --git a/testdata/fixtures/generics/inferred-instantiation/main.lua b/testdata/fixtures/generics/inferred-instantiation/main.lua new file mode 100644 index 00000000..03824953 --- /dev/null +++ b/testdata/fixtures/generics/inferred-instantiation/main.lua @@ -0,0 +1,4 @@ +local function identity(x: T): T + return x +end +local n: number = identity(42) diff --git a/testdata/fixtures/generics/method-returns-type-param/main.lua b/testdata/fixtures/generics/method-returns-type-param/main.lua new file mode 100644 index 00000000..abd3db73 --- /dev/null +++ b/testdata/fixtures/generics/method-returns-type-param/main.lua @@ -0,0 +1,11 @@ +type Container = { + _value: T, + get: (self: Container) -> T +} +local c: Container = { + _value = 42, + get = function(self: Container): number + return self._value + end +} +local n: number = c:get() diff --git a/testdata/fixtures/generics/nested-generic-instantiation/main.lua b/testdata/fixtures/generics/nested-generic-instantiation/main.lua new file mode 100644 index 00000000..81ec10c4 --- /dev/null +++ b/testdata/fixtures/generics/nested-generic-instantiation/main.lua @@ -0,0 +1,4 @@ +type Box = {value: T} +type DoubleBox = Box> +local db: DoubleBox = {value = {value = 42}} +local n: number = db.value.value diff --git a/testdata/fixtures/generics/pair-function/main.lua b/testdata/fixtures/generics/pair-function/main.lua new file mode 100644 index 00000000..1af391db --- /dev/null +++ b/testdata/fixtures/generics/pair-function/main.lua @@ -0,0 +1,6 @@ +local function pair(a: A, b: B): (A, B) + return a, b +end +local n, s = pair(42, "hello") +local x: number = n +local y: string = s diff --git a/testdata/fixtures/generics/swap-function/main.lua b/testdata/fixtures/generics/swap-function/main.lua new file mode 100644 index 00000000..c0b432de --- /dev/null +++ b/testdata/fixtures/generics/swap-function/main.lua @@ -0,0 +1,6 @@ +local function swap(a: A, b: B): (B, A) + return b, a +end +local s, n = swap(42, "hello") +local x: string = s +local y: number = n diff --git a/testdata/fixtures/regression/callback-correct-param-type/main.lua b/testdata/fixtures/regression/callback-correct-param-type/main.lua new file mode 100644 index 00000000..529e9f4b --- /dev/null +++ b/testdata/fixtures/regression/callback-correct-param-type/main.lua @@ -0,0 +1,7 @@ +type User = {name: string, age: number} +local function process_user(u: User, callback: (User) -> nil) + callback(u) +end +process_user({name = "Alice", age = 30}, function(u: User) + local n: string = u.name +end) diff --git a/testdata/fixtures/regression/callback-nested-preserves-types/main.lua b/testdata/fixtures/regression/callback-nested-preserves-types/main.lua new file mode 100644 index 00000000..1b9f1375 --- /dev/null +++ b/testdata/fixtures/regression/callback-nested-preserves-types/main.lua @@ -0,0 +1,6 @@ +local function outer(f: (number) -> number) + return f(10) +end +local result: number = outer(function(x: number): number + return x * 2 +end) diff --git a/testdata/fixtures/regression/callback-preserves-return-type/main.lua b/testdata/fixtures/regression/callback-preserves-return-type/main.lua new file mode 100644 index 00000000..1ab97546 --- /dev/null +++ b/testdata/fixtures/regression/callback-preserves-return-type/main.lua @@ -0,0 +1,6 @@ +local function fetch(url: string, on_done: (string) -> nil) + on_done("response") +end +fetch("http://example.com", function(data: string) + local s: string = data +end) diff --git a/testdata/fixtures/regression/field-access-after-boolean-check/main.lua b/testdata/fixtures/regression/field-access-after-boolean-check/main.lua new file mode 100644 index 00000000..cc8d4325 --- /dev/null +++ b/testdata/fixtures/regression/field-access-after-boolean-check/main.lua @@ -0,0 +1,10 @@ +type Success = {ok: true, value: string} +type Failure = {ok: false, error: string} +type Result = Success | Failure + +local function get_value(r: Result): string + if r.ok then + return r.value + end + return r.error +end diff --git a/testdata/fixtures/regression/generic-instantiate-concrete/main.lua b/testdata/fixtures/regression/generic-instantiate-concrete/main.lua new file mode 100644 index 00000000..bd7057b7 --- /dev/null +++ b/testdata/fixtures/regression/generic-instantiate-concrete/main.lua @@ -0,0 +1,4 @@ +local function first(arr: {T}): T? + return arr[1] +end +local n = first({1, 2, 3}) diff --git a/testdata/fixtures/regression/generic-multiple-type-params/main.lua b/testdata/fixtures/regression/generic-multiple-type-params/main.lua new file mode 100644 index 00000000..47f0d25b --- /dev/null +++ b/testdata/fixtures/regression/generic-multiple-type-params/main.lua @@ -0,0 +1,10 @@ +local function map(arr: {T}, fn: (T) -> U): {U} + local result: {U} = {} + for i, v in ipairs(arr) do + result[i] = fn(v) + end + return result +end +local nums = map({"a", "bb", "ccc"}, function(s: string): number + return #s +end) diff --git a/testdata/fixtures/regression/generic-nested-instantiate/main.lua b/testdata/fixtures/regression/generic-nested-instantiate/main.lua new file mode 100644 index 00000000..4e7763f5 --- /dev/null +++ b/testdata/fixtures/regression/generic-nested-instantiate/main.lua @@ -0,0 +1,4 @@ +type Wrapper = {inner: T} +type DoubleWrap = Wrapper> +local dw: DoubleWrap = {inner = {inner = "hello"}} +local s: string = dw.inner.inner diff --git a/testdata/fixtures/regression/generic-record-with-method/main.lua b/testdata/fixtures/regression/generic-record-with-method/main.lua new file mode 100644 index 00000000..97cc6e81 --- /dev/null +++ b/testdata/fixtures/regression/generic-record-with-method/main.lua @@ -0,0 +1,11 @@ +type Container = { + value: T, + get: (self: Container) -> T +} +local c: Container = { + value = "hello", + get = function(self: Container): string + return self.value + end +} +local s: string = c:get() diff --git a/testdata/fixtures/regression/generic-type-alias-instantiate/main.lua b/testdata/fixtures/regression/generic-type-alias-instantiate/main.lua new file mode 100644 index 00000000..82e39cde --- /dev/null +++ b/testdata/fixtures/regression/generic-type-alias-instantiate/main.lua @@ -0,0 +1,3 @@ +type Box = {value: T} +local b: Box = {value = 42} +local n: number = b.value diff --git a/testdata/fixtures/regression/method-call-after-narrowing/main.lua b/testdata/fixtures/regression/method-call-after-narrowing/main.lua new file mode 100644 index 00000000..91fa5c90 --- /dev/null +++ b/testdata/fixtures/regression/method-call-after-narrowing/main.lua @@ -0,0 +1,10 @@ +type A = {kind: "a", get_a: (self: A) -> string} +type B = {kind: "b", get_b: (self: B) -> number} +type AB = A | B + +local function process(x: AB): string + if x.kind == "a" then + return x:get_a() + end + return tostring(x:get_b()) +end diff --git a/testdata/fixtures/regression/nil-narrow-and-operator/main.lua b/testdata/fixtures/regression/nil-narrow-and-operator/main.lua new file mode 100644 index 00000000..1d4a4134 --- /dev/null +++ b/testdata/fixtures/regression/nil-narrow-and-operator/main.lua @@ -0,0 +1,6 @@ +local function safe_concat(a: string?, b: string): string + if a ~= nil then + return a .. b + end + return b +end diff --git a/testdata/fixtures/regression/nil-narrow-condition/main.lua b/testdata/fixtures/regression/nil-narrow-condition/main.lua new file mode 100644 index 00000000..08edba33 --- /dev/null +++ b/testdata/fixtures/regression/nil-narrow-condition/main.lua @@ -0,0 +1,6 @@ +local function get_length(s: string?): number + if s ~= nil then + return #s + end + return 0 +end diff --git a/testdata/fixtures/regression/nil-narrow-early-return/main.lua b/testdata/fixtures/regression/nil-narrow-early-return/main.lua new file mode 100644 index 00000000..4071cf16 --- /dev/null +++ b/testdata/fixtures/regression/nil-narrow-early-return/main.lua @@ -0,0 +1,6 @@ +local function require_value(x: string?): string + if x == nil then + return "missing" + end + return x +end diff --git a/testdata/fixtures/regression/nil-narrow-record-field/main.lua b/testdata/fixtures/regression/nil-narrow-record-field/main.lua new file mode 100644 index 00000000..dce8491b --- /dev/null +++ b/testdata/fixtures/regression/nil-narrow-record-field/main.lua @@ -0,0 +1,7 @@ +type Config = {name: string, port?: number} +local function get_port(c: Config): number + if c.port ~= nil then + return c.port + end + return 8080 +end diff --git a/testdata/fixtures/regression/nil-narrow-to-non-nil/main.lua b/testdata/fixtures/regression/nil-narrow-to-non-nil/main.lua new file mode 100644 index 00000000..73f99470 --- /dev/null +++ b/testdata/fixtures/regression/nil-narrow-to-non-nil/main.lua @@ -0,0 +1,6 @@ +local function process(x: string?): string + if x ~= nil then + return x + end + return "default" +end diff --git a/testdata/fixtures/regression/type-alias-chain/main.lua b/testdata/fixtures/regression/type-alias-chain/main.lua new file mode 100644 index 00000000..780d344d --- /dev/null +++ b/testdata/fixtures/regression/type-alias-chain/main.lua @@ -0,0 +1,5 @@ +type ID = string +type UserID = ID +type AdminID = UserID +local id: AdminID = "admin-123" +local s: string = id diff --git a/testdata/fixtures/regression/type-alias-equivalence/main.lua b/testdata/fixtures/regression/type-alias-equivalence/main.lua new file mode 100644 index 00000000..69682fe2 --- /dev/null +++ b/testdata/fixtures/regression/type-alias-equivalence/main.lua @@ -0,0 +1,3 @@ +type UserID = string +local id: UserID = "user-123" +local s: string = id diff --git a/testdata/fixtures/regression/type-alias-function-param/main.lua b/testdata/fixtures/regression/type-alias-function-param/main.lua new file mode 100644 index 00000000..cd6a394a --- /dev/null +++ b/testdata/fixtures/regression/type-alias-function-param/main.lua @@ -0,0 +1,5 @@ +type Amount = number +local function process(a: Amount): number + return a * 2 +end +local result = process(100) diff --git a/testdata/fixtures/regression/type-alias-function-return/main.lua b/testdata/fixtures/regression/type-alias-function-return/main.lua new file mode 100644 index 00000000..d1e0b8d8 --- /dev/null +++ b/testdata/fixtures/regression/type-alias-function-return/main.lua @@ -0,0 +1,6 @@ +type Result = {ok: boolean, data: any} +local function fetch(): Result + return {ok = true, data = "hello"} +end +local r = fetch() +local ok: boolean = r.ok diff --git a/testdata/fixtures/regression/type-alias-optional/main.lua b/testdata/fixtures/regression/type-alias-optional/main.lua new file mode 100644 index 00000000..b0cf2cf1 --- /dev/null +++ b/testdata/fixtures/regression/type-alias-optional/main.lua @@ -0,0 +1,3 @@ +type MaybeID = string? +local id: MaybeID = "123" +local id2: MaybeID = nil diff --git a/testdata/fixtures/regression/type-alias-record-field/main.lua b/testdata/fixtures/regression/type-alias-record-field/main.lua new file mode 100644 index 00000000..155ca969 --- /dev/null +++ b/testdata/fixtures/regression/type-alias-record-field/main.lua @@ -0,0 +1,4 @@ +type Name = string +type Person = {name: Name, age: number} +local p: Person = {name = "Alice", age = 30} +local n: string = p.name From f4686c4a938a3cc53f171d2ad757054c0ad68704 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 15:56:29 -0400 Subject: [PATCH 04/10] Retrofit core scenarios, type casts, and union narrowing to fixtures Migrate scenarios_test.go (65 cases) to testdata/fixtures/core/, type_cast_test.go (39 cases) to testdata/fixtures/types/cast-*, and union_narrowing_test.go (12 cases) to testdata/fixtures/narrowing/union-*. Remove original Go test files. Fix harness: expect-error annotation now absorbs all diagnostics on that line, not just the first match (handles multi-error lines). --- compiler/check/tests/core/scenarios_test.go | 651 ------------------ .../tests/narrowing/union_narrowing_test.go | 417 ----------- compiler/check/tests/types/type_cast_test.go | 541 --------------- .../core/annotation-array-inferred/main.lua | 1 + .../core/annotation-array-mismatch/main.lua | 1 + .../annotation-function-declared/main.lua | 1 + .../core/annotation-optional/main.lua | 1 + .../core/annotation-record-inferred/main.lua | 1 + .../fixtures/core/annotation-union/main.lua | 1 + .../fixtures/core/assign-widening/main.lua | 2 + .../core/attr-call-no-recurse/main.lua | 2 + .../fixtures/core/complex-closure/main.lua | 7 + .../core/complex-early-return-guard/main.lua | 6 + .../core/complex-nested-functions/main.lua | 6 + .../fixtures/core/complex-recursive/main.lua | 7 + .../core/complex-table-methods/main.lua | 6 + .../fixtures/core/control-do-block/main.lua | 1 + .../fixtures/core/control-for-ipairs/main.lua | 1 + .../fixtures/core/control-for-loop/main.lua | 1 + .../fixtures/core/control-for-pairs/main.lua | 1 + .../fixtures/core/control-for-step/main.lua | 1 + .../core/control-for-string-init/main.lua | 1 + .../fixtures/core/control-if-else/main.lua | 1 + .../fixtures/core/control-if-simple/main.lua | 1 + .../core/control-repeat-until/main.lua | 1 + .../fixtures/core/control-while-loop/main.lua | 1 + .../core/empty-table-map-assign/main.lua | 3 + .../fixtures/core/expr-arithmetic/main.lua | 1 + .../fixtures/core/expr-array-index/main.lua | 1 + .../fixtures/core/expr-comparison/main.lua | 1 + .../core/expr-concat-first-return/main.lua | 4 + .../fixtures/core/expr-function-call/main.lua | 1 + .../core/expr-length-first-return/main.lua | 4 + testdata/fixtures/core/expr-length/main.lua | 1 + .../fixtures/core/expr-logical-and/main.lua | 1 + .../fixtures/core/expr-logical-or/main.lua | 1 + .../core/expr-method-on-literal/main.lua | 1 + .../fixtures/core/expr-string-concat/main.lua | 1 + .../fixtures/core/expr-table-access/main.lua | 1 + .../fixtures/core/expr-unary-minus/main.lua | 1 + .../fixtures/core/expr-unary-not/main.lua | 1 + .../core/method-param-self-sub/main.lua | 3 + .../fixtures/core/module-local-def/main.lua | 8 + .../core/module-return-pattern/main.lua | 6 + .../fixtures/core/module-with-method/main.lua | 9 + testdata/fixtures/core/narrow-nested/main.lua | 8 + .../core/narrow-nil-else-inline/main.lua | 7 + .../fixtures/core/narrow-nil-then/main.lua | 5 + .../core/narrow-no-check-fails/main.lua | 3 + testdata/fixtures/core/narrow-truthy/main.lua | 5 + .../record-literal-to-intersection/main.lua | 3 + .../core/record-literal-to-record/main.lua | 2 + testdata/fixtures/core/stdlib-ipairs/main.lua | 1 + .../fixtures/core/stdlib-math-floor/main.lua | 1 + testdata/fixtures/core/stdlib-pairs/main.lua | 1 + testdata/fixtures/core/stdlib-print/main.lua | 1 + .../core/stdlib-string-upper/main.lua | 1 + .../core/stdlib-table-insert/main.lua | 1 + .../fixtures/core/stdlib-tonumber/main.lua | 1 + .../fixtures/core/stdlib-tostring/main.lua | 1 + testdata/fixtures/core/stdlib-type/main.lua | 1 + .../core/tonumber-skips-optional/main.lua | 5 + testdata/fixtures/core/type-is-basic/main.lua | 7 + .../core/type-is-direct-condition/main.lua | 7 + .../core/type-is-falsy-excludes/main.lua | 10 + .../fixtures/core/type-is-truthy/main.lua | 10 + .../core/untyped-params-missing-args/main.lua | 4 + .../fixtures/core/untyped-string-ops/main.lua | 7 + .../union-discriminated-literal/main.lua | 9 + .../union-else-remaining-variant/main.lua | 18 + .../narrowing/union-else-wrong-type/main.lua | 18 + .../union-local-assign-narrowed/main.lua | 24 + .../union-method-after-narrowing/main.lua | 33 + .../union-multiple-channels/main.lua | 33 + .../union-negated-condition/main.lua | 21 + .../union-nested-field-access/main.lua | 16 + .../union-no-narrowing-fails/main.lua | 12 + .../union-timeout-check-pattern/main.lua | 30 + .../narrowing/union-truthy-guard/main.lua | 14 + .../main.lua | 9 + .../fixtures/types/cast-and-library/main.lua | 3 + .../types/cast-arithmetic-mixed/main.lua | 4 + .../types/cast-arithmetic-multiple/main.lua | 4 + .../fixtures/types/cast-array-type/main.lua | 3 + .../types/cast-boolean-from-any/main.lua | 5 + .../types/cast-chained-any-fields/main.lua | 3 + .../fixtures/types/cast-custom-type/main.lua | 4 + .../types/cast-from-method-return/main.lua | 8 + .../types/cast-generic-record/main.lua | 4 + .../fixtures/types/cast-in-concat/main.lua | 3 + .../types/cast-int-bool-aliases/main.lua | 2 + .../types/cast-integer-arithmetic/main.lua | 3 + .../types/cast-integer-comparison/main.lua | 3 + .../types/cast-integer-return/main.lua | 4 + .../types/cast-integer-table-field/main.lua | 2 + .../types/cast-integer-to-function/main.lua | 5 + .../types/cast-integer-typed/main.lua | 2 + .../types/cast-multiple-in-statement/main.lua | 2 + .../types/cast-nested-record/main.lua | 6 + .../types/cast-number-arithmetic/main.lua | 3 + .../types/cast-number-to-function/main.lua | 5 + .../fixtures/types/cast-number-typed/main.lua | 2 + .../types/cast-return-integer/main.lua | 4 + .../types/cast-return-number/main.lua | 4 + .../types/cast-string-from-any/main.lua | 3 + .../types/cast-string-lib-works/main.lua | 4 + .../types/cast-table-constructor/main.lua | 5 + .../types/cast-tonumber-base/main.lua | 5 + .../types/cast-tonumber-optional/main.lua | 5 + .../types/cast-tostring-chained/main.lua | 2 + .../types/cast-tostring-concat/main.lua | 2 + .../types/cast-tostring-number/main.lua | 2 + .../types/cast-tostring-typed/main.lua | 2 + .../types/cast-type-is-basic/main.lua | 8 + .../types/cast-type-is-direct/main.lua | 7 + .../types/cast-type-is-falsy-fail/main.lua | 7 + .../types/cast-type-is-field-access/main.lua | 6 + .../types/cast-type-is-not-fail/main.lua | 6 + .../types/cast-type-is-stored/main.lua | 7 + 119 files changed, 589 insertions(+), 1609 deletions(-) delete mode 100644 compiler/check/tests/core/scenarios_test.go delete mode 100644 compiler/check/tests/narrowing/union_narrowing_test.go delete mode 100644 compiler/check/tests/types/type_cast_test.go create mode 100644 testdata/fixtures/core/annotation-array-inferred/main.lua create mode 100644 testdata/fixtures/core/annotation-array-mismatch/main.lua create mode 100644 testdata/fixtures/core/annotation-function-declared/main.lua create mode 100644 testdata/fixtures/core/annotation-optional/main.lua create mode 100644 testdata/fixtures/core/annotation-record-inferred/main.lua create mode 100644 testdata/fixtures/core/annotation-union/main.lua create mode 100644 testdata/fixtures/core/assign-widening/main.lua create mode 100644 testdata/fixtures/core/attr-call-no-recurse/main.lua create mode 100644 testdata/fixtures/core/complex-closure/main.lua create mode 100644 testdata/fixtures/core/complex-early-return-guard/main.lua create mode 100644 testdata/fixtures/core/complex-nested-functions/main.lua create mode 100644 testdata/fixtures/core/complex-recursive/main.lua create mode 100644 testdata/fixtures/core/complex-table-methods/main.lua create mode 100644 testdata/fixtures/core/control-do-block/main.lua create mode 100644 testdata/fixtures/core/control-for-ipairs/main.lua create mode 100644 testdata/fixtures/core/control-for-loop/main.lua create mode 100644 testdata/fixtures/core/control-for-pairs/main.lua create mode 100644 testdata/fixtures/core/control-for-step/main.lua create mode 100644 testdata/fixtures/core/control-for-string-init/main.lua create mode 100644 testdata/fixtures/core/control-if-else/main.lua create mode 100644 testdata/fixtures/core/control-if-simple/main.lua create mode 100644 testdata/fixtures/core/control-repeat-until/main.lua create mode 100644 testdata/fixtures/core/control-while-loop/main.lua create mode 100644 testdata/fixtures/core/empty-table-map-assign/main.lua create mode 100644 testdata/fixtures/core/expr-arithmetic/main.lua create mode 100644 testdata/fixtures/core/expr-array-index/main.lua create mode 100644 testdata/fixtures/core/expr-comparison/main.lua create mode 100644 testdata/fixtures/core/expr-concat-first-return/main.lua create mode 100644 testdata/fixtures/core/expr-function-call/main.lua create mode 100644 testdata/fixtures/core/expr-length-first-return/main.lua create mode 100644 testdata/fixtures/core/expr-length/main.lua create mode 100644 testdata/fixtures/core/expr-logical-and/main.lua create mode 100644 testdata/fixtures/core/expr-logical-or/main.lua create mode 100644 testdata/fixtures/core/expr-method-on-literal/main.lua create mode 100644 testdata/fixtures/core/expr-string-concat/main.lua create mode 100644 testdata/fixtures/core/expr-table-access/main.lua create mode 100644 testdata/fixtures/core/expr-unary-minus/main.lua create mode 100644 testdata/fixtures/core/expr-unary-not/main.lua create mode 100644 testdata/fixtures/core/method-param-self-sub/main.lua create mode 100644 testdata/fixtures/core/module-local-def/main.lua create mode 100644 testdata/fixtures/core/module-return-pattern/main.lua create mode 100644 testdata/fixtures/core/module-with-method/main.lua create mode 100644 testdata/fixtures/core/narrow-nested/main.lua create mode 100644 testdata/fixtures/core/narrow-nil-else-inline/main.lua create mode 100644 testdata/fixtures/core/narrow-nil-then/main.lua create mode 100644 testdata/fixtures/core/narrow-no-check-fails/main.lua create mode 100644 testdata/fixtures/core/narrow-truthy/main.lua create mode 100644 testdata/fixtures/core/record-literal-to-intersection/main.lua create mode 100644 testdata/fixtures/core/record-literal-to-record/main.lua create mode 100644 testdata/fixtures/core/stdlib-ipairs/main.lua create mode 100644 testdata/fixtures/core/stdlib-math-floor/main.lua create mode 100644 testdata/fixtures/core/stdlib-pairs/main.lua create mode 100644 testdata/fixtures/core/stdlib-print/main.lua create mode 100644 testdata/fixtures/core/stdlib-string-upper/main.lua create mode 100644 testdata/fixtures/core/stdlib-table-insert/main.lua create mode 100644 testdata/fixtures/core/stdlib-tonumber/main.lua create mode 100644 testdata/fixtures/core/stdlib-tostring/main.lua create mode 100644 testdata/fixtures/core/stdlib-type/main.lua create mode 100644 testdata/fixtures/core/tonumber-skips-optional/main.lua create mode 100644 testdata/fixtures/core/type-is-basic/main.lua create mode 100644 testdata/fixtures/core/type-is-direct-condition/main.lua create mode 100644 testdata/fixtures/core/type-is-falsy-excludes/main.lua create mode 100644 testdata/fixtures/core/type-is-truthy/main.lua create mode 100644 testdata/fixtures/core/untyped-params-missing-args/main.lua create mode 100644 testdata/fixtures/core/untyped-string-ops/main.lua create mode 100644 testdata/fixtures/narrowing/union-discriminated-literal/main.lua create mode 100644 testdata/fixtures/narrowing/union-else-remaining-variant/main.lua create mode 100644 testdata/fixtures/narrowing/union-else-wrong-type/main.lua create mode 100644 testdata/fixtures/narrowing/union-local-assign-narrowed/main.lua create mode 100644 testdata/fixtures/narrowing/union-method-after-narrowing/main.lua create mode 100644 testdata/fixtures/narrowing/union-multiple-channels/main.lua create mode 100644 testdata/fixtures/narrowing/union-negated-condition/main.lua create mode 100644 testdata/fixtures/narrowing/union-nested-field-access/main.lua create mode 100644 testdata/fixtures/narrowing/union-no-narrowing-fails/main.lua create mode 100644 testdata/fixtures/narrowing/union-timeout-check-pattern/main.lua create mode 100644 testdata/fixtures/narrowing/union-truthy-guard/main.lua create mode 100644 testdata/fixtures/narrowing/union-wrong-field-after-narrowing/main.lua create mode 100644 testdata/fixtures/types/cast-and-library/main.lua create mode 100644 testdata/fixtures/types/cast-arithmetic-mixed/main.lua create mode 100644 testdata/fixtures/types/cast-arithmetic-multiple/main.lua create mode 100644 testdata/fixtures/types/cast-array-type/main.lua create mode 100644 testdata/fixtures/types/cast-boolean-from-any/main.lua create mode 100644 testdata/fixtures/types/cast-chained-any-fields/main.lua create mode 100644 testdata/fixtures/types/cast-custom-type/main.lua create mode 100644 testdata/fixtures/types/cast-from-method-return/main.lua create mode 100644 testdata/fixtures/types/cast-generic-record/main.lua create mode 100644 testdata/fixtures/types/cast-in-concat/main.lua create mode 100644 testdata/fixtures/types/cast-int-bool-aliases/main.lua create mode 100644 testdata/fixtures/types/cast-integer-arithmetic/main.lua create mode 100644 testdata/fixtures/types/cast-integer-comparison/main.lua create mode 100644 testdata/fixtures/types/cast-integer-return/main.lua create mode 100644 testdata/fixtures/types/cast-integer-table-field/main.lua create mode 100644 testdata/fixtures/types/cast-integer-to-function/main.lua create mode 100644 testdata/fixtures/types/cast-integer-typed/main.lua create mode 100644 testdata/fixtures/types/cast-multiple-in-statement/main.lua create mode 100644 testdata/fixtures/types/cast-nested-record/main.lua create mode 100644 testdata/fixtures/types/cast-number-arithmetic/main.lua create mode 100644 testdata/fixtures/types/cast-number-to-function/main.lua create mode 100644 testdata/fixtures/types/cast-number-typed/main.lua create mode 100644 testdata/fixtures/types/cast-return-integer/main.lua create mode 100644 testdata/fixtures/types/cast-return-number/main.lua create mode 100644 testdata/fixtures/types/cast-string-from-any/main.lua create mode 100644 testdata/fixtures/types/cast-string-lib-works/main.lua create mode 100644 testdata/fixtures/types/cast-table-constructor/main.lua create mode 100644 testdata/fixtures/types/cast-tonumber-base/main.lua create mode 100644 testdata/fixtures/types/cast-tonumber-optional/main.lua create mode 100644 testdata/fixtures/types/cast-tostring-chained/main.lua create mode 100644 testdata/fixtures/types/cast-tostring-concat/main.lua create mode 100644 testdata/fixtures/types/cast-tostring-number/main.lua create mode 100644 testdata/fixtures/types/cast-tostring-typed/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-basic/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-direct/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-falsy-fail/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-field-access/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-not-fail/main.lua create mode 100644 testdata/fixtures/types/cast-type-is-stored/main.lua diff --git a/compiler/check/tests/core/scenarios_test.go b/compiler/check/tests/core/scenarios_test.go deleted file mode 100644 index 7271f208..00000000 --- a/compiler/check/tests/core/scenarios_test.go +++ /dev/null @@ -1,651 +0,0 @@ -package core - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestCoreScenarios tests various core type checking scenarios. -func TestCoreScenarios(t *testing.T) { - tests := []testutil.Case{ - { - Name: "assign widening allows mixed types", - Code: ` - local x = 1 - x = "ok" - `, - WantError: false, - Stdlib: true, - }, - { - Name: "method param self substitution", - Code: ` - type T = { eq: (self: T, other: T) -> boolean } - local t: T = { eq = function(self, other) return self == other end } - local ok = t:eq(t) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "empty table allows map assignments", - Code: ` - local t = {} - t["a"] = 1 - t["b"] = true - `, - WantError: false, - Stdlib: true, - }, - { - Name: "untyped params allow missing args", - Code: ` - local function eq(actual, expected, msg) - return actual == expected - end - eq(1, 1) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "tonumber skips optional error return", - Code: ` - type Request = { query: (self: Request, key: string) -> (string?, Error?) } - local function handler(req: Request) - local code = tonumber(req:query("code")) or 200 - return code - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "record literal assignable to record", - Code: ` - local person: {name: string, age: number} = { name = "Alice", age = 30 } - return person - `, - WantError: false, - Stdlib: true, - }, - { - Name: "record literal assignable to intersection", - Code: ` - type Person = {name: string} & {age: number} - local p: Person = { name = "Alice", age = 30 } - return p - `, - WantError: false, - Stdlib: true, - }, - { - Name: "attr call does not recurse", - Code: ` - local t = { print = function(msg) return msg end } - t.print("hi") - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestControlFlow tests control flow statements. -func TestControlFlow(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple if", - Code: `if true then end`, - WantError: false, - Stdlib: true, - }, - { - Name: "if else", - Code: `if true then local x = 1 else local x = 2 end`, - WantError: false, - Stdlib: true, - }, - { - Name: "while loop", - Code: `while true do break end`, - WantError: false, - Stdlib: true, - }, - { - Name: "for loop", - Code: `for i = 1, 10 do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "for loop with step", - Code: `for i = 1, 10, 2 do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "for loop string init", - Code: `for i = "a", 10 do end`, - WantError: true, - Stdlib: true, - }, - { - Name: "generic for with pairs", - Code: `for k, v in pairs({}) do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "generic for with ipairs", - Code: `for i, v in ipairs({}) do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "repeat until", - Code: `repeat local x = 1 until true`, - WantError: false, - Stdlib: true, - }, - { - Name: "do block", - Code: `do local x = 1 end`, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestExpressions tests expression evaluation. -func TestExpressions(t *testing.T) { - tests := []testutil.Case{ - { - Name: "arithmetic", - Code: `local x = 1 + 2 * 3`, - WantError: false, - Stdlib: true, - }, - { - Name: "string concat", - Code: `local s = "hello" .. " world"`, - WantError: false, - Stdlib: true, - }, - { - Name: "comparison", - Code: `local b = 1 < 2`, - WantError: false, - Stdlib: true, - }, - { - Name: "logical and", - Code: `local x = true and false`, - WantError: false, - Stdlib: true, - }, - { - Name: "logical or", - Code: `local x = nil or 1`, - WantError: false, - Stdlib: true, - }, - { - Name: "unary not", - Code: `local b = not true`, - WantError: false, - Stdlib: true, - }, - { - Name: "unary minus", - Code: `local x = -42`, - WantError: false, - Stdlib: true, - }, - { - Name: "length operator", - Code: `local n = #"hello"`, - WantError: false, - Stdlib: true, - }, - { - Name: "concat uses first return value", - Code: ` - local function f() - return "a", 1 - end - local s = f() .. "b" - `, - WantError: false, - Stdlib: true, - }, - { - Name: "length uses first return value", - Code: ` - local function f() - return {1, 2, 3}, 10 - end - local n = #f() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "function call", - Code: `print("hello")`, - WantError: false, - Stdlib: true, - }, - { - Name: "method call on literal", - Code: `local s = ("hello"):upper()`, - WantError: false, - Stdlib: true, - }, - { - Name: "table access", - Code: `local t = {x = 1}; local v = t.x`, - WantError: false, - Stdlib: true, - }, - { - Name: "array index", - Code: `local a = {1, 2, 3}; local v = a[1]`, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestTypeAnnotations tests type annotation scenarios. -func TestTypeAnnotations(t *testing.T) { - tests := []testutil.Case{ - { - Name: "optional type", - Code: `local x: number? = nil`, - WantError: false, - Stdlib: true, - }, - { - Name: "union type", - Code: `local x: number | string = 1`, - WantError: false, - Stdlib: true, - }, - { - Name: "array type inferred", - Code: `local arr = {1, 2, 3}`, - WantError: false, - Stdlib: true, - }, - { - Name: "record type inferred", - Code: `local r = {x = 1, y = "a"}`, - WantError: false, - Stdlib: true, - }, - { - Name: "function type declared", - Code: `local f: (number, string) -> boolean = function(a: number, b: string): boolean return true end`, - WantError: false, - Stdlib: true, - }, - { - Name: "array type mismatch", - Code: `local arr: {string} = {1, 2, 3}`, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestStdlibUsage tests stdlib function usage. -func TestStdlibUsage(t *testing.T) { - tests := []testutil.Case{ - { - Name: "print", - Code: `print("hello")`, - WantError: false, - Stdlib: true, - }, - { - Name: "type function", - Code: `local t = type(42)`, - WantError: false, - Stdlib: true, - }, - { - Name: "pairs", - Code: `for k, v in pairs({a = 1}) do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "ipairs", - Code: `for i, v in ipairs({1, 2, 3}) do end`, - WantError: false, - Stdlib: true, - }, - { - Name: "math.floor", - Code: `local x = math.floor(3.5)`, - WantError: false, - Stdlib: true, - }, - { - Name: "string.upper", - Code: `local s = string.upper("hello")`, - WantError: false, - Stdlib: true, - }, - { - Name: "table.insert", - Code: `local t = {}; table.insert(t, 1)`, - WantError: false, - Stdlib: true, - }, - { - Name: "tostring", - Code: `local s = tostring(42)`, - WantError: false, - Stdlib: true, - }, - { - Name: "tonumber", - Code: `local n = tonumber("42")`, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestBasicNarrowing tests basic narrowing scenarios. -func TestBasicNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "nil check then branch", - Code: ` - function f(x: string?) - if x ~= nil then - local s: string = x - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nil check else branch inline", - Code: ` - function f(x: string?) - if x == nil then - return - else - local s: string = x - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "truthy check", - Code: ` - function f(x: string?) - if x then - local s: string = x - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "no narrowing without check", - Code: ` - function f(x: string?) - local s: string = x - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "nested narrowing", - Code: ` - function f(x: string?, y: number?) - if x ~= nil then - if y ~= nil then - local s: string = x - local n: number = y - end - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestTypeIsNarrowing tests Type:is narrowing. -func TestTypeIsNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "Type:is basic pattern", - Code: ` - type Point = {x: number, y: number} - function validate(data: any) - local _, err = Point:is(data) - if err == nil then - local p: {x: number, y: number} = data - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is truthy check", - Code: ` - type Point = {x: number, y: number} - local function isPoint(x) - return Point:is(x) - end - function validate(data: any) - local val, err = isPoint(data) - if err == nil and val ~= nil then - local p: {x: number, y: number} = val - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is wrapper falsy excludes", - Code: ` - type Point = {x: number, y: number} - local function isPoint(x) - return Point:is(x) - end - function validate(data: any) - local val, err = isPoint(data) - if err ~= nil then - local p: {x: number, y: number} = val - end - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "Type:is direct condition narrows", - Code: ` - type Point = {x: number, y: number} - function validate(data: any) - local _, err = Point:is(data) - if err == nil then - local p: {x: number, y: number} = data - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestComplexScenarios tests complex code scenarios. -func TestComplexScenarios(t *testing.T) { - tests := []testutil.Case{ - { - Name: "recursive function", - Code: ` - function factorial(n: number): number - if n <= 1 then - return 1 - else - return n * factorial(n - 1) - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested functions", - Code: ` - function outer(x: number): number - local function inner(y: number): number - return y * 2 - end - return inner(x) + 1 - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "closure", - Code: ` - function counter(): () -> number - local count = 0 - return function(): number - count = count + 1 - return count - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "table with methods", - Code: ` - local obj = { - value = 0, - get = function(self): number - return self.value - end - } - `, - WantError: false, - Stdlib: true, - }, - { - Name: "early return guard", - Code: ` - function process(x: number?): number - if x == nil then - return 0 - end - return x * 2 - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestModulePatterns tests module definition patterns. -func TestModulePatterns(t *testing.T) { - tests := []testutil.Case{ - { - Name: "local module definition", - Code: ` - local M = {} - function M.add(a: number, b: number): number - return a + b - end - function M.sub(a: number, b: number): number - return a - b - end - local result: number = M.add(1, 2) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "module with method", - Code: ` - local Counter = {count = 0} - function Counter:increment() - self.count = self.count + 1 - end - function Counter:get(): number - return self.count - end - Counter:increment() - local n: number = Counter:get() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "module return pattern", - Code: ` - local M = {} - M.version = "1.0" - function M.init() - print("initialized") - end - return M - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestUntypedStringOps tests untyped parameter string operations. -func TestUntypedStringOps(t *testing.T) { - tests := []testutil.Case{ - { - Name: "concat and length on untyped params", - Code: ` - local function green(s) return "\027[32m" .. s .. "\027[0m" end - local function greet(name) - if name and #name > 0 then - return "Hello, " .. name - end - return green("stranger") - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/narrowing/union_narrowing_test.go b/compiler/check/tests/narrowing/union_narrowing_test.go deleted file mode 100644 index f8005632..00000000 --- a/compiler/check/tests/narrowing/union_narrowing_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package narrowing - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestUnionNarrowing_NestedFieldAccess tests that after narrowing a union, -// nested field access uses the narrowed type's field types. -func TestUnionNarrowing_NestedFieldAccess(t *testing.T) { - source := ` - type ChanInt = {__tag: "int"} - type ChanStr = {__tag: "str"} - type SelResult = - {channel: ChanInt, value: {error: string}, ok: boolean} | - {channel: ChanStr, value: {data: number}, ok: boolean} - - function get_result(a: ChanInt, b: ChanStr): SelResult - return {channel = a, value = {error = "oops"}, ok = true} - end - - function f(ch1: ChanInt, ch2: ChanStr) - local result = get_result(ch1, ch2) - if result.channel == ch1 then - local e: string = result.value.error - end - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error: %s", d.Message) - } - t.Errorf("expected no errors after union narrowing") - } -} - -// TestUnionNarrowing_FieldEquality tests narrowing by field value equality (discriminated unions). -func TestUnionNarrowing_FieldEquality(t *testing.T) { - tests := []testutil.Case{ - { - Name: "discriminated union by literal", - Code: ` - type A = {kind: "a", value_a: string} - type B = {kind: "b", value_b: number} - type AB = A | B - - function f(x: AB) - if x.kind == "a" then - local v: string = x.value_a - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrong field access after narrowing should fail", - Code: ` - type A = {kind: "a", value_a: string} - type B = {kind: "b", value_b: number} - type AB = A | B - - function f(x: AB) - if x.kind == "a" then - local v: number = x.value_b - end - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestUnionNarrowing_LocalAssignFromNarrowed tests that assigning a field -// from a narrowed union variable gets the narrowed type. -func TestUnionNarrowing_LocalAssignFromNarrowed(t *testing.T) { - source := ` - type EventCh = {__tag: "event"} - type TimeoutCh = {__tag: "timeout"} - type Event = {kind: string, error: string?} - type Time = {sec: number} - - type Result = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimeoutCh, value: Time, ok: boolean} - - function get_result(ch: EventCh, timeout: TimeoutCh): Result - return {channel = ch, value = {kind = "exit", error = nil}, ok = true} - end - - function f(events_ch: EventCh, timeout_ch: TimeoutCh) - local result = get_result(events_ch, timeout_ch) - if result.channel ~= events_ch then - return false, "timeout" - end - -- After the guard, result is narrowed to {channel: EventCh, value: Event, ok: boolean} - local event = result.value - -- event should be Event, not Event | Time - local k: string = event.kind - if event.error then - local e: string = event.error - end - return true - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error at line %d: %s", d.Position.Line, d.Message) - } - t.Errorf("expected no errors after channel comparison narrowing") - } -} - -// TestUnionNarrowing_NegatedCondition tests narrowing with ~= (not equals). -func TestUnionNarrowing_NegatedCondition(t *testing.T) { - source := ` - type EventCh = {__tag: "event"} - type TimeoutCh = {__tag: "timeout"} - type Event = {kind: string} - type Time = {sec: number} - - type Result = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimeoutCh, value: Time, ok: boolean} - - function get_result(ch: EventCh, timeout: TimeoutCh): Result - return {channel = ch, value = {kind = "exit"}, ok = true} - end - - function f(events_ch: EventCh, timeout_ch: TimeoutCh) - local result = get_result(events_ch, timeout_ch) - -- Early return on NOT matching events_ch - if result.channel ~= events_ch then - -- Inside here, result is narrowed to TimeoutCh variant - local t: Time = result.value - return false - end - -- After the if, result is narrowed to EventCh variant - local event: Event = result.value - return true - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error at line %d: %s", d.Position.Line, d.Message) - } - t.Errorf("expected no errors with negated condition narrowing") - } -} - -// TestUnionNarrowing_MultipleChannels tests narrowing with 3+ variants. -func TestUnionNarrowing_MultipleChannels(t *testing.T) { - source := ` - type Message = {_topic: string} - type Event = {kind: string} - type Timer = {elapsed: number} - - type MsgCh = {__tag: "msg"} - type EventCh = {__tag: "event"} - type TimerCh = {__tag: "timer"} - - type Result = {channel: MsgCh, value: Message, ok: boolean} | - {channel: EventCh, value: Event, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function do_select(m: MsgCh, e: EventCh, t: TimerCh): Result - return {channel = m, value = {_topic = "test"}, ok = true} - end - - function f(msg_ch: MsgCh, events_ch: EventCh, timeout: TimerCh) - local result = do_select(msg_ch, events_ch, timeout) - - if result.channel == timeout then - return nil, "timeout" - end - - if result.channel == events_ch then - local event = result.value - local k: string = event.kind - return "event", k - end - - -- Must be msg_ch - local msg = result.value - local topic: string = msg._topic - return "message", topic - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error at line %d: %s", d.Position.Line, d.Message) - } - t.Errorf("expected no errors with multi-variant narrowing") - } -} - -// TestUnionNarrowing_TruthyGuard tests narrowing union to members that have a truthy field. -func TestUnionNarrowing_TruthyGuard(t *testing.T) { - tests := []testutil.Case{ - { - Name: "truthy field narrows to variant with that field", - Code: ` - type Event = {kind: string, error: string?} - type Timer = {elapsed: number} - type SelectResult = Event | Timer - - function get_result(): SelectResult - return {kind = "exit", error = nil} - end - - function f() - local result = get_result() - -- result.kind only exists on Event, should narrow to Event - if result.kind then - local k: string = result.kind - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestUnionNarrowing_MethodCallAfterNarrowing tests that method calls work -// on types with methods after narrowing from a union. -func TestUnionNarrowing_MethodCallAfterNarrowing(t *testing.T) { - source := ` - type Message = { - _topic: string, - topic: (self: Message) -> string - } - - type Timer = {elapsed: number} - - type MsgCh = {__tag: "msg"} - type TimerCh = {__tag: "timer"} - - type Result = {channel: MsgCh, value: Message, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function select_fn(msg_ch: MsgCh, timer_ch: TimerCh): Result - return { - channel = msg_ch, - value = { - _topic = "test", - topic = function(s: Message): string return s._topic end - }, - ok = true - } - end - - function f(msg_ch: MsgCh, timer_ch: TimerCh) - local result = select_fn(msg_ch, timer_ch) - if result.channel == timer_ch then - return nil, "timeout" - end - -- result.value should be narrowed to Message - local msg = result.value - local topic: string = msg:topic() - return topic - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error at line %d: %s", d.Position.Line, d.Message) - } - t.Errorf("expected no errors calling methods after narrowing") - } -} - -// TestUnionNarrowing_TimeoutCheckPattern tests the common pattern of checking -// result.channel == timeout before accessing result.value fields/methods. -func TestUnionNarrowing_TimeoutCheckPattern(t *testing.T) { - source := ` - type Event = {kind: string, from: string, result: any?, error: any?} - type Timer = {elapsed: number} - - type EventCh = {__tag: "event"} - type TimerCh = {__tag: "timer"} - - type SelectResult = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function do_select(events: EventCh, timeout: TimerCh): SelectResult - return {channel = events, value = {kind = "EXIT", from = "test", result = nil, error = nil}, ok = true} - end - - function f(events_ch: EventCh) - local timeout: TimerCh = {__tag = "timer"} - local result = do_select(events_ch, timeout) - - if result.channel == timeout then - return false, "timeout" - end - - -- After checking timeout, result.value should be Event - local event = result.value - if event.kind ~= "EXIT" then - return false, "wrong event" - end - if event.error then - return false, "error" - end - return true - end - ` - - result := testutil.Check(source, testutil.WithStdlib()) - if result.HasError() { - for _, d := range result.Errors { - t.Logf("error at line %d: %s", d.Position.Line, d.Message) - } - t.Errorf("expected no errors with timeout check pattern") - } -} - -// TestUnionNarrowing_ElseBranchNarrowsToOther tests that the else branch -// narrows to the remaining variants. -func TestUnionNarrowing_ElseBranchNarrowsToOther(t *testing.T) { - tests := []testutil.Case{ - { - Name: "else branch gets remaining variant", - Code: ` - type ChanInt = {__tag: "int"} - type ChanStr = {__tag: "str"} - type SelResult = - {channel: ChanInt, value: number, ok: boolean} | - {channel: ChanStr, value: string, ok: boolean} - - function get_result(a: ChanInt, b: ChanStr): SelResult - return {channel = a, value = 42, ok = true} - end - - function f(ch1: ChanInt, ch2: ChanStr) - local result = get_result(ch1, ch2) - if result.channel == ch1 then - -- Narrowed to first variant, value is number - local n: number = result.value - else - -- Narrowed to second variant, value is string - local s: string = result.value - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrong type in else branch should fail", - Code: ` - type ChanInt = {__tag: "int"} - type ChanStr = {__tag: "str"} - type SelResult = - {channel: ChanInt, value: number, ok: boolean} | - {channel: ChanStr, value: string, ok: boolean} - - function get_result(a: ChanInt, b: ChanStr): SelResult - return {channel = a, value = 42, ok = true} - end - - function f(ch1: ChanInt, ch2: ChanStr) - local result = get_result(ch1, ch2) - if result.channel == ch1 then - local n: number = result.value - else - -- WRONG: else branch has string value, not number - local n: number = result.value - end - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestUnionNarrowing_WithoutNarrowingShouldFail tests that accessing -// variant-specific fields without narrowing produces an error. -func TestUnionNarrowing_WithoutNarrowingShouldFail(t *testing.T) { - tests := []testutil.Case{ - { - Name: "accessing variant field without narrowing fails", - Code: ` - type Event = {kind: string} - type Timer = {elapsed: number} - type Result = Event | Timer - - function get_result(): Result - return {kind = "exit"} - end - - function f() - local result = get_result() - -- NO narrowing - accessing .kind should fail because Timer has no .kind - local k: string = result.kind - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/types/type_cast_test.go b/compiler/check/tests/types/type_cast_test.go deleted file mode 100644 index b374b4ff..00000000 --- a/compiler/check/tests/types/type_cast_test.go +++ /dev/null @@ -1,541 +0,0 @@ -package types - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -func TestTypeCast_StringCast(t *testing.T) { - tests := []testutil.Case{ - { - Name: "string cast from any", - Code: ` - local x: any = "hello" - local s = string(x) - local len = #s - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_BooleanCast(t *testing.T) { - tests := []testutil.Case{ - { - Name: "boolean cast from any", - Code: ` - local x: any = true - local b = boolean(x) - if b then - local n = 1 - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_StringLibStillWorks(t *testing.T) { - tests := []testutil.Case{ - { - Name: "string library methods", - Code: ` - local s = "hello" - local upper = string.upper(s) - local lower = string.lower(s) - local len = string.len(s) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_BothCastAndLibrary(t *testing.T) { - tests := []testutil.Case{ - { - Name: "cast and library together", - Code: ` - local x: any = "hello" - local s = string(x) - local upper = string.upper(s) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_ChainedCast(t *testing.T) { - tests := []testutil.Case{ - { - Name: "chained casts from any fields", - Code: ` - local data: any = { name = "test", count = 42 } - local name = string(data.name) - local count = integer(data.count) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_AliasTypes(t *testing.T) { - tests := []testutil.Case{ - { - Name: "int and bool aliases", - Code: ` - local n = int(42) - local b = bool(true) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_CastInConcat(t *testing.T) { - tests := []testutil.Case{ - { - Name: "string cast in concatenation", - Code: ` - local prefix: any = "Hello, " - local name: any = "World" - local greeting = string(prefix) .. string(name) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_MultipleCastsInStatement(t *testing.T) { - tests := []testutil.Case{ - { - Name: "multiple casts in one statement", - Code: ` - local data: any = {a = "1", b = 2, c = true} - local s, n, b = string(data.a), integer(data.b), boolean(data.c) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_IntegerCast(t *testing.T) { - tests := []testutil.Case{ - { - Name: "integer cast assigned to typed variable", - Code: ` - local x: any = 100 - local n: integer = integer(x) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "integer cast in arithmetic", - Code: ` - local x: any = 100 - local n = integer(x) + 50 - local m: integer = n - `, - WantError: false, - Stdlib: true, - }, - { - Name: "integer cast passed to typed function", - Code: ` - local function double(n: integer): integer - return n * 2 - end - local x: any = 5 - local result = double(integer(x)) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "integer cast in comparison", - Code: ` - local x: any = 100 - local cmp = integer(x) > 50 - local b: boolean = cmp - `, - WantError: false, - Stdlib: true, - }, - { - Name: "integer cast return from function", - Code: ` - local function parse(s: any): integer - return integer(s) - end - local n: integer = parse("42") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "integer cast in table field", - Code: ` - local x: any = 100 - local t: {count: integer} = {count = integer(x)} - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_NumberCast(t *testing.T) { - tests := []testutil.Case{ - { - Name: "number cast assigned to typed variable", - Code: ` - local x: any = 3.14 - local n: number = number(x) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "number cast in arithmetic", - Code: ` - local x: any = 100 - local n = number(x) * 2.5 - local m: number = n - `, - WantError: false, - Stdlib: true, - }, - { - Name: "number cast passed to typed function", - Code: ` - local function half(n: number): number - return n / 2 - end - local x: any = 10 - local result = half(number(x)) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_TostringReturn(t *testing.T) { - tests := []testutil.Case{ - { - Name: "tostring result assigned to string variable", - Code: ` - local x: any = 42 - local s: string = tostring(x) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "tostring result in concatenation", - Code: ` - local x: any = 42 - local s: string = "value: " .. tostring(x) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "tostring on number", - Code: ` - local n: number = 3.14 - local s: string = tostring(n) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "tostring chained with integer cast", - Code: ` - local x: any = 100 - local s: string = tostring(integer(x)) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_CastInReturn(t *testing.T) { - tests := []testutil.Case{ - { - Name: "integer cast in function return", - Code: ` - local function getInt(data: any): integer - return integer(data) - end - local result: integer = getInt(42) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "number cast in function return", - Code: ` - local function getNum(data: any): number - return number(data) - end - local result: number = getNum(3.14) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_CastInArithmetic(t *testing.T) { - tests := []testutil.Case{ - { - Name: "multiple integer casts in arithmetic", - Code: ` - local a: any = 10 - local b: any = 20 - local sum = integer(a) + integer(b) - local result: integer = sum - `, - WantError: false, - Stdlib: true, - }, - { - Name: "mixed casts in arithmetic", - Code: ` - local a: any = 10 - local b: any = 2.5 - local result = integer(a) + number(b) - local n: number = result - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_CastInTableConstructor(t *testing.T) { - tests := []testutil.Case{ - { - Name: "casts in table constructor", - Code: ` - local raw: any = {name = "test", count = 42} - local config: {name: string, count: integer} = { - name = tostring(raw.name), - count = integer(raw.count) - } - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_TonumberOptional(t *testing.T) { - tests := []testutil.Case{ - { - Name: "tonumber returns optional", - Code: ` - local s = "123" - local n = tonumber(s) - if n then - local x: number = n - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "tonumber with base", - Code: ` - local s = "FF" - local n = tonumber(s, 16) - if n then - local x: number = n - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_CustomType(t *testing.T) { - tests := []testutil.Case{ - { - Name: "custom type cast", - Code: ` - type Point = {x: number, y: number} - local v: any = {x = 1, y = 2} - local p = Point(v) - local sum = p.x + p.y - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested record type cast", - Code: ` - type Address = {street: string, city: string} - type Person = {name: string, address: Address} - local data: any = {name = "Alice", address = {street = "123 Main", city = "NYC"}} - local p = Person(data) - local name = p.name - local city = p.address.city - `, - WantError: false, - Stdlib: true, - }, - { - Name: "array type cast", - Code: ` - type Numbers = {integer} - local data: any = {1, 2, 3} - local nums = Numbers(data) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "generic record type cast", - Code: ` - type StringResult = {ok: boolean, value: string} - local data: any = {ok = true, value = "success"} - local r = StringResult(data) - local v = r.value - `, - WantError: false, - Stdlib: true, - }, - { - Name: "cast from method return", - Code: ` - type Data = {value: string} - local obj = { - getData = function(self): any - return {value = "test"} - end - } - local d = Data(obj:getData()) - local v = d.value - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -func TestTypeCast_TypeIsMethod(t *testing.T) { - tests := []testutil.Case{ - { - Name: "Type:is basic pattern", - Code: ` - type Point = {x: number, y: number} - local function validate(data: any) - local val, err = Point:is(data) - if err == nil then - local p: {x: number, y: number} = val - local sum = p.x + p.y - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is result stored then checked", - Code: ` - type Point = {x: number, y: number} - local function validate(data: any) - local val, err = Point:is(data) - if err == nil then - local sum = val.x + val.y - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is direct condition narrows", - Code: ` - type Point = {x: number, y: number} - local function validate(data: any) - local _, err = Point:is(data) - if err == nil then - local p: {x: number, y: number} = data - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is with field access", - Code: ` - type Point = {x: number, y: number} - local v: any = {x = 1, y = 2} - local p, err = Point:is(v) - if err == nil then - local sum = p.x + p.y - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "Type:is falsy check should fail", - Code: ` - type Point = {x: number, y: number} - local function validate(data: any) - local val = Point:is(data) - if not val then - local p: {x: number, y: number} = data - end - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "Type:is with not condition should fail", - Code: ` - type Point = {x: number, y: number} - local function validate(data: any) - if not Point:is(data) then - local p: {x: number, y: number} = data - end - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/testdata/fixtures/core/annotation-array-inferred/main.lua b/testdata/fixtures/core/annotation-array-inferred/main.lua new file mode 100644 index 00000000..9da723d7 --- /dev/null +++ b/testdata/fixtures/core/annotation-array-inferred/main.lua @@ -0,0 +1 @@ +local arr = {1, 2, 3} diff --git a/testdata/fixtures/core/annotation-array-mismatch/main.lua b/testdata/fixtures/core/annotation-array-mismatch/main.lua new file mode 100644 index 00000000..b84e7ecf --- /dev/null +++ b/testdata/fixtures/core/annotation-array-mismatch/main.lua @@ -0,0 +1 @@ +local arr: {string} = {1, 2, 3} -- expect-error diff --git a/testdata/fixtures/core/annotation-function-declared/main.lua b/testdata/fixtures/core/annotation-function-declared/main.lua new file mode 100644 index 00000000..31a91aeb --- /dev/null +++ b/testdata/fixtures/core/annotation-function-declared/main.lua @@ -0,0 +1 @@ +local f: (number, string) -> boolean = function(a: number, b: string): boolean return true end diff --git a/testdata/fixtures/core/annotation-optional/main.lua b/testdata/fixtures/core/annotation-optional/main.lua new file mode 100644 index 00000000..ecafa14b --- /dev/null +++ b/testdata/fixtures/core/annotation-optional/main.lua @@ -0,0 +1 @@ +local x: number? = nil diff --git a/testdata/fixtures/core/annotation-record-inferred/main.lua b/testdata/fixtures/core/annotation-record-inferred/main.lua new file mode 100644 index 00000000..82759c3c --- /dev/null +++ b/testdata/fixtures/core/annotation-record-inferred/main.lua @@ -0,0 +1 @@ +local r = {x = 1, y = "a"} diff --git a/testdata/fixtures/core/annotation-union/main.lua b/testdata/fixtures/core/annotation-union/main.lua new file mode 100644 index 00000000..d148cce1 --- /dev/null +++ b/testdata/fixtures/core/annotation-union/main.lua @@ -0,0 +1 @@ +local x: number | string = 1 diff --git a/testdata/fixtures/core/assign-widening/main.lua b/testdata/fixtures/core/assign-widening/main.lua new file mode 100644 index 00000000..12785c45 --- /dev/null +++ b/testdata/fixtures/core/assign-widening/main.lua @@ -0,0 +1,2 @@ +local x = 1 +x = "ok" diff --git a/testdata/fixtures/core/attr-call-no-recurse/main.lua b/testdata/fixtures/core/attr-call-no-recurse/main.lua new file mode 100644 index 00000000..3405f158 --- /dev/null +++ b/testdata/fixtures/core/attr-call-no-recurse/main.lua @@ -0,0 +1,2 @@ +local t = { print = function(msg) return msg end } +t.print("hi") diff --git a/testdata/fixtures/core/complex-closure/main.lua b/testdata/fixtures/core/complex-closure/main.lua new file mode 100644 index 00000000..20f9f064 --- /dev/null +++ b/testdata/fixtures/core/complex-closure/main.lua @@ -0,0 +1,7 @@ +function counter(): () -> number + local count = 0 + return function(): number + count = count + 1 + return count + end +end diff --git a/testdata/fixtures/core/complex-early-return-guard/main.lua b/testdata/fixtures/core/complex-early-return-guard/main.lua new file mode 100644 index 00000000..0173d6d1 --- /dev/null +++ b/testdata/fixtures/core/complex-early-return-guard/main.lua @@ -0,0 +1,6 @@ +function process(x: number?): number + if x == nil then + return 0 + end + return x * 2 +end diff --git a/testdata/fixtures/core/complex-nested-functions/main.lua b/testdata/fixtures/core/complex-nested-functions/main.lua new file mode 100644 index 00000000..471deaa5 --- /dev/null +++ b/testdata/fixtures/core/complex-nested-functions/main.lua @@ -0,0 +1,6 @@ +function outer(x: number): number + local function inner(y: number): number + return y * 2 + end + return inner(x) + 1 +end diff --git a/testdata/fixtures/core/complex-recursive/main.lua b/testdata/fixtures/core/complex-recursive/main.lua new file mode 100644 index 00000000..ac0ed9e2 --- /dev/null +++ b/testdata/fixtures/core/complex-recursive/main.lua @@ -0,0 +1,7 @@ +function factorial(n: number): number + if n <= 1 then + return 1 + else + return n * factorial(n - 1) + end +end diff --git a/testdata/fixtures/core/complex-table-methods/main.lua b/testdata/fixtures/core/complex-table-methods/main.lua new file mode 100644 index 00000000..a83aeb33 --- /dev/null +++ b/testdata/fixtures/core/complex-table-methods/main.lua @@ -0,0 +1,6 @@ +local obj = { + value = 0, + get = function(self): number + return self.value + end +} diff --git a/testdata/fixtures/core/control-do-block/main.lua b/testdata/fixtures/core/control-do-block/main.lua new file mode 100644 index 00000000..51024e3b --- /dev/null +++ b/testdata/fixtures/core/control-do-block/main.lua @@ -0,0 +1 @@ +do local x = 1 end diff --git a/testdata/fixtures/core/control-for-ipairs/main.lua b/testdata/fixtures/core/control-for-ipairs/main.lua new file mode 100644 index 00000000..dbdf6cae --- /dev/null +++ b/testdata/fixtures/core/control-for-ipairs/main.lua @@ -0,0 +1 @@ +for i, v in ipairs({}) do end diff --git a/testdata/fixtures/core/control-for-loop/main.lua b/testdata/fixtures/core/control-for-loop/main.lua new file mode 100644 index 00000000..83af6691 --- /dev/null +++ b/testdata/fixtures/core/control-for-loop/main.lua @@ -0,0 +1 @@ +for i = 1, 10 do end diff --git a/testdata/fixtures/core/control-for-pairs/main.lua b/testdata/fixtures/core/control-for-pairs/main.lua new file mode 100644 index 00000000..aec4e5e8 --- /dev/null +++ b/testdata/fixtures/core/control-for-pairs/main.lua @@ -0,0 +1 @@ +for k, v in pairs({}) do end diff --git a/testdata/fixtures/core/control-for-step/main.lua b/testdata/fixtures/core/control-for-step/main.lua new file mode 100644 index 00000000..be1b9d17 --- /dev/null +++ b/testdata/fixtures/core/control-for-step/main.lua @@ -0,0 +1 @@ +for i = 1, 10, 2 do end diff --git a/testdata/fixtures/core/control-for-string-init/main.lua b/testdata/fixtures/core/control-for-string-init/main.lua new file mode 100644 index 00000000..263fbb53 --- /dev/null +++ b/testdata/fixtures/core/control-for-string-init/main.lua @@ -0,0 +1 @@ +for i = "a", 10 do end -- expect-error diff --git a/testdata/fixtures/core/control-if-else/main.lua b/testdata/fixtures/core/control-if-else/main.lua new file mode 100644 index 00000000..ef4a4c5e --- /dev/null +++ b/testdata/fixtures/core/control-if-else/main.lua @@ -0,0 +1 @@ +if true then local x = 1 else local x = 2 end diff --git a/testdata/fixtures/core/control-if-simple/main.lua b/testdata/fixtures/core/control-if-simple/main.lua new file mode 100644 index 00000000..3a2642fd --- /dev/null +++ b/testdata/fixtures/core/control-if-simple/main.lua @@ -0,0 +1 @@ +if true then end diff --git a/testdata/fixtures/core/control-repeat-until/main.lua b/testdata/fixtures/core/control-repeat-until/main.lua new file mode 100644 index 00000000..ef11b837 --- /dev/null +++ b/testdata/fixtures/core/control-repeat-until/main.lua @@ -0,0 +1 @@ +repeat local x = 1 until true diff --git a/testdata/fixtures/core/control-while-loop/main.lua b/testdata/fixtures/core/control-while-loop/main.lua new file mode 100644 index 00000000..43f4b979 --- /dev/null +++ b/testdata/fixtures/core/control-while-loop/main.lua @@ -0,0 +1 @@ +while true do break end diff --git a/testdata/fixtures/core/empty-table-map-assign/main.lua b/testdata/fixtures/core/empty-table-map-assign/main.lua new file mode 100644 index 00000000..206fccbb --- /dev/null +++ b/testdata/fixtures/core/empty-table-map-assign/main.lua @@ -0,0 +1,3 @@ +local t = {} +t["a"] = 1 +t["b"] = true diff --git a/testdata/fixtures/core/expr-arithmetic/main.lua b/testdata/fixtures/core/expr-arithmetic/main.lua new file mode 100644 index 00000000..f5d6e2e6 --- /dev/null +++ b/testdata/fixtures/core/expr-arithmetic/main.lua @@ -0,0 +1 @@ +local x = 1 + 2 * 3 diff --git a/testdata/fixtures/core/expr-array-index/main.lua b/testdata/fixtures/core/expr-array-index/main.lua new file mode 100644 index 00000000..d1fe9e43 --- /dev/null +++ b/testdata/fixtures/core/expr-array-index/main.lua @@ -0,0 +1 @@ +local a = {1, 2, 3}; local v = a[1] diff --git a/testdata/fixtures/core/expr-comparison/main.lua b/testdata/fixtures/core/expr-comparison/main.lua new file mode 100644 index 00000000..455a087c --- /dev/null +++ b/testdata/fixtures/core/expr-comparison/main.lua @@ -0,0 +1 @@ +local b = 1 < 2 diff --git a/testdata/fixtures/core/expr-concat-first-return/main.lua b/testdata/fixtures/core/expr-concat-first-return/main.lua new file mode 100644 index 00000000..9974c394 --- /dev/null +++ b/testdata/fixtures/core/expr-concat-first-return/main.lua @@ -0,0 +1,4 @@ +local function f() + return "a", 1 +end +local s = f() .. "b" diff --git a/testdata/fixtures/core/expr-function-call/main.lua b/testdata/fixtures/core/expr-function-call/main.lua new file mode 100644 index 00000000..11b15b1a --- /dev/null +++ b/testdata/fixtures/core/expr-function-call/main.lua @@ -0,0 +1 @@ +print("hello") diff --git a/testdata/fixtures/core/expr-length-first-return/main.lua b/testdata/fixtures/core/expr-length-first-return/main.lua new file mode 100644 index 00000000..b58ce4f9 --- /dev/null +++ b/testdata/fixtures/core/expr-length-first-return/main.lua @@ -0,0 +1,4 @@ +local function f() + return {1, 2, 3}, 10 +end +local n = #f() diff --git a/testdata/fixtures/core/expr-length/main.lua b/testdata/fixtures/core/expr-length/main.lua new file mode 100644 index 00000000..227cf516 --- /dev/null +++ b/testdata/fixtures/core/expr-length/main.lua @@ -0,0 +1 @@ +local n = #"hello" diff --git a/testdata/fixtures/core/expr-logical-and/main.lua b/testdata/fixtures/core/expr-logical-and/main.lua new file mode 100644 index 00000000..80efd0a4 --- /dev/null +++ b/testdata/fixtures/core/expr-logical-and/main.lua @@ -0,0 +1 @@ +local x = true and false diff --git a/testdata/fixtures/core/expr-logical-or/main.lua b/testdata/fixtures/core/expr-logical-or/main.lua new file mode 100644 index 00000000..9511b723 --- /dev/null +++ b/testdata/fixtures/core/expr-logical-or/main.lua @@ -0,0 +1 @@ +local x = nil or 1 diff --git a/testdata/fixtures/core/expr-method-on-literal/main.lua b/testdata/fixtures/core/expr-method-on-literal/main.lua new file mode 100644 index 00000000..76a4221f --- /dev/null +++ b/testdata/fixtures/core/expr-method-on-literal/main.lua @@ -0,0 +1 @@ +local s = ("hello"):upper() diff --git a/testdata/fixtures/core/expr-string-concat/main.lua b/testdata/fixtures/core/expr-string-concat/main.lua new file mode 100644 index 00000000..0756050d --- /dev/null +++ b/testdata/fixtures/core/expr-string-concat/main.lua @@ -0,0 +1 @@ +local s = "hello" .. " world" diff --git a/testdata/fixtures/core/expr-table-access/main.lua b/testdata/fixtures/core/expr-table-access/main.lua new file mode 100644 index 00000000..111e003f --- /dev/null +++ b/testdata/fixtures/core/expr-table-access/main.lua @@ -0,0 +1 @@ +local t = {x = 1}; local v = t.x diff --git a/testdata/fixtures/core/expr-unary-minus/main.lua b/testdata/fixtures/core/expr-unary-minus/main.lua new file mode 100644 index 00000000..d713e321 --- /dev/null +++ b/testdata/fixtures/core/expr-unary-minus/main.lua @@ -0,0 +1 @@ +local x = -42 diff --git a/testdata/fixtures/core/expr-unary-not/main.lua b/testdata/fixtures/core/expr-unary-not/main.lua new file mode 100644 index 00000000..8b637669 --- /dev/null +++ b/testdata/fixtures/core/expr-unary-not/main.lua @@ -0,0 +1 @@ +local b = not true diff --git a/testdata/fixtures/core/method-param-self-sub/main.lua b/testdata/fixtures/core/method-param-self-sub/main.lua new file mode 100644 index 00000000..836c86f2 --- /dev/null +++ b/testdata/fixtures/core/method-param-self-sub/main.lua @@ -0,0 +1,3 @@ +type T = { eq: (self: T, other: T) -> boolean } +local t: T = { eq = function(self, other) return self == other end } +local ok = t:eq(t) diff --git a/testdata/fixtures/core/module-local-def/main.lua b/testdata/fixtures/core/module-local-def/main.lua new file mode 100644 index 00000000..777e5438 --- /dev/null +++ b/testdata/fixtures/core/module-local-def/main.lua @@ -0,0 +1,8 @@ +local M = {} +function M.add(a: number, b: number): number + return a + b +end +function M.sub(a: number, b: number): number + return a - b +end +local result: number = M.add(1, 2) diff --git a/testdata/fixtures/core/module-return-pattern/main.lua b/testdata/fixtures/core/module-return-pattern/main.lua new file mode 100644 index 00000000..ab5ac449 --- /dev/null +++ b/testdata/fixtures/core/module-return-pattern/main.lua @@ -0,0 +1,6 @@ +local M = {} +M.version = "1.0" +function M.init() + print("initialized") +end +return M diff --git a/testdata/fixtures/core/module-with-method/main.lua b/testdata/fixtures/core/module-with-method/main.lua new file mode 100644 index 00000000..2ff7abe5 --- /dev/null +++ b/testdata/fixtures/core/module-with-method/main.lua @@ -0,0 +1,9 @@ +local Counter = {count = 0} +function Counter:increment() + self.count = self.count + 1 +end +function Counter:get(): number + return self.count +end +Counter:increment() +local n: number = Counter:get() diff --git a/testdata/fixtures/core/narrow-nested/main.lua b/testdata/fixtures/core/narrow-nested/main.lua new file mode 100644 index 00000000..76f0e941 --- /dev/null +++ b/testdata/fixtures/core/narrow-nested/main.lua @@ -0,0 +1,8 @@ +function f(x: string?, y: number?) + if x ~= nil then + if y ~= nil then + local s: string = x + local n: number = y + end + end +end diff --git a/testdata/fixtures/core/narrow-nil-else-inline/main.lua b/testdata/fixtures/core/narrow-nil-else-inline/main.lua new file mode 100644 index 00000000..5af2f1fe --- /dev/null +++ b/testdata/fixtures/core/narrow-nil-else-inline/main.lua @@ -0,0 +1,7 @@ +function f(x: string?) + if x == nil then + return + else + local s: string = x + end +end diff --git a/testdata/fixtures/core/narrow-nil-then/main.lua b/testdata/fixtures/core/narrow-nil-then/main.lua new file mode 100644 index 00000000..3c75d880 --- /dev/null +++ b/testdata/fixtures/core/narrow-nil-then/main.lua @@ -0,0 +1,5 @@ +function f(x: string?) + if x ~= nil then + local s: string = x + end +end diff --git a/testdata/fixtures/core/narrow-no-check-fails/main.lua b/testdata/fixtures/core/narrow-no-check-fails/main.lua new file mode 100644 index 00000000..ee5cf8fe --- /dev/null +++ b/testdata/fixtures/core/narrow-no-check-fails/main.lua @@ -0,0 +1,3 @@ +function f(x: string?) + local s: string = x -- expect-error +end diff --git a/testdata/fixtures/core/narrow-truthy/main.lua b/testdata/fixtures/core/narrow-truthy/main.lua new file mode 100644 index 00000000..756fef94 --- /dev/null +++ b/testdata/fixtures/core/narrow-truthy/main.lua @@ -0,0 +1,5 @@ +function f(x: string?) + if x then + local s: string = x + end +end diff --git a/testdata/fixtures/core/record-literal-to-intersection/main.lua b/testdata/fixtures/core/record-literal-to-intersection/main.lua new file mode 100644 index 00000000..86089f4d --- /dev/null +++ b/testdata/fixtures/core/record-literal-to-intersection/main.lua @@ -0,0 +1,3 @@ +type Person = {name: string} & {age: number} +local p: Person = { name = "Alice", age = 30 } +return p diff --git a/testdata/fixtures/core/record-literal-to-record/main.lua b/testdata/fixtures/core/record-literal-to-record/main.lua new file mode 100644 index 00000000..37cca66a --- /dev/null +++ b/testdata/fixtures/core/record-literal-to-record/main.lua @@ -0,0 +1,2 @@ +local person: {name: string, age: number} = { name = "Alice", age = 30 } +return person diff --git a/testdata/fixtures/core/stdlib-ipairs/main.lua b/testdata/fixtures/core/stdlib-ipairs/main.lua new file mode 100644 index 00000000..36ca57f0 --- /dev/null +++ b/testdata/fixtures/core/stdlib-ipairs/main.lua @@ -0,0 +1 @@ +for i, v in ipairs({1, 2, 3}) do end diff --git a/testdata/fixtures/core/stdlib-math-floor/main.lua b/testdata/fixtures/core/stdlib-math-floor/main.lua new file mode 100644 index 00000000..c66e1060 --- /dev/null +++ b/testdata/fixtures/core/stdlib-math-floor/main.lua @@ -0,0 +1 @@ +local x = math.floor(3.5) diff --git a/testdata/fixtures/core/stdlib-pairs/main.lua b/testdata/fixtures/core/stdlib-pairs/main.lua new file mode 100644 index 00000000..a85a5fbd --- /dev/null +++ b/testdata/fixtures/core/stdlib-pairs/main.lua @@ -0,0 +1 @@ +for k, v in pairs({a = 1}) do end diff --git a/testdata/fixtures/core/stdlib-print/main.lua b/testdata/fixtures/core/stdlib-print/main.lua new file mode 100644 index 00000000..11b15b1a --- /dev/null +++ b/testdata/fixtures/core/stdlib-print/main.lua @@ -0,0 +1 @@ +print("hello") diff --git a/testdata/fixtures/core/stdlib-string-upper/main.lua b/testdata/fixtures/core/stdlib-string-upper/main.lua new file mode 100644 index 00000000..9c5dd942 --- /dev/null +++ b/testdata/fixtures/core/stdlib-string-upper/main.lua @@ -0,0 +1 @@ +local s = string.upper("hello") diff --git a/testdata/fixtures/core/stdlib-table-insert/main.lua b/testdata/fixtures/core/stdlib-table-insert/main.lua new file mode 100644 index 00000000..98ca110e --- /dev/null +++ b/testdata/fixtures/core/stdlib-table-insert/main.lua @@ -0,0 +1 @@ +local t = {}; table.insert(t, 1) diff --git a/testdata/fixtures/core/stdlib-tonumber/main.lua b/testdata/fixtures/core/stdlib-tonumber/main.lua new file mode 100644 index 00000000..33e0986a --- /dev/null +++ b/testdata/fixtures/core/stdlib-tonumber/main.lua @@ -0,0 +1 @@ +local n = tonumber("42") diff --git a/testdata/fixtures/core/stdlib-tostring/main.lua b/testdata/fixtures/core/stdlib-tostring/main.lua new file mode 100644 index 00000000..0653edee --- /dev/null +++ b/testdata/fixtures/core/stdlib-tostring/main.lua @@ -0,0 +1 @@ +local s = tostring(42) diff --git a/testdata/fixtures/core/stdlib-type/main.lua b/testdata/fixtures/core/stdlib-type/main.lua new file mode 100644 index 00000000..12fcc0a4 --- /dev/null +++ b/testdata/fixtures/core/stdlib-type/main.lua @@ -0,0 +1 @@ +local t = type(42) diff --git a/testdata/fixtures/core/tonumber-skips-optional/main.lua b/testdata/fixtures/core/tonumber-skips-optional/main.lua new file mode 100644 index 00000000..2fdd7630 --- /dev/null +++ b/testdata/fixtures/core/tonumber-skips-optional/main.lua @@ -0,0 +1,5 @@ +type Request = { query: (self: Request, key: string) -> (string?, Error?) } +local function handler(req: Request) + local code = tonumber(req:query("code")) or 200 + return code +end diff --git a/testdata/fixtures/core/type-is-basic/main.lua b/testdata/fixtures/core/type-is-basic/main.lua new file mode 100644 index 00000000..7c94c69a --- /dev/null +++ b/testdata/fixtures/core/type-is-basic/main.lua @@ -0,0 +1,7 @@ +type Point = {x: number, y: number} +function validate(data: any) + local _, err = Point:is(data) + if err == nil then + local p: {x: number, y: number} = data + end +end diff --git a/testdata/fixtures/core/type-is-direct-condition/main.lua b/testdata/fixtures/core/type-is-direct-condition/main.lua new file mode 100644 index 00000000..7c94c69a --- /dev/null +++ b/testdata/fixtures/core/type-is-direct-condition/main.lua @@ -0,0 +1,7 @@ +type Point = {x: number, y: number} +function validate(data: any) + local _, err = Point:is(data) + if err == nil then + local p: {x: number, y: number} = data + end +end diff --git a/testdata/fixtures/core/type-is-falsy-excludes/main.lua b/testdata/fixtures/core/type-is-falsy-excludes/main.lua new file mode 100644 index 00000000..0539b0fc --- /dev/null +++ b/testdata/fixtures/core/type-is-falsy-excludes/main.lua @@ -0,0 +1,10 @@ +type Point = {x: number, y: number} +local function isPoint(x) + return Point:is(x) +end +function validate(data: any) + local val, err = isPoint(data) + if err ~= nil then + local p: {x: number, y: number} = val -- expect-error + end +end diff --git a/testdata/fixtures/core/type-is-truthy/main.lua b/testdata/fixtures/core/type-is-truthy/main.lua new file mode 100644 index 00000000..48f84793 --- /dev/null +++ b/testdata/fixtures/core/type-is-truthy/main.lua @@ -0,0 +1,10 @@ +type Point = {x: number, y: number} +local function isPoint(x) + return Point:is(x) +end +function validate(data: any) + local val, err = isPoint(data) + if err == nil and val ~= nil then + local p: {x: number, y: number} = val + end +end diff --git a/testdata/fixtures/core/untyped-params-missing-args/main.lua b/testdata/fixtures/core/untyped-params-missing-args/main.lua new file mode 100644 index 00000000..99a8a6e2 --- /dev/null +++ b/testdata/fixtures/core/untyped-params-missing-args/main.lua @@ -0,0 +1,4 @@ +local function eq(actual, expected, msg) + return actual == expected +end +eq(1, 1) diff --git a/testdata/fixtures/core/untyped-string-ops/main.lua b/testdata/fixtures/core/untyped-string-ops/main.lua new file mode 100644 index 00000000..9fe605ea --- /dev/null +++ b/testdata/fixtures/core/untyped-string-ops/main.lua @@ -0,0 +1,7 @@ +local function green(s) return "\027[32m" .. s .. "\027[0m" end +local function greet(name) + if name and #name > 0 then + return "Hello, " .. name + end + return green("stranger") +end diff --git a/testdata/fixtures/narrowing/union-discriminated-literal/main.lua b/testdata/fixtures/narrowing/union-discriminated-literal/main.lua new file mode 100644 index 00000000..06f54503 --- /dev/null +++ b/testdata/fixtures/narrowing/union-discriminated-literal/main.lua @@ -0,0 +1,9 @@ +type A = {kind: "a", value_a: string} +type B = {kind: "b", value_b: number} +type AB = A | B + +function f(x: AB) + if x.kind == "a" then + local v: string = x.value_a + end +end diff --git a/testdata/fixtures/narrowing/union-else-remaining-variant/main.lua b/testdata/fixtures/narrowing/union-else-remaining-variant/main.lua new file mode 100644 index 00000000..d4a13aa5 --- /dev/null +++ b/testdata/fixtures/narrowing/union-else-remaining-variant/main.lua @@ -0,0 +1,18 @@ +type ChanInt = {__tag: "int"} +type ChanStr = {__tag: "str"} +type SelResult = + {channel: ChanInt, value: number, ok: boolean} | + {channel: ChanStr, value: string, ok: boolean} + +function get_result(a: ChanInt, b: ChanStr): SelResult + return {channel = a, value = 42, ok = true} +end + +function f(ch1: ChanInt, ch2: ChanStr) + local result = get_result(ch1, ch2) + if result.channel == ch1 then + local n: number = result.value + else + local s: string = result.value + end +end diff --git a/testdata/fixtures/narrowing/union-else-wrong-type/main.lua b/testdata/fixtures/narrowing/union-else-wrong-type/main.lua new file mode 100644 index 00000000..b09b8469 --- /dev/null +++ b/testdata/fixtures/narrowing/union-else-wrong-type/main.lua @@ -0,0 +1,18 @@ +type ChanInt = {__tag: "int"} +type ChanStr = {__tag: "str"} +type SelResult = + {channel: ChanInt, value: number, ok: boolean} | + {channel: ChanStr, value: string, ok: boolean} + +function get_result(a: ChanInt, b: ChanStr): SelResult + return {channel = a, value = 42, ok = true} +end + +function f(ch1: ChanInt, ch2: ChanStr) + local result = get_result(ch1, ch2) + if result.channel == ch1 then + local n: number = result.value + else + local n: number = result.value -- expect-error + end +end diff --git a/testdata/fixtures/narrowing/union-local-assign-narrowed/main.lua b/testdata/fixtures/narrowing/union-local-assign-narrowed/main.lua new file mode 100644 index 00000000..92dbf7fc --- /dev/null +++ b/testdata/fixtures/narrowing/union-local-assign-narrowed/main.lua @@ -0,0 +1,24 @@ +type EventCh = {__tag: "event"} +type TimeoutCh = {__tag: "timeout"} +type Event = {kind: string, error: string?} +type Time = {sec: number} + +type Result = {channel: EventCh, value: Event, ok: boolean} | + {channel: TimeoutCh, value: Time, ok: boolean} + +function get_result(ch: EventCh, timeout: TimeoutCh): Result + return {channel = ch, value = {kind = "exit", error = nil}, ok = true} +end + +function f(events_ch: EventCh, timeout_ch: TimeoutCh) + local result = get_result(events_ch, timeout_ch) + if result.channel ~= events_ch then + return false, "timeout" + end + local event = result.value + local k: string = event.kind + if event.error then + local e: string = event.error + end + return true +end diff --git a/testdata/fixtures/narrowing/union-method-after-narrowing/main.lua b/testdata/fixtures/narrowing/union-method-after-narrowing/main.lua new file mode 100644 index 00000000..a1dc398a --- /dev/null +++ b/testdata/fixtures/narrowing/union-method-after-narrowing/main.lua @@ -0,0 +1,33 @@ +type Message = { + _topic: string, + topic: (self: Message) -> string +} + +type Timer = {elapsed: number} + +type MsgCh = {__tag: "msg"} +type TimerCh = {__tag: "timer"} + +type Result = {channel: MsgCh, value: Message, ok: boolean} | + {channel: TimerCh, value: Timer, ok: boolean} + +function select_fn(msg_ch: MsgCh, timer_ch: TimerCh): Result + return { + channel = msg_ch, + value = { + _topic = "test", + topic = function(s: Message): string return s._topic end + }, + ok = true + } +end + +function f(msg_ch: MsgCh, timer_ch: TimerCh) + local result = select_fn(msg_ch, timer_ch) + if result.channel == timer_ch then + return nil, "timeout" + end + local msg = result.value + local topic: string = msg:topic() + return topic +end diff --git a/testdata/fixtures/narrowing/union-multiple-channels/main.lua b/testdata/fixtures/narrowing/union-multiple-channels/main.lua new file mode 100644 index 00000000..05578429 --- /dev/null +++ b/testdata/fixtures/narrowing/union-multiple-channels/main.lua @@ -0,0 +1,33 @@ +type Message = {_topic: string} +type Event = {kind: string} +type Timer = {elapsed: number} + +type MsgCh = {__tag: "msg"} +type EventCh = {__tag: "event"} +type TimerCh = {__tag: "timer"} + +type Result = {channel: MsgCh, value: Message, ok: boolean} | + {channel: EventCh, value: Event, ok: boolean} | + {channel: TimerCh, value: Timer, ok: boolean} + +function do_select(m: MsgCh, e: EventCh, t: TimerCh): Result + return {channel = m, value = {_topic = "test"}, ok = true} +end + +function f(msg_ch: MsgCh, events_ch: EventCh, timeout: TimerCh) + local result = do_select(msg_ch, events_ch, timeout) + + if result.channel == timeout then + return nil, "timeout" + end + + if result.channel == events_ch then + local event = result.value + local k: string = event.kind + return "event", k + end + + local msg = result.value + local topic: string = msg._topic + return "message", topic +end diff --git a/testdata/fixtures/narrowing/union-negated-condition/main.lua b/testdata/fixtures/narrowing/union-negated-condition/main.lua new file mode 100644 index 00000000..e23b5529 --- /dev/null +++ b/testdata/fixtures/narrowing/union-negated-condition/main.lua @@ -0,0 +1,21 @@ +type EventCh = {__tag: "event"} +type TimeoutCh = {__tag: "timeout"} +type Event = {kind: string} +type Time = {sec: number} + +type Result = {channel: EventCh, value: Event, ok: boolean} | + {channel: TimeoutCh, value: Time, ok: boolean} + +function get_result(ch: EventCh, timeout: TimeoutCh): Result + return {channel = ch, value = {kind = "exit"}, ok = true} +end + +function f(events_ch: EventCh, timeout_ch: TimeoutCh) + local result = get_result(events_ch, timeout_ch) + if result.channel ~= events_ch then + local t: Time = result.value + return false + end + local event: Event = result.value + return true +end diff --git a/testdata/fixtures/narrowing/union-nested-field-access/main.lua b/testdata/fixtures/narrowing/union-nested-field-access/main.lua new file mode 100644 index 00000000..e1cfd9ba --- /dev/null +++ b/testdata/fixtures/narrowing/union-nested-field-access/main.lua @@ -0,0 +1,16 @@ +type ChanInt = {__tag: "int"} +type ChanStr = {__tag: "str"} +type SelResult = + {channel: ChanInt, value: {error: string}, ok: boolean} | + {channel: ChanStr, value: {data: number}, ok: boolean} + +function get_result(a: ChanInt, b: ChanStr): SelResult + return {channel = a, value = {error = "oops"}, ok = true} +end + +function f(ch1: ChanInt, ch2: ChanStr) + local result = get_result(ch1, ch2) + if result.channel == ch1 then + local e: string = result.value.error + end +end diff --git a/testdata/fixtures/narrowing/union-no-narrowing-fails/main.lua b/testdata/fixtures/narrowing/union-no-narrowing-fails/main.lua new file mode 100644 index 00000000..7bf81fe8 --- /dev/null +++ b/testdata/fixtures/narrowing/union-no-narrowing-fails/main.lua @@ -0,0 +1,12 @@ +type Event = {kind: string} +type Timer = {elapsed: number} +type Result = Event | Timer + +function get_result(): Result + return {kind = "exit"} +end + +function f() + local result = get_result() + local k: string = result.kind -- expect-error +end diff --git a/testdata/fixtures/narrowing/union-timeout-check-pattern/main.lua b/testdata/fixtures/narrowing/union-timeout-check-pattern/main.lua new file mode 100644 index 00000000..964c2f98 --- /dev/null +++ b/testdata/fixtures/narrowing/union-timeout-check-pattern/main.lua @@ -0,0 +1,30 @@ +type Event = {kind: string, from: string, result: any?, error: any?} +type Timer = {elapsed: number} + +type EventCh = {__tag: "event"} +type TimerCh = {__tag: "timer"} + +type SelectResult = {channel: EventCh, value: Event, ok: boolean} | + {channel: TimerCh, value: Timer, ok: boolean} + +function do_select(events: EventCh, timeout: TimerCh): SelectResult + return {channel = events, value = {kind = "EXIT", from = "test", result = nil, error = nil}, ok = true} +end + +function f(events_ch: EventCh) + local timeout: TimerCh = {__tag = "timer"} + local result = do_select(events_ch, timeout) + + if result.channel == timeout then + return false, "timeout" + end + + local event = result.value + if event.kind ~= "EXIT" then + return false, "wrong event" + end + if event.error then + return false, "error" + end + return true +end diff --git a/testdata/fixtures/narrowing/union-truthy-guard/main.lua b/testdata/fixtures/narrowing/union-truthy-guard/main.lua new file mode 100644 index 00000000..6c927a0a --- /dev/null +++ b/testdata/fixtures/narrowing/union-truthy-guard/main.lua @@ -0,0 +1,14 @@ +type Event = {kind: string, error: string?} +type Timer = {elapsed: number} +type SelectResult = Event | Timer + +function get_result(): SelectResult + return {kind = "exit", error = nil} +end + +function f() + local result = get_result() + if result.kind then + local k: string = result.kind + end +end diff --git a/testdata/fixtures/narrowing/union-wrong-field-after-narrowing/main.lua b/testdata/fixtures/narrowing/union-wrong-field-after-narrowing/main.lua new file mode 100644 index 00000000..bad1484c --- /dev/null +++ b/testdata/fixtures/narrowing/union-wrong-field-after-narrowing/main.lua @@ -0,0 +1,9 @@ +type A = {kind: "a", value_a: string} +type B = {kind: "b", value_b: number} +type AB = A | B + +function f(x: AB) + if x.kind == "a" then + local v: number = x.value_b -- expect-error + end +end diff --git a/testdata/fixtures/types/cast-and-library/main.lua b/testdata/fixtures/types/cast-and-library/main.lua new file mode 100644 index 00000000..862787b3 --- /dev/null +++ b/testdata/fixtures/types/cast-and-library/main.lua @@ -0,0 +1,3 @@ +local x: any = "hello" +local s = string(x) +local upper = string.upper(s) diff --git a/testdata/fixtures/types/cast-arithmetic-mixed/main.lua b/testdata/fixtures/types/cast-arithmetic-mixed/main.lua new file mode 100644 index 00000000..21c1bfe9 --- /dev/null +++ b/testdata/fixtures/types/cast-arithmetic-mixed/main.lua @@ -0,0 +1,4 @@ +local a: any = 10 +local b: any = 2.5 +local result = integer(a) + number(b) +local n: number = result diff --git a/testdata/fixtures/types/cast-arithmetic-multiple/main.lua b/testdata/fixtures/types/cast-arithmetic-multiple/main.lua new file mode 100644 index 00000000..7361702c --- /dev/null +++ b/testdata/fixtures/types/cast-arithmetic-multiple/main.lua @@ -0,0 +1,4 @@ +local a: any = 10 +local b: any = 20 +local sum = integer(a) + integer(b) +local result: integer = sum diff --git a/testdata/fixtures/types/cast-array-type/main.lua b/testdata/fixtures/types/cast-array-type/main.lua new file mode 100644 index 00000000..49947d5f --- /dev/null +++ b/testdata/fixtures/types/cast-array-type/main.lua @@ -0,0 +1,3 @@ +type Numbers = {integer} +local data: any = {1, 2, 3} +local nums = Numbers(data) diff --git a/testdata/fixtures/types/cast-boolean-from-any/main.lua b/testdata/fixtures/types/cast-boolean-from-any/main.lua new file mode 100644 index 00000000..bc789bf9 --- /dev/null +++ b/testdata/fixtures/types/cast-boolean-from-any/main.lua @@ -0,0 +1,5 @@ +local x: any = true +local b = boolean(x) +if b then + local n = 1 +end diff --git a/testdata/fixtures/types/cast-chained-any-fields/main.lua b/testdata/fixtures/types/cast-chained-any-fields/main.lua new file mode 100644 index 00000000..3d6283d4 --- /dev/null +++ b/testdata/fixtures/types/cast-chained-any-fields/main.lua @@ -0,0 +1,3 @@ +local data: any = { name = "test", count = 42 } +local name = string(data.name) +local count = integer(data.count) diff --git a/testdata/fixtures/types/cast-custom-type/main.lua b/testdata/fixtures/types/cast-custom-type/main.lua new file mode 100644 index 00000000..4d0ad9f7 --- /dev/null +++ b/testdata/fixtures/types/cast-custom-type/main.lua @@ -0,0 +1,4 @@ +type Point = {x: number, y: number} +local v: any = {x = 1, y = 2} +local p = Point(v) +local sum = p.x + p.y diff --git a/testdata/fixtures/types/cast-from-method-return/main.lua b/testdata/fixtures/types/cast-from-method-return/main.lua new file mode 100644 index 00000000..5f6ff08a --- /dev/null +++ b/testdata/fixtures/types/cast-from-method-return/main.lua @@ -0,0 +1,8 @@ +type Data = {value: string} +local obj = { + getData = function(self): any + return {value = "test"} + end +} +local d = Data(obj:getData()) +local v = d.value diff --git a/testdata/fixtures/types/cast-generic-record/main.lua b/testdata/fixtures/types/cast-generic-record/main.lua new file mode 100644 index 00000000..f574ae1b --- /dev/null +++ b/testdata/fixtures/types/cast-generic-record/main.lua @@ -0,0 +1,4 @@ +type StringResult = {ok: boolean, value: string} +local data: any = {ok = true, value = "success"} +local r = StringResult(data) +local v = r.value diff --git a/testdata/fixtures/types/cast-in-concat/main.lua b/testdata/fixtures/types/cast-in-concat/main.lua new file mode 100644 index 00000000..22e42740 --- /dev/null +++ b/testdata/fixtures/types/cast-in-concat/main.lua @@ -0,0 +1,3 @@ +local prefix: any = "Hello, " +local name: any = "World" +local greeting = string(prefix) .. string(name) diff --git a/testdata/fixtures/types/cast-int-bool-aliases/main.lua b/testdata/fixtures/types/cast-int-bool-aliases/main.lua new file mode 100644 index 00000000..7ab1ee03 --- /dev/null +++ b/testdata/fixtures/types/cast-int-bool-aliases/main.lua @@ -0,0 +1,2 @@ +local n = int(42) +local b = bool(true) diff --git a/testdata/fixtures/types/cast-integer-arithmetic/main.lua b/testdata/fixtures/types/cast-integer-arithmetic/main.lua new file mode 100644 index 00000000..60937ab4 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-arithmetic/main.lua @@ -0,0 +1,3 @@ +local x: any = 100 +local n = integer(x) + 50 +local m: integer = n diff --git a/testdata/fixtures/types/cast-integer-comparison/main.lua b/testdata/fixtures/types/cast-integer-comparison/main.lua new file mode 100644 index 00000000..7e324da6 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-comparison/main.lua @@ -0,0 +1,3 @@ +local x: any = 100 +local cmp = integer(x) > 50 +local b: boolean = cmp diff --git a/testdata/fixtures/types/cast-integer-return/main.lua b/testdata/fixtures/types/cast-integer-return/main.lua new file mode 100644 index 00000000..33aaf753 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-return/main.lua @@ -0,0 +1,4 @@ +local function parse(s: any): integer + return integer(s) +end +local n: integer = parse("42") diff --git a/testdata/fixtures/types/cast-integer-table-field/main.lua b/testdata/fixtures/types/cast-integer-table-field/main.lua new file mode 100644 index 00000000..bb9a0792 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-table-field/main.lua @@ -0,0 +1,2 @@ +local x: any = 100 +local t: {count: integer} = {count = integer(x)} diff --git a/testdata/fixtures/types/cast-integer-to-function/main.lua b/testdata/fixtures/types/cast-integer-to-function/main.lua new file mode 100644 index 00000000..ea9ecb00 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-to-function/main.lua @@ -0,0 +1,5 @@ +local function double(n: integer): integer + return n * 2 +end +local x: any = 5 +local result = double(integer(x)) diff --git a/testdata/fixtures/types/cast-integer-typed/main.lua b/testdata/fixtures/types/cast-integer-typed/main.lua new file mode 100644 index 00000000..20c35b71 --- /dev/null +++ b/testdata/fixtures/types/cast-integer-typed/main.lua @@ -0,0 +1,2 @@ +local x: any = 100 +local n: integer = integer(x) diff --git a/testdata/fixtures/types/cast-multiple-in-statement/main.lua b/testdata/fixtures/types/cast-multiple-in-statement/main.lua new file mode 100644 index 00000000..f2d49652 --- /dev/null +++ b/testdata/fixtures/types/cast-multiple-in-statement/main.lua @@ -0,0 +1,2 @@ +local data: any = {a = "1", b = 2, c = true} +local s, n, b = string(data.a), integer(data.b), boolean(data.c) diff --git a/testdata/fixtures/types/cast-nested-record/main.lua b/testdata/fixtures/types/cast-nested-record/main.lua new file mode 100644 index 00000000..c495602c --- /dev/null +++ b/testdata/fixtures/types/cast-nested-record/main.lua @@ -0,0 +1,6 @@ +type Address = {street: string, city: string} +type Person = {name: string, address: Address} +local data: any = {name = "Alice", address = {street = "123 Main", city = "NYC"}} +local p = Person(data) +local name = p.name +local city = p.address.city diff --git a/testdata/fixtures/types/cast-number-arithmetic/main.lua b/testdata/fixtures/types/cast-number-arithmetic/main.lua new file mode 100644 index 00000000..018e38de --- /dev/null +++ b/testdata/fixtures/types/cast-number-arithmetic/main.lua @@ -0,0 +1,3 @@ +local x: any = 100 +local n = number(x) * 2.5 +local m: number = n diff --git a/testdata/fixtures/types/cast-number-to-function/main.lua b/testdata/fixtures/types/cast-number-to-function/main.lua new file mode 100644 index 00000000..18301c42 --- /dev/null +++ b/testdata/fixtures/types/cast-number-to-function/main.lua @@ -0,0 +1,5 @@ +local function half(n: number): number + return n / 2 +end +local x: any = 10 +local result = half(number(x)) diff --git a/testdata/fixtures/types/cast-number-typed/main.lua b/testdata/fixtures/types/cast-number-typed/main.lua new file mode 100644 index 00000000..401f4c68 --- /dev/null +++ b/testdata/fixtures/types/cast-number-typed/main.lua @@ -0,0 +1,2 @@ +local x: any = 3.14 +local n: number = number(x) diff --git a/testdata/fixtures/types/cast-return-integer/main.lua b/testdata/fixtures/types/cast-return-integer/main.lua new file mode 100644 index 00000000..f91ff651 --- /dev/null +++ b/testdata/fixtures/types/cast-return-integer/main.lua @@ -0,0 +1,4 @@ +local function getInt(data: any): integer + return integer(data) +end +local result: integer = getInt(42) diff --git a/testdata/fixtures/types/cast-return-number/main.lua b/testdata/fixtures/types/cast-return-number/main.lua new file mode 100644 index 00000000..0ad97f2d --- /dev/null +++ b/testdata/fixtures/types/cast-return-number/main.lua @@ -0,0 +1,4 @@ +local function getNum(data: any): number + return number(data) +end +local result: number = getNum(3.14) diff --git a/testdata/fixtures/types/cast-string-from-any/main.lua b/testdata/fixtures/types/cast-string-from-any/main.lua new file mode 100644 index 00000000..ff24f594 --- /dev/null +++ b/testdata/fixtures/types/cast-string-from-any/main.lua @@ -0,0 +1,3 @@ +local x: any = "hello" +local s = string(x) +local len = #s diff --git a/testdata/fixtures/types/cast-string-lib-works/main.lua b/testdata/fixtures/types/cast-string-lib-works/main.lua new file mode 100644 index 00000000..7be65cd4 --- /dev/null +++ b/testdata/fixtures/types/cast-string-lib-works/main.lua @@ -0,0 +1,4 @@ +local s = "hello" +local upper = string.upper(s) +local lower = string.lower(s) +local len = string.len(s) diff --git a/testdata/fixtures/types/cast-table-constructor/main.lua b/testdata/fixtures/types/cast-table-constructor/main.lua new file mode 100644 index 00000000..a16303fb --- /dev/null +++ b/testdata/fixtures/types/cast-table-constructor/main.lua @@ -0,0 +1,5 @@ +local raw: any = {name = "test", count = 42} +local config: {name: string, count: integer} = { + name = tostring(raw.name), + count = integer(raw.count) +} diff --git a/testdata/fixtures/types/cast-tonumber-base/main.lua b/testdata/fixtures/types/cast-tonumber-base/main.lua new file mode 100644 index 00000000..fa386f53 --- /dev/null +++ b/testdata/fixtures/types/cast-tonumber-base/main.lua @@ -0,0 +1,5 @@ +local s = "FF" +local n = tonumber(s, 16) +if n then + local x: number = n +end diff --git a/testdata/fixtures/types/cast-tonumber-optional/main.lua b/testdata/fixtures/types/cast-tonumber-optional/main.lua new file mode 100644 index 00000000..b95524a3 --- /dev/null +++ b/testdata/fixtures/types/cast-tonumber-optional/main.lua @@ -0,0 +1,5 @@ +local s = "123" +local n = tonumber(s) +if n then + local x: number = n +end diff --git a/testdata/fixtures/types/cast-tostring-chained/main.lua b/testdata/fixtures/types/cast-tostring-chained/main.lua new file mode 100644 index 00000000..389ef15b --- /dev/null +++ b/testdata/fixtures/types/cast-tostring-chained/main.lua @@ -0,0 +1,2 @@ +local x: any = 100 +local s: string = tostring(integer(x)) diff --git a/testdata/fixtures/types/cast-tostring-concat/main.lua b/testdata/fixtures/types/cast-tostring-concat/main.lua new file mode 100644 index 00000000..1833a298 --- /dev/null +++ b/testdata/fixtures/types/cast-tostring-concat/main.lua @@ -0,0 +1,2 @@ +local x: any = 42 +local s: string = "value: " .. tostring(x) diff --git a/testdata/fixtures/types/cast-tostring-number/main.lua b/testdata/fixtures/types/cast-tostring-number/main.lua new file mode 100644 index 00000000..54505d40 --- /dev/null +++ b/testdata/fixtures/types/cast-tostring-number/main.lua @@ -0,0 +1,2 @@ +local n: number = 3.14 +local s: string = tostring(n) diff --git a/testdata/fixtures/types/cast-tostring-typed/main.lua b/testdata/fixtures/types/cast-tostring-typed/main.lua new file mode 100644 index 00000000..e9d1cebe --- /dev/null +++ b/testdata/fixtures/types/cast-tostring-typed/main.lua @@ -0,0 +1,2 @@ +local x: any = 42 +local s: string = tostring(x) diff --git a/testdata/fixtures/types/cast-type-is-basic/main.lua b/testdata/fixtures/types/cast-type-is-basic/main.lua new file mode 100644 index 00000000..6e8b4e62 --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-basic/main.lua @@ -0,0 +1,8 @@ +type Point = {x: number, y: number} +local function validate(data: any) + local val, err = Point:is(data) + if err == nil then + local p: {x: number, y: number} = val + local sum = p.x + p.y + end +end diff --git a/testdata/fixtures/types/cast-type-is-direct/main.lua b/testdata/fixtures/types/cast-type-is-direct/main.lua new file mode 100644 index 00000000..3cebe637 --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-direct/main.lua @@ -0,0 +1,7 @@ +type Point = {x: number, y: number} +local function validate(data: any) + local _, err = Point:is(data) + if err == nil then + local p: {x: number, y: number} = data + end +end diff --git a/testdata/fixtures/types/cast-type-is-falsy-fail/main.lua b/testdata/fixtures/types/cast-type-is-falsy-fail/main.lua new file mode 100644 index 00000000..d247f331 --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-falsy-fail/main.lua @@ -0,0 +1,7 @@ +type Point = {x: number, y: number} +local function validate(data: any) + local val = Point:is(data) + if not val then + local p: {x: number, y: number} = data -- expect-error + end +end diff --git a/testdata/fixtures/types/cast-type-is-field-access/main.lua b/testdata/fixtures/types/cast-type-is-field-access/main.lua new file mode 100644 index 00000000..b26adcbb --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-field-access/main.lua @@ -0,0 +1,6 @@ +type Point = {x: number, y: number} +local v: any = {x = 1, y = 2} +local p, err = Point:is(v) +if err == nil then + local sum = p.x + p.y +end diff --git a/testdata/fixtures/types/cast-type-is-not-fail/main.lua b/testdata/fixtures/types/cast-type-is-not-fail/main.lua new file mode 100644 index 00000000..a09b9e7c --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-not-fail/main.lua @@ -0,0 +1,6 @@ +type Point = {x: number, y: number} +local function validate(data: any) + if not Point:is(data) then + local p: {x: number, y: number} = data -- expect-error + end +end diff --git a/testdata/fixtures/types/cast-type-is-stored/main.lua b/testdata/fixtures/types/cast-type-is-stored/main.lua new file mode 100644 index 00000000..4866b9ec --- /dev/null +++ b/testdata/fixtures/types/cast-type-is-stored/main.lua @@ -0,0 +1,7 @@ +type Point = {x: number, y: number} +local function validate(data: any) + local val, err = Point:is(data) + if err == nil then + local sum = val.x + val.y + end +end From 25be35917db6ba8277237b2d9dc7d91e39df67ed Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 15:59:47 -0400 Subject: [PATCH 05/10] Add real-world fixtures and system package support Add "packages" field to manifest.json to declare system packages (channel, funcs) that the type checker resolves via predefined manifests. Create 5 real-world multi-file fixtures that exercise patterns found in production wippy code: - channel-select-pattern: event/timer select with narrowing (passes) - error-handling-chain: discriminated union result types (passes) - factory-constructor: OOP factory pattern (skipped - syntax gap) - module-with-generics: generic collection type (skipped - parser gap) - table-builder-pattern: fluent method chaining (skipped - return tracking) Skipped fixtures mark type system hardening targets derived from analyzing 687 :: any casts across 93 production Lua files. --- fixture_harness_test.go | 21 +++++++++ .../channel-select-pattern/handler.lua | 17 +++++++ .../realworld/channel-select-pattern/main.lua | 8 ++++ .../channel-select-pattern/manifest.json | 4 ++ .../channel-select-pattern/types.lua | 13 ++++++ .../realworld/error-handling-chain/errors.lua | 25 +++++++++++ .../realworld/error-handling-chain/main.lua | 13 ++++++ .../error-handling-chain/manifest.json | 3 ++ .../error-handling-chain/validator.lua | 17 +++++++ .../realworld/factory-constructor/counter.lua | 30 +++++++++++++ .../realworld/factory-constructor/main.lua | 14 ++++++ .../factory-constructor/manifest.json | 4 ++ .../module-with-generics/collection.lua | 26 +++++++++++ .../realworld/module-with-generics/main.lua | 13 ++++++ .../module-with-generics/manifest.json | 4 ++ .../table-builder-pattern/config.lua | 45 +++++++++++++++++++ .../realworld/table-builder-pattern/main.lua | 13 ++++++ .../table-builder-pattern/manifest.json | 4 ++ 18 files changed, 274 insertions(+) create mode 100644 testdata/fixtures/realworld/channel-select-pattern/handler.lua create mode 100644 testdata/fixtures/realworld/channel-select-pattern/main.lua create mode 100644 testdata/fixtures/realworld/channel-select-pattern/manifest.json create mode 100644 testdata/fixtures/realworld/channel-select-pattern/types.lua create mode 100644 testdata/fixtures/realworld/error-handling-chain/errors.lua create mode 100644 testdata/fixtures/realworld/error-handling-chain/main.lua create mode 100644 testdata/fixtures/realworld/error-handling-chain/manifest.json create mode 100644 testdata/fixtures/realworld/error-handling-chain/validator.lua create mode 100644 testdata/fixtures/realworld/factory-constructor/counter.lua create mode 100644 testdata/fixtures/realworld/factory-constructor/main.lua create mode 100644 testdata/fixtures/realworld/factory-constructor/manifest.json create mode 100644 testdata/fixtures/realworld/module-with-generics/collection.lua create mode 100644 testdata/fixtures/realworld/module-with-generics/main.lua create mode 100644 testdata/fixtures/realworld/module-with-generics/manifest.json create mode 100644 testdata/fixtures/realworld/table-builder-pattern/config.lua create mode 100644 testdata/fixtures/realworld/table-builder-pattern/main.lua create mode 100644 testdata/fixtures/realworld/table-builder-pattern/manifest.json diff --git a/fixture_harness_test.go b/fixture_harness_test.go index 23cb21df..aca1a58d 100644 --- a/fixture_harness_test.go +++ b/fixture_harness_test.go @@ -13,6 +13,7 @@ import ( "github.com/wippyai/go-lua/compiler/check/tests/testutil" "github.com/wippyai/go-lua/types/diag" + "github.com/wippyai/go-lua/types/io" ) // Suite describes a fixture suite loaded from manifest.json. @@ -20,6 +21,7 @@ type fixtureSuite struct { Description string `json:"description,omitempty"` Files []string `json:"files,omitempty"` Stdlib *bool `json:"stdlib,omitempty"` + Packages []string `json:"packages,omitempty"` // predefined system packages: "channel", "process", "time", "funcs" Check *fixtureCheck `json:"check,omitempty"` Run *fixtureRun `json:"run,omitempty"` Bench *fixtureBench `json:"bench,omitempty"` @@ -177,6 +179,13 @@ func runCheckPhase(t *testing.T, s namedSuite) { if stdlib { baseOpts = append(baseOpts, testutil.WithStdlib()) } + for _, pkg := range s.Suite.Packages { + if m := resolvePackageManifest(pkg); m != nil { + baseOpts = append(baseOpts, testutil.WithManifest(pkg, m)) + } else { + t.Fatalf("unknown system package: %s", pkg) + } + } // Collect all sources and their expectations sources := make(map[string]string) @@ -373,6 +382,18 @@ func runExecPhase(t *testing.T, s namedSuite) { verifyGoldenOutput(t, s, &buf) } +// resolvePackageManifest returns a predefined manifest for a system package name. +func resolvePackageManifest(name string) *io.Manifest { + switch name { + case "channel": + return testutil.ChannelManifest() + case "funcs": + return testutil.FuncsManifest() + default: + return nil + } +} + // installRequire sets up a require() global that loads modules from the given source map. // Modules are compiled, executed, cached, and returned — matching standard Lua require semantics. func installRequire(L *LState, sources map[string]string) { diff --git a/testdata/fixtures/realworld/channel-select-pattern/handler.lua b/testdata/fixtures/realworld/channel-select-pattern/handler.lua new file mode 100644 index 00000000..038ca743 --- /dev/null +++ b/testdata/fixtures/realworld/channel-select-pattern/handler.lua @@ -0,0 +1,17 @@ +local types = require("types") + +type HandlerResult = {ok: boolean, message: string} + +local M = {} + +function M.process_event(event: Event): HandlerResult + if event.error then + return {ok = false, message = "error: " .. tostring(event.error)} + end + if event.kind == "EXIT" then + return {ok = true, message = "exit from " .. event.from} + end + return {ok = true, message = "processed " .. event.kind} +end + +return M diff --git a/testdata/fixtures/realworld/channel-select-pattern/main.lua b/testdata/fixtures/realworld/channel-select-pattern/main.lua new file mode 100644 index 00000000..ec71ae0a --- /dev/null +++ b/testdata/fixtures/realworld/channel-select-pattern/main.lua @@ -0,0 +1,8 @@ +local types = require("types") +local handler = require("handler") + +local event = types.new_event("EXIT", "test") +local result = handler.process_event(event) + +local ok: boolean = result.ok +local msg: string = result.message diff --git a/testdata/fixtures/realworld/channel-select-pattern/manifest.json b/testdata/fixtures/realworld/channel-select-pattern/manifest.json new file mode 100644 index 00000000..a7410cfd --- /dev/null +++ b/testdata/fixtures/realworld/channel-select-pattern/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["types.lua", "handler.lua", "main.lua"], + "packages": ["channel"] +} diff --git a/testdata/fixtures/realworld/channel-select-pattern/types.lua b/testdata/fixtures/realworld/channel-select-pattern/types.lua new file mode 100644 index 00000000..9b96a3db --- /dev/null +++ b/testdata/fixtures/realworld/channel-select-pattern/types.lua @@ -0,0 +1,13 @@ +type Event = {kind: string, from: string, result: any?, error: any?} +type Timer = {elapsed: number} + +type EventCh = Channel +type TimerCh = Channel + +local M = {} + +function M.new_event(kind: string, from: string): Event + return {kind = kind, from = from, result = nil, error = nil} +end + +return M diff --git a/testdata/fixtures/realworld/error-handling-chain/errors.lua b/testdata/fixtures/realworld/error-handling-chain/errors.lua new file mode 100644 index 00000000..affd41bc --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/errors.lua @@ -0,0 +1,25 @@ +type AppError = { + code: string, + message: string, + retryable: boolean +} + +local M = {} + +function M.new(code: string, message: string, retryable: boolean?): AppError + return { + code = code, + message = message, + retryable = retryable or false + } +end + +function M.is_retryable(err: AppError): boolean + return err.retryable +end + +function M.wrap(err: AppError, context: string): AppError + return M.new(err.code, context .. ": " .. err.message, err.retryable) +end + +return M diff --git a/testdata/fixtures/realworld/error-handling-chain/main.lua b/testdata/fixtures/realworld/error-handling-chain/main.lua new file mode 100644 index 00000000..78160916 --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/main.lua @@ -0,0 +1,13 @@ +local errors = require("errors") +local validator = require("validator") + +local result = validator.validate_name("Alice") +if result.ok then + local name: string = result.value +else + local err: AppError = result.error + local wrapped = errors.wrap(err, "registration") + local code: string = wrapped.code + local msg: string = wrapped.message + local retry: boolean = errors.is_retryable(wrapped) +end diff --git a/testdata/fixtures/realworld/error-handling-chain/manifest.json b/testdata/fixtures/realworld/error-handling-chain/manifest.json new file mode 100644 index 00000000..a85abace --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/manifest.json @@ -0,0 +1,3 @@ +{ + "files": ["errors.lua", "validator.lua", "main.lua"] +} diff --git a/testdata/fixtures/realworld/error-handling-chain/validator.lua b/testdata/fixtures/realworld/error-handling-chain/validator.lua new file mode 100644 index 00000000..53026af5 --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/validator.lua @@ -0,0 +1,17 @@ +local errors = require("errors") + +type ValidationResult = {ok: true, value: string} | {ok: false, error: AppError} + +local M = {} + +function M.validate_name(input: string): ValidationResult + if #input == 0 then + return {ok = false, error = errors.new("EMPTY", "name is empty")} + end + if #input > 100 then + return {ok = false, error = errors.new("TOO_LONG", "name exceeds 100 chars")} + end + return {ok = true, value = input} +end + +return M diff --git a/testdata/fixtures/realworld/factory-constructor/counter.lua b/testdata/fixtures/realworld/factory-constructor/counter.lua new file mode 100644 index 00000000..bdd6be2d --- /dev/null +++ b/testdata/fixtures/realworld/factory-constructor/counter.lua @@ -0,0 +1,30 @@ +type Counter = { + count: number, + increment: (self: Counter) -> (), + decrement: (self: Counter) -> (), + get: (self: Counter) -> number, + reset: (self: Counter) -> () +} + +local M = {} + +function M.new(initial: number?): Counter + local c = { + count = initial or 0, + increment = function(self: Counter) + self.count = self.count + 1 + end, + decrement = function(self: Counter) + self.count = self.count - 1 + end, + get = function(self: Counter): number + return self.count + end, + reset = function(self: Counter) + self.count = 0 + end + } + return c +end + +return M diff --git a/testdata/fixtures/realworld/factory-constructor/main.lua b/testdata/fixtures/realworld/factory-constructor/main.lua new file mode 100644 index 00000000..212c093c --- /dev/null +++ b/testdata/fixtures/realworld/factory-constructor/main.lua @@ -0,0 +1,14 @@ +local counter = require("counter") + +local c = counter.new() +c:increment() +c:increment() +c:increment() +c:decrement() + +local count: number = c:get() +c:reset() +local zero: number = c:get() + +local c2 = counter.new(10) +local ten: number = c2:get() diff --git a/testdata/fixtures/realworld/factory-constructor/manifest.json b/testdata/fixtures/realworld/factory-constructor/manifest.json new file mode 100644 index 00000000..90f11b95 --- /dev/null +++ b/testdata/fixtures/realworld/factory-constructor/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["counter.lua", "main.lua"], + "check": {"skip": "factory return type not tracked cross-module: counter.lua has syntax issue with () -> () in record field"} +} diff --git a/testdata/fixtures/realworld/module-with-generics/collection.lua b/testdata/fixtures/realworld/module-with-generics/collection.lua new file mode 100644 index 00000000..470be39e --- /dev/null +++ b/testdata/fixtures/realworld/module-with-generics/collection.lua @@ -0,0 +1,26 @@ +type Collection = { + items: {T}, + count: (self: Collection) -> number, + get: (self: Collection, index: integer) -> T?, + add: (self: Collection, item: T) -> () +} + +local M = {} + +function M.new(): Collection + local c: Collection = { + items = {}, + count = function(self: Collection): number + return #self.items + end, + get = function(self: Collection, index: integer): T? + return self.items[index] + end, + add = function(self: Collection, item: T) + table.insert(self.items, item) + end + } + return c +end + +return M diff --git a/testdata/fixtures/realworld/module-with-generics/main.lua b/testdata/fixtures/realworld/module-with-generics/main.lua new file mode 100644 index 00000000..793b9b4c --- /dev/null +++ b/testdata/fixtures/realworld/module-with-generics/main.lua @@ -0,0 +1,13 @@ +local collection = require("collection") + +local nums = collection.new() +nums:add(1) +nums:add(2) +nums:add(3) +local count: number = nums:count() +local first = nums:get(1) + +local names = collection.new() +names:add("alice") +names:add("bob") +local name_count: number = names:count() diff --git a/testdata/fixtures/realworld/module-with-generics/manifest.json b/testdata/fixtures/realworld/module-with-generics/manifest.json new file mode 100644 index 00000000..39415313 --- /dev/null +++ b/testdata/fixtures/realworld/module-with-generics/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["collection.lua", "main.lua"], + "check": {"skip": "generic record type definition syntax not fully supported in parser"} +} diff --git a/testdata/fixtures/realworld/table-builder-pattern/config.lua b/testdata/fixtures/realworld/table-builder-pattern/config.lua new file mode 100644 index 00000000..aad3c555 --- /dev/null +++ b/testdata/fixtures/realworld/table-builder-pattern/config.lua @@ -0,0 +1,45 @@ +type Config = { + host: string, + port: number, + debug: boolean, + tags: {string} +} + +type ConfigBuilder = { + _config: Config, + host: (self: ConfigBuilder, h: string) -> ConfigBuilder, + port: (self: ConfigBuilder, p: number) -> ConfigBuilder, + debug: (self: ConfigBuilder, d: boolean) -> ConfigBuilder, + tag: (self: ConfigBuilder, t: string) -> ConfigBuilder, + build: (self: ConfigBuilder) -> Config +} + +local M = {} + +function M.new(): ConfigBuilder + local builder: ConfigBuilder = { + _config = {host = "localhost", port = 8080, debug = false, tags = {}}, + host = function(self: ConfigBuilder, h: string): ConfigBuilder + self._config.host = h + return self + end, + port = function(self: ConfigBuilder, p: number): ConfigBuilder + self._config.port = p + return self + end, + debug = function(self: ConfigBuilder, d: boolean): ConfigBuilder + self._config.debug = d + return self + end, + tag = function(self: ConfigBuilder, t: string): ConfigBuilder + table.insert(self._config.tags, t) + return self + end, + build = function(self: ConfigBuilder): Config + return self._config + end + } + return builder +end + +return M diff --git a/testdata/fixtures/realworld/table-builder-pattern/main.lua b/testdata/fixtures/realworld/table-builder-pattern/main.lua new file mode 100644 index 00000000..dba01ccc --- /dev/null +++ b/testdata/fixtures/realworld/table-builder-pattern/main.lua @@ -0,0 +1,13 @@ +local config = require("config") + +local cfg = config.new() + :host("example.com") + :port(9090) + :debug(true) + :tag("production") + :tag("v2") + :build() + +local host: string = cfg.host +local port: number = cfg.port +local debug_mode: boolean = cfg.debug diff --git a/testdata/fixtures/realworld/table-builder-pattern/manifest.json b/testdata/fixtures/realworld/table-builder-pattern/manifest.json new file mode 100644 index 00000000..8c1acfa8 --- /dev/null +++ b/testdata/fixtures/realworld/table-builder-pattern/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["config.lua", "main.lua"], + "check": {"skip": "builder chain loses return type through method chaining"} +} From 1e2c1d9ad5ac2d6071121a1fd8ed89593af4f54c Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 16:00:15 -0400 Subject: [PATCH 06/10] Replace skip with error count assertions for type system gaps Use check.errors count instead of skip for real-world fixtures with known type system limitations. This ensures gaps are actively tracked and tests fail when fixes reduce the error count, preventing regression and making progress visible. --- testdata/fixtures/realworld/factory-constructor/manifest.json | 2 +- testdata/fixtures/realworld/module-with-generics/manifest.json | 2 +- testdata/fixtures/realworld/table-builder-pattern/manifest.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testdata/fixtures/realworld/factory-constructor/manifest.json b/testdata/fixtures/realworld/factory-constructor/manifest.json index 90f11b95..de77c027 100644 --- a/testdata/fixtures/realworld/factory-constructor/manifest.json +++ b/testdata/fixtures/realworld/factory-constructor/manifest.json @@ -1,4 +1,4 @@ { "files": ["counter.lua", "main.lua"], - "check": {"skip": "factory return type not tracked cross-module: counter.lua has syntax issue with () -> () in record field"} + "check": {"errors": 6} } diff --git a/testdata/fixtures/realworld/module-with-generics/manifest.json b/testdata/fixtures/realworld/module-with-generics/manifest.json index 39415313..ab74a8cf 100644 --- a/testdata/fixtures/realworld/module-with-generics/manifest.json +++ b/testdata/fixtures/realworld/module-with-generics/manifest.json @@ -1,4 +1,4 @@ { "files": ["collection.lua", "main.lua"], - "check": {"skip": "generic record type definition syntax not fully supported in parser"} + "check": {"errors": 5} } diff --git a/testdata/fixtures/realworld/table-builder-pattern/manifest.json b/testdata/fixtures/realworld/table-builder-pattern/manifest.json index 8c1acfa8..325eda3c 100644 --- a/testdata/fixtures/realworld/table-builder-pattern/manifest.json +++ b/testdata/fixtures/realworld/table-builder-pattern/manifest.json @@ -1,4 +1,4 @@ { "files": ["config.lua", "main.lua"], - "check": {"skip": "builder chain loses return type through method chaining"} + "check": {"errors": 3} } From 60f73dd241e4f79402faa2fac1f467f9b22ae026 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 16:10:09 -0400 Subject: [PATCH 07/10] Retrofit method calls, string methods, assert narrowing, error patterns Migrate method_calls_test.go (39 cases) covering string methods, table patterns, pcall/xpcall, goto/break, higher-order functions, closures, and variadic functions to fixtures. Migrate assert_narrowing_test.go (26 cases) covering assert narrowing, custom assert libraries, error-return patterns, assert wrapper propagation, and inferred contracts. Remove optional_narrowing_test.go (duplicate of already-migrated cases). --- .../tests/functions/method_calls_test.go | 491 ------------------ .../tests/narrowing/assert_narrowing_test.go | 429 --------------- .../narrowing/optional_narrowing_test.go | 345 ------------ .../break-in-function-inside-loop/main.lua | 3 + .../functions/break-outside-loop/main.lua | 1 + .../callback-closure-counter/main.lua | 9 + .../functions/closure-captures-local/main.lua | 5 + .../closure-modifies-captured/main.lua | 10 + .../closure-nested-3-levels/main.lua | 8 + .../functions/curried-function/main.lua | 7 + .../functions/custom-method-self/main.lua | 7 + .../functions/function-composition/main.lua | 9 + .../fixtures/functions/goto-backward/main.lua | 2 + .../functions/goto-duplicate-label/main.lua | 2 + .../fixtures/functions/goto-forward/main.lua | 2 + .../functions/goto-undefined/main.lua | 1 + .../functions/higher-order-map/main.lua | 8 + .../functions/method-on-union-fails/main.lua | 3 + .../functions/mixed-array-fails/main.lua | 1 + .../functions/nested-table-access/main.lua | 7 + .../functions/pcall-returns-boolean/main.lua | 2 + .../functions/pcall-with-args/main.lua | 2 + .../functions/string-byte-method/main.lua | 3 + .../functions/string-chained-methods/main.lua | 3 + .../functions/string-find-method/main.lua | 3 + .../functions/string-format-method/main.lua | 3 + .../functions/string-gsub-method/main.lua | 3 + .../functions/string-len-method/main.lua | 3 + .../string-literal-receiver-sub/main.lua | 4 + .../functions/string-match-method/main.lua | 4 + .../functions/string-method-chain/main.lua | 2 + .../fixtures/functions/string-method/main.lua | 2 + .../functions/string-rep-method/main.lua | 3 + .../functions/string-reverse-method/main.lua | 3 + .../functions/string-sub-method/main.lua | 3 + .../table-concat-returns-string/main.lua | 2 + .../table-insert-preserves-type/main.lua | 4 + .../table-remove-returns-element/main.lua | 2 + .../functions/variadic-select/main.lua | 5 + .../fixtures/functions/variadic-sum/main.lua | 8 + .../variadic-with-fixed-params/main.lua | 4 + .../functions/xpcall-with-handler/main.lua | 2 + .../narrowing/assert-lib-chained/main.lua | 11 + .../narrowing/assert-lib-field-path/main.lua | 9 + .../assert-lib-is-nil-inverse/main.lua | 9 + .../narrowing/assert-lib-not-nil/main.lua | 9 + .../assert-lib-terminates-flow/main.lua | 9 + .../narrowing/assert-narrows-truthy/main.lua | 4 + .../assert-with-condition-gt/main.lua | 3 + .../narrowing/assert-with-message/main.lua | 4 + .../narrowing/error-propagation/main.lua | 10 + .../narrowing/error-return-multiple/main.lua | 11 + .../narrowing/error-return-pattern/main.lua | 8 + .../error-return-without-check/main.lua | 5 + .../narrowing/error-terminates-flow/main.lua | 6 + .../inferred-assert-eq-intersection/main.lua | 7 + .../inferred-conditional-error/main.lua | 9 + .../inferred-error-all-branches/main.lua | 9 + .../inferred-error-terminates/main.lua | 9 + .../inferred-return-non-nil/main.lua | 7 + .../narrowing/wrapper-calling-assert/main.lua | 7 + .../narrowing/wrapper-chained/main.lua | 9 + .../wrapper-conditional-fails/main.lua | 9 + .../narrowing/wrapper-custom-message/main.lua | 7 + .../narrowing/wrapper-field-path/main.lua | 7 + .../narrowing/wrapper-module-table/main.lua | 8 + .../narrowing/wrapper-nested-calls/main.lua | 10 + .../wrapper-without-assert-fails/main.lua | 9 + 68 files changed, 360 insertions(+), 1265 deletions(-) delete mode 100644 compiler/check/tests/functions/method_calls_test.go delete mode 100644 compiler/check/tests/narrowing/assert_narrowing_test.go delete mode 100644 compiler/check/tests/narrowing/optional_narrowing_test.go create mode 100644 testdata/fixtures/functions/break-in-function-inside-loop/main.lua create mode 100644 testdata/fixtures/functions/break-outside-loop/main.lua create mode 100644 testdata/fixtures/functions/callback-closure-counter/main.lua create mode 100644 testdata/fixtures/functions/closure-captures-local/main.lua create mode 100644 testdata/fixtures/functions/closure-modifies-captured/main.lua create mode 100644 testdata/fixtures/functions/closure-nested-3-levels/main.lua create mode 100644 testdata/fixtures/functions/curried-function/main.lua create mode 100644 testdata/fixtures/functions/custom-method-self/main.lua create mode 100644 testdata/fixtures/functions/function-composition/main.lua create mode 100644 testdata/fixtures/functions/goto-backward/main.lua create mode 100644 testdata/fixtures/functions/goto-duplicate-label/main.lua create mode 100644 testdata/fixtures/functions/goto-forward/main.lua create mode 100644 testdata/fixtures/functions/goto-undefined/main.lua create mode 100644 testdata/fixtures/functions/higher-order-map/main.lua create mode 100644 testdata/fixtures/functions/method-on-union-fails/main.lua create mode 100644 testdata/fixtures/functions/mixed-array-fails/main.lua create mode 100644 testdata/fixtures/functions/nested-table-access/main.lua create mode 100644 testdata/fixtures/functions/pcall-returns-boolean/main.lua create mode 100644 testdata/fixtures/functions/pcall-with-args/main.lua create mode 100644 testdata/fixtures/functions/string-byte-method/main.lua create mode 100644 testdata/fixtures/functions/string-chained-methods/main.lua create mode 100644 testdata/fixtures/functions/string-find-method/main.lua create mode 100644 testdata/fixtures/functions/string-format-method/main.lua create mode 100644 testdata/fixtures/functions/string-gsub-method/main.lua create mode 100644 testdata/fixtures/functions/string-len-method/main.lua create mode 100644 testdata/fixtures/functions/string-literal-receiver-sub/main.lua create mode 100644 testdata/fixtures/functions/string-match-method/main.lua create mode 100644 testdata/fixtures/functions/string-method-chain/main.lua create mode 100644 testdata/fixtures/functions/string-method/main.lua create mode 100644 testdata/fixtures/functions/string-rep-method/main.lua create mode 100644 testdata/fixtures/functions/string-reverse-method/main.lua create mode 100644 testdata/fixtures/functions/string-sub-method/main.lua create mode 100644 testdata/fixtures/functions/table-concat-returns-string/main.lua create mode 100644 testdata/fixtures/functions/table-insert-preserves-type/main.lua create mode 100644 testdata/fixtures/functions/table-remove-returns-element/main.lua create mode 100644 testdata/fixtures/functions/variadic-select/main.lua create mode 100644 testdata/fixtures/functions/variadic-sum/main.lua create mode 100644 testdata/fixtures/functions/variadic-with-fixed-params/main.lua create mode 100644 testdata/fixtures/functions/xpcall-with-handler/main.lua create mode 100644 testdata/fixtures/narrowing/assert-lib-chained/main.lua create mode 100644 testdata/fixtures/narrowing/assert-lib-field-path/main.lua create mode 100644 testdata/fixtures/narrowing/assert-lib-is-nil-inverse/main.lua create mode 100644 testdata/fixtures/narrowing/assert-lib-not-nil/main.lua create mode 100644 testdata/fixtures/narrowing/assert-lib-terminates-flow/main.lua create mode 100644 testdata/fixtures/narrowing/assert-narrows-truthy/main.lua create mode 100644 testdata/fixtures/narrowing/assert-with-condition-gt/main.lua create mode 100644 testdata/fixtures/narrowing/assert-with-message/main.lua create mode 100644 testdata/fixtures/narrowing/error-propagation/main.lua create mode 100644 testdata/fixtures/narrowing/error-return-multiple/main.lua create mode 100644 testdata/fixtures/narrowing/error-return-pattern/main.lua create mode 100644 testdata/fixtures/narrowing/error-return-without-check/main.lua create mode 100644 testdata/fixtures/narrowing/error-terminates-flow/main.lua create mode 100644 testdata/fixtures/narrowing/inferred-assert-eq-intersection/main.lua create mode 100644 testdata/fixtures/narrowing/inferred-conditional-error/main.lua create mode 100644 testdata/fixtures/narrowing/inferred-error-all-branches/main.lua create mode 100644 testdata/fixtures/narrowing/inferred-error-terminates/main.lua create mode 100644 testdata/fixtures/narrowing/inferred-return-non-nil/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-calling-assert/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-chained/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-conditional-fails/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-custom-message/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-field-path/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-module-table/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-nested-calls/main.lua create mode 100644 testdata/fixtures/narrowing/wrapper-without-assert-fails/main.lua diff --git a/compiler/check/tests/functions/method_calls_test.go b/compiler/check/tests/functions/method_calls_test.go deleted file mode 100644 index 4674d4d4..00000000 --- a/compiler/check/tests/functions/method_calls_test.go +++ /dev/null @@ -1,491 +0,0 @@ -package functions - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestMethodCalls tests method call syntax and type checking. -func TestMethodCalls(t *testing.T) { - tests := []testutil.Case{ - { - Name: "string method", - Code: ` - local s = "hello" - local u: string = s:upper() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string method chain", - Code: ` - local s = " hello " - local result: string = s:gsub(" ", ""):upper() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "custom method with self", - Code: ` - local obj = { - value = 0, - increment = function(self) - self.value = self.value + 1 - end - } - obj:increment() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "method on union requires narrowing", - Code: ` - function f(x: string | number) - x:upper() - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestTablePatterns tests common table usage patterns. -func TestTablePatterns(t *testing.T) { - tests := []testutil.Case{ - { - Name: "nested table access", - Code: ` - local config = { - server = { - host = "localhost", - port = 8080 - } - } - local port: number = config.server.port - `, - WantError: false, - Stdlib: true, - }, - { - Name: "table insert preserves type", - Code: ` - local arr: {number} = {} - table.insert(arr, 1) - table.insert(arr, 2) - local n: number = arr[1] - `, - WantError: false, - Stdlib: true, - }, - { - Name: "table remove returns element", - Code: ` - local arr: {string} = {"a", "b"} - local s: string? = table.remove(arr) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "table concat returns string", - Code: ` - local arr = {"a", "b", "c"} - local s: string = table.concat(arr, ",") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "mixed array fails", - Code: ` - local arr: {number} = {1, "two", 3} - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestPcallXpcall tests protected call functions. -func TestPcallXpcall(t *testing.T) { - tests := []testutil.Case{ - { - Name: "pcall returns boolean and results", - Code: ` - local ok, result = pcall(function() return 42 end) - local b: boolean = ok - `, - WantError: false, - Stdlib: true, - }, - { - Name: "pcall with args", - Code: ` - local ok, result = pcall(tostring, 42) - local b: boolean = ok - `, - WantError: false, - Stdlib: true, - }, - { - Name: "xpcall with handler", - Code: ` - local ok, result = xpcall(function() return "test" end, function(err) return err end) - local b: boolean = ok - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestGotoLabel tests goto and label statements. -func TestGotoLabel(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple goto forward", - Code: ` - goto target - ::target:: - `, - WantError: false, - Stdlib: true, - }, - { - Name: "goto backward", - Code: ` - ::start:: - goto start - `, - WantError: false, - Stdlib: true, - }, - { - Name: "goto undefined label", - Code: ` - goto undefined - `, - WantError: true, - Stdlib: true, - }, - { - Name: "duplicate label", - Code: ` - ::dup:: - ::dup:: - `, - WantError: true, - Stdlib: true, - }, - { - Name: "break outside loop", - Code: ` - break - `, - WantError: true, - Stdlib: true, - }, - { - Name: "break in function inside loop", - Code: ` - while true do - local f = function() break end - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestHigherOrderFunctions tests functions that take or return functions. -func TestHigherOrderFunctions(t *testing.T) { - tests := []testutil.Case{ - { - Name: "map function", - Code: ` - local function map(arr: {number}, fn: (number) -> number): {number} - local result: {number} = {} - for i, v in ipairs(arr) do - result[i] = fn(v) - end - return result - end - local doubled = map({1, 2, 3}, function(x: number): number return x * 2 end) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "callback with closure", - Code: ` - local function createCounter(): () -> number - local count = 0 - return function(): number - count = count + 1 - return count - end - end - local counter = createCounter() - local first: number = counter() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "function composition", - Code: ` - local function compose(f: (number) -> number, g: (number) -> number): (number) -> number - return function(x: number): number - return f(g(x)) - end - end - local function double(x: number): number return x * 2 end - local function addOne(x: number): number return x + 1 end - local composed = compose(double, addOne) - local result: number = composed(5) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "curried function", - Code: ` - local function createAdder(x: number): (number) -> number - return function(y: number): number - return x + y - end - end - local add5 = createAdder(5) - local result: number = add5(3) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestClosures tests closure behavior. -func TestClosures(t *testing.T) { - tests := []testutil.Case{ - { - Name: "closure captures local", - Code: ` - local x = 10 - local function getX(): number - return x - end - local result: number = getX() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "closure modifies captured", - Code: ` - local function counter() - local count = 0 - return { - inc = function() count = count + 1 end, - get = function(): number return count end - } - end - local c = counter() - c.inc() - local val: number = c.get() - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested closure 3 levels", - Code: ` - local function outer(a: number): (number) -> (number) -> number - return function(b: number): (number) -> number - return function(c: number): number - return a + b + c - end - end - end - local result: number = outer(1)(2)(3) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestStringMethods tests string metatable method calls. -func TestStringMethods(t *testing.T) { - tests := []testutil.Case{ - { - Name: "string_match_method", - Code: ` - local function parse_pair(s: string): (string, string) - local k, v = s:match("(%w+)=(%w+)") - return k or "", v or "" - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_sub_method", - Code: ` - local function first_char(s: string): string - return s:sub(1, 1) - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_literal_receiver_sub_method", - Code: ` - local function first_char_literal(): string - local s = "hello" - return s:sub(1, 1) - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_gsub_method", - Code: ` - local function normalize(s: string): string - return s:gsub("%s+", " ") - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_find_method", - Code: ` - local function contains(s: string, pattern: string): boolean - return s:find(pattern) ~= nil - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_len_method", - Code: ` - local function is_empty(s: string): boolean - return s:len() == 0 - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_format_method", - Code: ` - local function fmt(template: string, value: number): string - return template:format(value) - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "chained_string_methods", - Code: ` - local function clean(s: string): string - return s:lower():gsub("%s+", "") - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_rep_method", - Code: ` - local function repeat_str(s: string, n: integer): string - return s:rep(n) - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_reverse_method", - Code: ` - local function reverse(s: string): string - return s:reverse() - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "string_byte_method", - Code: ` - local function first_byte(s: string): number - return s:byte(1) - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestVariadicFunctions tests variadic function definitions. -func TestVariadicFunctions(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple variadic", - Code: ` - local function sum(...: number): number - local result = 0 - for _, v in ipairs({...}) do - result = result + v - end - return result - end - local total: number = sum(1, 2, 3) - `, - WantError: false, - Stdlib: true, - }, - { - Name: "variadic with fixed params", - Code: ` - local function printf(fmt: string, ...: any) - print(string.format(fmt, ...)) - end - printf("Hello %s", "world") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "select with variadic", - Code: ` - local function test(...: number) - local count = select("#", ...) - local first = select(1, ...) - end - test(1, 2, 3) - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/narrowing/assert_narrowing_test.go b/compiler/check/tests/narrowing/assert_narrowing_test.go deleted file mode 100644 index afdd3261..00000000 --- a/compiler/check/tests/narrowing/assert_narrowing_test.go +++ /dev/null @@ -1,429 +0,0 @@ -package narrowing - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestAssertNarrowing tests assert function narrowing. -func TestAssertNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "assert narrows truthy", - Code: ` - function f(x: string?) - assert(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert with message", - Code: ` - function f(x: number?) - assert(x, "x must not be nil") - local n: number = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert with condition", - Code: ` - function f(x: number) - assert(x > 0, "x must be positive") - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "error terminates flow", - Code: ` - function f(x: string?): string - if x == nil then - error("x is nil") - end - return x - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestAssertLibraryNarrowing tests custom assert library patterns. -func TestAssertLibraryNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "assert.not_nil narrows", - Code: ` - local assert = { - not_nil = function(val: any, msg: string?) - if val == nil then error(msg or "assertion failed") end - end - } - function process(x: string?) - assert.not_nil(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert.is_nil narrows inverse", - Code: ` - local assert = { - is_nil = function(val: any, msg: string?) - if val ~= nil then error(msg or "expected nil") end - end - } - function process(x: string?, err: string?) - assert.is_nil(err) - local s: string = x - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "assert function terminates flow", - Code: ` - local assert = { - not_nil = function(val: any, msg: string?) - if val == nil then error(msg or "nil") end - end - } - function getOrFail(x: string?): string - assert.not_nil(x, "x must not be nil") - return x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "chained assertions", - Code: ` - local assert = { - not_nil = function(val: any, msg: string?) - if val == nil then error(msg or "nil") end - end - } - function process(a: string?, b: number?) - assert.not_nil(a, "a") - assert.not_nil(b, "b") - local s: string = a - local n: number = b - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert on field path", - Code: ` - local assert = { - not_nil = function(val: any, msg: string?) - if val == nil then error(msg or "nil") end - end - } - function process(obj: {stream: {read: () -> string}?}) - assert.not_nil(obj.stream) - local s: string = obj.stream:read() - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestErrorReturnPattern tests the result, err return pattern. -func TestErrorReturnPattern(t *testing.T) { - tests := []testutil.Case{ - { - Name: "result, err pattern with check", - Code: ` - local function getData(): (string?, string?) - return "data", nil - end - local data, err = getData() - if err then - error(err) - end - local s: string = data - `, - WantError: false, - Stdlib: true, - }, - { - Name: "result, err pattern without check fails", - Code: ` - local function getData(): (string?, string?) - return "data", nil - end - local data, err = getData() - local s: string = data - `, - WantError: true, - Stdlib: true, - }, - { - Name: "multiple error returns", - Code: ` - local function process(x: number): (number?, string?) - if x < 0 then - return nil, "negative" - end - return x * 2, nil - end - local result, err = process(5) - if err ~= nil then - return - end - local n: number = result - `, - WantError: false, - Stdlib: true, - }, - { - Name: "error propagation", - Code: ` - local function inner(): (string?, string?) - return nil, "error" - end - local function outer(): (string?, string?) - local result, err = inner() - if err ~= nil then - return nil, err - end - return result, nil - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestAssertWrapperNarrowing tests that functions wrapping native assert() propagate constraints. -func TestAssertWrapperNarrowing(t *testing.T) { - tests := []testutil.Case{ - { - Name: "wrapper calling assert narrows", - Code: ` - local function assertNotNil(val: any) - assert(val, "value must not be nil") - end - function process(x: string?) - assertNotNil(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrapper with custom message narrows", - Code: ` - local function must(val: any, msg: string) - assert(val, "must: " .. msg) - end - function process(x: number?) - must(x, "x is required") - local n: number = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrapper on field path narrows", - Code: ` - local function assertNotNil(val: any) - assert(val, "value must not be nil") - end - function process(obj: {data: string?}) - assertNotNil(obj.data) - local s: string = obj.data - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "chained wrappers narrow", - Code: ` - local function assertNotNil(val: any) - assert(val, "not nil") - end - function process(a: string?, b: number?) - assertNotNil(a) - assertNotNil(b) - local s: string = a - local n: number = b - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrapper in module table narrows", - Code: ` - local check = {} - function check.notNil(val: any, msg: string?) - assert(val, msg or "value is nil") - end - function process(x: string?) - check.notNil(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested wrapper calls narrow", - Code: ` - local function innerAssert(val: any, msg: string) - assert(val, msg) - end - local function outerAssert(val: any) - innerAssert(val, "outer: value is nil") - end - function process(x: string?) - outerAssert(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "wrapper without assert does not narrow", - Code: ` - local function maybeCheck(val: any) - if val == nil then - print("warning: nil value") - end - end - function process(x: string?) - maybeCheck(x) - local s: string = x - end - `, - WantError: true, - Stdlib: true, - }, - { - Name: "conditional wrapper does not narrow", - Code: ` - local function conditionalAssert(val: any, check: boolean) - if check then - assert(val, "value is nil") - end - end - function process(x: string?) - conditionalAssert(x, true) - local s: string = x - end - `, - WantError: true, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestInferredContracts tests that functions infer narrowing effects. -func TestInferredContracts(t *testing.T) { - tests := []testutil.Case{ - { - Name: "function with internal error infers termination", - Code: ` - local function assertNotNil(val: any) - if val == nil then - error("value is nil") - end - end - function process(x: string?) - assertNotNil(x) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "function returning non-nil infers return type", - Code: ` - local function getOrDefault(val: string?, default: string): string - if val == nil then - return default - end - return val - end - local s: string = getOrDefault(nil, "default") - `, - WantError: false, - Stdlib: true, - }, - { - Name: "error in all branches terminates", - Code: ` - local function fail(msg: string) - error(msg) - end - function process(x: string?): string - if x == nil then - fail("x is nil") - end - return x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "conditional error does not fully terminate", - Code: ` - local function maybeError(cond: boolean) - if cond then - error("condition was true") - end - end - function process(x: string?) - maybeError(x == nil) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "assert eq narrows to intersection", - Code: ` - local function assertEq(a: any, b: any) - if a ~= b then error("not equal") end - end - function process(x: string | number, y: string) - assertEq(x, y) - local s: string = x - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/compiler/check/tests/narrowing/optional_narrowing_test.go b/compiler/check/tests/narrowing/optional_narrowing_test.go deleted file mode 100644 index 70138b12..00000000 --- a/compiler/check/tests/narrowing/optional_narrowing_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package narrowing - -import ( - "testing" - - "github.com/wippyai/go-lua/compiler/check/tests/testutil" -) - -// TestOptionalNarrowing_NestedIf tests that optional types are narrowed correctly in nested if blocks. -func TestOptionalNarrowing_NestedIf(t *testing.T) { - tests := []testutil.Case{ - { - Name: "simple if narrows optional record", - Code: ` - type Error = {kind: string, message: string} - local function test(): Error? - return {kind = "test", message = "msg"} - end - local err = test() - if err then - local msg = err.message - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested if preserves narrowing record", - Code: ` - type Error = {kind: string, message: string} - local function test(): Error? - return {kind = "test", message = "msg"} - end - local err = test() - local flag = true - if err then - if flag then - local msg = err.message - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "simple if narrows optional for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} - local function test(): Error? - return nil - end - local err = test() - if err then - local msg = err:message() - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "nested if preserves narrowing for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string} - local function test(): Error? - return nil - end - local err = test() - local flag = true - if err then - if flag then - local msg = err:message() - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "deeply nested if preserves narrowing for method call", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} - local function test(): Error? - return nil - end - local err = test() - local a, b, c = true, true, true - if err then - if a then - if b then - if c then - local k = err:kind() - local m = err:message() - local r = err:retryable() - end - end - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "multiple method calls after nil check", - Code: ` - type Error = {kind: (self: Error) -> string, message: (self: Error) -> string, retryable: (self: Error) -> boolean} - local function test(): Error? - return nil - end - local err = test() - if err then - local kind = err:kind() - if kind == "network" then - local retryable = err:retryable() - local message = err:message() - end - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} - -// TestUnionNarrowingE2E tests union narrowing patterns. -func TestUnionNarrowingE2E(t *testing.T) { - tests := []testutil.Case{ - { - Name: "nested field access after union narrowing", - Code: ` - type ChanInt = {__tag: "int"} - type ChanStr = {__tag: "str"} - type SelResult = - {channel: ChanInt, value: {error: string}, ok: boolean} | - {channel: ChanStr, value: {data: number}, ok: boolean} - - function get_result(a: ChanInt, b: ChanStr): SelResult - return {channel = a, value = {error = "oops"}, ok = true} - end - - function f(ch1: ChanInt, ch2: ChanStr) - local result = get_result(ch1, ch2) - if result.channel == ch1 then - local e: string = result.value.error - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "field equality narrows union", - Code: ` - type A = {kind: "a", value_a: string} - type B = {kind: "b", value_b: number} - type AB = A | B - - function f(x: AB) - if x.kind == "a" then - local v: string = x.value_a - end - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "local assign from narrowed union", - Code: ` - type EventCh = {__tag: "event"} - type TimeoutCh = {__tag: "timeout"} - type Event = {kind: string, error: string?} - type Time = {sec: number} - - type Result = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimeoutCh, value: Time, ok: boolean} - - function get_result(ch: EventCh, timeout: TimeoutCh): Result - return {channel = ch, value = {kind = "exit", error = nil}, ok = true} - end - - function f(events_ch: EventCh, timeout_ch: TimeoutCh) - local result = get_result(events_ch, timeout_ch) - if result.channel ~= events_ch then - return false, "timeout" - end - local event = result.value - local k: string = event.kind - if event.error then - local e: string = event.error - end - return true - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "method call after narrowing from union", - Code: ` - type Message = { - _topic: string, - _data: any, - topic: (self: Message) -> string, - payload: (self: Message) -> any - } - - type Timer = {elapsed: number} - - type MsgCh = {__tag: "msg"} - type TimerCh = {__tag: "timer"} - - type Result = {channel: MsgCh, value: Message, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function select_fn(msg_ch: MsgCh, timer_ch: TimerCh): Result - return {channel = msg_ch, value = {_topic = "test", _data = nil, topic = function(s) return s._topic end, payload = function(s) return s._data end}, ok = true} - end - - function f(msg_ch: MsgCh, timer_ch: TimerCh) - local result = select_fn(msg_ch, timer_ch) - if result.channel == timer_ch then - return nil, "timeout" - end - local msg = result.value - local topic: string = msg:topic() - local data = msg:payload() - return topic, data - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "timeout check pattern", - Code: ` - type Event = {kind: string, from: string, result: any?, error: any?} - type Timer = {elapsed: number} - - type EventCh = {__tag: "event"} - type TimerCh = {__tag: "timer"} - - type SelectResult = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function do_select(events: EventCh, timeout: TimerCh): SelectResult - return {channel = events, value = {kind = "EXIT", from = "test", result = nil, error = nil}, ok = true} - end - - function f(events_ch: EventCh) - local timeout: TimerCh = {__tag = "timer"} - local result = do_select(events_ch, timeout) - - if result.channel == timeout then - return false, "timeout" - end - - local event = result.value - if event.kind ~= "EXIT" then - return false, "wrong event" - end - if event.error then - return false, "error: " .. event.error - end - return true - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "multiple channels select", - Code: ` - type Message = { - _topic: string, - topic: (self: Message) -> string - } - - type Event = {kind: string, from: string} - type Timer = {elapsed: number} - - type MsgCh = {__tag: "msg"} - type EventCh = {__tag: "event"} - type TimerCh = {__tag: "timer"} - - type Result = {channel: MsgCh, value: Message, ok: boolean} | - {channel: EventCh, value: Event, ok: boolean} | - {channel: TimerCh, value: Timer, ok: boolean} - - function do_select(m: MsgCh, e: EventCh, t: TimerCh): Result - return {channel = m, value = {_topic = "test", topic = function(self) return self._topic end}, ok = true} - end - - function f(msg_ch: MsgCh, events_ch: EventCh, timeout: TimerCh) - local result = do_select(msg_ch, events_ch, timeout) - - if result.channel == timeout then - return nil, "timeout" - end - - if result.channel == events_ch then - local event = result.value - local k: string = event.kind - return "event", k - end - - local msg = result.value - local topic: string = msg:topic() - return "message", topic - end - `, - WantError: false, - Stdlib: true, - }, - { - Name: "negated condition narrowing", - Code: ` - type EventCh = {__tag: "event"} - type TimeoutCh = {__tag: "timeout"} - type Event = {kind: string} - type Time = {sec: number} - - type Result = {channel: EventCh, value: Event, ok: boolean} | - {channel: TimeoutCh, value: Time, ok: boolean} - - function get_result(ch: EventCh, timeout: TimeoutCh): Result - return {channel = ch, value = {kind = "exit"}, ok = true} - end - - function f(events_ch: EventCh, timeout_ch: TimeoutCh) - local result = get_result(events_ch, timeout_ch) - if result.channel ~= events_ch then - local t: Time = result.value - return false - end - local event: Event = result.value - return true - end - `, - WantError: false, - Stdlib: true, - }, - } - testutil.RunCases(t, tests) -} diff --git a/testdata/fixtures/functions/break-in-function-inside-loop/main.lua b/testdata/fixtures/functions/break-in-function-inside-loop/main.lua new file mode 100644 index 00000000..1ceae6df --- /dev/null +++ b/testdata/fixtures/functions/break-in-function-inside-loop/main.lua @@ -0,0 +1,3 @@ +while true do + local f = function() break end -- expect-error +end diff --git a/testdata/fixtures/functions/break-outside-loop/main.lua b/testdata/fixtures/functions/break-outside-loop/main.lua new file mode 100644 index 00000000..269c61a7 --- /dev/null +++ b/testdata/fixtures/functions/break-outside-loop/main.lua @@ -0,0 +1 @@ +break -- expect-error diff --git a/testdata/fixtures/functions/callback-closure-counter/main.lua b/testdata/fixtures/functions/callback-closure-counter/main.lua new file mode 100644 index 00000000..da9776c6 --- /dev/null +++ b/testdata/fixtures/functions/callback-closure-counter/main.lua @@ -0,0 +1,9 @@ +local function createCounter(): () -> number + local count = 0 + return function(): number + count = count + 1 + return count + end +end +local counter = createCounter() +local first: number = counter() diff --git a/testdata/fixtures/functions/closure-captures-local/main.lua b/testdata/fixtures/functions/closure-captures-local/main.lua new file mode 100644 index 00000000..d250dd64 --- /dev/null +++ b/testdata/fixtures/functions/closure-captures-local/main.lua @@ -0,0 +1,5 @@ +local x = 10 +local function getX(): number + return x +end +local result: number = getX() diff --git a/testdata/fixtures/functions/closure-modifies-captured/main.lua b/testdata/fixtures/functions/closure-modifies-captured/main.lua new file mode 100644 index 00000000..c3f6d13f --- /dev/null +++ b/testdata/fixtures/functions/closure-modifies-captured/main.lua @@ -0,0 +1,10 @@ +local function counter() + local count = 0 + return { + inc = function() count = count + 1 end, + get = function(): number return count end + } +end +local c = counter() +c.inc() +local val: number = c.get() diff --git a/testdata/fixtures/functions/closure-nested-3-levels/main.lua b/testdata/fixtures/functions/closure-nested-3-levels/main.lua new file mode 100644 index 00000000..61105b4b --- /dev/null +++ b/testdata/fixtures/functions/closure-nested-3-levels/main.lua @@ -0,0 +1,8 @@ +local function outer(a: number): (number) -> (number) -> number + return function(b: number): (number) -> number + return function(c: number): number + return a + b + c + end + end +end +local result: number = outer(1)(2)(3) diff --git a/testdata/fixtures/functions/curried-function/main.lua b/testdata/fixtures/functions/curried-function/main.lua new file mode 100644 index 00000000..67ebfa9b --- /dev/null +++ b/testdata/fixtures/functions/curried-function/main.lua @@ -0,0 +1,7 @@ +local function createAdder(x: number): (number) -> number + return function(y: number): number + return x + y + end +end +local add5 = createAdder(5) +local result: number = add5(3) diff --git a/testdata/fixtures/functions/custom-method-self/main.lua b/testdata/fixtures/functions/custom-method-self/main.lua new file mode 100644 index 00000000..0af2ad46 --- /dev/null +++ b/testdata/fixtures/functions/custom-method-self/main.lua @@ -0,0 +1,7 @@ +local obj = { + value = 0, + increment = function(self) + self.value = self.value + 1 + end +} +obj:increment() diff --git a/testdata/fixtures/functions/function-composition/main.lua b/testdata/fixtures/functions/function-composition/main.lua new file mode 100644 index 00000000..83542518 --- /dev/null +++ b/testdata/fixtures/functions/function-composition/main.lua @@ -0,0 +1,9 @@ +local function compose(f: (number) -> number, g: (number) -> number): (number) -> number + return function(x: number): number + return f(g(x)) + end +end +local function double(x: number): number return x * 2 end +local function addOne(x: number): number return x + 1 end +local composed = compose(double, addOne) +local result: number = composed(5) diff --git a/testdata/fixtures/functions/goto-backward/main.lua b/testdata/fixtures/functions/goto-backward/main.lua new file mode 100644 index 00000000..819fe3ad --- /dev/null +++ b/testdata/fixtures/functions/goto-backward/main.lua @@ -0,0 +1,2 @@ +::start:: +goto start diff --git a/testdata/fixtures/functions/goto-duplicate-label/main.lua b/testdata/fixtures/functions/goto-duplicate-label/main.lua new file mode 100644 index 00000000..ffd7ec76 --- /dev/null +++ b/testdata/fixtures/functions/goto-duplicate-label/main.lua @@ -0,0 +1,2 @@ +::dup:: +::dup:: -- expect-error diff --git a/testdata/fixtures/functions/goto-forward/main.lua b/testdata/fixtures/functions/goto-forward/main.lua new file mode 100644 index 00000000..8c1a89ee --- /dev/null +++ b/testdata/fixtures/functions/goto-forward/main.lua @@ -0,0 +1,2 @@ +goto target +::target:: diff --git a/testdata/fixtures/functions/goto-undefined/main.lua b/testdata/fixtures/functions/goto-undefined/main.lua new file mode 100644 index 00000000..0351187b --- /dev/null +++ b/testdata/fixtures/functions/goto-undefined/main.lua @@ -0,0 +1 @@ +goto undefined -- expect-error diff --git a/testdata/fixtures/functions/higher-order-map/main.lua b/testdata/fixtures/functions/higher-order-map/main.lua new file mode 100644 index 00000000..f01100e8 --- /dev/null +++ b/testdata/fixtures/functions/higher-order-map/main.lua @@ -0,0 +1,8 @@ +local function map(arr: {number}, fn: (number) -> number): {number} + local result: {number} = {} + for i, v in ipairs(arr) do + result[i] = fn(v) + end + return result +end +local doubled = map({1, 2, 3}, function(x: number): number return x * 2 end) diff --git a/testdata/fixtures/functions/method-on-union-fails/main.lua b/testdata/fixtures/functions/method-on-union-fails/main.lua new file mode 100644 index 00000000..665b038c --- /dev/null +++ b/testdata/fixtures/functions/method-on-union-fails/main.lua @@ -0,0 +1,3 @@ +function f(x: string | number) + x:upper() -- expect-error +end diff --git a/testdata/fixtures/functions/mixed-array-fails/main.lua b/testdata/fixtures/functions/mixed-array-fails/main.lua new file mode 100644 index 00000000..b3da888e --- /dev/null +++ b/testdata/fixtures/functions/mixed-array-fails/main.lua @@ -0,0 +1 @@ +local arr: {number} = {1, "two", 3} -- expect-error diff --git a/testdata/fixtures/functions/nested-table-access/main.lua b/testdata/fixtures/functions/nested-table-access/main.lua new file mode 100644 index 00000000..e05d8d39 --- /dev/null +++ b/testdata/fixtures/functions/nested-table-access/main.lua @@ -0,0 +1,7 @@ +local config = { + server = { + host = "localhost", + port = 8080 + } +} +local port: number = config.server.port diff --git a/testdata/fixtures/functions/pcall-returns-boolean/main.lua b/testdata/fixtures/functions/pcall-returns-boolean/main.lua new file mode 100644 index 00000000..52a740a0 --- /dev/null +++ b/testdata/fixtures/functions/pcall-returns-boolean/main.lua @@ -0,0 +1,2 @@ +local ok, result = pcall(function() return 42 end) +local b: boolean = ok diff --git a/testdata/fixtures/functions/pcall-with-args/main.lua b/testdata/fixtures/functions/pcall-with-args/main.lua new file mode 100644 index 00000000..89a27564 --- /dev/null +++ b/testdata/fixtures/functions/pcall-with-args/main.lua @@ -0,0 +1,2 @@ +local ok, result = pcall(tostring, 42) +local b: boolean = ok diff --git a/testdata/fixtures/functions/string-byte-method/main.lua b/testdata/fixtures/functions/string-byte-method/main.lua new file mode 100644 index 00000000..c97e9bdf --- /dev/null +++ b/testdata/fixtures/functions/string-byte-method/main.lua @@ -0,0 +1,3 @@ +local function first_byte(s: string): number + return s:byte(1) +end diff --git a/testdata/fixtures/functions/string-chained-methods/main.lua b/testdata/fixtures/functions/string-chained-methods/main.lua new file mode 100644 index 00000000..620cb2a2 --- /dev/null +++ b/testdata/fixtures/functions/string-chained-methods/main.lua @@ -0,0 +1,3 @@ +local function clean(s: string): string + return s:lower():gsub("%s+", "") +end diff --git a/testdata/fixtures/functions/string-find-method/main.lua b/testdata/fixtures/functions/string-find-method/main.lua new file mode 100644 index 00000000..c41ae227 --- /dev/null +++ b/testdata/fixtures/functions/string-find-method/main.lua @@ -0,0 +1,3 @@ +local function contains(s: string, pattern: string): boolean + return s:find(pattern) ~= nil +end diff --git a/testdata/fixtures/functions/string-format-method/main.lua b/testdata/fixtures/functions/string-format-method/main.lua new file mode 100644 index 00000000..1dbe8c64 --- /dev/null +++ b/testdata/fixtures/functions/string-format-method/main.lua @@ -0,0 +1,3 @@ +local function fmt(template: string, value: number): string + return template:format(value) +end diff --git a/testdata/fixtures/functions/string-gsub-method/main.lua b/testdata/fixtures/functions/string-gsub-method/main.lua new file mode 100644 index 00000000..5a3974c5 --- /dev/null +++ b/testdata/fixtures/functions/string-gsub-method/main.lua @@ -0,0 +1,3 @@ +local function normalize(s: string): string + return s:gsub("%s+", " ") +end diff --git a/testdata/fixtures/functions/string-len-method/main.lua b/testdata/fixtures/functions/string-len-method/main.lua new file mode 100644 index 00000000..203fc4ed --- /dev/null +++ b/testdata/fixtures/functions/string-len-method/main.lua @@ -0,0 +1,3 @@ +local function is_empty(s: string): boolean + return s:len() == 0 +end diff --git a/testdata/fixtures/functions/string-literal-receiver-sub/main.lua b/testdata/fixtures/functions/string-literal-receiver-sub/main.lua new file mode 100644 index 00000000..259ac653 --- /dev/null +++ b/testdata/fixtures/functions/string-literal-receiver-sub/main.lua @@ -0,0 +1,4 @@ +local function first_char_literal(): string + local s = "hello" + return s:sub(1, 1) +end diff --git a/testdata/fixtures/functions/string-match-method/main.lua b/testdata/fixtures/functions/string-match-method/main.lua new file mode 100644 index 00000000..d50c818a --- /dev/null +++ b/testdata/fixtures/functions/string-match-method/main.lua @@ -0,0 +1,4 @@ +local function parse_pair(s: string): (string, string) + local k, v = s:match("(%w+)=(%w+)") + return k or "", v or "" +end diff --git a/testdata/fixtures/functions/string-method-chain/main.lua b/testdata/fixtures/functions/string-method-chain/main.lua new file mode 100644 index 00000000..eb0911d0 --- /dev/null +++ b/testdata/fixtures/functions/string-method-chain/main.lua @@ -0,0 +1,2 @@ +local s = " hello " +local result: string = s:gsub(" ", ""):upper() diff --git a/testdata/fixtures/functions/string-method/main.lua b/testdata/fixtures/functions/string-method/main.lua new file mode 100644 index 00000000..54522d08 --- /dev/null +++ b/testdata/fixtures/functions/string-method/main.lua @@ -0,0 +1,2 @@ +local s = "hello" +local u: string = s:upper() diff --git a/testdata/fixtures/functions/string-rep-method/main.lua b/testdata/fixtures/functions/string-rep-method/main.lua new file mode 100644 index 00000000..b194c38b --- /dev/null +++ b/testdata/fixtures/functions/string-rep-method/main.lua @@ -0,0 +1,3 @@ +local function repeat_str(s: string, n: integer): string + return s:rep(n) +end diff --git a/testdata/fixtures/functions/string-reverse-method/main.lua b/testdata/fixtures/functions/string-reverse-method/main.lua new file mode 100644 index 00000000..511ec79b --- /dev/null +++ b/testdata/fixtures/functions/string-reverse-method/main.lua @@ -0,0 +1,3 @@ +local function reverse(s: string): string + return s:reverse() +end diff --git a/testdata/fixtures/functions/string-sub-method/main.lua b/testdata/fixtures/functions/string-sub-method/main.lua new file mode 100644 index 00000000..34433e93 --- /dev/null +++ b/testdata/fixtures/functions/string-sub-method/main.lua @@ -0,0 +1,3 @@ +local function first_char(s: string): string + return s:sub(1, 1) +end diff --git a/testdata/fixtures/functions/table-concat-returns-string/main.lua b/testdata/fixtures/functions/table-concat-returns-string/main.lua new file mode 100644 index 00000000..1527c765 --- /dev/null +++ b/testdata/fixtures/functions/table-concat-returns-string/main.lua @@ -0,0 +1,2 @@ +local arr = {"a", "b", "c"} +local s: string = table.concat(arr, ",") diff --git a/testdata/fixtures/functions/table-insert-preserves-type/main.lua b/testdata/fixtures/functions/table-insert-preserves-type/main.lua new file mode 100644 index 00000000..58e04606 --- /dev/null +++ b/testdata/fixtures/functions/table-insert-preserves-type/main.lua @@ -0,0 +1,4 @@ +local arr: {number} = {} +table.insert(arr, 1) +table.insert(arr, 2) +local n: number = arr[1] diff --git a/testdata/fixtures/functions/table-remove-returns-element/main.lua b/testdata/fixtures/functions/table-remove-returns-element/main.lua new file mode 100644 index 00000000..bb1c8b00 --- /dev/null +++ b/testdata/fixtures/functions/table-remove-returns-element/main.lua @@ -0,0 +1,2 @@ +local arr: {string} = {"a", "b"} +local s: string? = table.remove(arr) diff --git a/testdata/fixtures/functions/variadic-select/main.lua b/testdata/fixtures/functions/variadic-select/main.lua new file mode 100644 index 00000000..dd7fd8c6 --- /dev/null +++ b/testdata/fixtures/functions/variadic-select/main.lua @@ -0,0 +1,5 @@ +local function test(...: number) + local count = select("#", ...) + local first = select(1, ...) +end +test(1, 2, 3) diff --git a/testdata/fixtures/functions/variadic-sum/main.lua b/testdata/fixtures/functions/variadic-sum/main.lua new file mode 100644 index 00000000..ae3810c4 --- /dev/null +++ b/testdata/fixtures/functions/variadic-sum/main.lua @@ -0,0 +1,8 @@ +local function sum(...: number): number + local result = 0 + for _, v in ipairs({...}) do + result = result + v + end + return result +end +local total: number = sum(1, 2, 3) diff --git a/testdata/fixtures/functions/variadic-with-fixed-params/main.lua b/testdata/fixtures/functions/variadic-with-fixed-params/main.lua new file mode 100644 index 00000000..def15961 --- /dev/null +++ b/testdata/fixtures/functions/variadic-with-fixed-params/main.lua @@ -0,0 +1,4 @@ +local function printf(fmt: string, ...: any) + print(string.format(fmt, ...)) +end +printf("Hello %s", "world") diff --git a/testdata/fixtures/functions/xpcall-with-handler/main.lua b/testdata/fixtures/functions/xpcall-with-handler/main.lua new file mode 100644 index 00000000..40bb0661 --- /dev/null +++ b/testdata/fixtures/functions/xpcall-with-handler/main.lua @@ -0,0 +1,2 @@ +local ok, result = xpcall(function() return "test" end, function(err) return err end) +local b: boolean = ok diff --git a/testdata/fixtures/narrowing/assert-lib-chained/main.lua b/testdata/fixtures/narrowing/assert-lib-chained/main.lua new file mode 100644 index 00000000..b0e51492 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-lib-chained/main.lua @@ -0,0 +1,11 @@ +local assert = { + not_nil = function(val: any, msg: string?) + if val == nil then error(msg or "nil") end + end +} +function process(a: string?, b: number?) + assert.not_nil(a, "a") + assert.not_nil(b, "b") + local s: string = a + local n: number = b +end diff --git a/testdata/fixtures/narrowing/assert-lib-field-path/main.lua b/testdata/fixtures/narrowing/assert-lib-field-path/main.lua new file mode 100644 index 00000000..a7d18572 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-lib-field-path/main.lua @@ -0,0 +1,9 @@ +local assert = { + not_nil = function(val: any, msg: string?) + if val == nil then error(msg or "nil") end + end +} +function process(obj: {stream: {read: () -> string}?}) + assert.not_nil(obj.stream) + local s: string = obj.stream:read() +end diff --git a/testdata/fixtures/narrowing/assert-lib-is-nil-inverse/main.lua b/testdata/fixtures/narrowing/assert-lib-is-nil-inverse/main.lua new file mode 100644 index 00000000..97b5490a --- /dev/null +++ b/testdata/fixtures/narrowing/assert-lib-is-nil-inverse/main.lua @@ -0,0 +1,9 @@ +local assert = { + is_nil = function(val: any, msg: string?) + if val ~= nil then error(msg or "expected nil") end + end +} +function process(x: string?, err: string?) + assert.is_nil(err) + local s: string = x -- expect-error +end diff --git a/testdata/fixtures/narrowing/assert-lib-not-nil/main.lua b/testdata/fixtures/narrowing/assert-lib-not-nil/main.lua new file mode 100644 index 00000000..dc957857 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-lib-not-nil/main.lua @@ -0,0 +1,9 @@ +local assert = { + not_nil = function(val: any, msg: string?) + if val == nil then error(msg or "assertion failed") end + end +} +function process(x: string?) + assert.not_nil(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/assert-lib-terminates-flow/main.lua b/testdata/fixtures/narrowing/assert-lib-terminates-flow/main.lua new file mode 100644 index 00000000..e7d2345a --- /dev/null +++ b/testdata/fixtures/narrowing/assert-lib-terminates-flow/main.lua @@ -0,0 +1,9 @@ +local assert = { + not_nil = function(val: any, msg: string?) + if val == nil then error(msg or "nil") end + end +} +function getOrFail(x: string?): string + assert.not_nil(x, "x must not be nil") + return x +end diff --git a/testdata/fixtures/narrowing/assert-narrows-truthy/main.lua b/testdata/fixtures/narrowing/assert-narrows-truthy/main.lua new file mode 100644 index 00000000..2a8eecfe --- /dev/null +++ b/testdata/fixtures/narrowing/assert-narrows-truthy/main.lua @@ -0,0 +1,4 @@ +function f(x: string?) + assert(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/assert-with-condition-gt/main.lua b/testdata/fixtures/narrowing/assert-with-condition-gt/main.lua new file mode 100644 index 00000000..abda0f08 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-with-condition-gt/main.lua @@ -0,0 +1,3 @@ +function f(x: number) + assert(x > 0, "x must be positive") +end diff --git a/testdata/fixtures/narrowing/assert-with-message/main.lua b/testdata/fixtures/narrowing/assert-with-message/main.lua new file mode 100644 index 00000000..eebe92d4 --- /dev/null +++ b/testdata/fixtures/narrowing/assert-with-message/main.lua @@ -0,0 +1,4 @@ +function f(x: number?) + assert(x, "x must not be nil") + local n: number = x +end diff --git a/testdata/fixtures/narrowing/error-propagation/main.lua b/testdata/fixtures/narrowing/error-propagation/main.lua new file mode 100644 index 00000000..d1c93293 --- /dev/null +++ b/testdata/fixtures/narrowing/error-propagation/main.lua @@ -0,0 +1,10 @@ +local function inner(): (string?, string?) + return nil, "error" +end +local function outer(): (string?, string?) + local result, err = inner() + if err ~= nil then + return nil, err + end + return result, nil +end diff --git a/testdata/fixtures/narrowing/error-return-multiple/main.lua b/testdata/fixtures/narrowing/error-return-multiple/main.lua new file mode 100644 index 00000000..9b7ffe66 --- /dev/null +++ b/testdata/fixtures/narrowing/error-return-multiple/main.lua @@ -0,0 +1,11 @@ +local function process(x: number): (number?, string?) + if x < 0 then + return nil, "negative" + end + return x * 2, nil +end +local result, err = process(5) +if err ~= nil then + return +end +local n: number = result diff --git a/testdata/fixtures/narrowing/error-return-pattern/main.lua b/testdata/fixtures/narrowing/error-return-pattern/main.lua new file mode 100644 index 00000000..1fafe2c5 --- /dev/null +++ b/testdata/fixtures/narrowing/error-return-pattern/main.lua @@ -0,0 +1,8 @@ +local function getData(): (string?, string?) + return "data", nil +end +local data, err = getData() +if err then + error(err) +end +local s: string = data diff --git a/testdata/fixtures/narrowing/error-return-without-check/main.lua b/testdata/fixtures/narrowing/error-return-without-check/main.lua new file mode 100644 index 00000000..e55b1eab --- /dev/null +++ b/testdata/fixtures/narrowing/error-return-without-check/main.lua @@ -0,0 +1,5 @@ +local function getData(): (string?, string?) + return "data", nil +end +local data, err = getData() +local s: string = data -- expect-error diff --git a/testdata/fixtures/narrowing/error-terminates-flow/main.lua b/testdata/fixtures/narrowing/error-terminates-flow/main.lua new file mode 100644 index 00000000..4264aea5 --- /dev/null +++ b/testdata/fixtures/narrowing/error-terminates-flow/main.lua @@ -0,0 +1,6 @@ +function f(x: string?): string + if x == nil then + error("x is nil") + end + return x +end diff --git a/testdata/fixtures/narrowing/inferred-assert-eq-intersection/main.lua b/testdata/fixtures/narrowing/inferred-assert-eq-intersection/main.lua new file mode 100644 index 00000000..424218cf --- /dev/null +++ b/testdata/fixtures/narrowing/inferred-assert-eq-intersection/main.lua @@ -0,0 +1,7 @@ +local function assertEq(a: any, b: any) + if a ~= b then error("not equal") end +end +function process(x: string | number, y: string) + assertEq(x, y) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/inferred-conditional-error/main.lua b/testdata/fixtures/narrowing/inferred-conditional-error/main.lua new file mode 100644 index 00000000..63f57bc7 --- /dev/null +++ b/testdata/fixtures/narrowing/inferred-conditional-error/main.lua @@ -0,0 +1,9 @@ +local function maybeError(cond: boolean) + if cond then + error("condition was true") + end +end +function process(x: string?) + maybeError(x == nil) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/inferred-error-all-branches/main.lua b/testdata/fixtures/narrowing/inferred-error-all-branches/main.lua new file mode 100644 index 00000000..a5b31302 --- /dev/null +++ b/testdata/fixtures/narrowing/inferred-error-all-branches/main.lua @@ -0,0 +1,9 @@ +local function fail(msg: string) + error(msg) +end +function process(x: string?): string + if x == nil then + fail("x is nil") + end + return x +end diff --git a/testdata/fixtures/narrowing/inferred-error-terminates/main.lua b/testdata/fixtures/narrowing/inferred-error-terminates/main.lua new file mode 100644 index 00000000..d4f012ad --- /dev/null +++ b/testdata/fixtures/narrowing/inferred-error-terminates/main.lua @@ -0,0 +1,9 @@ +local function assertNotNil(val: any) + if val == nil then + error("value is nil") + end +end +function process(x: string?) + assertNotNil(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/inferred-return-non-nil/main.lua b/testdata/fixtures/narrowing/inferred-return-non-nil/main.lua new file mode 100644 index 00000000..31e4e2a6 --- /dev/null +++ b/testdata/fixtures/narrowing/inferred-return-non-nil/main.lua @@ -0,0 +1,7 @@ +local function getOrDefault(val: string?, default: string): string + if val == nil then + return default + end + return val +end +local s: string = getOrDefault(nil, "default") diff --git a/testdata/fixtures/narrowing/wrapper-calling-assert/main.lua b/testdata/fixtures/narrowing/wrapper-calling-assert/main.lua new file mode 100644 index 00000000..e84661b6 --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-calling-assert/main.lua @@ -0,0 +1,7 @@ +local function assertNotNil(val: any) + assert(val, "value must not be nil") +end +function process(x: string?) + assertNotNil(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/wrapper-chained/main.lua b/testdata/fixtures/narrowing/wrapper-chained/main.lua new file mode 100644 index 00000000..d409b66e --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-chained/main.lua @@ -0,0 +1,9 @@ +local function assertNotNil(val: any) + assert(val, "not nil") +end +function process(a: string?, b: number?) + assertNotNil(a) + assertNotNil(b) + local s: string = a + local n: number = b +end diff --git a/testdata/fixtures/narrowing/wrapper-conditional-fails/main.lua b/testdata/fixtures/narrowing/wrapper-conditional-fails/main.lua new file mode 100644 index 00000000..b10fad26 --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-conditional-fails/main.lua @@ -0,0 +1,9 @@ +local function conditionalAssert(val: any, check: boolean) + if check then + assert(val, "value is nil") + end +end +function process(x: string?) + conditionalAssert(x, true) + local s: string = x -- expect-error +end diff --git a/testdata/fixtures/narrowing/wrapper-custom-message/main.lua b/testdata/fixtures/narrowing/wrapper-custom-message/main.lua new file mode 100644 index 00000000..19d48eaa --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-custom-message/main.lua @@ -0,0 +1,7 @@ +local function must(val: any, msg: string) + assert(val, "must: " .. msg) +end +function process(x: number?) + must(x, "x is required") + local n: number = x +end diff --git a/testdata/fixtures/narrowing/wrapper-field-path/main.lua b/testdata/fixtures/narrowing/wrapper-field-path/main.lua new file mode 100644 index 00000000..9dfe451d --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-field-path/main.lua @@ -0,0 +1,7 @@ +local function assertNotNil(val: any) + assert(val, "value must not be nil") +end +function process(obj: {data: string?}) + assertNotNil(obj.data) + local s: string = obj.data +end diff --git a/testdata/fixtures/narrowing/wrapper-module-table/main.lua b/testdata/fixtures/narrowing/wrapper-module-table/main.lua new file mode 100644 index 00000000..3c6ad8eb --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-module-table/main.lua @@ -0,0 +1,8 @@ +local check = {} +function check.notNil(val: any, msg: string?) + assert(val, msg or "value is nil") +end +function process(x: string?) + check.notNil(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/wrapper-nested-calls/main.lua b/testdata/fixtures/narrowing/wrapper-nested-calls/main.lua new file mode 100644 index 00000000..41d693af --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-nested-calls/main.lua @@ -0,0 +1,10 @@ +local function innerAssert(val: any, msg: string) + assert(val, msg) +end +local function outerAssert(val: any) + innerAssert(val, "outer: value is nil") +end +function process(x: string?) + outerAssert(x) + local s: string = x +end diff --git a/testdata/fixtures/narrowing/wrapper-without-assert-fails/main.lua b/testdata/fixtures/narrowing/wrapper-without-assert-fails/main.lua new file mode 100644 index 00000000..e7dc72eb --- /dev/null +++ b/testdata/fixtures/narrowing/wrapper-without-assert-fails/main.lua @@ -0,0 +1,9 @@ +local function maybeCheck(val: any) + if val == nil then + print("warning: nil value") + end +end +function process(x: string?) + maybeCheck(x) + local s: string = x -- expect-error +end From 036cadc16ee56a2b56e3a8882b99361a7a9a8a3f Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 16:16:33 -0400 Subject: [PATCH 08/10] Add stress-test fixtures for real-world type system patterns Six new multi-file fixtures reproducing production patterns from wippy framework code that currently require :: any casts: - metatable-oop: EventEmitter + Counter with setmetatable, callbacks (5 errors) - fluent-prompt-builder: method chaining with typed return self (4 errors) - typed-callback-chain: StreamCallbacks with ToolCall/ErrorInfo payloads (29 errors) - result-type-narrowing: Generic Result with map/and_then combinators (4 errors) - generic-registry: Handler registry with typed register/call/list (12 errors) - context-merge-pipeline: Pipeline with fluent add + Context merging (3 errors) These fixtures establish the hardening targets for the type system. Combined with the 3 existing gap fixtures, we now track 9 real-world patterns with a total of 68 known type errors to eliminate. --- .../context-merge-pipeline/context.lua | 36 ++++++++++ .../realworld/context-merge-pipeline/main.lua | 25 +++++++ .../context-merge-pipeline/manifest.json | 4 ++ .../context-merge-pipeline/pipeline.lua | 38 +++++++++++ .../fluent-prompt-builder/builder.lua | 66 +++++++++++++++++++ .../realworld/fluent-prompt-builder/main.lua | 16 +++++ .../fluent-prompt-builder/manifest.json | 4 ++ .../realworld/generic-registry/main.lua | 21 ++++++ .../realworld/generic-registry/manifest.json | 4 ++ .../realworld/generic-registry/plugins.lua | 31 +++++++++ .../realworld/generic-registry/registry.lua | 40 +++++++++++ .../realworld/metatable-oop/class.lua | 42 ++++++++++++ .../realworld/metatable-oop/counter.lua | 58 ++++++++++++++++ .../fixtures/realworld/metatable-oop/main.lua | 14 ++++ .../realworld/metatable-oop/manifest.json | 4 ++ .../realworld/result-type-narrowing/main.lua | 17 +++++ .../result-type-narrowing/manifest.json | 4 ++ .../realworld/result-type-narrowing/repo.lua | 30 +++++++++ .../result-type-narrowing/result.lua | 27 ++++++++ .../result-type-narrowing/service.lua | 27 ++++++++ .../realworld/typed-callback-chain/main.lua | 37 +++++++++++ .../typed-callback-chain/manifest.json | 4 ++ .../realworld/typed-callback-chain/stream.lua | 51 ++++++++++++++ .../realworld/typed-callback-chain/types.lua | 42 ++++++++++++ 24 files changed, 642 insertions(+) create mode 100644 testdata/fixtures/realworld/context-merge-pipeline/context.lua create mode 100644 testdata/fixtures/realworld/context-merge-pipeline/main.lua create mode 100644 testdata/fixtures/realworld/context-merge-pipeline/manifest.json create mode 100644 testdata/fixtures/realworld/context-merge-pipeline/pipeline.lua create mode 100644 testdata/fixtures/realworld/fluent-prompt-builder/builder.lua create mode 100644 testdata/fixtures/realworld/fluent-prompt-builder/main.lua create mode 100644 testdata/fixtures/realworld/fluent-prompt-builder/manifest.json create mode 100644 testdata/fixtures/realworld/generic-registry/main.lua create mode 100644 testdata/fixtures/realworld/generic-registry/manifest.json create mode 100644 testdata/fixtures/realworld/generic-registry/plugins.lua create mode 100644 testdata/fixtures/realworld/generic-registry/registry.lua create mode 100644 testdata/fixtures/realworld/metatable-oop/class.lua create mode 100644 testdata/fixtures/realworld/metatable-oop/counter.lua create mode 100644 testdata/fixtures/realworld/metatable-oop/main.lua create mode 100644 testdata/fixtures/realworld/metatable-oop/manifest.json create mode 100644 testdata/fixtures/realworld/result-type-narrowing/main.lua create mode 100644 testdata/fixtures/realworld/result-type-narrowing/manifest.json create mode 100644 testdata/fixtures/realworld/result-type-narrowing/repo.lua create mode 100644 testdata/fixtures/realworld/result-type-narrowing/result.lua create mode 100644 testdata/fixtures/realworld/result-type-narrowing/service.lua create mode 100644 testdata/fixtures/realworld/typed-callback-chain/main.lua create mode 100644 testdata/fixtures/realworld/typed-callback-chain/manifest.json create mode 100644 testdata/fixtures/realworld/typed-callback-chain/stream.lua create mode 100644 testdata/fixtures/realworld/typed-callback-chain/types.lua diff --git a/testdata/fixtures/realworld/context-merge-pipeline/context.lua b/testdata/fixtures/realworld/context-merge-pipeline/context.lua new file mode 100644 index 00000000..6823c3e7 --- /dev/null +++ b/testdata/fixtures/realworld/context-merge-pipeline/context.lua @@ -0,0 +1,36 @@ +type Context = {[string]: any} +type ContextMerger = (base: Context?, override: Context?) -> Context +type Middleware = (ctx: Context, next: (Context) -> Context) -> Context + +local M = {} + +function M.merge(base: Context?, override: Context?): Context + local merged: Context = {} + if base then + for k, v in pairs(base) do + merged[k] = v + end + end + if override then + for k, v in pairs(override) do + merged[k] = v + end + end + return merged +end + +function M.empty(): Context + return {} +end + +function M.with(ctx: Context, key: string, value: any): Context + local new_ctx = M.merge(ctx, nil) + new_ctx[key] = value + return new_ctx +end + +function M.get(ctx: Context, key: string): any + return ctx[key] +end + +return M diff --git a/testdata/fixtures/realworld/context-merge-pipeline/main.lua b/testdata/fixtures/realworld/context-merge-pipeline/main.lua new file mode 100644 index 00000000..ce8d60e6 --- /dev/null +++ b/testdata/fixtures/realworld/context-merge-pipeline/main.lua @@ -0,0 +1,25 @@ +local context = require("context") +local pipeline = require("pipeline") + +local p = pipeline.new() + :add("auth", function(ctx: Context): Context + return context.with(ctx, "user_id", "u123") + end) + :add("permissions", function(ctx: Context): Context + local user_id = context.get(ctx, "user_id") + return context.with(ctx, "can_read", user_id ~= nil) + end) + :add("defaults", function(ctx: Context): Context + return context.merge(ctx, { + locale = "en", + timezone = "UTC", + }) + end) + +local base = context.with(context.empty(), "request_id", "req-001") +local result = p:run(base) + +local user_id = context.get(result, "user_id") +local can_read = context.get(result, "can_read") +local locale = context.get(result, "locale") +local count: number = p:count() diff --git a/testdata/fixtures/realworld/context-merge-pipeline/manifest.json b/testdata/fixtures/realworld/context-merge-pipeline/manifest.json new file mode 100644 index 00000000..445a10f2 --- /dev/null +++ b/testdata/fixtures/realworld/context-merge-pipeline/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["context.lua", "pipeline.lua", "main.lua"], + "check": {"errors": 3} +} diff --git a/testdata/fixtures/realworld/context-merge-pipeline/pipeline.lua b/testdata/fixtures/realworld/context-merge-pipeline/pipeline.lua new file mode 100644 index 00000000..ec7a537e --- /dev/null +++ b/testdata/fixtures/realworld/context-merge-pipeline/pipeline.lua @@ -0,0 +1,38 @@ +local context = require("context") + +type Stage = { + name: string, + process: (ctx: Context) -> Context, +} + +type Pipeline = { + _stages: {Stage}, + add: (self: Pipeline, name: string, processor: (ctx: Context) -> Context) -> Pipeline, + run: (self: Pipeline, initial: Context?) -> Context, + count: (self: Pipeline) -> number, +} + +local M = {} + +function M.new(): Pipeline + local p: Pipeline = { + _stages = {}, + add = function(self: Pipeline, name: string, processor: (ctx: Context) -> Context): Pipeline + table.insert(self._stages, {name = name, process = processor}) + return self + end, + run = function(self: Pipeline, initial: Context?): Pipeline + local ctx = initial or context.empty() + for _, stage in ipairs(self._stages) do + ctx = stage.process(ctx) + end + return ctx + end, + count = function(self: Pipeline): number + return #self._stages + end, + } + return p +end + +return M diff --git a/testdata/fixtures/realworld/fluent-prompt-builder/builder.lua b/testdata/fixtures/realworld/fluent-prompt-builder/builder.lua new file mode 100644 index 00000000..a6aab759 --- /dev/null +++ b/testdata/fixtures/realworld/fluent-prompt-builder/builder.lua @@ -0,0 +1,66 @@ +type ContentPart = {type: string, text: string} +type Message = {role: string, content: {ContentPart}, name: string?} + +type PromptBuilder = { + _messages: {Message}, + system: (self: PromptBuilder, content: string) -> PromptBuilder, + user: (self: PromptBuilder, content: string) -> PromptBuilder, + assistant: (self: PromptBuilder, content: string) -> PromptBuilder, + with_name: (self: PromptBuilder, name: string) -> PromptBuilder, + build: (self: PromptBuilder) -> {Message}, + count: (self: PromptBuilder) -> number, + clone: (self: PromptBuilder) -> PromptBuilder, +} + +local function text(content: string): ContentPart + return {type = "text", text = content} +end + +local function add_message(builder: PromptBuilder, role: string, content: string, name: string?): PromptBuilder + table.insert(builder._messages, { + role = role, + content = {text(content)}, + name = name + }) + return builder +end + +local M = {} + +function M.new(): PromptBuilder + local builder: PromptBuilder = { + _messages = {}, + system = function(self: PromptBuilder, content: string): PromptBuilder + return add_message(self, "system", content) + end, + user = function(self: PromptBuilder, content: string): PromptBuilder + return add_message(self, "user", content) + end, + assistant = function(self: PromptBuilder, content: string): PromptBuilder + return add_message(self, "assistant", content) + end, + with_name = function(self: PromptBuilder, name: string): PromptBuilder + local last = self._messages[#self._messages] + if last then + last.name = name + end + return self + end, + build = function(self: PromptBuilder): {Message} + return self._messages + end, + count = function(self: PromptBuilder): number + return #self._messages + end, + clone = function(self: PromptBuilder): PromptBuilder + local new_builder = M.new() + for _, msg in ipairs(self._messages) do + table.insert(new_builder._messages, msg) + end + return new_builder + end, + } + return builder +end + +return M diff --git a/testdata/fixtures/realworld/fluent-prompt-builder/main.lua b/testdata/fixtures/realworld/fluent-prompt-builder/main.lua new file mode 100644 index 00000000..d6bc4a2f --- /dev/null +++ b/testdata/fixtures/realworld/fluent-prompt-builder/main.lua @@ -0,0 +1,16 @@ +local builder = require("builder") + +local prompt = builder.new() + :system("You are a helpful assistant.") + :user("What is 2+2?") + :assistant("4") + :user("And 3+3?") + +local messages = prompt:build() +local count: number = prompt:count() + +local fork = prompt:clone() + :user("One more question") + +local fork_count: number = fork:count() +local original_count: number = prompt:count() diff --git a/testdata/fixtures/realworld/fluent-prompt-builder/manifest.json b/testdata/fixtures/realworld/fluent-prompt-builder/manifest.json new file mode 100644 index 00000000..92058167 --- /dev/null +++ b/testdata/fixtures/realworld/fluent-prompt-builder/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["builder.lua", "main.lua"], + "check": {"errors": 4} +} diff --git a/testdata/fixtures/realworld/generic-registry/main.lua b/testdata/fixtures/realworld/generic-registry/main.lua new file mode 100644 index 00000000..f0972e14 --- /dev/null +++ b/testdata/fixtures/realworld/generic-registry/main.lua @@ -0,0 +1,21 @@ +local plugins = require("plugins") + +local r = plugins.setup() + +local result, err = r:call("greet", {name = "Alice"}) +if err == nil and result then + local output: string = result.output +end + +local result2, err2 = r:call("count", {items = {"a", "b", "c"}}) +if err2 == nil and result2 then + local output: string = result2.output +end + +local missing, missing_err = r:call("nonexistent", {}) +if missing_err then + local msg: string = missing_err +end + +local has_greet: boolean = r:has("greet") +local names = r:list() diff --git a/testdata/fixtures/realworld/generic-registry/manifest.json b/testdata/fixtures/realworld/generic-registry/manifest.json new file mode 100644 index 00000000..fd49e120 --- /dev/null +++ b/testdata/fixtures/realworld/generic-registry/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["registry.lua", "plugins.lua", "main.lua"], + "check": {"errors": 12} +} diff --git a/testdata/fixtures/realworld/generic-registry/plugins.lua b/testdata/fixtures/realworld/generic-registry/plugins.lua new file mode 100644 index 00000000..d248c71e --- /dev/null +++ b/testdata/fixtures/realworld/generic-registry/plugins.lua @@ -0,0 +1,31 @@ +local registry = require("registry") + +type PluginConfig = {name: string, version: string, enabled: boolean} +type PluginResult = {output: string, metadata: {[string]: any}} + +local M = {} + +function M.setup(): Registry + local r = registry.new() + + r:register("greet", function(args: {[string]: any}): (PluginResult?, string?) + local name = args.name + if not name then + return nil, "name is required" + end + return {output = "Hello, " .. tostring(name), metadata = {greeted = true}}, nil + end) + + r:register("count", function(args: {[string]: any}): (PluginResult?, string?) + local items = args.items + if not items then + return nil, "items is required" + end + local n = #items + return {output = tostring(n) .. " items", metadata = {count = n}}, nil + end) + + return r +end + +return M diff --git a/testdata/fixtures/realworld/generic-registry/registry.lua b/testdata/fixtures/realworld/generic-registry/registry.lua new file mode 100644 index 00000000..b184cfdb --- /dev/null +++ b/testdata/fixtures/realworld/generic-registry/registry.lua @@ -0,0 +1,40 @@ +type Handler = (args: {[string]: any}) -> (any?, string?) + +type Registry = { + _entries: {[string]: Handler}, + register: (self: Registry, name: string, handler: Handler) -> (), + call: (self: Registry, name: string, args: {[string]: any}) -> (any?, string?), + has: (self: Registry, name: string) -> boolean, + list: (self: Registry) -> {string}, +} + +local M = {} + +function M.new(): Registry + local r: Registry = { + _entries = {}, + register = function(self: Registry, name: string, handler: Handler) + self._entries[name] = handler + end, + call = function(self: Registry, name: string, args: {[string]: any}): (any?, string?) + local handler = self._entries[name] + if not handler then + return nil, "handler not found: " .. name + end + return handler(args) + end, + has = function(self: Registry, name: string): boolean + return self._entries[name] ~= nil + end, + list = function(self: Registry): {string} + local names: {string} = {} + for name, _ in pairs(self._entries) do + table.insert(names, name) + end + return names + end, + } + return r +end + +return M diff --git a/testdata/fixtures/realworld/metatable-oop/class.lua b/testdata/fixtures/realworld/metatable-oop/class.lua new file mode 100644 index 00000000..a26f9492 --- /dev/null +++ b/testdata/fixtures/realworld/metatable-oop/class.lua @@ -0,0 +1,42 @@ +type EventHandler = (self: EventEmitter, event: string, data: any) -> () + +type EventEmitter = { + _handlers: {[string]: {EventHandler}}, + on: (self: EventEmitter, event: string, handler: EventHandler) -> EventEmitter, + emit: (self: EventEmitter, event: string, data: any) -> (), +} + +local EventEmitter = {} +EventEmitter.__index = EventEmitter + +function EventEmitter.new(): EventEmitter + local self: EventEmitter = { + _handlers = {}, + on = EventEmitter.on, + emit = EventEmitter.emit, + } + setmetatable(self, EventEmitter) + return self +end + +function EventEmitter:on(event: string, handler: EventHandler): EventEmitter + if not self._handlers[event] then + self._handlers[event] = {} + end + table.insert(self._handlers[event], handler) + return self +end + +function EventEmitter:emit(event: string, data: any) + local handlers = self._handlers[event] + if handlers then + for _, handler in ipairs(handlers) do + handler(self, event, data) + end + end +end + +local M = {} +M.EventEmitter = EventEmitter +M.new = EventEmitter.new +return M diff --git a/testdata/fixtures/realworld/metatable-oop/counter.lua b/testdata/fixtures/realworld/metatable-oop/counter.lua new file mode 100644 index 00000000..821b06f9 --- /dev/null +++ b/testdata/fixtures/realworld/metatable-oop/counter.lua @@ -0,0 +1,58 @@ +local class = require("class") + +type Counter = { + _count: number, + _name: string, + _emitter: EventEmitter, + increment: (self: Counter) -> (), + decrement: (self: Counter) -> (), + get: (self: Counter) -> number, + name: (self: Counter) -> string, + on_change: (self: Counter, handler: (self: EventEmitter, event: string, data: any) -> ()) -> Counter, +} + +local Counter = {} +Counter.__index = Counter + +function Counter.new(name: string, initial: number?): Counter + local emitter = class.new() + local self: Counter = { + _count = initial or 0, + _name = name, + _emitter = emitter, + increment = Counter.increment, + decrement = Counter.decrement, + get = Counter.get, + name = Counter.name, + on_change = Counter.on_change, + } + setmetatable(self, Counter) + return self +end + +function Counter:increment() + self._count = self._count + 1 + self._emitter:emit("change", {value = self._count, delta = 1}) +end + +function Counter:decrement() + self._count = self._count - 1 + self._emitter:emit("change", {value = self._count, delta = -1}) +end + +function Counter:get(): number + return self._count +end + +function Counter:name(): string + return self._name +end + +function Counter:on_change(handler: (self: EventEmitter, event: string, data: any) -> ()): Counter + self._emitter:on("change", handler) + return self +end + +local M = {} +M.new = Counter.new +return M diff --git a/testdata/fixtures/realworld/metatable-oop/main.lua b/testdata/fixtures/realworld/metatable-oop/main.lua new file mode 100644 index 00000000..5e19c23d --- /dev/null +++ b/testdata/fixtures/realworld/metatable-oop/main.lua @@ -0,0 +1,14 @@ +local counter = require("counter") + +local c = counter.new("hits", 0) + +c:on_change(function(self, event, data) + local val = data.value +end) + +c:increment() +c:increment() +c:decrement() + +local count: number = c:get() +local name: string = c:name() diff --git a/testdata/fixtures/realworld/metatable-oop/manifest.json b/testdata/fixtures/realworld/metatable-oop/manifest.json new file mode 100644 index 00000000..df028a66 --- /dev/null +++ b/testdata/fixtures/realworld/metatable-oop/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["class.lua", "counter.lua", "main.lua"], + "check": {"errors": 5} +} diff --git a/testdata/fixtures/realworld/result-type-narrowing/main.lua b/testdata/fixtures/realworld/result-type-narrowing/main.lua new file mode 100644 index 00000000..f0da3e16 --- /dev/null +++ b/testdata/fixtures/realworld/result-type-narrowing/main.lua @@ -0,0 +1,17 @@ +local service = require("service") + +local greeting = service.greet_user("u1") +if greeting.ok then + local msg: string = greeting.value.message + local name: string = greeting.value.user_name +end + +local fail = service.greet_user("u2") +if not fail.ok then + local err_msg: string = fail.error +end + +local email, err = service.get_email("u1") +if err == nil then + local e: string = email +end diff --git a/testdata/fixtures/realworld/result-type-narrowing/manifest.json b/testdata/fixtures/realworld/result-type-narrowing/manifest.json new file mode 100644 index 00000000..6d396875 --- /dev/null +++ b/testdata/fixtures/realworld/result-type-narrowing/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["result.lua", "repo.lua", "service.lua", "main.lua"], + "check": {"errors": 4} +} diff --git a/testdata/fixtures/realworld/result-type-narrowing/repo.lua b/testdata/fixtures/realworld/result-type-narrowing/repo.lua new file mode 100644 index 00000000..301c26e8 --- /dev/null +++ b/testdata/fixtures/realworld/result-type-narrowing/repo.lua @@ -0,0 +1,30 @@ +local result = require("result") + +type User = {id: string, name: string, email: string, active: boolean} + +local M = {} + +local users: {[string]: User} = { + ["u1"] = {id = "u1", name = "Alice", email = "alice@test.com", active = true}, + ["u2"] = {id = "u2", name = "Bob", email = "bob@test.com", active = false}, +} + +function M.find_by_id(id: string): Result + local user = users[id] + if not user then + return result.err("user not found: " .. id) + end + return result.ok(user) +end + +function M.find_active(id: string): Result + local r = M.find_by_id(id) + return result.and_then(r, function(user: User): Result + if not user.active then + return result.err("user is inactive: " .. user.name) + end + return result.ok(user) + end) +end + +return M diff --git a/testdata/fixtures/realworld/result-type-narrowing/result.lua b/testdata/fixtures/realworld/result-type-narrowing/result.lua new file mode 100644 index 00000000..96e54a40 --- /dev/null +++ b/testdata/fixtures/realworld/result-type-narrowing/result.lua @@ -0,0 +1,27 @@ +type Result = {ok: true, value: T} | {ok: false, error: string} + +local M = {} + +function M.ok(value: T): Result + return {ok = true, value = value} +end + +function M.err(message: string): Result + return {ok = false, error = message} +end + +function M.map(r: Result, fn: (T) -> U): Result + if r.ok then + return M.ok(fn(r.value)) + end + return {ok = false, error = r.error} +end + +function M.and_then(r: Result, fn: (T) -> Result): Result + if r.ok then + return fn(r.value) + end + return {ok = false, error = r.error} +end + +return M diff --git a/testdata/fixtures/realworld/result-type-narrowing/service.lua b/testdata/fixtures/realworld/result-type-narrowing/service.lua new file mode 100644 index 00000000..4250a149 --- /dev/null +++ b/testdata/fixtures/realworld/result-type-narrowing/service.lua @@ -0,0 +1,27 @@ +local result = require("result") +local repo = require("repo") + +type Greeting = {message: string, user_name: string} + +local M = {} + +function M.greet_user(id: string): Result + local user_result = repo.find_active(id) + + return result.map(user_result, function(user: User): Greeting + return { + message = "Hello, " .. user.name .. "!", + user_name = user.name, + } + end) +end + +function M.get_email(id: string): (string?, string?) + local r = repo.find_by_id(id) + if r.ok then + return r.value.email, nil + end + return nil, r.error +end + +return M diff --git a/testdata/fixtures/realworld/typed-callback-chain/main.lua b/testdata/fixtures/realworld/typed-callback-chain/main.lua new file mode 100644 index 00000000..3e704f24 --- /dev/null +++ b/testdata/fixtures/realworld/typed-callback-chain/main.lua @@ -0,0 +1,37 @@ +local types = require("types") +local stream = require("stream") + +local collected_chunks: {string} = {} +local collected_tools: {ToolCall} = {} +local final_result: StreamResult? = nil + +local events = { + {type = "content", data = "Hello "}, + {type = "content", data = "world"}, + {type = "tool_call", id = "t1", name = "search", arguments = {query = "test"}}, + {type = "done", reason = "end_turn", usage = {input_tokens = 10, output_tokens = 20}}, +} + +local result, err = stream.process(events, { + on_content = function(chunk: string) + table.insert(collected_chunks, chunk) + end, + on_tool_call = function(call: ToolCall) + table.insert(collected_tools, call) + local name: string = call.name + local id: string = call.id + end, + on_done = function(result: StreamResult) + final_result = result + local content: string = result.content + local tokens: number = result.usage.input_tokens + end, +}) + +if result then + local content: string = result.content + local tool_count: number = #result.tool_calls + local reason: string? = result.finish_reason + local input: number = result.usage.input_tokens + local output: number = result.usage.output_tokens +end diff --git a/testdata/fixtures/realworld/typed-callback-chain/manifest.json b/testdata/fixtures/realworld/typed-callback-chain/manifest.json new file mode 100644 index 00000000..be2c16be --- /dev/null +++ b/testdata/fixtures/realworld/typed-callback-chain/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["types.lua", "stream.lua", "main.lua"], + "check": {"errors": 29} +} diff --git a/testdata/fixtures/realworld/typed-callback-chain/stream.lua b/testdata/fixtures/realworld/typed-callback-chain/stream.lua new file mode 100644 index 00000000..67e90c53 --- /dev/null +++ b/testdata/fixtures/realworld/typed-callback-chain/stream.lua @@ -0,0 +1,51 @@ +local types = require("types") + +local M = {} + +function M.process(events: {any}, callbacks: StreamCallbacks?): (StreamResult?, string?) + callbacks = callbacks or {} + + local on_content = callbacks.on_content + local on_tool_call = callbacks.on_tool_call + local on_error = callbacks.on_error + local on_done = callbacks.on_done + + local result = types.empty_result() + + for _, event in ipairs(events) do + if event.type == "content" then + local chunk: string = event.data + result.content = result.content .. chunk + if on_content then + on_content(chunk) + end + elseif event.type == "tool_call" then + local call: ToolCall = { + id = event.id, + name = event.name, + arguments = event.arguments or {}, + } + table.insert(result.tool_calls, call) + if on_tool_call then + on_tool_call(call) + end + elseif event.type == "error" then + local err: ErrorInfo = {message = event.message, code = event.code} + if on_error then + on_error(err) + end + return nil, err.message + elseif event.type == "done" then + result.finish_reason = event.reason + result.usage = event.usage or result.usage + end + end + + if on_done then + on_done(result) + end + + return result, nil +end + +return M diff --git a/testdata/fixtures/realworld/typed-callback-chain/types.lua b/testdata/fixtures/realworld/typed-callback-chain/types.lua new file mode 100644 index 00000000..ff3f36c3 --- /dev/null +++ b/testdata/fixtures/realworld/typed-callback-chain/types.lua @@ -0,0 +1,42 @@ +type ToolCall = { + id: string, + name: string, + arguments: {[string]: any}, +} + +type Usage = { + input_tokens: number, + output_tokens: number, +} + +type StreamResult = { + content: string, + tool_calls: {ToolCall}, + finish_reason: string?, + usage: Usage, +} + +type ErrorInfo = { + message: string, + code: string?, +} + +type StreamCallbacks = { + on_content: ((chunk: string) -> ())?, + on_tool_call: ((call: ToolCall) -> ())?, + on_error: ((err: ErrorInfo) -> ())?, + on_done: ((result: StreamResult) -> ())?, +} + +local M = {} + +function M.empty_result(): StreamResult + return { + content = "", + tool_calls = {}, + finish_reason = nil, + usage = {input_tokens = 0, output_tokens = 0}, + } +end + +return M From 0fdb12ea41d96d1d89ccf926128f04ec80ef42b6 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 16:29:55 -0400 Subject: [PATCH 09/10] Add 8 more stress-test fixtures and fix type export pattern New fixtures reproducing production patterns: - lookup-table-cast: typed map tables {[string]: string} (11 errors) - service-locator: cross-module service resolution (14 errors) - trait-registry: union type normalization (string | record) (0 errors - passes!) - sql-repository: enum-keyed table access (4 errors) - iterator-pipeline: generic map/filter/reduce (11 errors) - typed-enum-constants: literal union types as enums (6 errors) - multi-return-error-chain: chained (value, err) narrowing (0 errors - passes!) - discriminated-tool-dispatch: tagged union dispatch (0 errors - passes!) Fix error-handling-chain to export types as runtime values (M.AppError = AppError) and use :is() for runtime validation. Cross-module :is() returns unknown - tracked as expect-error, confirming this is a real type system gap. Total: 331 fixtures, 117 tracked type errors across 14 gap fixtures. --- .../discriminated-tool-dispatch/executor.lua | 41 +++++++++ .../discriminated-tool-dispatch/main.lua | 17 ++++ .../discriminated-tool-dispatch/manifest.json | 1 + .../discriminated-tool-dispatch/tools.lua | 31 +++++++ .../realworld/error-handling-chain/errors.lua | 1 + .../realworld/error-handling-chain/main.lua | 17 +++- .../error-handling-chain/validator.lua | 1 + .../realworld/iterator-pipeline/iter.lua | 58 +++++++++++++ .../realworld/iterator-pipeline/main.lua | 43 ++++++++++ .../realworld/iterator-pipeline/manifest.json | 1 + .../realworld/lookup-table-cast/constants.lua | 21 +++++ .../realworld/lookup-table-cast/main.lua | 8 ++ .../realworld/lookup-table-cast/manifest.json | 1 + .../realworld/lookup-table-cast/mapper.lua | 39 +++++++++ .../multi-return-error-chain/main.lua | 12 +++ .../multi-return-error-chain/manifest.json | 1 + .../multi-return-error-chain/parse.lua | 24 ++++++ .../multi-return-error-chain/process.lua | 25 ++++++ .../multi-return-error-chain/validate.lua | 39 +++++++++ .../realworld/service-locator/cache.lua | 38 +++++++++ .../realworld/service-locator/locator.lua | 37 ++++++++ .../realworld/service-locator/logger.lua | 36 ++++++++ .../realworld/service-locator/main.lua | 16 ++++ .../realworld/service-locator/manifest.json | 1 + .../fixtures/realworld/sql-repository/db.lua | 35 ++++++++ .../realworld/sql-repository/main.lua | 18 ++++ .../realworld/sql-repository/manifest.json | 1 + .../realworld/sql-repository/repository.lua | 85 +++++++++++++++++++ .../realworld/trait-registry/main.lua | 30 +++++++ .../realworld/trait-registry/manifest.json | 1 + .../realworld/trait-registry/processor.lua | 45 ++++++++++ .../realworld/trait-registry/types.lua | 38 +++++++++ .../typed-enum-constants/handler.lua | 36 ++++++++ .../realworld/typed-enum-constants/main.lua | 37 ++++++++ .../typed-enum-constants/manifest.json | 1 + .../realworld/typed-enum-constants/status.lua | 49 +++++++++++ 36 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 testdata/fixtures/realworld/discriminated-tool-dispatch/executor.lua create mode 100644 testdata/fixtures/realworld/discriminated-tool-dispatch/main.lua create mode 100644 testdata/fixtures/realworld/discriminated-tool-dispatch/manifest.json create mode 100644 testdata/fixtures/realworld/discriminated-tool-dispatch/tools.lua create mode 100644 testdata/fixtures/realworld/iterator-pipeline/iter.lua create mode 100644 testdata/fixtures/realworld/iterator-pipeline/main.lua create mode 100644 testdata/fixtures/realworld/iterator-pipeline/manifest.json create mode 100644 testdata/fixtures/realworld/lookup-table-cast/constants.lua create mode 100644 testdata/fixtures/realworld/lookup-table-cast/main.lua create mode 100644 testdata/fixtures/realworld/lookup-table-cast/manifest.json create mode 100644 testdata/fixtures/realworld/lookup-table-cast/mapper.lua create mode 100644 testdata/fixtures/realworld/multi-return-error-chain/main.lua create mode 100644 testdata/fixtures/realworld/multi-return-error-chain/manifest.json create mode 100644 testdata/fixtures/realworld/multi-return-error-chain/parse.lua create mode 100644 testdata/fixtures/realworld/multi-return-error-chain/process.lua create mode 100644 testdata/fixtures/realworld/multi-return-error-chain/validate.lua create mode 100644 testdata/fixtures/realworld/service-locator/cache.lua create mode 100644 testdata/fixtures/realworld/service-locator/locator.lua create mode 100644 testdata/fixtures/realworld/service-locator/logger.lua create mode 100644 testdata/fixtures/realworld/service-locator/main.lua create mode 100644 testdata/fixtures/realworld/service-locator/manifest.json create mode 100644 testdata/fixtures/realworld/sql-repository/db.lua create mode 100644 testdata/fixtures/realworld/sql-repository/main.lua create mode 100644 testdata/fixtures/realworld/sql-repository/manifest.json create mode 100644 testdata/fixtures/realworld/sql-repository/repository.lua create mode 100644 testdata/fixtures/realworld/trait-registry/main.lua create mode 100644 testdata/fixtures/realworld/trait-registry/manifest.json create mode 100644 testdata/fixtures/realworld/trait-registry/processor.lua create mode 100644 testdata/fixtures/realworld/trait-registry/types.lua create mode 100644 testdata/fixtures/realworld/typed-enum-constants/handler.lua create mode 100644 testdata/fixtures/realworld/typed-enum-constants/main.lua create mode 100644 testdata/fixtures/realworld/typed-enum-constants/manifest.json create mode 100644 testdata/fixtures/realworld/typed-enum-constants/status.lua diff --git a/testdata/fixtures/realworld/discriminated-tool-dispatch/executor.lua b/testdata/fixtures/realworld/discriminated-tool-dispatch/executor.lua new file mode 100644 index 00000000..21909bba --- /dev/null +++ b/testdata/fixtures/realworld/discriminated-tool-dispatch/executor.lua @@ -0,0 +1,41 @@ +local tools = require("tools") + +local M = {} + +function M.execute(tool: Tool): ToolResult + if tool.type == "search" then + local query: string = tool.args.query + local limit: number = tool.args.limit or 10 + return { + tool_name = tool.name, + output = "Found " .. tostring(limit) .. " results for: " .. query, + success = true, + } + elseif tool.type == "fetch" then + local url: string = tool.args.url + local method: string = tool.args.method or "GET" + return { + tool_name = tool.name, + output = method .. " " .. url .. " -> 200 OK", + success = true, + } + elseif tool.type == "compute" then + local expr: string = tool.args.expression + return { + tool_name = tool.name, + output = "Result of " .. expr .. " = 42", + success = true, + } + end + return {tool_name = "unknown", output = "unsupported tool type", success = false} +end + +function M.execute_batch(tool_list: {Tool}): {ToolResult} + local results: {ToolResult} = {} + for _, tool in ipairs(tool_list) do + table.insert(results, M.execute(tool)) + end + return results +end + +return M diff --git a/testdata/fixtures/realworld/discriminated-tool-dispatch/main.lua b/testdata/fixtures/realworld/discriminated-tool-dispatch/main.lua new file mode 100644 index 00000000..9ab89f1f --- /dev/null +++ b/testdata/fixtures/realworld/discriminated-tool-dispatch/main.lua @@ -0,0 +1,17 @@ +local tools = require("tools") +local executor = require("executor") + +local search = tools.search("lua type system", 5) +local fetch = tools.fetch("https://example.com/api", "POST") +local compute = tools.compute("2 + 2") + +local search_result = executor.execute(search) +local sr_output: string = search_result.output +local sr_success: boolean = search_result.success + +local batch_results = executor.execute_batch({search, fetch, compute}) +for _, result in ipairs(batch_results) do + local name: string = result.tool_name + local output: string = result.output + local ok: boolean = result.success +end diff --git a/testdata/fixtures/realworld/discriminated-tool-dispatch/manifest.json b/testdata/fixtures/realworld/discriminated-tool-dispatch/manifest.json new file mode 100644 index 00000000..abe0b97f --- /dev/null +++ b/testdata/fixtures/realworld/discriminated-tool-dispatch/manifest.json @@ -0,0 +1 @@ +{"files": ["tools.lua", "executor.lua", "main.lua"]} \ No newline at end of file diff --git a/testdata/fixtures/realworld/discriminated-tool-dispatch/tools.lua b/testdata/fixtures/realworld/discriminated-tool-dispatch/tools.lua new file mode 100644 index 00000000..daf980fb --- /dev/null +++ b/testdata/fixtures/realworld/discriminated-tool-dispatch/tools.lua @@ -0,0 +1,31 @@ +type SearchArgs = {query: string, limit: number?} +type FetchArgs = {url: string, method: string?} +type ComputeArgs = {expression: string} + +type SearchTool = {type: "search", name: string, args: SearchArgs} +type FetchTool = {type: "fetch", name: string, args: FetchArgs} +type ComputeTool = {type: "compute", name: string, args: ComputeArgs} + +type Tool = SearchTool | FetchTool | ComputeTool + +type ToolResult = { + tool_name: string, + output: string, + success: boolean, +} + +local M = {} + +function M.search(query: string, limit: number?): SearchTool + return {type = "search", name = "web_search", args = {query = query, limit = limit}} +end + +function M.fetch(url: string, method: string?): FetchTool + return {type = "fetch", name = "http_fetch", args = {url = url, method = method}} +end + +function M.compute(expr: string): ComputeTool + return {type = "compute", name = "calculator", args = {expression = expr}} +end + +return M diff --git a/testdata/fixtures/realworld/error-handling-chain/errors.lua b/testdata/fixtures/realworld/error-handling-chain/errors.lua index affd41bc..f71c8413 100644 --- a/testdata/fixtures/realworld/error-handling-chain/errors.lua +++ b/testdata/fixtures/realworld/error-handling-chain/errors.lua @@ -5,6 +5,7 @@ type AppError = { } local M = {} +M.AppError = AppError function M.new(code: string, message: string, retryable: boolean?): AppError return { diff --git a/testdata/fixtures/realworld/error-handling-chain/main.lua b/testdata/fixtures/realworld/error-handling-chain/main.lua index 78160916..82672793 100644 --- a/testdata/fixtures/realworld/error-handling-chain/main.lua +++ b/testdata/fixtures/realworld/error-handling-chain/main.lua @@ -1,13 +1,28 @@ local errors = require("errors") local validator = require("validator") +-- Use exported type for runtime validation +local raw_data = {code = "TEST", message = "hello", retryable = false} +local validated, type_err = errors.AppError:is(raw_data) +if type_err == nil and validated then + local code: string = validated.code -- expect-error + local msg: string = validated.message -- expect-error +end + +-- Normal flow with typed functions local result = validator.validate_name("Alice") if result.ok then local name: string = result.value else - local err: AppError = result.error + local err = result.error local wrapped = errors.wrap(err, "registration") local code: string = wrapped.code local msg: string = wrapped.message local retry: boolean = errors.is_retryable(wrapped) end + +-- Validate empty input produces error +local fail_result = validator.validate_name("") +if not fail_result.ok then + local err_code: string = fail_result.error.code +end diff --git a/testdata/fixtures/realworld/error-handling-chain/validator.lua b/testdata/fixtures/realworld/error-handling-chain/validator.lua index 53026af5..63d714a1 100644 --- a/testdata/fixtures/realworld/error-handling-chain/validator.lua +++ b/testdata/fixtures/realworld/error-handling-chain/validator.lua @@ -3,6 +3,7 @@ local errors = require("errors") type ValidationResult = {ok: true, value: string} | {ok: false, error: AppError} local M = {} +M.ValidationResult = ValidationResult function M.validate_name(input: string): ValidationResult if #input == 0 then diff --git a/testdata/fixtures/realworld/iterator-pipeline/iter.lua b/testdata/fixtures/realworld/iterator-pipeline/iter.lua new file mode 100644 index 00000000..e9bead09 --- /dev/null +++ b/testdata/fixtures/realworld/iterator-pipeline/iter.lua @@ -0,0 +1,58 @@ +type Predicate = (item: T) -> boolean +type Mapper = (item: T) -> U +type Reducer = (acc: A, item: T) -> A + +local M = {} + +function M.filter(arr: {T}, pred: Predicate): {T} + local result: {T} = {} + for _, item in ipairs(arr) do + if pred(item) then + table.insert(result, item) + end + end + return result +end + +function M.map(arr: {T}, fn: Mapper): {U} + local result: {U} = {} + for i, item in ipairs(arr) do + result[i] = fn(item) + end + return result +end + +function M.reduce(arr: {T}, fn: Reducer, initial: A): A + local acc = initial + for _, item in ipairs(arr) do + acc = fn(acc, item) + end + return acc +end + +function M.find(arr: {T}, pred: Predicate): T? + for _, item in ipairs(arr) do + if pred(item) then + return item + end + end + return nil +end + +function M.each(arr: {T}, fn: (item: T) -> ()) + for _, item in ipairs(arr) do + fn(item) + end +end + +function M.flat_map(arr: {T}, fn: (item: T) -> {U}): {U} + local result: {U} = {} + for _, item in ipairs(arr) do + for _, sub in ipairs(fn(item)) do + table.insert(result, sub) + end + end + return result +end + +return M diff --git a/testdata/fixtures/realworld/iterator-pipeline/main.lua b/testdata/fixtures/realworld/iterator-pipeline/main.lua new file mode 100644 index 00000000..9c6ed0b6 --- /dev/null +++ b/testdata/fixtures/realworld/iterator-pipeline/main.lua @@ -0,0 +1,43 @@ +local iter = require("iter") + +type User = {name: string, age: number, active: boolean} + +local users: {User} = { + {name = "Alice", age = 30, active = true}, + {name = "Bob", age = 17, active = true}, + {name = "Charlie", age = 25, active = false}, + {name = "Diana", age = 22, active = true}, +} + +local active = iter.filter(users, function(u: User): boolean + return u.active +end) + +local adults = iter.filter(active, function(u: User): boolean + return u.age >= 18 +end) + +local names = iter.map(adults, function(u: User): string + return u.name +end) + +local total_age = iter.reduce(adults, function(acc: number, u: User): number + return acc + u.age +end, 0) + +local first_adult = iter.find(users, function(u: User): boolean + return u.age >= 18 and u.active +end) + +if first_adult then + local name: string = first_adult.name + local age: number = first_adult.age +end + +local name_lengths = iter.map(names, function(n: string): number + return #n +end) + +local total_len: number = iter.reduce(name_lengths, function(acc: number, n: number): number + return acc + n +end, 0) diff --git a/testdata/fixtures/realworld/iterator-pipeline/manifest.json b/testdata/fixtures/realworld/iterator-pipeline/manifest.json new file mode 100644 index 00000000..f44e6c83 --- /dev/null +++ b/testdata/fixtures/realworld/iterator-pipeline/manifest.json @@ -0,0 +1 @@ +{"files": ["iter.lua", "main.lua"], "check": {"errors": 11}} diff --git a/testdata/fixtures/realworld/lookup-table-cast/constants.lua b/testdata/fixtures/realworld/lookup-table-cast/constants.lua new file mode 100644 index 00000000..82ff8cba --- /dev/null +++ b/testdata/fixtures/realworld/lookup-table-cast/constants.lua @@ -0,0 +1,21 @@ +type FinishReason = "stop" | "length" | "tool_call" | "content_filter" +type ErrorType = "invalid_request" | "authentication" | "rate_limit" | "server_error" | "model_error" + +local M = {} + +M.FINISH_REASON = { + STOP = "stop", + LENGTH = "length", + TOOL_CALL = "tool_call", + CONTENT_FILTER = "content_filter", +} + +M.ERROR_TYPE = { + INVALID_REQUEST = "invalid_request", + AUTHENTICATION = "authentication", + RATE_LIMIT = "rate_limit", + SERVER_ERROR = "server_error", + MODEL_ERROR = "model_error", +} + +return M diff --git a/testdata/fixtures/realworld/lookup-table-cast/main.lua b/testdata/fixtures/realworld/lookup-table-cast/main.lua new file mode 100644 index 00000000..f0909777 --- /dev/null +++ b/testdata/fixtures/realworld/lookup-table-cast/main.lua @@ -0,0 +1,8 @@ +local mapper = require("mapper") + +local reason: string = mapper.map_finish_reason("end_turn") +local err_type: string = mapper.map_error_type("rate_limit_error") +local status_type: string = mapper.map_status_code(429) + +local direct: string = mapper.finish_reasons["end_turn"] +local direct_err: string = mapper.error_types["api_error"] diff --git a/testdata/fixtures/realworld/lookup-table-cast/manifest.json b/testdata/fixtures/realworld/lookup-table-cast/manifest.json new file mode 100644 index 00000000..08ac459d --- /dev/null +++ b/testdata/fixtures/realworld/lookup-table-cast/manifest.json @@ -0,0 +1 @@ +{"files": ["constants.lua", "mapper.lua", "main.lua"], "check": {"errors": 11}} diff --git a/testdata/fixtures/realworld/lookup-table-cast/mapper.lua b/testdata/fixtures/realworld/lookup-table-cast/mapper.lua new file mode 100644 index 00000000..59410c4c --- /dev/null +++ b/testdata/fixtures/realworld/lookup-table-cast/mapper.lua @@ -0,0 +1,39 @@ +local constants = require("constants") + +type FinishReasonMap = {[string]: string} +type ErrorTypeMap = {[string]: string} +type StatusCodeMap = {[number]: string} + +local M = {} + +M.finish_reasons: FinishReasonMap = {} +M.finish_reasons["end_turn"] = constants.FINISH_REASON.STOP +M.finish_reasons["max_tokens"] = constants.FINISH_REASON.LENGTH +M.finish_reasons["stop_sequence"] = constants.FINISH_REASON.STOP +M.finish_reasons["tool_use"] = constants.FINISH_REASON.TOOL_CALL + +M.error_types: ErrorTypeMap = {} +M.error_types["invalid_request_error"] = constants.ERROR_TYPE.INVALID_REQUEST +M.error_types["authentication_error"] = constants.ERROR_TYPE.AUTHENTICATION +M.error_types["rate_limit_error"] = constants.ERROR_TYPE.RATE_LIMIT +M.error_types["api_error"] = constants.ERROR_TYPE.SERVER_ERROR + +M.status_codes: StatusCodeMap = {} +M.status_codes[400] = constants.ERROR_TYPE.INVALID_REQUEST +M.status_codes[401] = constants.ERROR_TYPE.AUTHENTICATION +M.status_codes[429] = constants.ERROR_TYPE.RATE_LIMIT +M.status_codes[500] = constants.ERROR_TYPE.SERVER_ERROR + +function M.map_finish_reason(api_reason: string): string + return M.finish_reasons[api_reason] or "unknown" +end + +function M.map_error_type(api_error: string): string + return M.error_types[api_error] or constants.ERROR_TYPE.SERVER_ERROR +end + +function M.map_status_code(code: number): string + return M.status_codes[code] or constants.ERROR_TYPE.SERVER_ERROR +end + +return M diff --git a/testdata/fixtures/realworld/multi-return-error-chain/main.lua b/testdata/fixtures/realworld/multi-return-error-chain/main.lua new file mode 100644 index 00000000..39657676 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/main.lua @@ -0,0 +1,12 @@ +local process = require("process") + +local result, err = process.run("some config input") +if err then + print("Error: " .. err) +end +if result then + local msg: string = result.message + local host: string = result.config.host + local port: number = result.config.port + local validated: true = result.config.validated +end diff --git a/testdata/fixtures/realworld/multi-return-error-chain/manifest.json b/testdata/fixtures/realworld/multi-return-error-chain/manifest.json new file mode 100644 index 00000000..6b4abb16 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/manifest.json @@ -0,0 +1 @@ +{"files": ["parse.lua", "validate.lua", "process.lua", "main.lua"]} \ No newline at end of file diff --git a/testdata/fixtures/realworld/multi-return-error-chain/parse.lua b/testdata/fixtures/realworld/multi-return-error-chain/parse.lua new file mode 100644 index 00000000..3e15c5b1 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/parse.lua @@ -0,0 +1,24 @@ +type ParsedConfig = { + host: string, + port: number, + debug: boolean, +} + +local M = {} + +function M.parse_string(input: string): (ParsedConfig?, string?) + if #input == 0 then + return nil, "empty input" + end + return {host = "localhost", port = 8080, debug = false}, nil +end + +function M.parse_number(input: string): (number?, string?) + local n = tonumber(input) + if not n then + return nil, "invalid number: " .. input + end + return n, nil +end + +return M diff --git a/testdata/fixtures/realworld/multi-return-error-chain/process.lua b/testdata/fixtures/realworld/multi-return-error-chain/process.lua new file mode 100644 index 00000000..b46e1422 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/process.lua @@ -0,0 +1,25 @@ +local validate = require("validate") + +type ProcessResult = { + message: string, + config: ValidConfig, +} + +local M = {} + +function M.run(input: string): (ProcessResult?, string?) + local config, err = validate.parse_and_validate(input) + if err then + return nil, "process: " .. err + end + if not config then + return nil, "config is nil" + end + local result: ProcessResult = { + message = "Configured " .. config.host .. ":" .. tostring(config.port), + config = config, + } + return result, nil +end + +return M diff --git a/testdata/fixtures/realworld/multi-return-error-chain/validate.lua b/testdata/fixtures/realworld/multi-return-error-chain/validate.lua new file mode 100644 index 00000000..ecae5632 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/validate.lua @@ -0,0 +1,39 @@ +local parse = require("parse") + +type ValidConfig = { + host: string, + port: number, + debug: boolean, + validated: true, +} + +local M = {} + +function M.validate(config: ParsedConfig): (ValidConfig?, string?) + if #config.host == 0 then + return nil, "host is empty" + end + if config.port < 1 or config.port > 65535 then + return nil, "port out of range: " .. tostring(config.port) + end + local valid: ValidConfig = { + host = config.host, + port = config.port, + debug = config.debug, + validated = true, + } + return valid, nil +end + +function M.parse_and_validate(input: string): (ValidConfig?, string?) + local parsed, parse_err = parse.parse_string(input) + if parse_err then + return nil, "parse: " .. parse_err + end + if not parsed then + return nil, "parse returned nil" + end + return M.validate(parsed) +end + +return M diff --git a/testdata/fixtures/realworld/service-locator/cache.lua b/testdata/fixtures/realworld/service-locator/cache.lua new file mode 100644 index 00000000..16579ae9 --- /dev/null +++ b/testdata/fixtures/realworld/service-locator/cache.lua @@ -0,0 +1,38 @@ +type CacheEntry = {value: T, expires_at: number} + +type Cache = { + _store: {[string]: any}, + get: (self: Cache, key: string) -> any?, + set: (self: Cache, key: string, value: any, ttl: number?) -> (), + delete: (self: Cache, key: string) -> (), + has: (self: Cache, key: string) -> boolean, + clear: (self: Cache) -> (), +} + +local M = {} + +function M.new(): Cache + local c: Cache = { + _store = {}, + get = function(self: Cache, key: string): any? + local entry = self._store[key] + if not entry then return nil end + return entry.value + end, + set = function(self: Cache, key: string, value: any, ttl: number?) + self._store[key] = {value = value, expires_at = ttl or 0} + end, + delete = function(self: Cache, key: string) + self._store[key] = nil + end, + has = function(self: Cache, key: string): boolean + return self._store[key] ~= nil + end, + clear = function(self: Cache) + self._store = {} + end, + } + return c +end + +return M diff --git a/testdata/fixtures/realworld/service-locator/locator.lua b/testdata/fixtures/realworld/service-locator/locator.lua new file mode 100644 index 00000000..82f11484 --- /dev/null +++ b/testdata/fixtures/realworld/service-locator/locator.lua @@ -0,0 +1,37 @@ +local logger = require("logger") +local cache = require("cache") + +type Services = { + logger: Logger, + cache: Cache, +} + +local M = {} + +local _services: Services? = nil + +function M.init(log_level: LogLevel?): Services + local s: Services = { + logger = logger.new(log_level), + cache = cache.new(), + } + _services = s + return s +end + +function M.get(): Services + if not _services then + return M.init() + end + return _services +end + +function M.logger(): Logger + return M.get().logger +end + +function M.cache(): Cache + return M.get().cache +end + +return M diff --git a/testdata/fixtures/realworld/service-locator/logger.lua b/testdata/fixtures/realworld/service-locator/logger.lua new file mode 100644 index 00000000..5df03278 --- /dev/null +++ b/testdata/fixtures/realworld/service-locator/logger.lua @@ -0,0 +1,36 @@ +type LogLevel = "debug" | "info" | "warn" | "error" + +type Logger = { + level: LogLevel, + log: (self: Logger, level: LogLevel, msg: string) -> (), + debug: (self: Logger, msg: string) -> (), + info: (self: Logger, msg: string) -> (), + warn: (self: Logger, msg: string) -> (), + error: (self: Logger, msg: string) -> (), +} + +local M = {} + +function M.new(level: LogLevel?): Logger + local logger: Logger = { + level = level or "info", + log = function(self: Logger, level: LogLevel, msg: string) + print("[" .. level .. "] " .. msg) + end, + debug = function(self: Logger, msg: string) + self:log("debug", msg) + end, + info = function(self: Logger, msg: string) + self:log("info", msg) + end, + warn = function(self: Logger, msg: string) + self:log("warn", msg) + end, + error = function(self: Logger, msg: string) + self:log("error", msg) + end, + } + return logger +end + +return M diff --git a/testdata/fixtures/realworld/service-locator/main.lua b/testdata/fixtures/realworld/service-locator/main.lua new file mode 100644 index 00000000..9decbe0f --- /dev/null +++ b/testdata/fixtures/realworld/service-locator/main.lua @@ -0,0 +1,16 @@ +local locator = require("locator") + +local services = locator.init("debug") + +services.logger:info("starting up") +services.cache:set("session", "abc123", 3600) + +local log = locator.logger() +log:debug("cache populated") + +local c = locator.cache() +local has_session: boolean = c:has("session") +local session = c:get("session") + +c:delete("session") +c:clear() diff --git a/testdata/fixtures/realworld/service-locator/manifest.json b/testdata/fixtures/realworld/service-locator/manifest.json new file mode 100644 index 00000000..f8903270 --- /dev/null +++ b/testdata/fixtures/realworld/service-locator/manifest.json @@ -0,0 +1 @@ +{"files": ["logger.lua", "cache.lua", "locator.lua", "main.lua"], "check": {"errors": 14}} diff --git a/testdata/fixtures/realworld/sql-repository/db.lua b/testdata/fixtures/realworld/sql-repository/db.lua new file mode 100644 index 00000000..8f76d4c5 --- /dev/null +++ b/testdata/fixtures/realworld/sql-repository/db.lua @@ -0,0 +1,35 @@ +type DbType = "postgres" | "sqlite" | "mysql" +type QueryResult = {[string]: any} + +type Database = { + db_type: DbType, + type: (self: Database) -> (DbType, string?), + query: (self: Database, sql: string, params: {any}?) -> ({QueryResult}?, string?), + execute: (self: Database, sql: string, params: {any}?) -> (boolean, string?), +} + +local M = {} + +M.type = { + POSTGRES = "postgres", + SQLITE = "sqlite", + MYSQL = "mysql", +} + +function M.mock(db_type: DbType): Database + local db: Database = { + db_type = db_type, + type = function(self: Database): (DbType, string?) + return self.db_type, nil + end, + query = function(self: Database, sql: string, params: {any}?): ({QueryResult}?, string?) + return {{exists = true, count = 1}}, nil + end, + execute = function(self: Database, sql: string, params: {any}?): (boolean, string?) + return true, nil + end, + } + return db +end + +return M diff --git a/testdata/fixtures/realworld/sql-repository/main.lua b/testdata/fixtures/realworld/sql-repository/main.lua new file mode 100644 index 00000000..cd18a6e7 --- /dev/null +++ b/testdata/fixtures/realworld/sql-repository/main.lua @@ -0,0 +1,18 @@ +local db = require("db") +local repository = require("repository") + +local database = db.mock("postgres") + +local exists, err = repository.table_exists(database) +if err then + print("Error: " .. err) +end + +local ok, init_err = repository.init(database) + +local recorded, rec_err = repository.record(database, "001_init", "Initial schema") + +local applied, app_err = repository.is_applied(database, "001_init") +if applied then + print("Migration 001_init is applied") +end diff --git a/testdata/fixtures/realworld/sql-repository/manifest.json b/testdata/fixtures/realworld/sql-repository/manifest.json new file mode 100644 index 00000000..8179b4af --- /dev/null +++ b/testdata/fixtures/realworld/sql-repository/manifest.json @@ -0,0 +1 @@ +{"files": ["db.lua", "repository.lua", "main.lua"], "check": {"errors": 4}} diff --git a/testdata/fixtures/realworld/sql-repository/repository.lua b/testdata/fixtures/realworld/sql-repository/repository.lua new file mode 100644 index 00000000..0cc71dc8 --- /dev/null +++ b/testdata/fixtures/realworld/sql-repository/repository.lua @@ -0,0 +1,85 @@ +local db = require("db") + +type MigrationRecord = { + id: string, + applied_at: any, + description: string?, +} + +local M = {} + +M.schemas = { + postgres = [[CREATE TABLE IF NOT EXISTS _migrations ( + id VARCHAR(512) PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT NOW(), + description TEXT + )]], + sqlite = [[CREATE TABLE IF NOT EXISTS _migrations ( + id VARCHAR(512) PRIMARY KEY, + applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + description TEXT + )]], + mysql = [[CREATE TABLE IF NOT EXISTS _migrations ( + id VARCHAR(512) PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + description TEXT + )]], +} + +M.table_exists_queries = { + postgres = [[SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = '_migrations')]], + sqlite = [[SELECT COUNT(*) AS count FROM sqlite_master WHERE type='table' AND name='_migrations']], + mysql = [[SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_name = '_migrations']], +} + +function M.table_exists(database: Database): (boolean?, string?) + local db_type, err = database:type() + if err then + return nil, "Failed to determine database type: " .. tostring(err) + end + local check_query = M.table_exists_queries[db_type] + if not check_query then + return nil, "Unsupported database type: " .. db_type + end + local result, query_err = database:query(check_query) + if query_err then + return nil, "Query failed: " .. tostring(query_err) + end + if result and result[1] then + return result[1].exists or (result[1].count and result[1].count > 0), nil + end + return false, nil +end + +function M.init(database: Database): (boolean, string?) + local exists, err = M.table_exists(database) + if err then return false, err end + if exists then return true, nil end + + local db_type, type_err = database:type() + if type_err then return false, type_err end + + local schema = M.schemas[db_type] + if not schema then + return false, "Unsupported database type: " .. db_type + end + return database:execute(schema) +end + +function M.record(database: Database, id: string, description: string?): (boolean, string?) + return database:execute( + "INSERT INTO _migrations (id, description) VALUES (?, ?)", + {id, description or ""} + ) +end + +function M.is_applied(database: Database, id: string): (boolean, string?) + local result, err = database:query( + "SELECT id FROM _migrations WHERE id = ?", + {id} + ) + if err then return false, err end + return result ~= nil and #result > 0, nil +end + +return M diff --git a/testdata/fixtures/realworld/trait-registry/main.lua b/testdata/fixtures/realworld/trait-registry/main.lua new file mode 100644 index 00000000..4fe3058d --- /dev/null +++ b/testdata/fixtures/realworld/trait-registry/main.lua @@ -0,0 +1,30 @@ +local types = require("types") +local processor = require("processor") + +local entry: TraitRegistryEntry = { + id = "search-trait", + meta = {type = types.TRAIT_TYPE, name = "Search", comment = "Web search capability"}, + data = { + prompt = "You can search the web using the search tool.", + tools = { + "tool:web-search", + {id = "tool:scrape", description = "Scrape a URL", alias = "fetch"}, + {id = "tool:summarize", context = {max_length = 500}}, + }, + context = {api_key = "sk-123"}, + }, +} + +local spec, err = processor.build_trait(entry) +if err == nil and spec then + local name: string = spec.name + local prompt: string = spec.prompt + local tool_count: number = #spec.tools + local first_tool_id: string = spec.tools[1].id +end + +local normalized = processor.normalize_tool("tool:simple") +local simple_id: string = normalized.id + +local complex = processor.normalize_tool({id = "tool:complex", alias = "cx"}) +local complex_alias: string? = complex.alias diff --git a/testdata/fixtures/realworld/trait-registry/manifest.json b/testdata/fixtures/realworld/trait-registry/manifest.json new file mode 100644 index 00000000..5100ded7 --- /dev/null +++ b/testdata/fixtures/realworld/trait-registry/manifest.json @@ -0,0 +1 @@ +{"files": ["types.lua", "processor.lua", "main.lua"]} \ No newline at end of file diff --git a/testdata/fixtures/realworld/trait-registry/processor.lua b/testdata/fixtures/realworld/trait-registry/processor.lua new file mode 100644 index 00000000..69759c91 --- /dev/null +++ b/testdata/fixtures/realworld/trait-registry/processor.lua @@ -0,0 +1,45 @@ +local types = require("types") + +local M = {} + +function M.normalize_tool(tool_def: TraitToolDef): TraitToolEntry + if type(tool_def) == "string" then + return {id = tool_def} + end + local entry: TraitToolEntry = { + id = tool_def.id, + context = tool_def.context, + description = tool_def.description, + alias = tool_def.alias, + } + return entry +end + +function M.normalize_tools(tools_data: {TraitToolDef}?): {TraitToolEntry} + if not tools_data or #tools_data == 0 then + return {} + end + local result: {TraitToolEntry} = {} + for _, tool_def in ipairs(tools_data) do + table.insert(result, M.normalize_tool(tool_def)) + end + return result +end + +function M.build_trait(entry: TraitRegistryEntry): (TraitSpec?, string?) + if not entry.data then + return nil, "trait has no data: " .. entry.id + end + local data = entry.data + local spec: TraitSpec = { + id = entry.id, + name = entry.meta and entry.meta.name or entry.id, + description = entry.meta and entry.meta.comment or "", + prompt = data.prompt or "", + tools = M.normalize_tools(data.tools), + context = data.context or {}, + } + return spec, nil +end + +return M diff --git a/testdata/fixtures/realworld/trait-registry/types.lua b/testdata/fixtures/realworld/trait-registry/types.lua new file mode 100644 index 00000000..f3850b47 --- /dev/null +++ b/testdata/fixtures/realworld/trait-registry/types.lua @@ -0,0 +1,38 @@ +type TraitToolDef = string | { + id: string, + context: {[string]: any}?, + description: string?, + alias: string?, +} + +type TraitToolEntry = { + id: string, + context: {[string]: any}?, + description: string?, + alias: string?, +} + +type TraitSpec = { + id: string, + name: string, + description: string, + prompt: string, + tools: {TraitToolEntry}, + context: {[string]: any}, +} + +type TraitRegistryEntry = { + id: string, + meta: {type: string?, name: string?, comment: string?}?, + data: { + prompt: string?, + tools: {TraitToolDef}?, + context: {[string]: any}?, + }?, +} + +local M = {} + +M.TRAIT_TYPE = "agent.trait" + +return M diff --git a/testdata/fixtures/realworld/typed-enum-constants/handler.lua b/testdata/fixtures/realworld/typed-enum-constants/handler.lua new file mode 100644 index 00000000..38e06278 --- /dev/null +++ b/testdata/fixtures/realworld/typed-enum-constants/handler.lua @@ -0,0 +1,36 @@ +local status = require("status") + +type Route = { + method: HttpMethod, + path: string, + handler: (req: Request) -> Response, +} + +type Router = { + _routes: {Route}, + add: (self: Router, method: HttpMethod, path: string, handler: (req: Request) -> Response) -> Router, + handle: (self: Router, req: Request) -> Response, +} + +local M = {} + +function M.new(): Router + local router: Router = { + _routes = {}, + add = function(self: Router, method: HttpMethod, path: string, handler: (req: Request) -> Response): Router + table.insert(self._routes, {method = method, path = path, handler = handler}) + return self + end, + handle = function(self: Router, req: Request): Response + for _, route in ipairs(self._routes) do + if route.method == req.method and route.path == req.path then + return route.handler(req) + end + end + return status.error(404, "Not found: " .. req.method .. " " .. req.path) + end, + } + return router +end + +return M diff --git a/testdata/fixtures/realworld/typed-enum-constants/main.lua b/testdata/fixtures/realworld/typed-enum-constants/main.lua new file mode 100644 index 00000000..88526721 --- /dev/null +++ b/testdata/fixtures/realworld/typed-enum-constants/main.lua @@ -0,0 +1,37 @@ +local status = require("status") +local handler = require("handler") + +local router = handler.new() + :add("GET", "/users", function(req: Request): Response + return status.ok({users = {"Alice", "Bob"}}) + end) + :add("POST", "/users", function(req: Request): Response + if not req.body then + return status.error(400, "Missing body") + end + return status.created({id = "new-user"}) + end) + :add("DELETE", "/users", function(req: Request): Response + return status.ok() + end) + +local get_resp = router:handle({ + method = "GET", + path = "/users", + headers = {}, +}) +local get_status: number = get_resp.status + +local post_resp = router:handle({ + method = "POST", + path = "/users", + body = {name = "Charlie"}, + headers = {["content-type"] = "application/json"}, +}) + +local not_found = router:handle({ + method = "GET", + path = "/missing", + headers = {}, +}) +local nf_status: number = not_found.status diff --git a/testdata/fixtures/realworld/typed-enum-constants/manifest.json b/testdata/fixtures/realworld/typed-enum-constants/manifest.json new file mode 100644 index 00000000..56f0a98a --- /dev/null +++ b/testdata/fixtures/realworld/typed-enum-constants/manifest.json @@ -0,0 +1 @@ +{"files": ["status.lua", "handler.lua", "main.lua"], "check": {"errors": 6}} diff --git a/testdata/fixtures/realworld/typed-enum-constants/status.lua b/testdata/fixtures/realworld/typed-enum-constants/status.lua new file mode 100644 index 00000000..766500d5 --- /dev/null +++ b/testdata/fixtures/realworld/typed-enum-constants/status.lua @@ -0,0 +1,49 @@ +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" +type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500 + +type Request = { + method: HttpMethod, + path: string, + body: any?, + headers: {[string]: string}, +} + +type Response = { + status: StatusCode, + body: any?, + headers: {[string]: string}, +} + +local M = {} + +M.METHOD = { + GET = "GET", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", + PATCH = "PATCH", +} + +M.STATUS = { + OK = 200, + CREATED = 201, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + SERVER_ERROR = 500, +} + +function M.ok(body: any?): Response + return {status = 200, body = body, headers = {}} +end + +function M.created(body: any?): Response + return {status = 201, body = body, headers = {}} +end + +function M.error(status: StatusCode, message: string): Response + return {status = status, body = {error = message}, headers = {}} +end + +return M From e3c6f80d1e72d32e5e067c7cc6f7efe7cbff92f6 Mon Sep 17 00:00:00 2001 From: Wolfy-J Date: Fri, 3 Apr 2026 16:43:47 -0400 Subject: [PATCH 10/10] Fix non-deterministic module ordering and add type export pattern Fix module iteration in check phase to use deterministic file order instead of map iteration. This resolves flaky test results caused by Go map randomization affecting checker fixpoint convergence. Update error-handling-chain to demonstrate idiomatic type export pattern (M.AppError = AppError) with runtime :is() validation. Cross-module :is() returning unknown is tracked as expect-error. Set error counts for multi-return-error-chain (was passing due to lucky map ordering, now deterministically tracked). --- fixture_harness_test.go | 18 +++++++++++------- .../multi-return-error-chain/manifest.json | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/fixture_harness_test.go b/fixture_harness_test.go index aca1a58d..3b92a2a7 100644 --- a/fixture_harness_test.go +++ b/fixture_harness_test.go @@ -196,24 +196,28 @@ func runCheckPhase(t *testing.T, s namedSuite) { allExpectations = append(allExpectations, parseExpectations(f, src)...) } - // Check and export dependency modules (all except entry) - modules := make(map[string]*testutil.ModuleResult) + // Check and export dependency modules (all except entry), preserving file order + type namedModule struct { + name string + mod *testutil.ModuleResult + } + var moduleOrder []namedModule var allDiagnostics []diag.Diagnostic for _, f := range files[:len(files)-1] { modOpts := append([]testutil.Option{}, baseOpts...) - for depName, depMod := range modules { - modOpts = append(modOpts, testutil.WithModule(depName, depMod)) + for _, nm := range moduleOrder { + modOpts = append(modOpts, testutil.WithModule(nm.name, nm.mod)) } name := strings.TrimSuffix(f, ".lua") mod := testutil.CheckAndExport(sources[f], name, modOpts...) - modules[name] = mod + moduleOrder = append(moduleOrder, namedModule{name, mod}) allDiagnostics = append(allDiagnostics, mod.Errors...) } // Check entry point entryOpts := append([]testutil.Option{}, baseOpts...) - for name, mod := range modules { - entryOpts = append(entryOpts, testutil.WithModule(name, mod)) + for _, nm := range moduleOrder { + entryOpts = append(entryOpts, testutil.WithModule(nm.name, nm.mod)) } entryFile := files[len(files)-1] result := testutil.Check(sources[entryFile], entryOpts...) diff --git a/testdata/fixtures/realworld/multi-return-error-chain/manifest.json b/testdata/fixtures/realworld/multi-return-error-chain/manifest.json index 6b4abb16..c98a07e7 100644 --- a/testdata/fixtures/realworld/multi-return-error-chain/manifest.json +++ b/testdata/fixtures/realworld/multi-return-error-chain/manifest.json @@ -1 +1 @@ -{"files": ["parse.lua", "validate.lua", "process.lua", "main.lua"]} \ No newline at end of file +{"files": ["parse.lua", "validate.lua", "process.lua", "main.lua"]}