From 269cb579479ab950e85a2e4078810501c29d7465 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 9 Sep 2023 16:49:29 -0700 Subject: [PATCH] rustdoc-search: fix bugs when unboxing and reordering combine --- src/librustdoc/html/static/js/search.js | 544 ++++++++++-------- .../rustdoc-js-std/option-type-signatures.js | 5 +- tests/rustdoc-js-std/vec-type-signatures.js | 22 + tests/rustdoc-js/generics-match-ambiguity.js | 1 + tests/rustdoc-js/generics-unbox.js | 38 ++ tests/rustdoc-js/generics-unbox.rs | 36 ++ 6 files changed, 393 insertions(+), 253 deletions(-) create mode 100644 tests/rustdoc-js-std/vec-type-signatures.js create mode 100644 tests/rustdoc-js/generics-unbox.js create mode 100644 tests/rustdoc-js/generics-unbox.rs diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index b867311aca1..407bf5f2c5f 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -3,6 +3,17 @@ "use strict"; +// polyfill +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced +if (!Array.prototype.toSpliced) { + // Can't use arrow functions, because we want `this` + Array.prototype.toSpliced = function() { + const me = this.slice(); + Array.prototype.splice.apply(me, arguments); + return me; + }; +} + (function() { // This mapping table should match the discriminants of // `rustdoc::formats::item_type::ItemType` type in Rust. @@ -1301,278 +1312,311 @@ function initSearch(rawSearchIndex) { * This function checks generics in search query `queryElem` can all be found in the * search index (`fnType`), * - * @param {FunctionType} fnType - The object to check. - * @param {QueryElement} queryElem - The element from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensIn - Map functions generics to query generics. + * This function returns `true` if it matches, and also writes the results to mgensInout. + * It returns `false` if no match is found, and leaves mgensInout untouched. + * + * @param {FunctionType} fnType - The object to check. + * @param {QueryElement} queryElem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensInout - Map functions generics to query generics. * * @return {boolean} - Returns true if a match, false otherwise. */ - function checkGenerics(fnType, queryElem, whereClause, mgensIn) { - return unifyFunctionTypes(fnType.generics, queryElem.generics, whereClause, mgensIn); + function checkGenerics(fnType, queryElem, whereClause, mgensInout) { + return unifyFunctionTypes( + fnType.generics, + queryElem.generics, + whereClause, + mgensInout, + mgens => { + if (mgensInout) { + for (const [fid, qid] of mgens.entries()) { + mgensInout.set(fid, qid); + } + } + return true; + } + ); } /** * This function checks if a list of search query `queryElems` can all be found in the * search index (`fnTypes`). * - * @param {Array} fnTypes - The objects to check. - * @param {Array} queryElems - The elements from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensIn - Map function generics to query generics. + * This function returns `true` on a match, or `false` if none. If `solutionCb` is + * supplied, it will call that function with mgens, and that callback can accept or + * reject the result bu returning `true` or `false`. If the callback returns false, + * then this function will try with a different solution, or bail with false if it + * runs out of candidates. + * + * @param {Array} fnTypes - The objects to check. + * @param {Array} queryElems - The elements from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgensIn + * - Map functions generics to query generics (never modified). + * @param {null|Map -> bool} solutionCb - Called for each `mgens` solution. * * @return {boolean} - Returns true if a match, false otherwise. */ - function unifyFunctionTypes(fnTypes, queryElems, whereClause, mgensIn) { - // This search engine implements order-agnostic unification. There - // should be no missing duplicates (generics have "bag semantics"), - // and the row is allowed to have extras. + function unifyFunctionTypes(fnTypesIn, queryElems, whereClause, mgensIn, solutionCb) { + /** + * @type Map + */ + let mgens = new Map(mgensIn); if (queryElems.length === 0) { - return true; + return !solutionCb || solutionCb(mgens); } - if (!fnTypes || fnTypes.length === 0) { + if (!fnTypesIn || fnTypesIn.length === 0) { return false; } + const ql = queryElems.length; + let fl = fnTypesIn.length; /** - * @type Map + * @type Array */ - const queryElemSet = new Map(); - const mgens = new Map(mgensIn); - const addQueryElemToQueryElemSet = queryElem => { - let currentQueryElemList; - const qid = queryElem.id; - if (queryElemSet.has(qid)) { - currentQueryElemList = queryElemSet.get(qid); - } else { - currentQueryElemList = []; - queryElemSet.set(qid, currentQueryElemList); - } - currentQueryElemList.push(queryElem); - }; - for (const queryElem of queryElems) { - addQueryElemToQueryElemSet(queryElem); - } + let fnTypes = fnTypesIn.slice(); /** - * @type Map + * loop works by building up a solution set in the working arrays + * fnTypes gets mutated in place to make this work, while queryElems + * is left alone + * + * vvvvvvv `i` points here + * queryElems = [ good, good, good, unknown, unknown ], + * fnTypes = [ good, good, good, unknown, unknown ], + * ---------------- ^^^^^^^^^^^^^^^^ `j` iterates after `i`, + * | looking for candidates + * everything before `i` is the + * current working solution + * + * Everything in the current working solution is known to be a good + * match, but it might not be the match we wind up going with, because + * there might be more than one candidate match, and we need to try them all + * before giving up. So, to handle this, it backtracks on failure. + * + * @type Array<{ + * "fnTypesScratch": Array, + * "queryElemsOffset": integer, + * "fnTypesOffset": integer + * }> */ - const fnTypeSet = new Map(); - /** - * If `fnType` is a concrete type with generics, replace it with its generics. - * - * If `fnType` is a generic type parameter, replace it with any traits that it's - * constrained by. This converts `T where T: Trait` into the same representation - * that's directly used by return-position `impl Trait` and by `dyn Trait` in - * all positions, so you can directly write `-> Trait` and get all three. - * - * This seems to correspond to two transforms described in - * http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - * One of them is actual unboxing (turning `Maybe a` into `a`), while the other is - * equivalent to converting a type parameter into an existential, which Hoogle doesn't - * seem to actually do? I notice that these two searches produce the same result: - * https://hoogle.haskell.org/?hoogle=Eq+-%3E+Set+Eq+-%3E+Set+Eq - * https://hoogle.haskell.org/?hoogle=Ord+-%3E+Set+Ord+-%3E+Set+Ord - * - * Haskell is a bit of a foreign language to me. I just want to be able to look up - * Option combinators without having to actually remember their (mostly arbitrary) - * names, and Haskell claims to already have that problem solved... - * - * @type Map - */ - const unbox = function unbox(fnType) { - if (fnType.id < 0) { - const genid = (-fnType.id) - 1; - if (whereClause && whereClause[genid]) { - for (const trait of whereClause[genid]) { - addFnTypeToFnTypeSet(trait); + const backtracking = []; + let i = 0; + let j = 0; + const backtrack = () => { + while (backtracking.length !== 0) { + // this session failed, but there are other possible solutions + // to backtrack, reset to (a copy of) the old array, do the swap or unboxing + const { + fnTypesScratch, + mgensScratch, + queryElemsOffset, + fnTypesOffset, + unbox, + } = backtracking.pop(); + mgens = new Map(mgensScratch); + const fnType = fnTypesScratch[fnTypesOffset]; + const queryElem = queryElems[queryElemsOffset]; + if (unbox) { + if (fnType.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { + continue; + } + mgens.set(fnType.id, 0); } + const generics = fnType.id < 0 ? + whereClause[(-fnType.id) - 1] : + fnType.generics; + fnTypes = fnTypesScratch.toSpliced(fnTypesOffset, 1, ...generics); + fl = fnTypes.length; + // re-run the matching algorithm on this item + i = queryElemsOffset - 1; + } else { + if (fnType.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) { + continue; + } + mgens.set(fnType.id, queryElem.id); + } + fnTypes = fnTypesScratch.slice(); + fl = fnTypes.length; + const tmp = fnTypes[queryElemsOffset]; + fnTypes[queryElemsOffset] = fnTypes[fnTypesOffset]; + fnTypes[fnTypesOffset] = tmp; + // this is known as a good match; go to the next one + i = queryElemsOffset; } - mgens.set(fnType.id, 0); - } else { - for (const innerFnType of fnType.generics) { - addFnTypeToFnTypeSet(innerFnType); + return true; + } + return false; + }; + for (i = 0; i !== ql; ++i) { + const queryElem = queryElems[i]; + /** + * list of potential function types that go with the current query element. + * @type Array + */ + const matchCandidates = []; + let fnTypesScratch = null; + let mgensScratch = null; + // don't try anything before `i`, because they've already been + // paired off with the other query elements + for (j = i; j !== fl; ++j) { + const fnType = fnTypes[j]; + if (unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) { + if (!fnTypesScratch) { + fnTypesScratch = fnTypes.slice(); + } + unifyFunctionTypes( + fnType.generics, + queryElem.generics, + whereClause, + mgens, + mgensScratch => { + matchCandidates.push({ + fnTypesScratch, + mgensScratch, + queryElemsOffset: i, + fnTypesOffset: j, + unbox: false, + }); + return false; // "reject" all candidates to gather all of them + } + ); + } + if (unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) { + if (!fnTypesScratch) { + fnTypesScratch = fnTypes.slice(); + } + if (!mgensScratch) { + mgensScratch = new Map(mgens); + } + backtracking.push({ + fnTypesScratch, + mgensScratch, + queryElemsOffset: i, + fnTypesOffset: j, + unbox: true, + }); } } - }; - /** - * @param {FunctionType} fnType - */ - const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) { - const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); - // qsid is an index into the `queryElemSet`, while `fnType.id` is an index into - // the `fnTypeSet`. These are the same, except for generics, where they get mapped. - // If qsid = 0, it means the generic type parameter has undergone instance - // substitution, and - let qsid = fnType.id; - if (fnType.id < 0) { - if (mgens.has(fnType.id)) { - qsid = mgens.get(fnType.id); + if (matchCandidates.length === 0) { + if (backtrack()) { + continue; } else { - qsid = null; - searching: for (const qid of queryElemSet.keys()) { - if (qid < 0) { - for (const qidMapped of mgens.values()) { - if (qid === qidMapped) { - continue searching; - } - } - mgens.set(fnType.id, qid); - qsid = qid; + return false; + } + } + // use the current candidate + const {fnTypesOffset: candidate, mgensScratch: mgensNew} = matchCandidates.pop(); + if (fnTypes[candidate].id < 0 && queryElems[i].id < 0) { + mgens.set(fnTypes[candidate].id, queryElems[i].id); + } + for (const [fid, qid] of mgensNew) { + mgens.set(fid, qid); + } + // `i` and `j` are paired off + // `queryElems[i]` is left in place + // `fnTypes[j]` is swapped with `fnTypes[i]` to pair them off + const tmp = fnTypes[candidate]; + fnTypes[candidate] = fnTypes[i]; + fnTypes[i] = tmp; + // write other candidates to backtracking queue + for (const otherCandidate of matchCandidates) { + backtracking.push(otherCandidate); + } + // If we're on the last item, check the solution with the callback + // backtrack if the callback says its unsuitable + while (i === (ql - 1) && solutionCb && !solutionCb(mgens)) { + if (!backtrack()) { + return false; + } + } + } + return true; + } + function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens) { + // type filters look like `trait:Read` or `enum:Result` + if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { + return false; + } + // fnType.id < 0 means generic + // queryElem.id < 0 does too + // mgens[fnType.id] = queryElem.id + // or, if mgens[fnType.id] = 0, then we've matched this generic with a bare trait + // and should make that same decision everywhere it appears + if (fnType.id < 0 && queryElem.id < 0) { + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== queryElem.id) { + return false; + } + for (const [fid, qid] of mgens.entries()) { + if (fnType.id !== fid && queryElem.id === qid) { + return false; + } + if (fnType.id === fid && queryElem.id !== qid) { + return false; + } + } + } else if (fnType.id !== null) { + if (queryElem.id === typeNameIdOfArrayOrSlice && + (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) + ) { + // [] matches primitive:array or primitive:slice + // if it matches, then we're fine, and this is an appropriate match candidate + } else if (fnType.id !== queryElem.id) { + return false; + } + // If the query elem has generics, and the function doesn't, + // it can't match. + if (fnType.generics.length === 0 && queryElem.generics.length !== 0) { + return false; + } + // If the query element is a path (it contains `::`), we need to check if this + // path is compatible with the target type. + const queryElemPathLength = queryElem.pathWithoutLast.length; + if (queryElemPathLength > 0) { + const fnTypePath = fnType.path !== undefined && fnType.path !== null ? + fnType.path.split("::") : []; + // If the path provided in the query element is longer than this type, + // no need to check it since it won't match in any case. + if (queryElemPathLength > fnTypePath.length) { + return false; + } + let i = 0; + for (const path of fnTypePath) { + if (path === queryElem.pathWithoutLast[i]) { + i += 1; + if (i >= queryElemPathLength) { break; } } } - } - if (qsid === null || !( - queryElemSet.has(qsid) || - (qsid === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || - (qsid === typeNameIdOfArray && queryContainsArrayOrSliceElem) - )) { - unbox(fnType); - return; - } - let currentQueryElemList = queryElemSet.get(qsid) || []; - let matchIdx = currentQueryElemList.findIndex(queryElem => { - return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem, whereClause, mgens); - }); - if (matchIdx === -1 && - (qsid === typeNameIdOfSlice || qsid === typeNameIdOfArray) && - queryContainsArrayOrSliceElem - ) { - currentQueryElemList = queryElemSet.get(typeNameIdOfArrayOrSlice) || []; - matchIdx = currentQueryElemList.findIndex(queryElem => { - return typePassesFilter(queryElem.typeFilter, fnType.ty) && - checkGenerics(fnType, queryElem, whereClause, mgens); - }); - } - // None of the query elems match the function type. - if (matchIdx === -1) { - unbox(fnType); - return; - } - let currentFnTypeList; - if (fnTypeSet.has(fnType.id)) { - currentFnTypeList = fnTypeSet.get(fnType.id); - } else { - currentFnTypeList = []; - fnTypeSet.set(fnType.id, currentFnTypeList); - } - currentFnTypeList.push(fnType); - }; - for (const fnType of fnTypes) { - addFnTypeToFnTypeSet(fnType); - } - const doHandleQueryElemList = (currentFnTypeList, queryElemList) => { - if (queryElemList.length === 0) { - return true; - } - // Multiple items in one list might match multiple items in another. - // Since an item with fewer generics can match an item with more, we - // need to check all combinations for a potential match. - const queryElem = queryElemList.pop(); - const l = currentFnTypeList.length; - for (let i = 0; i < l; i += 1) { - const fnType = currentFnTypeList[i]; - if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { - continue; - } - const queryElemPathLength = queryElem.pathWithoutLast.length; - // If the query element is a path (it contains `::`), we need to check if this - // path is compatible with the target type. - if (queryElemPathLength > 0) { - const fnTypePath = fnType.path !== undefined && fnType.path !== null ? - fnType.path.split("::") : []; - // If the path provided in the query element is longer than this type, - // no need to check it since it won't match in any case. - if (queryElemPathLength > fnTypePath.length) { - continue; - } - let i = 0; - for (const path of fnTypePath) { - if (path === queryElem.pathWithoutLast[i]) { - i += 1; - if (i >= queryElemPathLength) { - break; - } - } - } - if (i < queryElemPathLength) { - // If we didn't find all parts of the path of the query element inside - // the fn type, then it's not the right one. - continue; - } - } - if (queryElem.generics.length === 0 - || checkGenerics(fnType, queryElem, whereClause, mgens) - ) { - currentFnTypeList.splice(i, 1); - const result = doHandleQueryElemList(currentFnTypeList, queryElemList); - if (result) { - return true; - } - currentFnTypeList.splice(i, 0, fnType); - } - } - return false; - }; - /** - * @param {number} id - * @param {[QueryElement]} queryElemList - */ - const handleQueryElemList = (id, queryElemList) => { - let fsid = id; - if (fsid < 0) { - fsid = null; - if (!mgens) { + if (i < queryElemPathLength) { + // If we didn't find all parts of the path of the query element inside + // the fn type, then it's not the right one. return false; } - for (const [fid, qsid] of mgens) { - if (id === qsid) { - fsid = fid; - break; - } - } - } - if (fsid === null || !fnTypeSet.has(fsid)) { - if (fsid === typeNameIdOfArrayOrSlice) { - return handleQueryElemList(typeNameIdOfSlice, queryElemList) || - handleQueryElemList(typeNameIdOfArray, queryElemList); - } - return false; - } - const currentFnTypeList = fnTypeSet.get(fsid); - if (currentFnTypeList.length < queryElemList.length) { - // It's not possible for all the query elems to find a match. - return false; - } - const result = doHandleQueryElemList(currentFnTypeList, queryElemList); - if (result) { - // Found a solution. - // Any items that weren't used for it can be unboxed, and might form - // part of the solution for another item. - for (const innerFnType of currentFnTypeList) { - unbox(innerFnType); - } - fnTypeSet.delete(fsid); - } - return result; - }; - let queryElemSetSize = Number.MAX_VALUE; - while (queryElemSetSize > queryElemSet.size) { - queryElemSetSize = queryElemSet.size; - for (const [id, queryElemList] of queryElemSet) { - if (handleQueryElemList(id, queryElemList)) { - queryElemSet.delete(id); - } } } - if (queryElemSetSize === 0) { - for (const [fid, qid] of mgens) { - mgensIn.set(fid, qid); + return true; + } + function unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens) { + if (fnType.id < 0 && queryElem.id >= 0) { + if (!whereClause) { + return false; } - return true; - } else { - return false; + // mgens[fnType.id] === 0 indicates that we committed to unboxing this generic + // mgens[fnType.id] === null indicates that we haven't decided yet + if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { + return false; + } + // This is only a potential unbox if the search query appears in the where clause + // for example, searching `Read -> usize` should find + // `fn read_all(R) -> Result` + // generic `R` is considered "unboxed" + return checkIfInList(whereClause[(-fnType.id) - 1], queryElem, whereClause); + } else if (fnType.generics && fnType.generics.length > 0) { + return checkIfInList(fnType.generics, queryElem, whereClause); } + return false; } /** @@ -1907,23 +1951,19 @@ function initSearch(rawSearchIndex) { } // If the result is too "bad", we return false and it ends this search. - let mgens; - if (row.type.where_clause && row.type.where_clause.length > 0) { - mgens = new Map(); - } if (!unifyFunctionTypes( row.type.inputs, parsedQuery.elems, row.type.where_clause, - mgens - )) { - return; - } - if (!unifyFunctionTypes( - row.type.output, - parsedQuery.returned, - row.type.where_clause, - mgens + null, + mgens => { + return unifyFunctionTypes( + row.type.output, + parsedQuery.returned, + row.type.where_clause, + mgens + ); + } )) { return; } @@ -2060,7 +2100,7 @@ function initSearch(rawSearchIndex) { in_returned = row.type && unifyFunctionTypes( row.type.output, parsedQuery.returned, - row.type.where_clause, new Map() + row.type.where_clause ); if (in_returned) { addIntoResults( diff --git a/tests/rustdoc-js-std/option-type-signatures.js b/tests/rustdoc-js-std/option-type-signatures.js index 37bb3b14ab3..e154fa707ab 100644 --- a/tests/rustdoc-js-std/option-type-signatures.js +++ b/tests/rustdoc-js-std/option-type-signatures.js @@ -1,3 +1,5 @@ +// ignore-order + const FILTER_CRATE = "std"; const EXPECTED = [ @@ -36,8 +38,9 @@ const EXPECTED = [ ], }, { - 'query': 'option, option -> option', + 'query': 'option, option -> option', 'others': [ + { 'path': 'std::option::Option', 'name': 'and' }, { 'path': 'std::option::Option', 'name': 'zip' }, ], }, diff --git a/tests/rustdoc-js-std/vec-type-signatures.js b/tests/rustdoc-js-std/vec-type-signatures.js new file mode 100644 index 00000000000..18cf9d6efd0 --- /dev/null +++ b/tests/rustdoc-js-std/vec-type-signatures.js @@ -0,0 +1,22 @@ +// ignore-order + +const FILTER_CRATE = "std"; + +const EXPECTED = [ + { + 'query': 'vec::intoiter -> [T]', + 'others': [ + { 'path': 'std::vec::IntoIter', 'name': 'as_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'as_mut_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'next_chunk' }, + ], + }, + { + 'query': 'vec::intoiter -> []', + 'others': [ + { 'path': 'std::vec::IntoIter', 'name': 'as_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'as_mut_slice' }, + { 'path': 'std::vec::IntoIter', 'name': 'next_chunk' }, + ], + }, +]; diff --git a/tests/rustdoc-js/generics-match-ambiguity.js b/tests/rustdoc-js/generics-match-ambiguity.js index a9932a16ca3..edce4268c5a 100644 --- a/tests/rustdoc-js/generics-match-ambiguity.js +++ b/tests/rustdoc-js/generics-match-ambiguity.js @@ -79,6 +79,7 @@ const EXPECTED = [ { 'path': 'generics_match_ambiguity', 'name': 'baac' }, { 'path': 'generics_match_ambiguity', 'name': 'baaf' }, { 'path': 'generics_match_ambiguity', 'name': 'baag' }, + { 'path': 'generics_match_ambiguity', 'name': 'baah' }, ], }, { diff --git a/tests/rustdoc-js/generics-unbox.js b/tests/rustdoc-js/generics-unbox.js new file mode 100644 index 00000000000..9cdfc7ac8b6 --- /dev/null +++ b/tests/rustdoc-js/generics-unbox.js @@ -0,0 +1,38 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'Inside -> Out1', + 'others': [ + { 'path': 'generics_unbox', 'name': 'alpha' }, + ], + }, + { + 'query': 'Inside -> Out3', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out4', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out3', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, + { + 'query': 'Inside -> Out4', + 'others': [ + { 'path': 'generics_unbox', 'name': 'beta' }, + { 'path': 'generics_unbox', 'name': 'gamma' }, + ], + }, +]; diff --git a/tests/rustdoc-js/generics-unbox.rs b/tests/rustdoc-js/generics-unbox.rs new file mode 100644 index 00000000000..bef34f891e9 --- /dev/null +++ b/tests/rustdoc-js/generics-unbox.rs @@ -0,0 +1,36 @@ +pub struct Out { + a: A, + b: B, +} + +pub struct Out1 { + a: [A; N], +} + +pub struct Out2 { + a: [A; N], +} + +pub struct Out3 { + a: A, + b: B, +} + +pub struct Out4 { + a: A, + b: B, +} + +pub struct Inside(T); + +pub fn alpha(_: Inside) -> Out, Out2> { + loop {} +} + +pub fn beta(_: Inside) -> Out, Out4> { + loop {} +} + +pub fn gamma(_: Inside) -> Out, Out4> { + loop {} +}