Skip to content

Commit 7f427f8

Browse files
committed
rustdoc-search: parse and search with ML-style HOF
Option::map, for example, looks like this: option<t>, (t -> u) -> option<u> This syntax searches all of the HOFs in Rust: traits Fn, FnOnce, and FnMut, and bare fn primitives.
1 parent 5aad51d commit 7f427f8

File tree

7 files changed

+650
-50
lines changed

7 files changed

+650
-50
lines changed

src/doc/rustdoc/src/read-documentation/search.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ Before describing the syntax in more detail, here's a few sample searches of
6363
the standard library and functions that are included in the results list:
6464

6565
| Query | Results |
66-
|-------|--------|
66+
|-------|---------|
6767
| [`usize -> vec`][] | `slice::repeat` and `Vec::with_capacity` |
6868
| [`vec, vec -> bool`][] | `Vec::eq` |
6969
| [`option<T>, fnonce -> option<U>`][] | `Option::map` and `Option::and_then` |
70-
| [`option<T>, fnonce -> option<T>`][] | `Option::filter` and `Option::inspect` |
70+
| [`option<T>, (fnonce (T) -> bool) -> option<T>`][optionfilter] | `Option::filter` |
71+
| [`option<T>, (T -> bool) -> option<T>`][optionfilter2] | `Option::filter` |
7172
| [`option -> default`][] | `Option::unwrap_or_default` |
7273
| [`stdout, [u8]`][stdoutu8] | `Stdout::write` |
7374
| [`any -> !`][] | `panic::panic_any` |
@@ -77,7 +78,8 @@ the standard library and functions that are included in the results list:
7778
[`usize -> vec`]: ../../std/vec/struct.Vec.html?search=usize%20-%3E%20vec&filter-crate=std
7879
[`vec, vec -> bool`]: ../../std/vec/struct.Vec.html?search=vec,%20vec%20-%3E%20bool&filter-crate=std
7980
[`option<T>, fnonce -> option<U>`]: ../../std/vec/struct.Vec.html?search=option<T>%2C%20fnonce%20->%20option<U>&filter-crate=std
80-
[`option<T>, fnonce -> option<T>`]: ../../std/vec/struct.Vec.html?search=option<T>%2C%20fnonce%20->%20option<T>&filter-crate=std
81+
[optionfilter]: ../../std/vec/struct.Vec.html?search=option<T>%2C+(fnonce+(T)+->+bool)+->+option<T>&filter-crate=std
82+
[optionfilter2]: ../../std/vec/struct.Vec.html?search=option<T>%2C+(T+->+bool)+->+option<T>&filter-crate=std
8183
[`option -> default`]: ../../std/vec/struct.Vec.html?search=option%20-%3E%20default&filter-crate=std
8284
[`any -> !`]: ../../std/vec/struct.Vec.html?search=any%20-%3E%20!&filter-crate=std
8385
[stdoutu8]: ../../std/vec/struct.Vec.html?search=stdout%2C%20[u8]&filter-crate=std

src/librustdoc/html/render/search_index.rs

+39-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::collections::{BTreeMap, VecDeque};
44
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
55
use rustc_middle::ty::TyCtxt;
66
use rustc_span::def_id::DefId;
7+
use rustc_span::sym;
78
use rustc_span::symbol::Symbol;
89
use serde::ser::{Serialize, SerializeSeq, SerializeStruct, Serializer};
910
use thin_vec::ThinVec;
@@ -566,6 +567,7 @@ fn get_index_type_id(
566567
// The type parameters are converted to generics in `simplify_fn_type`
567568
clean::Slice(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Slice)),
568569
clean::Array(_, _) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Array)),
570+
clean::BareFunction(_) => Some(RenderTypeId::Primitive(clean::PrimitiveType::Fn)),
569571
clean::Tuple(ref n) if n.is_empty() => {
570572
Some(RenderTypeId::Primitive(clean::PrimitiveType::Unit))
571573
}
@@ -584,7 +586,7 @@ fn get_index_type_id(
584586
}
585587
}
586588
// Not supported yet
587-
clean::BareFunction(_) | clean::Generic(_) | clean::ImplTrait(_) | clean::Infer => None,
589+
clean::Generic(_) | clean::ImplTrait(_) | clean::Infer => None,
588590
}
589591
}
590592

