Skip to content

Commit d82b374

Browse files
committed
rustdoc-search: switch to recursive backtracking
This is significantly faster, because - It allows the one-element fast path to kick in on multi- element queries. - It constructs intermediate data structures more lazily than the old system did. It's measurably faster than the old algo even without the fast path, but that fast path still helps significantly.
1 parent a66972d commit d82b374

File tree

1 file changed

+87
-157
lines changed

1 file changed

+87
-157
lines changed

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

+87-157
Original file line numberDiff line numberDiff line change
@@ -1331,18 +1331,18 @@ function initSearch(rawSearchIndex) {
13311331
/**
13321332
* @type Map<integer, integer>|null
13331333
*/
1334-
let mgens = mgensIn === null ? null : new Map(mgensIn);
1334+
const mgens = mgensIn === null ? null : new Map(mgensIn);
13351335
if (queryElems.length === 0) {
13361336
return !solutionCb || solutionCb(mgens);
13371337
}
13381338
if (!fnTypesIn || fnTypesIn.length === 0) {
13391339
return false;
13401340
}
13411341
const ql = queryElems.length;
1342-
let fl = fnTypesIn.length;
1342+
const fl = fnTypesIn.length;
13431343

1344-
// Fast path
1345-
if (queryElems.length === 1 && queryElems[0].generics.length === 0) {
1344+
// One element fast path / base case
1345+
if (ql === 1 && queryElems[0].generics.length === 0) {
13461346
const queryElem = queryElems[0];
13471347
for (const fnType of fnTypesIn) {
13481348
if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
@@ -1396,183 +1396,113 @@ function initSearch(rawSearchIndex) {
13961396
return false;
13971397
}
13981398

1399-
// Slow path
1399+
// Multiple element recursive case
14001400
/**
14011401
* @type Array<FunctionType>
14021402
*/
1403-
let fnTypes = fnTypesIn.slice();
1403+
const fnTypes = fnTypesIn.slice();
14041404
/**
1405-
* loop works by building up a solution set in the working arrays
1405+
* Algorithm works by building up a solution set in the working arrays
14061406
* fnTypes gets mutated in place to make this work, while queryElems
1407-
* is left alone
1407+
* is left alone.
14081408
*
1409-
* vvvvvvv `i` points here
1410-
* queryElems = [ good, good, good, unknown, unknown ],
1411-
* fnTypes = [ good, good, good, unknown, unknown ],
1412-
* ---------------- ^^^^^^^^^^^^^^^^ `j` iterates after `i`,
1413-
* | looking for candidates
1414-
* everything before `i` is the
1415-
* current working solution
1409+
* It works backwards, because arrays can be cheaply truncated that way.
1410+
*
1411+
* vvvvvvv `queryElem`
1412+
* queryElems = [ unknown, unknown, good, good, good ]
1413+
* fnTypes = [ unknown, unknown, good, good, good ]
1414+
* ^^^^^^^^^^^^^^^^ loop over these elements to find candidates
14161415
*
14171416
* Everything in the current working solution is known to be a good
14181417
* match, but it might not be the match we wind up going with, because
14191418
* there might be more than one candidate match, and we need to try them all
14201419
* before giving up. So, to handle this, it backtracks on failure.
1421-
*
1422-
* @type Array<{
1423-
* "fnTypesScratch": Array<FunctionType>,
1424-
* "queryElemsOffset": integer,
1425-
* "fnTypesOffset": integer
1426-
* }>
14271420
*/
1428-
const backtracking = [];
1429-
let i = 0;
1430-
let j = 0;
1431-
const backtrack = () => {
1432-
while (backtracking.length !== 0) {
1433-
// this session failed, but there are other possible solutions
1434-
// to backtrack, reset to (a copy of) the old array, do the swap or unboxing
1435-
const {
1436-
fnTypesScratch,
1437-
mgensScratch,
1438-
queryElemsOffset,
1439-
fnTypesOffset,
1440-
unbox,
1441-
} = backtracking.pop();
1442-
mgens = mgensScratch !== null ? new Map(mgensScratch) : null;
1443-
const fnType = fnTypesScratch[fnTypesOffset];
1444-
const queryElem = queryElems[queryElemsOffset];
1445-
if (unbox) {
1446-
if (fnType.id < 0) {
1447-
if (mgens === null) {
1448-
mgens = new Map();
1449-
} else if (mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) {
1450-
continue;
1451-
}
1452-
mgens.set(fnType.id, 0);
1453-
}
1454-
const generics = fnType.id < 0 ?
1455-
whereClause[(-fnType.id) - 1] :
1456-
fnType.generics;
1457-
fnTypes = fnTypesScratch.toSpliced(fnTypesOffset, 1, ...generics);
1458-
fl = fnTypes.length;
1459-
// re-run the matching algorithm on this item
1460-
i = queryElemsOffset - 1;
1461-
} else {
1462-
if (fnType.id < 0) {
1463-
if (mgens === null) {
1464-
mgens = new Map();
1465-
} else if (mgens.has(fnType.id) &&
1466-
mgens.get(fnType.id) !== queryElem.id) {
1467-
continue;
1468-
}
1469-
mgens.set(fnType.id, queryElem.id);
1470-
}
1471-
fnTypes = fnTypesScratch.slice();
1472-
fl = fnTypes.length;
1473-
const tmp = fnTypes[queryElemsOffset];
1474-
fnTypes[queryElemsOffset] = fnTypes[fnTypesOffset];
1475-
fnTypes[fnTypesOffset] = tmp;
1476-
// this is known as a good match; go to the next one
1477-
i = queryElemsOffset;
1478-
}
1479-
return true;
1421+
const flast = fl - 1;
1422+
const qlast = ql - 1;
1423+
const queryElem = queryElems[qlast];
1424+
let queryElemsTmp = null;
1425+
for (let i = flast; i >= 0; i -= 1) {
1426+
const fnType = fnTypes[i];
1427+
if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
1428+
continue;
14801429
}
1481-
return false;
1482-
};
1483-
for (i = 0; i !== ql; ++i) {
1484-
const queryElem = queryElems[i];
1485-
/**
1486-
* list of potential function types that go with the current query element.
1487-
* @type Array<integer>
1488-
*/
1489-
const matchCandidates = [];
1490-
let fnTypesScratch = null;
1491-
let mgensScratch = null;
1492-
// don't try anything before `i`, because they've already been
1493-
// paired off with the other query elements
1494-
for (j = i; j !== fl; ++j) {
1495-
const fnType = fnTypes[j];
1496-
if (unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens)) {
1497-
if (!fnTypesScratch) {
1498-
fnTypesScratch = fnTypes.slice();
1430+
let mgensScratch;
1431+
if (fnType.id < 0) {
1432+
mgensScratch = new Map(mgens);
1433+
if (mgensScratch.has(fnType.id)
1434+
&& mgensScratch.get(fnType.id) !== queryElem.id) {
1435+
continue;
1436+
}
1437+
mgensScratch.set(fnType.id, queryElem.id);
1438+
} else {
1439+
mgensScratch = mgens;
1440+
}
1441+
// fnTypes[i] is a potential match
1442+
// fnTypes[flast] is the last item in the list
1443+
// swap them, and drop the potential match from the list
1444+
// check if the remaining function types also match
1445+
fnTypes[i] = fnTypes[flast];
1446+
fnTypes.length = flast;
1447+
if (!queryElemsTmp) {
1448+
queryElemsTmp = queryElems.slice(0, qlast);
1449+
}
1450+
const passesUnification = unifyFunctionTypes(
1451+
fnTypes,
1452+
queryElemsTmp,
1453+
whereClause,
1454+
mgensScratch,
1455+
mgensScratch => {
1456+
if (fnType.generics.length === 0 && queryElem.generics.length === 0) {
1457+
return !solutionCb || solutionCb(mgensScratch);
14991458
}
1500-
unifyFunctionTypes(
1459+
return unifyFunctionTypes(
15011460
fnType.generics,
15021461
queryElem.generics,
15031462
whereClause,
1504-
mgens,
1505-
mgensScratch => {
1506-
matchCandidates.push({
1507-
fnTypesScratch,
1508-
mgensScratch,
1509-
queryElemsOffset: i,
1510-
fnTypesOffset: j,
1511-
unbox: false,
1512-
});
1513-
return false; // "reject" all candidates to gather all of them
1514-
}
1515-
);
1516-
}
1517-
if (unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) {
1518-
if (!fnTypesScratch) {
1519-
fnTypesScratch = fnTypes.slice();
1520-
}
1521-
if (!mgensScratch && mgens !== null) {
1522-
mgensScratch = new Map(mgens);
1523-
}
1524-
backtracking.push({
1525-
fnTypesScratch,
15261463
mgensScratch,
1527-
queryElemsOffset: i,
1528-
fnTypesOffset: j,
1529-
unbox: true,
1530-
});
1531-
}
1532-
}
1533-
if (matchCandidates.length === 0) {
1534-
if (backtrack()) {
1535-
continue;
1536-
} else {
1537-
return false;
1538-
}
1539-
}
1540-
// use the current candidate
1541-
const {fnTypesOffset: candidate, mgensScratch: mgensNew} = matchCandidates.pop();
1542-
if (fnTypes[candidate].id < 0 && queryElems[i].id < 0) {
1543-
if (mgens === null) {
1544-
mgens = new Map();
1464+
solutionCb
1465+
);
15451466
}
1546-
mgens.set(fnTypes[candidate].id, queryElems[i].id);
1467+
);
1468+
if (passesUnification) {
1469+
return true;
15471470
}
1548-
if (mgensNew !== null) {
1549-
if (mgens === null) {
1550-
mgens = mgensNew;
1551-
} else {
1552-
for (const [fid, qid] of mgensNew) {
1553-
mgens.set(fid, qid);
1554-
}
1555-
}
1471+
// backtrack
1472+
fnTypes[flast] = fnTypes[i];
1473+
fnTypes[i] = fnType;
1474+
fnTypes.length = fl;
1475+
}
1476+
for (let i = flast; i >= 0; i -= 1) {
1477+
const fnType = fnTypes[i];
1478+
if (!unifyFunctionTypeIsUnboxCandidate(fnType, queryElem, whereClause, mgens)) {
1479+
continue;
15561480
}
1557-
// `i` and `j` are paired off
1558-
// `queryElems[i]` is left in place
1559-
// `fnTypes[j]` is swapped with `fnTypes[i]` to pair them off
1560-
const tmp = fnTypes[candidate];
1561-
fnTypes[candidate] = fnTypes[i];
1562-
fnTypes[i] = tmp;
1563-
// write other candidates to backtracking queue
1564-
for (const otherCandidate of matchCandidates) {
1565-
backtracking.push(otherCandidate);
1566-
}
1567-
// If we're on the last item, check the solution with the callback
1568-
// backtrack if the callback says its unsuitable
1569-
while (i === (ql - 1) && solutionCb && !solutionCb(mgens)) {
1570-
if (!backtrack()) {
1571-
return false;
1481+
let mgensScratch;
1482+
if (fnType.id < 0) {
1483+
mgensScratch = new Map(mgens);
1484+
if (mgensScratch.has(fnType.id) && mgensScratch.get(fnType.id) !== 0) {
1485+
continue;
15721486
}
1487+
mgensScratch.set(fnType.id, 0);
1488+
} else {
1489+
mgensScratch = mgens;
1490+
}
1491+
const generics = fnType.id < 0 ?
1492+
whereClause[(-fnType.id) - 1] :
1493+
fnType.generics;
1494+
const passesUnification = unifyFunctionTypes(
1495+
fnTypes.toSpliced(i, 1, ...generics),
1496+
queryElems,
1497+
whereClause,
1498+
mgensScratch,
1499+
solutionCb
1500+
);
1501+
if (passesUnification) {
1502+
return true;
15731503
}
15741504
}
1575-
return true;
1505+
return false;
15761506
}
15771507
function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, whereClause, mgens) {
15781508
// type filters look like `trait:Read` or `enum:Result`

0 commit comments

Comments
 (0)