From 97653e44612477de75859690c422e7fbf0b402d5 Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Fri, 4 Sep 2026 04:47:35 +0300 Subject: [PATCH] fix: emit bxor, not bnot, for binary '~' at gen-target 5.1 The '~' token is both unary bitwise NOT and binary bitwise XOR, and the 5.1 compat pass matched on the operator token alone. Binary '~' was rewritten to bit32.bnot(a), dropping the right operand and changing the operation; a __bxor metamethod call was dispatched to __bnot. Check the arity so binary '~' falls through to the existing bit_operators branch. spec/util.lua built its line arrays with gmatch("([^\n]*)\n"), which drops the final line, so a util.gen mismatch confined to the last line of generated output was never reported. That hid the metamethod half of this bug from the new test and from the existing __bnot metamethod test. --- spec/lang/compat/lua_versions_spec.lua | 50 ++++++++++++++++++++++++++ spec/util.lua | 4 +-- teal.lua | 3 +- teal/gen/lua_compat.lua | 3 +- teal/gen/lua_compat.tl | 3 +- tl.lua | 3 +- 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/spec/lang/compat/lua_versions_spec.lua b/spec/lang/compat/lua_versions_spec.lua index 25b53a66..240a15a9 100644 --- a/spec/lang/compat/lua_versions_spec.lua +++ b/spec/lang/compat/lua_versions_spec.lua @@ -30,6 +30,20 @@ describe("Lua version compatibility", function() local x = bit32.band(2, (bit32.bor(bit32.rshift(c, bit32.bnot(4)), 0xff))) ]], "5.1")) + it("distinguishes unary and binary '~' operators", util.gen([[ + + + local c = 0xcafebabe + local x = c ~ 0xff + local y = ~c ~ c + ]], [[ + local bit32 = bit32; if not bit32 then local p, m = pcall(require, 'bit32'); if p then bit32 = m end end + + local c = 0xcafebabe + local x = bit32.bxor(c, 0xff) + local y = bit32.bxor(bit32.bnot(c), c) + ]], "5.1")) + it("generates compat code for bitwise unary operator metamethods", util.gen([[ local type Rec = record @@ -100,6 +114,42 @@ describe("Lua version compatibility", function() print(_tl_mt("__shl", 1, r, s)) ]], "5.1")) + it("generates compat code for the binary '~' operator metamethod", util.gen([[ + + local type Rec = record + x: number + metamethod __bxor: function(Rec, Rec): number + end + + local rec_mt: metatable = { + __bxor = function(a: Rec, b: Rec): number + return a.x + b.x + end + } + + local r = setmetatable({} as Rec, rec_mt) + local s = setmetatable({} as Rec, rec_mt) + + print(r ~ s) + ]], [[ + local _tl_mt = function(m, s, a, b) return (getmetatable(s == 1 and a or b)[m](a, b)) end + + + + + + local rec_mt = { + __bxor = function(a, b) + return a.x + b.x + end, + } + + local r = setmetatable({}, rec_mt) + local s = setmetatable({}, rec_mt) + + print(_tl_mt("__bxor", 1, r, s)) + ]], "5.1")) + -- varargs it("generates compatibility code for named varargs with Lua 5.1", util.gen([[ diff --git a/spec/util.lua b/spec/util.lua index 53311a38..769f28a5 100644 --- a/spec/util.lua +++ b/spec/util.lua @@ -743,12 +743,12 @@ local function gen(lax, code, expected, gen_target, type_errors) local expected_code = util.dedent(expected):match("^(.-)%s*$") local expected_lines = {} - for line in expected_code:gmatch("([^\n]*)\n") do + for line in expected_code:gmatch("([^\n]*)\n?") do table.insert(expected_lines, line) end local output_lines = {} - for line in output_code:gmatch("([^\n]*)\n") do + for line in output_code:gmatch("([^\n]*)\n?") do table.insert(output_lines, line) end diff --git a/teal.lua b/teal.lua index 8101f29c..74ec65ab 100644 --- a/teal.lua +++ b/teal.lua @@ -11277,7 +11277,8 @@ local function adjust_code(ast, needs_compat, gen_compat, gen_target) needs_compat[key] = true end end - elseif node.op.op == "~" and gen_target == "5.1" then + + elseif node.op.op == "~" and node.op.arity == 1 and gen_target == "5.1" then if node.op.meta_on_operand then needs_compat["mt"] = true convert_node_to_compat_mt_call(node, unop_to_metamethod[node.op.op], 1, node.e1) diff --git a/teal/gen/lua_compat.lua b/teal/gen/lua_compat.lua index f1550508..2e62c55a 100644 --- a/teal/gen/lua_compat.lua +++ b/teal/gen/lua_compat.lua @@ -231,7 +231,8 @@ local function adjust_code(ast, needs_compat, gen_compat, gen_target) needs_compat[key] = true end end - elseif node.op.op == "~" and gen_target == "5.1" then + + elseif node.op.op == "~" and node.op.arity == 1 and gen_target == "5.1" then if node.op.meta_on_operand then needs_compat["mt"] = true convert_node_to_compat_mt_call(node, unop_to_metamethod[node.op.op], 1, node.e1) diff --git a/teal/gen/lua_compat.tl b/teal/gen/lua_compat.tl index bfadd2a7..6a0ed412 100644 --- a/teal/gen/lua_compat.tl +++ b/teal/gen/lua_compat.tl @@ -231,7 +231,8 @@ local function adjust_code(ast: Node, needs_compat: {string:boolean}, gen_compat needs_compat[key] = true end end - elseif node.op.op == "~" and gen_target == "5.1" then + -- unary '~' only; binary '~' is bxor, handled below + elseif node.op.op == "~" and node.op.arity == 1 and gen_target == "5.1" then if node.op.meta_on_operand then needs_compat["mt"] = true convert_node_to_compat_mt_call(node, unop_to_metamethod[node.op.op], 1, node.e1) diff --git a/tl.lua b/tl.lua index 3903f19d..d8521d3e 100644 --- a/tl.lua +++ b/tl.lua @@ -11531,7 +11531,8 @@ local function adjust_code(ast, needs_compat, gen_compat, gen_target) needs_compat[key] = true end end - elseif node.op.op == "~" and gen_target == "5.1" then + + elseif node.op.op == "~" and node.op.arity == 1 and gen_target == "5.1" then if node.op.meta_on_operand then needs_compat["mt"] = true convert_node_to_compat_mt_call(node, unop_to_metamethod[node.op.op], 1, node.e1)