diff --git a/changelog.md b/changelog.md index daefdb5ae..70b4d508b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,8 @@ # changelog ## Unreleased +* `FEAT` Operators can have generic parameters through `---@operator add(List): T`. +* `FIX` Generic classes with generic methods now have correct type inference. ## 3.19.2 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..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 @@ -1626,6 +1631,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 +1656,11 @@ local docSwitch = util.switch() result.finish = ret.finish end + -- Bind operator-local generics so that doc.type.name nodes for U are converted to doc.generic.name. + if result.signs then + bindLocalGenerics(result, result.signs) + end + return result end) : case 'source' diff --git a/script/vm/compiler.lua b/script/vm/compiler.lua index 20ab3ac41..fcf8c1c05 100644 --- a/script/vm/compiler.lua +++ b/script/vm/compiler.lua @@ -1834,28 +1834,21 @@ 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 - end - end) - if onlyClassGenerics 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) - break - end - end + -- 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 + guide.eachSourceType(rtn, 'doc.generic.name', checkGeneric) + if not hasClassGenerics then + guide.eachSourceType(rtn, 'doc.type.name', checkGeneric) + end + if hasClassGenerics then + local sign = vm.getSign(mfunc) + returnNode = vm.resolveGenericReturn(guide.getUri(source), genericMap, sign, resolveArgs, rtn) + end break end end diff --git a/script/vm/generic.lua b/script/vm/generic.lua index 0b171622c..ce73e1994 100644 --- a/script/vm/generic.lua +++ b/script/vm/generic.lua @@ -23,15 +23,36 @@ 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 + local copied = source._resolved:copy() + vm.setNode(newName, copied, true) + newName._resolved = copied + end end return newName end @@ -88,11 +109,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 +135,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 +181,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 @@ -181,7 +212,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) @@ -276,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 ed53cdfc3..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,11 +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 - result:merge(vm.compileNode(extends)) + local opSign + if operator.signs and #operator.signs > 0 and vm.containsGenericName(exp) then + 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 return result end end @@ -124,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/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/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') +]] diff --git a/test/type_inference/generic_operator.lua b/test/type_inference/generic_operator.lua index 69c1c931e..c52fb2c99 100644 --- a/test/type_inference/generic_operator.lua +++ b/test/type_inference/generic_operator.lua @@ -200,3 +200,111 @@ 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" +]] + +-- 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 +]]