@@ -1318,7 +1318,7 @@ function initSearch(rawSearchIndex) {
13181318 * then this function will try with a different solution, or bail with false if it
13191319 * runs out of candidates.
13201320 *
1321- * @param {Array<FunctionType> } fnTypes - The objects to check.
1321+ * @param {Array<FunctionType> } fnTypesIn - The objects to check.
13221322 * @param {Array<QueryElement> } queryElems - The elements from the parsed query.
13231323 * @param {[FunctionType] } whereClause - Trait bounds for generic items.
13241324 * @param {Map<number,number>|null } mgensIn
@@ -1329,179 +1329,180 @@ function initSearch(rawSearchIndex) {
13291329 */
13301330 function unifyFunctionTypes ( fnTypesIn , queryElems , whereClause , mgensIn , solutionCb ) {
13311331 /**
1332- * @type Map<integer, integer>
1332+ * @type Map<integer, integer>|null
13331333 */
1334- let mgens = 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 ;
1343+
1344+ // One element fast path / base case
1345+ if ( ql === 1 && queryElems [ 0 ] . generics . length === 0 ) {
1346+ const queryElem = queryElems [ 0 ] ;
1347+ for ( const fnType of fnTypesIn ) {
1348+ if ( ! unifyFunctionTypeIsMatchCandidate ( fnType , queryElem , whereClause , mgens ) ) {
1349+ continue ;
1350+ }
1351+ if ( fnType . id < 0 && queryElem . id < 0 ) {
1352+ if ( mgens && mgens . has ( fnType . id ) &&
1353+ mgens . get ( fnType . id ) !== queryElem . id ) {
1354+ continue ;
1355+ }
1356+ const mgensScratch = new Map ( mgens ) ;
1357+ mgensScratch . set ( fnType . id , queryElem . id ) ;
1358+ if ( ! solutionCb || solutionCb ( mgensScratch ) ) {
1359+ return true ;
1360+ }
1361+ } else if ( ! solutionCb || solutionCb ( mgens ? new Map ( mgens ) : null ) ) {
1362+ // unifyFunctionTypeIsMatchCandidate already checks that ids match
1363+ return true ;
1364+ }
1365+ }
1366+ for ( const fnType of fnTypesIn ) {
1367+ if ( ! unifyFunctionTypeIsUnboxCandidate ( fnType , queryElem , whereClause , mgens ) ) {
1368+ continue ;
1369+ }
1370+ if ( fnType . id < 0 ) {
1371+ if ( mgens && mgens . has ( fnType . id ) &&
1372+ mgens . get ( fnType . id ) !== 0 ) {
1373+ continue ;
1374+ }
1375+ const mgensScratch = new Map ( mgens ) ;
1376+ mgensScratch . set ( fnType . id , 0 ) ;
1377+ if ( unifyFunctionTypes (
1378+ whereClause [ ( - fnType . id ) - 1 ] ,
1379+ queryElems ,
1380+ whereClause ,
1381+ mgensScratch ,
1382+ solutionCb
1383+ ) ) {
1384+ return true ;
1385+ }
1386+ } else if ( unifyFunctionTypes (
1387+ fnType . generics ,
1388+ queryElems ,
1389+ whereClause ,
1390+ mgens ? new Map ( mgens ) : null ,
1391+ solutionCb
1392+ ) ) {
1393+ return true ;
1394+ }
1395+ }
1396+ return false ;
1397+ }
1398+
1399+ // Multiple element recursive case
13431400 /**
13441401 * @type Array<FunctionType>
13451402 */
1346- let fnTypes = fnTypesIn . slice ( ) ;
1403+ const fnTypes = fnTypesIn . slice ( ) ;
13471404 /**
1348- * 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
13491406 * fnTypes gets mutated in place to make this work, while queryElems
1350- * is left alone
1407+ * is left alone.
13511408 *
1352- * vvvvvvv `i` points here
1353- * queryElems = [ good, good, good, unknown, unknown ],
1354- * fnTypes = [ good, good, good, unknown, unknown ],
1355- * ---------------- ^^^^^^^^^^^^^^^^ `j` iterates after `i`,
1356- * | looking for candidates
1357- * everything before `i` is the
1358- * 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
13591415 *
13601416 * Everything in the current working solution is known to be a good
13611417 * match, but it might not be the match we wind up going with, because
13621418 * there might be more than one candidate match, and we need to try them all
13631419 * before giving up. So, to handle this, it backtracks on failure.
1364- *
1365- * @type Array<{
1366- * "fnTypesScratch": Array<FunctionType>,
1367- * "queryElemsOffset": integer,
1368- * "fnTypesOffset": integer
1369- * }>
13701420 */
1371- const backtracking = [ ] ;
1372- let i = 0 ;
1373- let j = 0 ;
1374- const backtrack = ( ) => {
1375- while ( backtracking . length !== 0 ) {
1376- // this session failed, but there are other possible solutions
1377- // to backtrack, reset to (a copy of) the old array, do the swap or unboxing
1378- const {
1379- fnTypesScratch,
1380- mgensScratch,
1381- queryElemsOffset,
1382- fnTypesOffset,
1383- unbox,
1384- } = backtracking . pop ( ) ;
1385- mgens = new Map ( mgensScratch ) ;
1386- const fnType = fnTypesScratch [ fnTypesOffset ] ;
1387- const queryElem = queryElems [ queryElemsOffset ] ;
1388- if ( unbox ) {
1389- if ( fnType . id < 0 ) {
1390- if ( mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== 0 ) {
1391- continue ;
1392- }
1393- mgens . set ( fnType . id , 0 ) ;
1394- }
1395- const generics = fnType . id < 0 ?
1396- whereClause [ ( - fnType . id ) - 1 ] :
1397- fnType . generics ;
1398- fnTypes = fnTypesScratch . toSpliced ( fnTypesOffset , 1 , ...generics ) ;
1399- fl = fnTypes . length ;
1400- // re-run the matching algorithm on this item
1401- i = queryElemsOffset - 1 ;
1402- } else {
1403- if ( fnType . id < 0 ) {
1404- if ( mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== queryElem . id ) {
1405- continue ;
1406- }
1407- mgens . set ( fnType . id , queryElem . id ) ;
1408- }
1409- fnTypes = fnTypesScratch . slice ( ) ;
1410- fl = fnTypes . length ;
1411- const tmp = fnTypes [ queryElemsOffset ] ;
1412- fnTypes [ queryElemsOffset ] = fnTypes [ fnTypesOffset ] ;
1413- fnTypes [ fnTypesOffset ] = tmp ;
1414- // this is known as a good match; go to the next one
1415- i = queryElemsOffset ;
1416- }
1417- 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 ;
14181429 }
1419- return false ;
1420- } ;
1421- for ( i = 0 ; i !== ql ; ++ i ) {
1422- const queryElem = queryElems [ i ] ;
1423- /**
1424- * list of potential function types that go with the current query element.
1425- * @type Array<integer>
1426- */
1427- const matchCandidates = [ ] ;
1428- let fnTypesScratch = null ;
1429- let mgensScratch = null ;
1430- // don't try anything before `i`, because they've already been
1431- // paired off with the other query elements
1432- for ( j = i ; j !== fl ; ++ j ) {
1433- const fnType = fnTypes [ j ] ;
1434- if ( unifyFunctionTypeIsMatchCandidate ( fnType , queryElem , whereClause , mgens ) ) {
1435- if ( ! fnTypesScratch ) {
1436- 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 ) ;
14371458 }
1438- unifyFunctionTypes (
1459+ return unifyFunctionTypes (
14391460 fnType . generics ,
14401461 queryElem . generics ,
14411462 whereClause ,
1442- mgens ,
1443- mgensScratch => {
1444- matchCandidates . push ( {
1445- fnTypesScratch,
1446- mgensScratch,
1447- queryElemsOffset : i ,
1448- fnTypesOffset : j ,
1449- unbox : false ,
1450- } ) ;
1451- return false ; // "reject" all candidates to gather all of them
1452- }
1453- ) ;
1454- }
1455- if ( unifyFunctionTypeIsUnboxCandidate ( fnType , queryElem , whereClause , mgens ) ) {
1456- if ( ! fnTypesScratch ) {
1457- fnTypesScratch = fnTypes . slice ( ) ;
1458- }
1459- if ( ! mgensScratch ) {
1460- mgensScratch = new Map ( mgens ) ;
1461- }
1462- backtracking . push ( {
1463- fnTypesScratch,
14641463 mgensScratch ,
1465- queryElemsOffset : i ,
1466- fnTypesOffset : j ,
1467- unbox : true ,
1468- } ) ;
1464+ solutionCb
1465+ ) ;
14691466 }
1467+ ) ;
1468+ if ( passesUnification ) {
1469+ return true ;
14701470 }
1471- if ( matchCandidates . length === 0 ) {
1472- if ( backtrack ( ) ) {
1473- continue ;
1474- } else {
1475- return false ;
1476- }
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 ;
14771480 }
1478- // use the current candidate
1479- const { fnTypesOffset : candidate , mgensScratch : mgensNew } = matchCandidates . pop ( ) ;
1480- if ( fnTypes [ candidate ] . id < 0 && queryElems [ i ] . id < 0 ) {
1481- mgens . set ( fnTypes [ candidate ] . id , queryElems [ i ] . id ) ;
1482- }
1483- for ( const [ fid , qid ] of mgensNew ) {
1484- mgens . set ( fid , qid ) ;
1485- }
1486- // `i` and `j` are paired off
1487- // `queryElems[i]` is left in place
1488- // `fnTypes[j]` is swapped with `fnTypes[i]` to pair them off
1489- const tmp = fnTypes [ candidate ] ;
1490- fnTypes [ candidate ] = fnTypes [ i ] ;
1491- fnTypes [ i ] = tmp ;
1492- // write other candidates to backtracking queue
1493- for ( const otherCandidate of matchCandidates ) {
1494- backtracking . push ( otherCandidate ) ;
1495- }
1496- // If we're on the last item, check the solution with the callback
1497- // backtrack if the callback says its unsuitable
1498- while ( i === ( ql - 1 ) && solutionCb && ! solutionCb ( mgens ) ) {
1499- if ( ! backtrack ( ) ) {
1500- 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 ;
15011486 }
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 ;
15021503 }
15031504 }
1504- return true ;
1505+ return false ;
15051506 }
15061507 function unifyFunctionTypeIsMatchCandidate ( fnType , queryElem , whereClause , mgens ) {
15071508 // type filters look like `trait:Read` or `enum:Result`
@@ -1514,15 +1515,17 @@ function initSearch(rawSearchIndex) {
15141515 // or, if mgens[fnType.id] = 0, then we've matched this generic with a bare trait
15151516 // and should make that same decision everywhere it appears
15161517 if ( fnType . id < 0 && queryElem . id < 0 ) {
1517- if ( mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== queryElem . id ) {
1518- return false ;
1519- }
1520- for ( const [ fid , qid ] of mgens . entries ( ) ) {
1521- if ( fnType . id !== fid && queryElem . id === qid ) {
1518+ if ( mgens !== null ) {
1519+ if ( mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== queryElem . id ) {
15221520 return false ;
15231521 }
1524- if ( fnType . id === fid && queryElem . id !== qid ) {
1525- return false ;
1522+ for ( const [ fid , qid ] of mgens . entries ( ) ) {
1523+ if ( fnType . id !== fid && queryElem . id === qid ) {
1524+ return false ;
1525+ }
1526+ if ( fnType . id === fid && queryElem . id !== qid ) {
1527+ return false ;
1528+ }
15261529 }
15271530 }
15281531 } else {
@@ -1575,7 +1578,7 @@ function initSearch(rawSearchIndex) {
15751578 }
15761579 // mgens[fnType.id] === 0 indicates that we committed to unboxing this generic
15771580 // mgens[fnType.id] === null indicates that we haven't decided yet
1578- if ( mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== 0 ) {
1581+ if ( mgens !== null && mgens . has ( fnType . id ) && mgens . get ( fnType . id ) !== 0 ) {
15791582 return false ;
15801583 }
15811584 // This is only a potential unbox if the search query appears in the where clause
0 commit comments