@@ -785,6 +787,42 @@ fn simplify_fn_type<'tcx, 'a>(
785787
);
786788
}
787789
res.push(get_index_type(arg, ty_generics, rgen));
790+
} else if let Type::BareFunction(ref bf) = *arg {
791+
let mut ty_generics = Vec::new();
792+
for ty in bf.decl.inputs.values.iter().map(|arg| &arg.type_) {
793+
simplify_fn_type(
794+
self_,
795+
generics,
796+
ty,
797+
tcx,
798+
recurse + 1,
799+
&mut ty_generics,
800+
rgen,
801+
is_return,
802+
cache,
803+
);
804+
}
805+
// The search index, for simplicity's sake, represents fn pointers and closures
806+
// the same way: as a tuple for the parameters, and an associated type for the
807+
// return type.
808+
let mut ty_output = Vec::new();
809+
simplify_fn_type(
810+
self_,
811+
generics,
812+
&bf.decl.output,
813+
tcx,
814+
recurse + 1,
815+
&mut ty_output,
816+
rgen,
817+
is_return,
818+
cache,
819+
);
820+
let ty_bindings = vec![(RenderTypeId::AssociatedType(sym::Output), ty_output)];
821+
res.push(RenderType {
822+
id: get_index_type_id(&arg, rgen),
823+
bindings: Some(ty_bindings),
824+
generics: Some(ty_generics),
825+
});
788826
} else {
789827
// This is not a type parameter. So for example if we have `T, U: Option<T>`, and we're
790828
// looking at `Option`, we enter this "else" condition, otherwise if it's `T`, we don't.

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

+122-44
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,22 @@ function initSearch(rawSearchIndex) {
272272
* Special type name IDs for searching by both tuple and unit (`()` syntax).
273273
*/
274274
let typeNameIdOfTupleOrUnit;
275+
/**
276+
* Special type name IDs for searching `fn`.
277+
*/
278+
let typeNameIdOfFn;
279+
/**
280+
* Special type name IDs for searching `fnmut`.
281+
*/
282+
let typeNameIdOfFnMut;
283+
/**
284+
* Special type name IDs for searching `fnonce`.
285+
*/
286+
let typeNameIdOfFnOnce;
287+
/**
288+
* Special type name IDs for searching higher order functions (`->` syntax).
289+
*/
290+
let typeNameIdOfHof;
275291

276292
/**
277293
* Add an item to the type Name->ID map, or, if one already exists, use it.
@@ -464,6 +480,21 @@ function initSearch(rawSearchIndex) {
464480
}
465481
}
466482

483+
function makePrimitiveElement(name, extra) {
484+
return Object.assign({
485+
name,
486+
id: null,
487+
fullPath: [name],
488+
pathWithoutLast: [],
489+
pathLast: name,
490+
normalizedPathLast: name,
491+
generics: [],
492+
bindings: new Map(),
493+
typeFilter: "primitive",
494+
bindingName: null,
495+
}, extra);
496+
}
497+
467498
/**
468499
* @param {ParsedQuery} query
469500
* @param {ParserState} parserState
@@ -501,18 +532,7 @@ function initSearch(rawSearchIndex) {
501532
}
502533
const bindingName = parserState.isInBinding;
503534
parserState.isInBinding = null;
504-
return {
505-
name: "never",
506-
id: null,
507-
fullPath: ["never"],
508-
pathWithoutLast: [],
509-
pathLast: "never",
510-
normalizedPathLast: "never",
511-
generics: [],
512-
bindings: new Map(),
513-
typeFilter: "primitive",
514-
bindingName,
515-
};
535+
return makePrimitiveElement("never", { bindingName });
516536
}
517537
const quadcolon = /::\s*::/.exec(path);
518538
if (path.startsWith("::")) {
@@ -671,28 +691,19 @@ function initSearch(rawSearchIndex) {
671691
let start = parserState.pos;
672692
let end;
673693
if ("[(".indexOf(parserState.userQuery[parserState.pos]) !== -1) {
674-
let endChar = ")";
675-
let name = "()";
676-
let friendlyName = "tuple";
677-
678-
if (parserState.userQuery[parserState.pos] === "[") {
679-
endChar = "]";
680-
name = "[]";
681-
friendlyName = "slice";
682-
}
694+
let endChar = ")";
695+
let name = "()";
696+
let friendlyName = "tuple";
697+
698+
if (parserState.userQuery[parserState.pos] === "[") {
699+
endChar = "]";
700+
name = "[]";
701+
friendlyName = "slice";
702+
}
683703
parserState.pos += 1;
684704
const { foundSeparator } = getItemsBefore(query, parserState, generics, endChar);
685705
const typeFilter = parserState.typeFilter;
686-
const isInBinding = parserState.isInBinding;
687-
if (typeFilter !== null && typeFilter !== "primitive") {
688-
throw [
689-
"Invalid search type: primitive ",
690-
name,
691-
" and ",
692-
typeFilter,
693-
" both specified",
694-
];
695-
}
706+
const bindingName = parserState.isInBinding;
696707
parserState.typeFilter = null;
697708
parserState.isInBinding = null;
698709
for (const gen of generics) {
@@ -702,23 +713,26 @@ if (parserState.userQuery[parserState.pos] === "[") {
702713
}
703714
if (name === "()" && !foundSeparator && generics.length === 1 && typeFilter === null) {
704715
elems.push(generics[0]);
716+
} else if (name === "()" && generics.length === 1 && generics[0].name === "->") {
717+
// `primitive:(a -> b)` parser to `primitive:"->"<output=b, (a,)>`
718+
// not `primitive:"()"<"->"<output=b, (a,)>>`
719+
generics[0].typeFilter = typeFilter;
720+
elems.push(generics[0]);
705721
} else {
722+
if (typeFilter !== null && typeFilter !== "primitive") {
723+
throw [
724+
"Invalid search type: primitive ",
725+
name,
726+
" and ",
727+
typeFilter,
728+
" both specified",
729+
];
730+
}
706731
parserState.totalElems += 1;
707732
if (isInGenerics) {
708733
parserState.genericsElems += 1;
709734
}
710-
elems.push({
711-
name: name,
712-
id: null,
713-
fullPath: [name],
714-
pathWithoutLast: [],
715-
pathLast: name,
716-
normalizedPathLast: name,
717-
generics,
718-
bindings: new Map(),
719-
typeFilter: "primitive",
720-
bindingName: isInBinding,
721-
});
735+
elems.push(makePrimitiveElement(name, { bindingName, generics }));
722736
}
723737
} else {
724738
const isStringElem = parserState.userQuery[start] === "\"";
@@ -805,6 +819,19 @@ if (parserState.userQuery[parserState.pos] === "[") {
805819
const oldIsInBinding = parserState.isInBinding;
806820
parserState.isInBinding = null;
807821

822+
// ML-style Higher Order Function notation
823+
//
824+
// a way to search for any closure or fn pointer regardless of
825+
// which closure trait is used
826+
//
827+
// Looks like this:
828+
//
829+
// `option<t>, (t -> u) -> option<u>`
830+
// ^^^^^^
831+
//
832+
// The Rust-style closure notation is implemented in getNextElem
833+
let hofParameters = null;
834+
808835
let extra = "";
809836
if (endChar === ">") {
810837
extra = "<";
@@ -825,6 +852,21 @@ if (parserState.userQuery[parserState.pos] === "[") {
825852
throw ["Unexpected ", endChar, " after ", "="];
826853
}
827854
break;
855+
} else if (endChar !== "" && isReturnArrow(parserState)) {
856+
// ML-style HOF notation only works when delimited in something,
857+
// otherwise a function arrow starts the return type of the top
858+
if (parserState.isInBinding) {
859+
throw ["Unexpected ", "->", " after ", "="];
860+
}
861+
hofParameters = [...elems];
862+
elems.length = 0;
863+
parserState.pos += 2;
864+
foundStopChar = true;
865+
foundSeparator = false;
866+
continue;
867+
} else if (c === " ") {
868+
parserState.pos += 1;
869+
continue;
828870
} else if (isSeparatorCharacter(c)) {
829871
parserState.pos += 1;
830872
foundStopChar = true;
@@ -904,6 +946,27 @@ if (parserState.userQuery[parserState.pos] === "[") {
904946
// in any case.
905947
parserState.pos += 1;
906948

949+
if (hofParameters) {
950+
// Commas in a HOF don't cause wrapping parens to become a tuple.
951+
// If you want a one-tuple with a HOF in it, write `((a -> b),)`.
952+
foundSeparator = false;
953+
// HOFs can't have directly nested bindings.
954+
if ([...elems, ...hofParameters].some(x => x.bindingName) || parserState.isInBinding) {
955+
throw ["Unexpected ", "=", " within ", "->"];
956+
}
957+
// HOFs are represented the same way closures are.
958+
// The arguments are wrapped in a tuple, and the output
959+
// is a binding, even though the compiler doesn't technically
960+
// represent fn pointers that way.
961+
const hofElem = makePrimitiveElement("->", {
962+
generics: hofParameters,
963+
bindings: new Map([["output", [...elems]]]),
964+
typeFilter: null,
965+
});
966+
elems.length = 0;
967+
elems[0] = hofElem;
968+
}
969+
907970
parserState.typeFilter = oldTypeFilter;
908971
parserState.isInBinding = oldIsInBinding;
909972

@@ -1635,6 +1698,12 @@ if (parserState.userQuery[parserState.pos] === "[") {
16351698
) {
16361699
// () matches primitive:tuple or primitive:unit
16371700
// if it matches, then we're fine, and this is an appropriate match candidate
1701+
} else if (queryElem.id === typeNameIdOfHof &&
1702+
(fnType.id === typeNameIdOfFn || fnType.id === typeNameIdOfFnMut ||
1703+
fnType.id === typeNameIdOfFnOnce)
1704+
) {
1705+
// -> matches fn, fnonce, and fnmut
1706+
// if it matches, then we're fine, and this is an appropriate match candidate
16381707
} else if (fnType.id !== queryElem.id || queryElem.id === null) {
16391708
return false;
16401709
}
@@ -1829,6 +1898,7 @@ if (parserState.userQuery[parserState.pos] === "[") {
18291898
typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 &&
18301899
// special case
18311900
elem.id !== typeNameIdOfArrayOrSlice && elem.id !== typeNameIdOfTupleOrUnit
1901+
&& elem.id !== typeNameIdOfHof
18321902
) {
18331903
return row.id === elem.id || checkIfInList(
18341904
row.generics,
@@ -2991,7 +3061,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
29913061
*/
29923062
function buildFunctionTypeFingerprint(type, output, fps) {
29933063
let input = type.id;
2994-
// All forms of `[]`/`()` get collapsed down to one thing in the bloom filter.
3064+
// All forms of `[]`/`()`/`->` get collapsed down to one thing in the bloom filter.
29953065
// Differentiating between arrays and slices, if the user asks for it, is
29963066
// still done in the matching algorithm.
29973067
if (input === typeNameIdOfArray || input === typeNameIdOfSlice) {
@@ -3000,6 +3070,10 @@ ${item.displayPath}<span class="${type}">${name}</span>\
30003070
if (input === typeNameIdOfTuple || input === typeNameIdOfUnit) {
30013071
input = typeNameIdOfTupleOrUnit;
30023072
}
3073+
if (input === typeNameIdOfFn || input === typeNameIdOfFnMut ||
3074+
input === typeNameIdOfFnOnce) {
3075+
input = typeNameIdOfHof;
3076+
}
30033077
// http://burtleburtle.net/bob/hash/integer.html
30043078
// ~~ is toInt32. It's used before adding, so
30053079
// the number stays in safe integer range.
@@ -3103,6 +3177,10 @@ ${item.displayPath}<span class="${type}">${name}</span>\
31033177
typeNameIdOfUnit = buildTypeMapIndex("unit");
31043178
typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]");
31053179
typeNameIdOfTupleOrUnit = buildTypeMapIndex("()");
3180+
typeNameIdOfFn = buildTypeMapIndex("fn");
3181+
typeNameIdOfFnMut = buildTypeMapIndex("fnmut");
3182+
typeNameIdOfFnOnce = buildTypeMapIndex("fnonce");
3183+
typeNameIdOfHof = buildTypeMapIndex("->");
31063184

31073185
// Function type fingerprints are 128-bit bloom filters that are used to
31083186
// estimate the distance between function and query.

tests/rustdoc-js-std/parser-errors.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const PARSED = [
114114
original: "(p -> p",
115115
returned: [],
116116
userQuery: "(p -> p",
117-
error: "Unexpected `-` after `(`",
117+
error: "Unclosed `(`",
118118
},
119119
{
120120
query: "::a::b",
@@ -330,7 +330,7 @@ const PARSED = [
330330
original: 'a<->',
331331
returned: [],
332332
userQuery: 'a<->',
333-
error: 'Unexpected `-` after `<`',
333+
error: 'Unclosed `<`',
334334
},
335335
{
336336
query: "a<a>:",

0 commit comments

Comments
 (0)