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/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/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/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/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/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/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/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/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/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/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/fixture_harness_test.go b/fixture_harness_test.go new file mode 100644 index 00000000..3b92a2a7 --- /dev/null +++ b/fixture_harness_test.go @@ -0,0 +1,527 @@ +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" + "github.com/wippyai/go-lua/types/io" +) + +// 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"` + 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"` + 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()) + } + 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) + 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), 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 _, nm := range moduleOrder { + modOpts = append(modOpts, testutil.WithModule(nm.name, nm.mod)) + } + name := strings.TrimSuffix(f, ".lua") + mod := testutil.CheckAndExport(sources[f], name, modOpts...) + moduleOrder = append(moduleOrder, namedModule{name, mod}) + allDiagnostics = append(allDiagnostics, mod.Errors...) + } + + // Check entry point + entryOpts := append([]testutil.Option{}, baseOpts...) + for _, nm := range moduleOrder { + entryOpts = append(entryOpts, testutil.WithModule(nm.name, nm.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 + // 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 + } + 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) +} + +// 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) { + 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/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/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/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/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/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/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-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-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-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/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/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-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/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/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/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/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/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/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/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/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/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/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/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-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/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/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/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/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/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-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-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-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/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/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/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/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/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/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-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 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/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/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 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/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/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 new file mode 100644 index 00000000..f71c8413 --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/errors.lua @@ -0,0 +1,26 @@ +type AppError = { + code: string, + message: string, + retryable: boolean +} + +local M = {} +M.AppError = AppError + +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..82672793 --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/main.lua @@ -0,0 +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 = 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/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..63d714a1 --- /dev/null +++ b/testdata/fixtures/realworld/error-handling-chain/validator.lua @@ -0,0 +1,18 @@ +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 + 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..de77c027 --- /dev/null +++ b/testdata/fixtures/realworld/factory-constructor/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["counter.lua", "main.lua"], + "check": {"errors": 6} +} 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/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/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/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..ab74a8cf --- /dev/null +++ b/testdata/fixtures/realworld/module-with-generics/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["collection.lua", "main.lua"], + "check": {"errors": 5} +} 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..c98a07e7 --- /dev/null +++ b/testdata/fixtures/realworld/multi-return-error-chain/manifest.json @@ -0,0 +1 @@ +{"files": ["parse.lua", "validate.lua", "process.lua", "main.lua"]} 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/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/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/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..325eda3c --- /dev/null +++ b/testdata/fixtures/realworld/table-builder-pattern/manifest.json @@ -0,0 +1,4 @@ +{ + "files": ["config.lua", "main.lua"], + "check": {"errors": 3} +} 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-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 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 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 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/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 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