Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,333 @@ mod test {

assert_eq!(ws.expr_ty("result"), ws.ty("string"));
}

#[test]
fn test_apply_return_infer_prefers_structural_callback_over_function_fallback() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, R
---@param f fun(x: A): R
---@param x A
---@return R
local function apply(f, x)
return f(x)
end

---@overload fun<T>(cb: fun(): T): T
---@param cb function
---@return boolean
local function run(cb) end

---@overload fun<T>(cb: fun(x: integer): T): integer
---@overload fun<T>(cb: fun(x: string): T): string
---@overload fun(cb: function): boolean
local function classify(cb) end

---@overload fun<T>(cb: fun(): T): { value: T }
---@overload fun(cb: function): boolean
local function wrap(cb) end

local source ---@type table

---@return integer
local function cb_concrete()
return 1
end

---@type fun(): unknown
local cb_unknown

---@type fun(x: integer): unknown
local cb_param_unknown

---@type fun(x: string): unknown
local cb_param_unknown_string

---@param x integer
local function cb_param_unresolved(x)
return source.missing
end

---@param x string
local function cb_param_unresolved_string(x)
return source.missing
end

local function cb_named_unresolved()
return source.missing
end

run_concrete = apply(run, cb_concrete)

run_unknown = apply(run, cb_unknown)

run_unresolved = apply(run, function()
return source.missing
end)

run_named_unresolved = apply(run, cb_named_unresolved)

wrap_named_unresolved = apply(wrap, cb_named_unresolved)

classify_unknown = apply(classify, cb_param_unknown)

classify_unresolved = apply(classify, cb_param_unresolved)

classify_string_unknown = apply(classify, cb_param_unknown_string)

classify_string_unresolved = apply(classify, cb_param_unresolved_string)
"#,
);

// The callback return is concrete, so `T` is inferred as `integer` and the generic
// overload is more informative than the `function -> boolean` fallback.
assert_eq!(ws.expr_ty("run_concrete"), ws.ty("integer"));

// `function` is an erased fallback. A structural `fun(): unknown` callback should keep
// the generic overload and preserve the unknown return.
assert_eq!(ws.expr_ty("run_unknown"), ws.ty("unknown"));

// An unresolved closure return is treated the same as an explicit `unknown` return.
assert_eq!(ws.expr_ty("run_unresolved"), ws.ty("unknown"));

// The named-callback path should stay aligned with the inline unresolved callback case.
assert_eq!(ws.expr_ty("run_named_unresolved"), ws.ty("unknown"));

// The structural overload still wins when the unknown is nested in the return shape.
assert_eq!(
ws.expr_ty("wrap_named_unresolved"),
ws.ty("{ value: unknown }")
);

// The callback's parameter type is known, so the generic `fun(x: integer): T` overload
// should still win even though the callback return is only `unknown`.
assert_eq!(ws.expr_ty("classify_unknown"), ws.ty("integer"));

// The callback return is unresolved, but its parameter is still `integer`, so overload
// ranking should keep using that known shape and pick the generic integer branch.
assert_eq!(ws.expr_ty("classify_unresolved"), ws.ty("integer"));

// The callback's parameter type is `string`, so overload selection should not fall back
// to the first generic branch when the callback return is only `unknown`.
assert_eq!(ws.expr_ty("classify_string_unknown"), ws.ty("string"));

// The same `string`-parameter branch should still win when the callback return is
// unresolved and carried through a named callback value.
assert_eq!(ws.expr_ty("classify_string_unresolved"), ws.ty("string"));
}

#[test]
fn test_apply_return_infer_leaves_result_unknown_when_no_callable_member_matches_arg_shape() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, R
---@param f fun(x: A): R
---@param x A
---@return R
local function apply(f, x)
return f(x)
end

---@alias FnInt fun(x: integer): integer
---@alias FnString fun(x: string): string

---@type FnInt | FnString
local run

---@type boolean
local b

result = apply(run, b)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(result_ty, ws.ty("unknown"));
}

#[test]
fn test_apply_return_infer_uses_function_fallback_when_no_structural_overload_matches() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, R
---@param f fun(x: A): R
---@param x A
---@return R
local function apply(f, x)
return f(x)
end

---@param cb function
---@return boolean
local function run(cb) end

---@type fun(): unknown
local cb

result = apply(run, cb)
"#,
);

assert_eq!(ws.expr_ty("result"), ws.ty("boolean"));
}

#[test]
fn test_apply_return_infer_keeps_only_arity_compatible_fallbacks() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, B, R
---@param f fun(x: A, y: B): R
---@param x A
---@param y B
---@return R
local function apply2(f, x, y)
return f(x, y)
end

---@overload fun(x: integer): integer
---@param x integer
---@param y string
---@return string
local function run(x, y) end

local source ---@type table

result = apply2(run, 1, source.missing)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(ws.humanize_type(result_ty), "string");
}

#[test]
fn test_apply_return_infer_keeps_same_arity_overload_returns_when_tail_is_unknown() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, B, R
---@param f fun(x: A, y: B): R
---@param x A
---@param y B
---@return R
local function apply2(f, x, y)
return f(x, y)
end

---@overload fun(x: integer, y: number): number
---@param x integer
---@param y string
---@return string
local function run(x, y) end

local source ---@type table

result = apply2(run, 1, source.missing)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(result_ty, ws.ty("number|string"));
}

#[test]
fn test_apply_return_infer_keeps_unknown_return_when_arg_shape_is_unknown() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic A, R
---@param f fun(x: A): R
---@param x A
---@return R
local function apply(f, x)
return f(x)
end

---@overload fun(x: integer): unknown
---@param x string
---@return string
local function run(x) end

local source ---@type table

result = apply(run, source.missing)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(result_ty, ws.ty("unknown|string"));
}

#[test]
fn test_union_call_ignores_non_matching_generic_callable_member() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@type (fun<T: string>(x: T): T) | fun(x: integer): integer
local run

result = run(1)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(ws.humanize_type(result_ty), "integer");
}

#[test]
fn test_union_call_ignores_non_matching_generic_alias_member() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@alias GenericStr<T: string> fun(x: T): T

---@type GenericStr | fun(x: integer): integer
local run

result = run(1)
"#,
);

let result_ty = ws.expr_ty("result");
assert_eq!(ws.humanize_type(result_ty), "integer");
}

#[test]
fn test_direct_callable_union_unions_same_domain_returns() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@alias FnA fun(x: integer): integer
---@alias FnB fun(x: integer): boolean

---@type FnA | FnB
local run

result = run(1)
"#,
);

assert_eq!(ws.expr_ty("result"), ws.ty("integer|boolean"));
}

#[test]
fn test_plain_function_call_returns_unknown_values() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@type function
local f

a, b = f(1)
"#,
);

assert_eq!(ws.expr_ty("a"), ws.ty("unknown"));
assert_eq!(ws.expr_ty("b"), ws.ty("unknown"));
}
}
Loading
Loading