Skip to content

Commit 9946d67

Browse files
committed
rustdoc-search: build args, return, and generics on one unifier
This enhances generics with the "unboxing" behavior where A<T> matches T. It makes this unboxing transitive over generics.
1 parent 04f4493 commit 9946d67

File tree

4 files changed

+178
-138
lines changed

4 files changed

+178
-138
lines changed

src/librustdoc/html/static/js/externs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ let ParsedQuery;
5353
* parent: (Object|null|undefined),
5454
* path: string,
5555
* ty: (Number|null|number),
56-
* type: (Array<?>|null)
56+
* type: FunctionSearchType?
5757
* }}
5858
*/
5959
let Row;
@@ -135,7 +135,7 @@ let RawFunctionType;
135135
/**
136136
* @typedef {{
137137
* inputs: Array<FunctionType>,
138-
* outputs: Array<FunctionType>,
138+
* output: Array<FunctionType>,
139139
* }}
140140
*/
141141
let FunctionSearchType;

src/librustdoc/html/static/js/search.js

Lines changed: 103 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,98 +1178,137 @@ function initSearch(rawSearchIndex) {
11781178
}
11791179

11801180
/**
1181-
* This function checks if the object (`row`) generics match the given type (`elem`)
1182-
* generics.
1181+
* This function checks generics in search query `queryElem` can all be found in the
1182+
* search index (`fnType`),
11831183
*
1184-
* @param {Row} row - The object to check.
1185-
* @param {QueryElement} elem - The element from the parsed query.
1184+
* @param {FunctionType} fnType - The object to check.
1185+
* @param {QueryElement} queryElem - The element from the parsed query.
11861186
*
1187-
* @return {boolean} - Returns true if a match, false otherwise.
1187+
* @return {boolean} - Returns true if a match, false otherwise.
11881188
*/
1189-
function checkGenerics(row, elem) {
1190-
if (row.generics.length === 0 || elem.generics.length === 0) {
1191-
return false;
1192-
}
1193-
// This function is called if the names match, but we need to make
1194-
// sure that all generics match as well.
1195-
//
1189+
function checkGenerics(fnType, queryElem) {
1190+
return unifyFunctionTypes(fnType.generics, queryElem.generics);
1191+
}
1192+
/**
1193+
* This function checks if a list of search query `queryElems` can all be found in the
1194+
* search index (`fnTypes`).
1195+
*
1196+
* @param {Array<FunctionType>} fnTypes - The objects to check.
1197+
* @param {Array<QueryElement>} queryElems - The elements from the parsed query.
1198+
*
1199+
* @return {boolean} - Returns true if a match, false otherwise.
1200+
*/
1201+
function unifyFunctionTypes(fnTypes, queryElems) {
11961202
// This search engine implements order-agnostic unification. There
11971203
// should be no missing duplicates (generics have "bag semantics"),
11981204
// and the row is allowed to have extras.
1199-
if (elem.generics.length <= 0 || row.generics.length < elem.generics.length) {
1205+
if (queryElems.length === 0) {
1206+
return true;
1207+
}
1208+
if (!fnTypes || fnTypes.length === 0) {
12001209
return false;
12011210
}
1202-
const elems = new Map();
1203-
const addEntryToElems = function addEntryToElems(entry) {
1204-
if (entry.id === -1) {
1211+
/**
1212+
* @type Map<integer, FunctionType[]>
1213+
*/
1214+
const fnTypeSet = new Map();
1215+
const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) {
1216+
if (fnType.id === -1) {
12051217
// Pure generic, needs to check into it.
1206-
for (const inner_entry of entry.generics) {
1207-
addEntryToElems(inner_entry);
1218+
for (const innerFnType of fnType.generics) {
1219+
addFnTypeToFnTypeSet(innerFnType);
12081220
}
12091221
return;
12101222
}
1211-
let currentEntryElems;
1212-
if (elems.has(entry.id)) {
1213-
currentEntryElems = elems.get(entry.id);
1223+
let currentFnTypeList;
1224+
if (fnTypeSet.has(fnType.id)) {
1225+
currentFnTypeList = fnTypeSet.get(fnType.id);
12141226
} else {
1215-
currentEntryElems = [];
1216-
elems.set(entry.id, currentEntryElems);
1227+
currentFnTypeList = [];
1228+
fnTypeSet.set(fnType.id, currentFnTypeList);
12171229
}
1218-
currentEntryElems.push(entry);
1230+
currentFnTypeList.push(fnType);
12191231
};
1220-
for (const entry of row.generics) {
1221-
addEntryToElems(entry);
1232+
for (const fnType of fnTypes) {
1233+
addFnTypeToFnTypeSet(fnType);
12221234
}
12231235
// We need to find the type that matches the most to remove it in order
12241236
// to move forward.
1225-
const handleGeneric = generic => {
1226-
if (!elems.has(generic.id)) {
1237+
const handleQueryElem = queryElem => {
1238+
if (!fnTypeSet.has(queryElem.id)) {
12271239
return false;
12281240
}
1229-
const matchElems = elems.get(generic.id);
1230-
const matchIdx = matchElems.findIndex(tmp_elem => {
1231-
if (generic.generics.length > 0 && !checkGenerics(tmp_elem, generic)) {
1241+
const currentFnTypeList = fnTypeSet.get(queryElem.id);
1242+
const matchIdx = currentFnTypeList.findIndex(fnType => {
1243+
if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) {
12321244
return false;
12331245
}
1234-
return typePassesFilter(generic.typeFilter, tmp_elem.ty);
1246+
return queryElem.generics.length === 0 || checkGenerics(fnType, queryElem);
12351247
});
12361248
if (matchIdx === -1) {
12371249
return false;
12381250
}
1239-
matchElems.splice(matchIdx, 1);
1240-
if (matchElems.length === 0) {
1241-
elems.delete(generic.id);
1251+
currentFnTypeList.splice(matchIdx, 1);
1252+
if (currentFnTypeList.length === 0) {
1253+
fnTypeSet.delete(queryElem.id);
12421254
}
12431255
return true;
12441256
};
12451257
// To do the right thing with type filters, we first process generics
12461258
// that have them, removing matching ones from the "bag," then do the
12471259
// ones with no type filter, which can match any entry regardless of its
12481260
// own type.
1249-
for (const generic of elem.generics) {
1250-
if (generic.typeFilter === TY_PRIMITIVE &&
1251-
generic.id === typeNameIdOfArrayOrSlice) {
1252-
const genericArray = {
1261+
const needsUnboxed = [];
1262+
for (const queryElem of queryElems) {
1263+
if (queryElem.typeFilter === TY_PRIMITIVE &&
1264+
queryElem.id === typeNameIdOfArrayOrSlice) {
1265+
const queryElemArray = {
12531266
id: typeNameIdOfArray,
12541267
typeFilter: TY_PRIMITIVE,
1255-
generics: generic.generics,
1268+
generics: queryElem.generics,
12561269
};
1257-
const genericSlice = {
1270+
const queryElemSlice = {
12581271
id: typeNameIdOfSlice,
12591272
typeFilter: TY_PRIMITIVE,
1260-
generics: generic.generics,
1273+
generics: queryElem.generics,
12611274
};
1262-
if (!handleGeneric(genericArray) && !handleGeneric(genericSlice)) {
1263-
return false;
1275+
if (!handleQueryElem(queryElemArray) && !handleQueryElem(queryElemSlice)) {
1276+
needsUnboxed.push(queryElem);
12641277
}
1265-
} else if (generic.typeFilter !== -1 && !handleGeneric(generic)) {
1266-
return false;
1278+
} else if (queryElem.typeFilter !== -1 && !handleQueryElem(queryElem)) {
1279+
needsUnboxed.push(queryElem);
12671280
}
12681281
}
1269-
for (const generic of elem.generics) {
1270-
if (generic.typeFilter === -1 && !handleGeneric(generic)) {
1271-
return false;
1282+
for (const queryElem of queryElems) {
1283+
if (queryElem.typeFilter === -1 && !handleQueryElem(queryElem)) {
1284+
needsUnboxed.push(queryElem);
1285+
}
1286+
}
1287+
// If the current item does not match, try [unboxing] the generic.
1288+
// [unboxing]:
1289+
// https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf
1290+
unboxing: while (needsUnboxed.length !== 0) {
1291+
for (const [i, queryElem] of needsUnboxed.entries()) {
1292+
if (handleQueryElem(queryElem)) {
1293+
needsUnboxed.splice(i, 1);
1294+
continue unboxing;
1295+
}
1296+
}
1297+
for (const [id, fnTypeList] of fnTypeSet) {
1298+
for (const [i, fnType] of fnTypeList.entries()) {
1299+
if (fnType.generics.length !== 0) {
1300+
fnTypeList.splice(i, 1);
1301+
for (const innerFnType of fnType.generics) {
1302+
addFnTypeToFnTypeSet(innerFnType);
1303+
}
1304+
if (fnTypeList.length === 0) {
1305+
fnTypeSet.delete(id);
1306+
}
1307+
continue unboxing;
1308+
}
1309+
}
12721310
}
1311+
return false;
12731312
}
12741313
return true;
12751314
}
@@ -1278,13 +1317,13 @@ function initSearch(rawSearchIndex) {
12781317
* This function checks if the object (`row`) matches the given type (`elem`) and its
12791318
* generics (if any).
12801319
*
1281-
* @param {Row} row
1320+
* @param {Array<FunctionType>} list
12821321
* @param {QueryElement} elem - The element from the parsed query.
12831322
*
12841323
* @return {boolean} - Returns true if found, false otherwise.
12851324
*/
1286-
function checkIfInGenerics(row, elem) {
1287-
for (const entry of row.generics) {
1325+
function checkIfInList(list, elem) {
1326+
for (const entry of list) {
12881327
if (checkType(entry, elem)) {
12891328
return true;
12901329
}
@@ -1304,7 +1343,7 @@ function initSearch(rawSearchIndex) {
13041343
function checkType(row, elem) {
13051344
if (row.id === -1) {
13061345
// This is a pure "generic" search, no need to run other checks.
1307-
return row.generics.length > 0 ? checkIfInGenerics(row, elem) : false;
1346+
return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false;
13081347
}
13091348

13101349
const matchesExact = row.id === elem.id;
@@ -1322,59 +1361,7 @@ function initSearch(rawSearchIndex) {
13221361
// If the current item does not match, try [unboxing] the generic.
13231362
// [unboxing]:
13241363
// https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf
1325-
return checkIfInGenerics(row, elem);
1326-
}
1327-
1328-
/**
1329-
* This function checks if the object (`row`) has an argument with the given type (`elem`).
1330-
*
1331-
* @param {Row} row
1332-
* @param {QueryElement} elem - The element from the parsed query.
1333-
* @param {Array<integer>} skipPositions - Do not return one of these positions.
1334-
*
1335-
* @return {integer} - Returns the position of the match, or -1 if none.
1336-
*/
1337-
function findArg(row, elem, skipPositions) {
1338-
if (row && row.type && row.type.inputs && row.type.inputs.length > 0) {
1339-
let i = 0;
1340-
for (const input of row.type.inputs) {
1341-
if (skipPositions.indexOf(i) !== -1) {
1342-
i += 1;
1343-
continue;
1344-
}
1345-
if (checkType(input, elem)) {
1346-
return i;
1347-
}
1348-
i += 1;
1349-
}
1350-
}
1351-
return -1;
1352-
}
1353-
1354-
/**
1355-
* This function checks if the object (`row`) returns the given type (`elem`).
1356-
*
1357-
* @param {Row} row
1358-
* @param {QueryElement} elem - The element from the parsed query.
1359-
* @param {Array<integer>} skipPositions - Do not return one of these positions.
1360-
*
1361-
* @return {integer} - Returns the position of the matching item, or -1 if none.
1362-
*/
1363-
function checkReturned(row, elem, skipPositions) {
1364-
if (row && row.type && row.type.output.length > 0) {
1365-
let i = 0;
1366-
for (const ret_ty of row.type.output) {
1367-
if (skipPositions.indexOf(i) !== -1) {
1368-
i += 1;
1369-
continue;
1370-
}
1371-
if (checkType(ret_ty, elem)) {
1372-
return i;
1373-
}
1374-
i += 1;
1375-
}
1376-
}
1377-
return -1;
1364+
return checkIfInList(row.generics, elem);
13781365
}
13791366

13801367
function checkPath(contains, ty, maxEditDistance) {
@@ -1575,14 +1562,14 @@ function initSearch(rawSearchIndex) {
15751562
const fullId = row.id;
15761563
const searchWord = searchWords[pos];
15771564

1578-
const in_args = findArg(row, elem, []);
1579-
if (in_args !== -1) {
1565+
const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem);
1566+
if (in_args) {
15801567
// path_dist is 0 because no parent path information is currently stored
15811568
// in the search index
15821569
addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance);
15831570
}
1584-
const returned = checkReturned(row, elem, []);
1585-
if (returned !== -1) {
1571+
const returned = row.type && row.type.output && checkIfInList(row.type.output, elem);
1572+
if (returned) {
15861573
addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance);
15871574
}
15881575

@@ -1638,32 +1625,15 @@ function initSearch(rawSearchIndex) {
16381625
* @param {Object} results
16391626
*/
16401627
function handleArgs(row, pos, results) {
1641-
if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
1628+
if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) {
16421629
return;
16431630
}
16441631

16451632
// If the result is too "bad", we return false and it ends this search.
1646-
function checkArgs(elems, callback) {
1647-
const skipPositions = [];
1648-
for (const elem of elems) {
1649-
// There is more than one parameter to the query so all checks should be "exact"
1650-
const position = callback(
1651-
row,
1652-
elem,
1653-
skipPositions
1654-
);
1655-
if (position !== -1) {
1656-
skipPositions.push(position);
1657-
} else {
1658-
return false;
1659-
}
1660-
}
1661-
return true;
1662-
}
1663-
if (!checkArgs(parsedQuery.elems, findArg)) {
1633+
if (!unifyFunctionTypes(row.type.inputs, parsedQuery.elems)) {
16641634
return;
16651635
}
1666-
if (!checkArgs(parsedQuery.returned, checkReturned)) {
1636+
if (!unifyFunctionTypes(row.type.output, parsedQuery.returned)) {
16671637
return;
16681638
}
16691639

@@ -1750,12 +1720,9 @@ function initSearch(rawSearchIndex) {
17501720
elem = parsedQuery.returned[0];
17511721
for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) {
17521722
row = searchIndex[i];
1753-
in_returned = checkReturned(
1754-
row,
1755-
elem,
1756-
[]
1757-
);
1758-
if (in_returned !== -1) {
1723+
in_returned = row.type &&
1724+
unifyFunctionTypes(row.type.output, parsedQuery.returned);
1725+
if (in_returned) {
17591726
addIntoResults(
17601727
results_others,
17611728
row.id,

0 commit comments

Comments
 (0)