From 94e3e89da22987936c40b31b1a0a527c45fd432d Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:28:29 +1300 Subject: [PATCH 1/5] fix: generic classes with generic methods --- script/vm/compiler.lua | 39 ++++++++---- script/vm/generic.lua | 50 ++++++++++++--- test/type_inference/common.lua | 108 +++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 21 deletions(-) diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 20ab3ac41..2d4561636 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1834,26 +1834,43 @@ local function bindReturnOfFunction(source, mfunc, index, args) if doc.type == 'doc.return' then for _, rtn in ipairs(doc.returns) do if rtn.returnIndex == index then - -- Only clone if the return type only references class-level generics - -- (i.e. generics that exist in the genericMap from the receiver). - -- Method-level generics (e.g. V not in {T=string}) are - -- already resolved correctly in the first round above. - local onlyClassGenerics = true - guide.eachSourceType(rtn, 'doc.generic.name', function(src) - if not genericMap[src[1]] then - onlyClassGenerics = false + -- Check if rtn references any class generics from the receiver. + local hasClassGenerics = false + local function checkGeneric(src) + if genericMap[src[1]] then + hasClassGenerics = true end - end) - if onlyClassGenerics then + end + guide.eachSourceType(rtn, 'doc.generic.name', checkGeneric) + if not hasClassGenerics then + guide.eachSourceType(rtn, 'doc.type.name', checkGeneric) + end + if hasClassGenerics then local newRtn = vm.cloneObject(rtn, genericMap) if newRtn then returnNode = vm.compileNode(newRtn) for rnode in returnNode:eachObject() do if rnode.type == 'generic' then - returnNode = rnode:resolve(guide.getUri(source), args) + returnNode = rnode:resolve(guide.getUri(source), resolveArgs) break end end + local sign = vm.getSign(mfunc) + if sign and resolveArgs and #resolveArgs > 0 then + local resolved = sign:resolve(guide.getUri(source), resolveArgs) + if resolved and next(resolved) then + local newReturnNode = vm.createNode() + for rnode in returnNode:eachObject() do + local cloned = vm.cloneObject(rnode, resolved) + if cloned then + newReturnNode:merge(vm.compileNode(cloned)) + else + newReturnNode:merge(rnode) + end + end + returnNode = newReturnNode + end + end end end break diff --git a/script/vm/generic.lua b/script/vm/generic.lua index 0b171622c..b6c095245 100644 --- a/script/vm/generic.lua +++ b/script/vm/generic.lua @@ -23,15 +23,35 @@ local function cloneObject(source, resolved) if source.type == 'doc.generic.name' then local key = source[1] local newName = { - type = source.type, - start = source.start, - finish = source.finish, - parent = source.parent, - [1] = source[1], + type = source.type, + start = source.start, + finish = source.finish, + parent = source.parent, + literal = source.literal, + generic = source.generic, + [1] = source[1], } if resolved[key] then vm.setNode(newName, resolved[key], true) newName._resolved = resolved[key] + elseif source._resolved then + local resolvedNode = vm.createNode() + local anyResolved = false + for rn in source._resolved:eachObject() do + if rn.type == 'doc.generic.name' and resolved[rn[1]] then + resolvedNode:merge(resolved[rn[1]]) + anyResolved = true + else + resolvedNode:merge(rn) + end + end + if anyResolved then + vm.setNode(newName, resolvedNode, true) + newName._resolved = resolvedNode + else + vm.setNode(newName, source._resolved, true) + newName._resolved = source._resolved + end end return newName end @@ -88,11 +108,12 @@ local function cloneObject(source, resolved) end if source.type == 'doc.type.table' then local newTable = { - type = source.type, - start = source.start, - finish = source.finish, - parent = source.parent, - fields = {}, + type = source.type, + start = source.start, + finish = source.finish, + parent = source.parent, + isTuple = source.isTuple, + fields = {}, } for i, field in ipairs(source.fields) do local newField = { @@ -113,6 +134,8 @@ local function cloneObject(source, resolved) start = source.start, finish = source.finish, parent = source.parent, + async = source.async, + signs = source.signs, args = {}, returns = {}, } @@ -157,6 +180,13 @@ local function cloneObject(source, resolved) guide.eachSourceType(sign, 'doc.generic.name', function (src) if hasConcreteResolution(src[1]) then needsClone = true + elseif src._resolved then + for rn in src._resolved:eachObject() do + if rn.type == 'doc.generic.name' and hasConcreteResolution(rn[1]) then + needsClone = true + break + end + end end end) end diff --git a/test/type_inference/common.lua b/test/type_inference/common.lua index 2fe6417d2..6ff643f48 100644 --- a/test/type_inference/common.lua +++ b/test/type_inference/common.lua @@ -5278,3 +5278,111 @@ local myClass local = myClass:pass(42) ]] + +-- Generic method on generic class via @field +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x:add(y) +]] + +TEST 'integer?' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +local = x:parse('foo') +]] + +TEST '[integer, string]?' [[ +---@class Parser +---@field parse fun(self: Parser, content: string): T? +---@field add fun(self: Parser, other: Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local a = x:add(y) +local = a:parse('foo') +]] + +-- Generic method on generic class via ---@generic +TEST 'Parser<[integer, string]>' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +---@type Parser +local v +local = u:add(v) +]] + +TEST 'integer?' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +local = u:parse('foo') +]] + +TEST '[integer, string]?' [[ +---@class Parser +local parser = {} + +---@generic U +---@param other Parser +---@return Parser<[T, U]> +function parser:add(other) + return {} +end + +---@param content string +---@return T? +function parser:parse(content) + return +end + +---@type Parser +local u +---@type Parser +local v +local w = u:add(v) +local = w:parse('foo') +]] From f44c0e82319c8b59455e58cbaaa78cf51f62c037 Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:35:18 +1300 Subject: [PATCH 2/5] chore: update changelog.md --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index daefdb5ae..278dce2a3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ # changelog ## Unreleased +* `FIX` Generic classes with generic methods now have correct type inference. ## 3.19.2 From 95d50bfc659bc8432ee6c20253fa35ef28d98ef4 Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:23:26 +1300 Subject: [PATCH 3/5] feat: add support for generic operators --- changelog.md | 1 + script/parser/guide.lua | 2 +- script/parser/luadoc.lua | 45 ++++++++++++++++++++++++ script/vm/generic.lua | 2 +- script/vm/operator.lua | 21 ++++++++++- script/vm/sign.lua | 2 +- test/type_inference/generic_operator.lua | 36 +++++++++++++++++++ 7 files changed, 105 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 278dce2a3..b62003deb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ # changelog ## Unreleased +* `FEAT` Operators can have generic parameters. * `FIX` Generic classes with generic methods now have correct type inference. diff --git a/script/parser/guide.lua b/script/parser/guide.lua index e3c7660b3..f3c145d40 100644 --- a/script/parser/guide.lua +++ b/script/parser/guide.lua @@ -195,7 +195,7 @@ local childMap = { ['doc.as'] = {'as'}, ['doc.cast'] = {'name', '#casts'}, ['doc.cast.block'] = {'extends'}, - ['doc.operator'] = {'op', 'exp', 'extends'}, + ['doc.operator'] = {'op', '#signs', 'exp', 'extends'}, ['doc.meta'] = {'name'}, ['doc.attr'] = {'#names'}, } diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua index 6d59b09c3..9ec2e8673 100644 --- a/script/parser/luadoc.lua +++ b/script/parser/luadoc.lua @@ -1626,6 +1626,9 @@ local docSwitch = util.switch() result.op = op result.finish = op.finish + -- Parse optional operator-level generic parameters: add + result.signs = parseSigns(result) + if checkToken('symbol', '(', 1) then nextToken() if checkToken('symbol', ')', 1) then @@ -1648,6 +1651,48 @@ local docSwitch = util.switch() result.finish = ret.finish end + -- Bind operator-local generics so that doc.type.name nodes for T are converted to doc.generic.name. + if result.signs then + local operatorGenerics = {} + for _, sign in ipairs(result.signs) do + operatorGenerics[sign[1]] = sign + end + local function bindOperatorTypeNames(obj) + if not obj then return end + if obj.type == 'doc.type.name' and operatorGenerics[obj[1]] then + obj.type = 'doc.generic.name' + obj.generic = operatorGenerics[obj[1]] + elseif obj.type == 'doc.type' and obj.types then + for _, t in ipairs(obj.types) do + bindOperatorTypeNames(t) + end + elseif obj.type == 'doc.type.array' then + bindOperatorTypeNames(obj.node) + elseif obj.type == 'doc.type.table' and obj.fields then + for _, field in ipairs(obj.fields) do + bindOperatorTypeNames(field.name) + bindOperatorTypeNames(field.extends) + end + elseif obj.type == 'doc.type.sign' then + bindOperatorTypeNames(obj.node) + if obj.signs then + for _, s in ipairs(obj.signs) do + bindOperatorTypeNames(s) + end + end + elseif obj.type == 'doc.type.function' then + for _, arg in ipairs(obj.args) do + bindOperatorTypeNames(arg.extends) + end + for _, rtn in ipairs(obj.returns) do + bindOperatorTypeNames(rtn) + end + end + end + bindOperatorTypeNames(result.exp) + bindOperatorTypeNames(result.extends) + end + return result end) : case 'source' diff --git a/script/vm/generic.lua b/script/vm/generic.lua index b6c095245..6de9351c3 100644 --- a/script/vm/generic.lua +++ b/script/vm/generic.lua @@ -211,7 +211,7 @@ local function cloneObject(source, resolved) end ---@param uri uri ----@param args parser.object +---@param args parser.object[] ---@return vm.node function mt:resolve(uri, args) local resolved = self.sign:resolve(uri, args) diff --git a/script/vm/operator.lua b/script/vm/operator.lua index ed53cdfc3..2ba44c867 100644 --- a/script/vm/operator.lua +++ b/script/vm/operator.lua @@ -116,7 +116,26 @@ local function checkOperators(operators, op, value, result, uri, classGlobal, si if not result then result = vm.createNode() end - result:merge(vm.compileNode(extends)) + -- Resolve operator-level generics by matching `exp` against the actual `value` argument. + local resolvedExtends = extends + if operator.signs and #operator.signs > 0 and vm.containsGenericName(exp) then + local opSign = vm.createSign() + if exp.type == 'doc.type' then + for _, t in ipairs(exp.types or {}) do + opSign:addSign(vm.compileNode(t)) + end + else + opSign:addSign(vm.compileNode(exp)) + end + local opResolved = opSign:resolve(opUri, {value}) + if opResolved and next(opResolved) then + local cloned = vm.cloneObject(extends, opResolved) + if cloned then + resolvedExtends = cloned + end + end + end + result:merge(vm.compileNode(resolvedExtends)) return result end end diff --git a/script/vm/sign.lua b/script/vm/sign.lua index 0b340a850..0094e941a 100644 --- a/script/vm/sign.lua +++ b/script/vm/sign.lua @@ -68,7 +68,7 @@ function mt:addDocGeneric(doc) end ---@param uri uri ----@param args parser.object +---@param args parser.object[] ---@return table? function mt:resolve(uri, args) if not args then diff --git a/test/type_inference/generic_operator.lua b/test/type_inference/generic_operator.lua index 69c1c931e..5dbb54bca 100644 --- a/test/type_inference/generic_operator.lua +++ b/test/type_inference/generic_operator.lua @@ -200,3 +200,39 @@ TESTGENERICNAMES '' [[ ---@operator call(): integer local Plain ]] + +-- Generic operators on a generic class +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@operator add(Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Chaining of operators +TEST 'Parser<[[integer, string], boolean]>' [[ +---@class Parser +---@operator add(Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +---@type Parser +local z +local = (x + y) + z +]] + +-- Operators work on concrete classes +TEST 'string' [[ +---@class Become +---@operator mul(T): T + +---@type Become +local x +local = x * "hello" +]] From 06860cef03723fd68ff317497b5869fd6d03ac70 Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:05:39 +1300 Subject: [PATCH 4/5] refactor: unify code with generic methods (fixes some bugs) --- script/parser/luadoc.lua | 130 +++++++++-------------- script/vm/compiler.lua | 28 +---- script/vm/generic.lua | 52 ++++++++- script/vm/operator.lua | 61 ++++++----- test/type_inference/generic_operator.lua | 72 +++++++++++++ 5 files changed, 206 insertions(+), 137 deletions(-) diff --git a/script/parser/luadoc.lua b/script/parser/luadoc.lua index 9ec2e8673..786487d44 100644 --- a/script/parser/luadoc.lua +++ b/script/parser/luadoc.lua @@ -511,6 +511,52 @@ local function parseDots(tp, parent) return dots end +local function bindLocalGenerics(rootObj, signs) + if not signs or not rootObj then + return + end + local generics = {} + for _, sign in ipairs(signs) do + generics[sign[1]] = sign + end + local function walk(obj) + if not obj then return end + if obj.type == 'doc.type.name' and generics[obj[1]] then + obj.type = 'doc.generic.name' + obj.generic = generics[obj[1]] + elseif obj.type == 'doc.type' and obj.types then + for _, t in ipairs(obj.types) do + walk(t) + end + elseif obj.type == 'doc.type.array' then + walk(obj.node) + elseif obj.type == 'doc.type.table' and obj.fields then + for _, field in ipairs(obj.fields) do + walk(field.name) + walk(field.extends) + end + elseif obj.type == 'doc.type.sign' then + walk(obj.node) + if obj.signs then + for _, s in ipairs(obj.signs) do + walk(s) + end + end + elseif obj.type == 'doc.type.function' then + for _, arg in ipairs(obj.args) do + walk(arg.extends) + end + for _, ret in ipairs(obj.returns) do + walk(ret) + end + elseif obj.type == 'doc.operator' then + walk(obj.exp) + walk(obj.extends) + end + end + walk(rootObj) +end + local function parseTypeUnitFunction(parent) if not checkToken('name', 'fun', 1) then return nil @@ -621,48 +667,7 @@ local function parseTypeUnitFunction(parent) typeUnit.finish = getFinish() -- Bind local generics from fun to type names within this function if typeUnit.signs then - local generics = {} - for _, sign in ipairs(typeUnit.signs) do - generics[sign[1]] = sign - end - local function bindTypeNames(obj) - if not obj then return end - if obj.type == 'doc.type.name' and generics[obj[1]] then - obj.type = 'doc.generic.name' - obj.generic = generics[obj[1]] - elseif obj.type == 'doc.type' and obj.types then - for _, t in ipairs(obj.types) do - bindTypeNames(t) - end - elseif obj.type == 'doc.type.array' then - bindTypeNames(obj.node) - elseif obj.type == 'doc.type.table' and obj.fields then - for _, field in ipairs(obj.fields) do - bindTypeNames(field.name) - bindTypeNames(field.extends) - end - elseif obj.type == 'doc.type.sign' then - bindTypeNames(obj.node) - if obj.signs then - for _, s in ipairs(obj.signs) do - bindTypeNames(s) - end - end - elseif obj.type == 'doc.type.function' then - for _, arg in ipairs(obj.args) do - bindTypeNames(arg.extends) - end - for _, ret in ipairs(obj.returns) do - bindTypeNames(ret) - end - end - end - for _, arg in ipairs(typeUnit.args) do - bindTypeNames(arg.extends) - end - for _, ret in ipairs(typeUnit.returns) do - bindTypeNames(ret) - end + bindLocalGenerics(typeUnit, typeUnit.signs) end return typeUnit end @@ -1651,46 +1656,9 @@ local docSwitch = util.switch() result.finish = ret.finish end - -- Bind operator-local generics so that doc.type.name nodes for T are converted to doc.generic.name. + -- Bind operator-local generics so that doc.type.name nodes for U are converted to doc.generic.name. if result.signs then - local operatorGenerics = {} - for _, sign in ipairs(result.signs) do - operatorGenerics[sign[1]] = sign - end - local function bindOperatorTypeNames(obj) - if not obj then return end - if obj.type == 'doc.type.name' and operatorGenerics[obj[1]] then - obj.type = 'doc.generic.name' - obj.generic = operatorGenerics[obj[1]] - elseif obj.type == 'doc.type' and obj.types then - for _, t in ipairs(obj.types) do - bindOperatorTypeNames(t) - end - elseif obj.type == 'doc.type.array' then - bindOperatorTypeNames(obj.node) - elseif obj.type == 'doc.type.table' and obj.fields then - for _, field in ipairs(obj.fields) do - bindOperatorTypeNames(field.name) - bindOperatorTypeNames(field.extends) - end - elseif obj.type == 'doc.type.sign' then - bindOperatorTypeNames(obj.node) - if obj.signs then - for _, s in ipairs(obj.signs) do - bindOperatorTypeNames(s) - end - end - elseif obj.type == 'doc.type.function' then - for _, arg in ipairs(obj.args) do - bindOperatorTypeNames(arg.extends) - end - for _, rtn in ipairs(obj.returns) do - bindOperatorTypeNames(rtn) - end - end - end - bindOperatorTypeNames(result.exp) - bindOperatorTypeNames(result.extends) + bindLocalGenerics(result, result.signs) end return result diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 2d4561636..fcf8c1c05 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1846,32 +1846,8 @@ local function bindReturnOfFunction(source, mfunc, index, args) guide.eachSourceType(rtn, 'doc.type.name', checkGeneric) end if hasClassGenerics then - local newRtn = vm.cloneObject(rtn, genericMap) - if newRtn then - returnNode = vm.compileNode(newRtn) - for rnode in returnNode:eachObject() do - if rnode.type == 'generic' then - returnNode = rnode:resolve(guide.getUri(source), resolveArgs) - break - end - end - local sign = vm.getSign(mfunc) - if sign and resolveArgs and #resolveArgs > 0 then - local resolved = sign:resolve(guide.getUri(source), resolveArgs) - if resolved and next(resolved) then - local newReturnNode = vm.createNode() - for rnode in returnNode:eachObject() do - local cloned = vm.cloneObject(rnode, resolved) - if cloned then - newReturnNode:merge(vm.compileNode(cloned)) - else - newReturnNode:merge(rnode) - end - end - returnNode = newReturnNode - end - end - end + local sign = vm.getSign(mfunc) + returnNode = vm.resolveGenericReturn(guide.getUri(source), genericMap, sign, resolveArgs, rtn) end break end diff --git a/script/vm/generic.lua b/script/vm/generic.lua index 6de9351c3..ce73e1994 100644 --- a/script/vm/generic.lua +++ b/script/vm/generic.lua @@ -49,8 +49,9 @@ local function cloneObject(source, resolved) vm.setNode(newName, resolvedNode, true) newName._resolved = resolvedNode else - vm.setNode(newName, source._resolved, true) - newName._resolved = source._resolved + local copied = source._resolved:copy() + vm.setNode(newName, copied, true) + newName._resolved = copied end end return newName @@ -306,3 +307,50 @@ end function vm.cloneObject(source, resolved) return cloneObject(source, resolved) end + +---Resolves generic return types for generic methods and generic operators +---by combining receiver class generics and member generic signs. +---@param uri uri +---@param classGenericMap? table +---@param sign? vm.sign +---@param args? parser.object[] +---@param returnAst parser.object +---@return vm.node? +function vm.resolveGenericReturn(uri, classGenericMap, sign, args, returnAst) + local combinedMap = classGenericMap + if sign and args and #args > 0 then + local memberResolved = sign:resolve(uri, args) + if memberResolved and next(memberResolved) then + combinedMap = {} + if classGenericMap then + for k, v in pairs(classGenericMap) do + combinedMap[k] = v + end + end + for k, v in pairs(memberResolved) do + combinedMap[k] = v + end + end + end + + local returnNode + if combinedMap and next(combinedMap) then + local clonedAst = vm.cloneObject(returnAst, combinedMap) + if clonedAst then + returnNode = vm.compileNode(clonedAst) + end + end + if not returnNode then + returnNode = vm.compileNode(returnAst) + end + + for rnode in returnNode:eachObject() do + if rnode.type == 'generic' and args then + returnNode = rnode:resolve(uri, args) + break + end + end + + return returnNode +end + diff --git a/script/vm/operator.lua b/script/vm/operator.lua index 2ba44c867..560df20f5 100644 --- a/script/vm/operator.lua +++ b/script/vm/operator.lua @@ -91,20 +91,33 @@ local function checkOperators(operators, op, value, result, uri, classGlobal, si -- 类型注解仅供 doc.class 等使用),克隆结果可能是 vm.generic,故声明联合类型。 ---@type parser.object|vm.generic local exp = operator.exp - ---@type parser.object|vm.generic local extends = operator.extends - if genericMap then - if exp and vm.containsGenericName(exp) then - exp = vm.cloneObject(exp, genericMap) or exp - end - if vm.containsGenericName(extends) then - extends = vm.cloneObject(extends, genericMap) or extends - end + if genericMap and exp and vm.containsGenericName(exp) then + exp = vm.cloneObject(exp, genericMap) or exp end if value and exp then local valueNode = vm.compileNode(value) local expNode = vm.compileNode(exp) local opUri = guide.getUri(operator) + local checkExpNode = expNode + for en in expNode:eachObject() do + if en.type == 'doc.type.sign' and en.node and en.node[1] then + if checkExpNode == expNode then + checkExpNode = vm.createNode() + for prev in expNode:eachObject() do + if prev == en then + checkExpNode:merge(vm.getGlobal('type', en.node[1]) or en) + else + checkExpNode:merge(prev) + end + end + else + checkExpNode:merge(vm.getGlobal('type', en.node[1]) or en) + end + elseif checkExpNode ~= expNode then + checkExpNode:merge(en) + end + end for vo in valueNode:eachObject() do local child = vo -- `vm.isSubType` has no notion of `doc.type.sign`; match an @@ -112,30 +125,19 @@ local function checkOperators(operators, op, value, result, uri, classGlobal, si if child.type == 'doc.type.sign' and child.node and child.node[1] then child = vm.getGlobal('type', child.node[1]) or child end - if vm.isSubType(opUri, child, expNode) then + if vm.isSubType(opUri, child, checkExpNode) then if not result then result = vm.createNode() end - -- Resolve operator-level generics by matching `exp` against the actual `value` argument. - local resolvedExtends = extends + local opSign if operator.signs and #operator.signs > 0 and vm.containsGenericName(exp) then - local opSign = vm.createSign() - if exp.type == 'doc.type' then - for _, t in ipairs(exp.types or {}) do - opSign:addSign(vm.compileNode(t)) - end - else - opSign:addSign(vm.compileNode(exp)) - end - local opResolved = opSign:resolve(opUri, {value}) - if opResolved and next(opResolved) then - local cloned = vm.cloneObject(extends, opResolved) - if cloned then - resolvedExtends = cloned - end - end + opSign = vm.createSign() + opSign:addSign(vm.compileNode(exp)) + end + local opResult = vm.resolveGenericReturn(uri or opUri, genericMap, opSign, {value}, extends) + if opResult then + result:merge(opResult) end - result:merge(vm.compileNode(resolvedExtends)) return result end end @@ -143,7 +145,10 @@ local function checkOperators(operators, op, value, result, uri, classGlobal, si if not result then result = vm.createNode() end - result:merge(vm.compileNode(extends)) + local opResult = vm.resolveGenericReturn(uri or opUri, genericMap, nil, nil, extends) + if opResult then + result:merge(opResult) + end return result end ::CONTINUE:: diff --git a/test/type_inference/generic_operator.lua b/test/type_inference/generic_operator.lua index 5dbb54bca..c52fb2c99 100644 --- a/test/type_inference/generic_operator.lua +++ b/test/type_inference/generic_operator.lua @@ -236,3 +236,75 @@ TEST 'string' [[ local x local = x * "hello" ]] + +-- Optional return type +TEST 'Parser<[integer, string]>?' [[ +---@class Parser +---@operator add(Parser): Parser<[T, U]>? + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Union return type +TEST 'boolean|Parser<[integer, string]>' [[ +---@class Parser +---@operator add(Parser): Parser<[T, U]>|boolean + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Optional operand +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@operator add(Parser?): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Union operand (generic in first branch) +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@operator add(Parser|boolean): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Union operand (generic in second branch) +TEST 'Parser<[integer, string]>' [[ +---@class Parser +---@operator add(boolean|Parser): Parser<[T, U]> + +---@type Parser +local x +---@type Parser +local y +local = x + y +]] + +-- Optional generic in return type +TEST 'string?' [[ +---@class Box +---@operator add(Parser): U? + +---@type Box +local b +---@type Parser +local p +local = b + p +]] From 03032041494940ca8814f4542e56b2d5873a17ee Mon Sep 17 00:00:00 2001 From: Nathan <47441022+NathanSnail@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:18:38 +1300 Subject: [PATCH 5/5] chore: improve changelog --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index b62003deb..70b4d508b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,7 @@ # changelog ## Unreleased -* `FEAT` Operators can have generic parameters. +* `FEAT` Operators can have generic parameters through `---@operator add(List): T`. * `FIX` Generic classes with generic methods now have correct type inference.