diff --git a/spec/metamethods/le_spec.lua b/spec/metamethods/le_spec.lua new file mode 100644 index 000000000..c0eaaa36c --- /dev/null +++ b/spec/metamethods/le_spec.lua @@ -0,0 +1,205 @@ +local util = require("spec.util") + +describe("binary metamethod __le using <=", function() + it("can be set on a record", util.check([[ + local type Rec = record + x: number + metamethod __call: function(Rec, string, number): string + metamethod __le: function(Rec, Rec): boolean + end + + local rec_mt: metatable + rec_mt = { + __call = function(self: Rec, s: string, n: number): string + return tostring(self.x + n) .. s + end, + __le = function(a: Rec, b: Rec): boolean + return a.x <= b.x + end + } + + local r = setmetatable({ x = 10 } as Rec, rec_mt) + local s = setmetatable({ x = 20 } as Rec, rec_mt) + + if r <= s then + print("yes") + end + ]])) + + it("can be used on a record prototype", util.check([[ + local record A + value: number + metamethod __call: function(A, number): A + metamethod __le: function(A, A): boolean + end + local A_mt: metatable + A_mt = { + __call = function(a: A, v: number): A + return setmetatable({value = v} as A, A_mt) + end, + __le = function(a: A, b: A): boolean + return a.value <= b.value + end, + } + + A.value = 10 + if A <= A then + print("wat!?") + end + ]])) + + it("can be used via the second argument", util.check([[ + local type Rec = record + x: number + metamethod __le: function(number, Rec): Rec + end + + local rec_mt: metatable + rec_mt = { + __le = function(a: number, b: Rec): boolean + return a <= b.x + end + } + + local s = setmetatable({ y = 20 } as Rec, rec_mt) + + if 10 <= s then + print("yes") + end + ]])) + + it("preserves nominal type checking when resolving metamethods for operators", util.check_type_error([[ + local type Temperature = record + n: number + metamethod __le: function(t1: Temperature, t2: Temperature): boolean + end + + local type Date = record + n: number + metamethod __le: function(t1: Date, t2: Date): boolean + end + + local temp2: Temperature = { n = 45 } + local birthday2 : Date = { n = 34 } + + setmetatable(temp2, { + __le = function(t1: Temperature, t2: Temperature): boolean + return t1.n <= t2.n + end, + }) + + setmetatable(birthday2, { + __le = function(t1: Date, t2: Date): boolean + return t1.n <= t2.n + end, + }) + + if temp2 <= birthday2 then + print("wat") + end + ]], { + { y = 26, msg = "Date is not a Temperature" }, + })) +end) + +describe("binary metamethod __le using >=", function() + it("can be set on a record", util.check([[ + local type Rec = record + x: number + metamethod __call: function(Rec, string, number): string + metamethod __le: function(Rec, Rec): boolean + end + + local rec_mt: metatable + rec_mt = { + __call = function(self: Rec, s: string, n: number): string + return tostring(self.x + n) .. s + end, + __le = function(a: Rec, b: Rec): boolean + return a.x <= b.x + end + } + + local r = setmetatable({ x = 10 } as Rec, rec_mt) + local s = setmetatable({ x = 20 } as Rec, rec_mt) + + if s >= r then + print("yes") + end + ]])) + + it("can be used on a record prototype", util.check([[ + local record A + value: number + metamethod __call: function(A, number): A + metamethod __le: function(A, A): boolean + end + local A_mt: metatable + A_mt = { + __call = function(a: A, v: number): A + return setmetatable({value = v} as A, A_mt) + end, + __le = function(a: A, b: A): boolean + return a.value <= b.value + end, + } + + A.value = 10 + if A >= A then + print("wat!?") + end + ]])) + + it("can be used via the second argument", util.check([[ + local type Rec = record + x: number + metamethod __le: function(number, Rec): Rec + end + + local rec_mt: metatable + rec_mt = { + __le = function(a: number, b: Rec): boolean + return a <= b.x + end + } + + local s = setmetatable({ y = 20 } as Rec, rec_mt) + + if s >= 10 then + print("yes") + end + ]])) + + it("preserves nominal type checking when resolving metamethods for operators", util.check_type_error([[ + local type Temperature = record + n: number + metamethod __le: function(t1: Temperature, t2: Temperature): boolean + end + + local type Date = record + n: number + metamethod __le: function(t1: Date, t2: Date): boolean + end + + local temp2: Temperature = { n = 45 } + local birthday2 : Date = { n = 34 } + + setmetatable(temp2, { + __le = function(t1: Temperature, t2: Temperature): boolean + return t1.n <= t2.n + end, + }) + + setmetatable(birthday2, { + __le = function(t1: Date, t2: Date): boolean + return t1.n <= t2.n + end, + }) + + if birthday2 >= temp2 then + print("wat") + end + ]], { + { y = 26, msg = "Date is not a Temperature" }, + })) +end) diff --git a/spec/metamethods/lt_spec.lua b/spec/metamethods/lt_spec.lua new file mode 100644 index 000000000..56c87bbfb --- /dev/null +++ b/spec/metamethods/lt_spec.lua @@ -0,0 +1,205 @@ +local util = require("spec.util") + +describe("binary metamethod __lt using <", function() + it("can be set on a record", util.check([[ + local type Rec = record + x: number + metamethod __call: function(Rec, string, number): string + metamethod __lt: function(Rec, Rec): boolean + end + + local rec_mt: metatable + rec_mt = { + __call = function(self: Rec, s: string, n: number): string + return tostring(self.x + n) .. s + end, + __lt = function(a: Rec, b: Rec): boolean + return a.x < b.x + end + } + + local r = setmetatable({ x = 10 } as Rec, rec_mt) + local s = setmetatable({ x = 20 } as Rec, rec_mt) + + if r < s then + print("yes") + end + ]])) + + it("can be used on a record prototype", util.check([[ + local record A + value: number + metamethod __call: function(A, number): A + metamethod __lt: function(A, A): boolean + end + local A_mt: metatable + A_mt = { + __call = function(a: A, v: number): A + return setmetatable({value = v} as A, A_mt) + end, + __lt = function(a: A, b: A): boolean + return a.value < b.value + end, + } + + A.value = 10 + if A < A then + print("wat!?") + end + ]])) + + it("can be used via the second argument", util.check([[ + local type Rec = record + x: number + metamethod __lt: function(number, Rec): Rec + end + + local rec_mt: metatable + rec_mt = { + __lt = function(a: number, b: Rec): boolean + return a < b.x + end + } + + local s = setmetatable({ y = 20 } as Rec, rec_mt) + + if 10 < s then + print("yes") + end + ]])) + + it("preserves nominal type checking when resolving metamethods for operators", util.check_type_error([[ + local type Temperature = record + n: number + metamethod __lt: function(t1: Temperature, t2: Temperature): boolean + end + + local type Date = record + n: number + metamethod __lt: function(t1: Date, t2: Date): boolean + end + + local temp2: Temperature = { n = 45 } + local birthday2 : Date = { n = 34 } + + setmetatable(temp2, { + __lt = function(t1: Temperature, t2: Temperature): boolean + return t1.n < t2.n + end, + }) + + setmetatable(birthday2, { + __lt = function(t1: Date, t2: Date): boolean + return t1.n < t2.n + end, + }) + + if temp2 < birthday2 then + print("wat") + end + ]], { + { y = 26, msg = "Date is not a Temperature" }, + })) +end) + +describe("binary metamethod __lt using >", function() + it("can be set on a record", util.check([[ + local type Rec = record + x: number + metamethod __call: function(Rec, string, number): string + metamethod __lt: function(Rec, Rec): boolean + end + + local rec_mt: metatable + rec_mt = { + __call = function(self: Rec, s: string, n: number): string + return tostring(self.x + n) .. s + end, + __lt = function(a: Rec, b: Rec): boolean + return a.x < b.x + end + } + + local r = setmetatable({ x = 10 } as Rec, rec_mt) + local s = setmetatable({ x = 20 } as Rec, rec_mt) + + if s > r then + print("yes") + end + ]])) + + it("can be used on a record prototype", util.check([[ + local record A + value: number + metamethod __call: function(A, number): A + metamethod __lt: function(A, A): boolean + end + local A_mt: metatable + A_mt = { + __call = function(a: A, v: number): A + return setmetatable({value = v} as A, A_mt) + end, + __lt = function(a: A, b: A): boolean + return a.value < b.value + end, + } + + A.value = 10 + if A > A then + print("wat!?") + end + ]])) + + it("can be used via the second argument", util.check([[ + local type Rec = record + x: number + metamethod __lt: function(number, Rec): Rec + end + + local rec_mt: metatable + rec_mt = { + __lt = function(a: number, b: Rec): boolean + return a < b.x + end + } + + local s = setmetatable({ y = 20 } as Rec, rec_mt) + + if s > 10 then + print("yes") + end + ]])) + + it("preserves nominal type checking when resolving metamethods for operators", util.check_type_error([[ + local type Temperature = record + n: number + metamethod __lt: function(t1: Temperature, t2: Temperature): boolean + end + + local type Date = record + n: number + metamethod __lt: function(t1: Date, t2: Date): boolean + end + + local temp2: Temperature = { n = 45 } + local birthday2 : Date = { n = 34 } + + setmetatable(temp2, { + __lt = function(t1: Temperature, t2: Temperature): boolean + return t1.n < t2.n + end, + }) + + setmetatable(birthday2, { + __lt = function(t1: Date, t2: Date): boolean + return t1.n < t2.n + end, + }) + + if birthday2 > temp2 then + print("wat") + end + ]], { + { y = 26, msg = "Date is not a Temperature" }, + })) +end) diff --git a/tl.lua b/tl.lua index 551e291f7..da4af9cd9 100644 --- a/tl.lua +++ b/tl.lua @@ -6359,6 +6359,11 @@ local binop_to_metamethod = { ["is"] = "__is", } +local flip_binop_to_metamethod = { + [">"] = "__lt", + [">="] = "__le", +} + local function is_unknown(t) return t.typename == "unknown" or t.typename == "unresolved_emptytable_value" @@ -11809,6 +11814,13 @@ self:expand_type(node, values, elements) }) local meta_on_operator if not t then local mt_name = binop_to_metamethod[node.op.op] + if not mt_name then + mt_name = flip_binop_to_metamethod[node.op.op] + if mt_name then + ra, rb = rb, ra + ua, ub = ub, ua + end + end if mt_name then t, meta_on_operator = self:check_metamethod(node, mt_name, ra, rb, ua, ub) end diff --git a/tl.tl b/tl.tl index 96599dcbe..808a8bb5d 100644 --- a/tl.tl +++ b/tl.tl @@ -6359,6 +6359,11 @@ local binop_to_metamethod: {string:string} = { ["is"] = "__is", } +local flip_binop_to_metamethod: {string:string} = { + [">"] = "__lt", + [">="] = "__le", +} + local function is_unknown(t: Type): boolean return t.typename == "unknown" or t.typename == "unresolved_emptytable_value" @@ -11809,6 +11814,13 @@ do local meta_on_operator: integer if not t then local mt_name = binop_to_metamethod[node.op.op] + if not mt_name then + mt_name = flip_binop_to_metamethod[node.op.op] + if mt_name then + ra, rb = rb, ra + ua, ub = ub, ua + end + end if mt_name then t, meta_on_operator = self:check_metamethod(node, mt_name, ra, rb, ua, ub) end