1
Fork 0

rustdoc-search: add search query syntax Fn(T) -> U

This is implemented, in addition to the ML-style one,
because Rust does it. If we don't, we'll never hear the end of it.

This commit also refactors some duplicate parts of the parser
into a dedicated function.
This commit is contained in:
Michael Howell 2024-01-06 16:01:10 -07:00
parent 23e931fd07
commit 7b926555b7
6 changed files with 530 additions and 81 deletions

View file

@ -153,16 +153,26 @@ will match these queries:
But it *does not* match `Result<Vec, u8>` or `Result<u8<Vec>>`. But it *does not* match `Result<Vec, u8>` or `Result<u8<Vec>>`.
To search for a function that accepts a function as a parameter,
like `Iterator::all`, wrap the nested signature in parenthesis,
as in [`Iterator<T>, (T -> bool) -> bool`][iterator-all].
You can also search for a specific closure trait,
such as `Iterator<T>, (FnMut(T) -> bool) -> bool`,
but you need to know which one you want.
[iterator-all]: ../../std/vec/struct.Vec.html?search=Iterator<T>%2C+(T+->+bool)+->+bool&filter-crate=std
### Primitives with Special Syntax ### Primitives with Special Syntax
| Shorthand | Explicit names | | Shorthand | Explicit names |
| --------- | ------------------------------------------------ | | ---------------- | ------------------------------------------------- |
| `[]` | `primitive:slice` and/or `primitive:array` | | `[]` | `primitive:slice` and/or `primitive:array` |
| `[T]` | `primitive:slice<T>` and/or `primitive:array<T>` | | `[T]` | `primitive:slice<T>` and/or `primitive:array<T>` |
| `()` | `primitive:unit` and/or `primitive:tuple` | | `()` | `primitive:unit` and/or `primitive:tuple` |
| `(T)` | `T` | | `(T)` | `T` |
| `(T,)` | `primitive:tuple<T>` | | `(T,)` | `primitive:tuple<T>` |
| `!` | `primitive:never` | | `!` | `primitive:never` |
| `(T, U -> V, W)` | `fn(T, U) -> (V, W)`, `Fn`, `FnMut`, and `FnOnce` |
When searching for `[]`, Rustdoc will return search results with either slices When searching for `[]`, Rustdoc will return search results with either slices
or arrays. If you know which one you want, you can force it to return results or arrays. If you know which one you want, you can force it to return results
@ -182,6 +192,10 @@ results for types that match tuples, even though it also matches the type on
its own. That is, `(u32)` matches `(u32,)` for the exact same reason that it its own. That is, `(u32)` matches `(u32,)` for the exact same reason that it
also matches `Result<u32, Error>`. also matches `Result<u32, Error>`.
The `->` operator has lower precedence than comma. If it's not wrapped
in brackets, it delimits the return value for the function being searched for.
To search for functions that take functions as parameters, use parenthesis.
### Limitations and quirks of type-based search ### Limitations and quirks of type-based search
Type-based search is still a buggy, experimental, work-in-progress feature. Type-based search is still a buggy, experimental, work-in-progress feature.
@ -220,9 +234,6 @@ Most of these limitations should be addressed in future version of Rustdoc.
* Searching for lifetimes is not supported. * Searching for lifetimes is not supported.
* It's impossible to search for closures based on their parameters or
return values.
* It's impossible to search based on the length of an array. * It's impossible to search based on the length of an array.
## Item filtering ## Item filtering
@ -239,19 +250,21 @@ Item filters can be used in both name-based and type signature-based searches.
```text ```text
ident = *(ALPHA / DIGIT / "_") ident = *(ALPHA / DIGIT / "_")
path = ident *(DOUBLE-COLON ident) [!] path = ident *(DOUBLE-COLON ident) [BANG]
slice-like = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET slice-like = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET
tuple-like = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN tuple-like = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN
arg = [type-filter *WS COLON *WS] (path [generics] / slice-like / tuple-like / [!]) arg = [type-filter *WS COLON *WS] (path [generics] / slice-like / tuple-like)
type-sep = COMMA/WS *(COMMA/WS) type-sep = COMMA/WS *(COMMA/WS)
nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) [ return-args ]
generic-arg-list = *(type-sep) arg [ EQUAL arg ] *(type-sep arg [ EQUAL arg ]) *(type-sep) generic-arg-list = *(type-sep) arg [ EQUAL arg ] *(type-sep arg [ EQUAL arg ]) *(type-sep)
generics = OPEN-ANGLE-BRACKET [ generic-arg-list ] *(type-sep) normal-generics = OPEN-ANGLE-BRACKET [ generic-arg-list ] *(type-sep)
CLOSE-ANGLE-BRACKET CLOSE-ANGLE-BRACKET
fn-like-generics = OPEN-PAREN [ nonempty-arg-list ] CLOSE-PAREN [ RETURN-ARROW arg ]
generics = normal-generics / fn-like-generics
return-args = RETURN-ARROW *(type-sep) nonempty-arg-list return-args = RETURN-ARROW *(type-sep) nonempty-arg-list
exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ]
type-search = [ nonempty-arg-list ] [ return-args ] type-search = [ nonempty-arg-list ]
query = *WS (exact-search / type-search) *WS query = *WS (exact-search / type-search) *WS
@ -296,6 +309,7 @@ QUOTE = %x22
COMMA = "," COMMA = ","
RETURN-ARROW = "->" RETURN-ARROW = "->"
EQUAL = "=" EQUAL = "="
BANG = "!"
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
DIGIT = %x30-39 DIGIT = %x30-39

View file

@ -1,3 +1,4 @@
// ignore-tidy-filelength
/* global addClass, getNakedUrl, getSettingValue */ /* global addClass, getNakedUrl, getSettingValue */
/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */ /* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */
@ -578,7 +579,10 @@ function initSearch(rawSearchIndex) {
// Syntactically, bindings are parsed as generics, // Syntactically, bindings are parsed as generics,
// but the query engine treats them differently. // but the query engine treats them differently.
if (gen.bindingName !== null) { if (gen.bindingName !== null) {
bindings.set(gen.bindingName.name, [gen, ...gen.bindingName.generics]); if (gen.name !== null) {
gen.bindingName.generics.unshift(gen);
}
bindings.set(gen.bindingName.name, gen.bindingName.generics);
return false; return false;
} }
return true; return true;
@ -678,6 +682,38 @@ function initSearch(rawSearchIndex) {
return end; return end;
} }
function getFilteredNextElem(query, parserState, elems, isInGenerics) {
const start = parserState.pos;
if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) {
throw ["Expected type filter before ", ":"];
}
getNextElem(query, parserState, elems, isInGenerics);
if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) {
if (parserState.typeFilter !== null) {
throw [
"Unexpected ",
":",
" (expected path after type filter ",
parserState.typeFilter + ":",
")",
];
}
if (elems.length === 0) {
throw ["Expected type filter before ", ":"];
} else if (query.literalSearch) {
throw ["Cannot use quotes on type filter"];
}
// The type filter doesn't count as an element since it's a modifier.
const typeFilterElem = elems.pop();
checkExtraTypeFilterCharacters(start, parserState);
parserState.typeFilter = typeFilterElem.name;
parserState.pos += 1;
parserState.totalElems -= 1;
query.literalSearch = false;
getNextElem(query, parserState, elems, isInGenerics);
}
}
/** /**
* @param {ParsedQuery} query * @param {ParsedQuery} query
* @param {ParserState} parserState * @param {ParserState} parserState
@ -752,6 +788,32 @@ function initSearch(rawSearchIndex) {
} }
parserState.pos += 1; parserState.pos += 1;
getItemsBefore(query, parserState, generics, ">"); getItemsBefore(query, parserState, generics, ">");
} else if (parserState.pos < parserState.length &&
parserState.userQuery[parserState.pos] === "("
) {
if (start >= end) {
throw ["Found generics without a path"];
}
if (parserState.isInBinding) {
throw ["Unexpected ", "(", " after ", "="];
}
parserState.pos += 1;
const typeFilter = parserState.typeFilter;
parserState.typeFilter = null;
getItemsBefore(query, parserState, generics, ")");
skipWhitespace(parserState);
if (isReturnArrow(parserState)) {
parserState.pos += 2;
skipWhitespace(parserState);
getFilteredNextElem(query, parserState, generics, isInGenerics);
generics[generics.length - 1].bindingName = makePrimitiveElement("output");
} else {
generics.push(makePrimitiveElement(null, {
bindingName: makePrimitiveElement("output"),
typeFilter: null,
}));
}
parserState.typeFilter = typeFilter;
} }
if (isStringElem) { if (isStringElem) {
skipWhitespace(parserState); skipWhitespace(parserState);
@ -811,7 +873,6 @@ function initSearch(rawSearchIndex) {
function getItemsBefore(query, parserState, elems, endChar) { function getItemsBefore(query, parserState, elems, endChar) {
let foundStopChar = true; let foundStopChar = true;
let foundSeparator = false; let foundSeparator = false;
let start = parserState.pos;
// If this is a generic, keep the outer item's type filter around. // If this is a generic, keep the outer item's type filter around.
const oldTypeFilter = parserState.typeFilter; const oldTypeFilter = parserState.typeFilter;
@ -874,24 +935,6 @@ function initSearch(rawSearchIndex) {
continue; continue;
} else if (c === ":" && isPathStart(parserState)) { } else if (c === ":" && isPathStart(parserState)) {
throw ["Unexpected ", "::", ": paths cannot start with ", "::"]; throw ["Unexpected ", "::", ": paths cannot start with ", "::"];
} else if (c === ":") {
if (parserState.typeFilter !== null) {
throw ["Unexpected ", ":"];
}
if (elems.length === 0) {
throw ["Expected type filter before ", ":"];
} else if (query.literalSearch) {
throw ["Cannot use quotes on type filter"];
}
// The type filter doesn't count as an element since it's a modifier.
const typeFilterElem = elems.pop();
checkExtraTypeFilterCharacters(start, parserState);
parserState.typeFilter = typeFilterElem.name;
parserState.pos += 1;
parserState.totalElems -= 1;
query.literalSearch = false;
foundStopChar = true;
continue;
} else if (isEndCharacter(c)) { } else if (isEndCharacter(c)) {
throw ["Unexpected ", c, " after ", extra]; throw ["Unexpected ", c, " after ", extra];
} }
@ -926,8 +969,7 @@ function initSearch(rawSearchIndex) {
]; ];
} }
const posBefore = parserState.pos; const posBefore = parserState.pos;
start = parserState.pos; getFilteredNextElem(query, parserState, elems, endChar !== "");
getNextElem(query, parserState, elems, endChar !== "");
if (endChar !== "" && parserState.pos >= parserState.length) { if (endChar !== "" && parserState.pos >= parserState.length) {
throw ["Unclosed ", extra]; throw ["Unclosed ", extra];
} }
@ -1004,7 +1046,6 @@ function initSearch(rawSearchIndex) {
*/ */
function parseInput(query, parserState) { function parseInput(query, parserState) {
let foundStopChar = true; let foundStopChar = true;
let start = parserState.pos;
while (parserState.pos < parserState.length) { while (parserState.pos < parserState.length) {
const c = parserState.userQuery[parserState.pos]; const c = parserState.userQuery[parserState.pos];
@ -1022,29 +1063,6 @@ function initSearch(rawSearchIndex) {
throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]]; throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]];
} }
throw ["Unexpected ", c]; throw ["Unexpected ", c];
} else if (c === ":" && !isPathStart(parserState)) {
if (parserState.typeFilter !== null) {
throw [
"Unexpected ",
":",
" (expected path after type filter ",
parserState.typeFilter + ":",
")",
];
} else if (query.elems.length === 0) {
throw ["Expected type filter before ", ":"];
} else if (query.literalSearch) {
throw ["Cannot use quotes on type filter"];
}
// The type filter doesn't count as an element since it's a modifier.
const typeFilterElem = query.elems.pop();
checkExtraTypeFilterCharacters(start, parserState);
parserState.typeFilter = typeFilterElem.name;
parserState.pos += 1;
parserState.totalElems -= 1;
query.literalSearch = false;
foundStopChar = true;
continue;
} else if (c === " ") { } else if (c === " ") {
skipWhitespace(parserState); skipWhitespace(parserState);
continue; continue;
@ -1080,8 +1098,7 @@ function initSearch(rawSearchIndex) {
]; ];
} }
const before = query.elems.length; const before = query.elems.length;
start = parserState.pos; getFilteredNextElem(query, parserState, query.elems, false);
getNextElem(query, parserState, query.elems, false);
if (query.elems.length === before) { if (query.elems.length === before) {
// Nothing was added, weird... Let's increase the position to not remain stuck. // Nothing was added, weird... Let's increase the position to not remain stuck.
parserState.pos += 1; parserState.pos += 1;

View file

@ -195,7 +195,7 @@ const PARSED = [
original: "a (b:", original: "a (b:",
returned: [], returned: [],
userQuery: "a (b:", userQuery: "a (b:",
error: "Expected `,`, `:` or `->`, found `(`", error: "Unclosed `(`",
}, },
{ {
query: "_:", query: "_:",
@ -357,7 +357,16 @@ const PARSED = [
original: "a,:", original: "a,:",
returned: [], returned: [],
userQuery: "a,:", userQuery: "a,:",
error: 'Unexpected `,` in type filter (before `:`)', error: 'Expected type filter before `:`',
},
{
query: "a!:",
elems: [],
foundElems: 0,
original: "a!:",
returned: [],
userQuery: "a!:",
error: 'Unexpected `!` in type filter (before `:`)',
}, },
{ {
query: " a<> :", query: " a<> :",
@ -366,7 +375,7 @@ const PARSED = [
original: "a<> :", original: "a<> :",
returned: [], returned: [],
userQuery: "a<> :", userQuery: "a<> :",
error: 'Unexpected `<` in type filter (before `:`)', error: 'Expected `,`, `:` or `->` after `>`, found `:`',
}, },
{ {
query: "mod : :", query: "mod : :",

View file

@ -1,4 +1,5 @@
const PARSED = [ const PARSED = [
// ML-style HOF
{ {
query: "(-> F<P>)", query: "(-> F<P>)",
elems: [{ elems: [{
@ -373,4 +374,339 @@ const PARSED = [
userQuery: "x, trait:(aaaaa, b -> a)", userQuery: "x, trait:(aaaaa, b -> a)",
error: null, error: null,
}, },
// Rust-style HOF
{
query: "Fn () -> F<P>",
elems: [{
name: "fn",
fullPath: ["fn"],
pathWithoutLast: [],
pathLast: "fn",
generics: [],
bindings: [
[
"output",
[{
name: "f",
fullPath: ["f"],
pathWithoutLast: [],
pathLast: "f",
generics: [
{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
},
],
typeFilter: -1,
}],
],
],
typeFilter: -1,
}],
foundElems: 1,
original: "Fn () -> F<P>",
returned: [],
userQuery: "fn () -> f<p>",
error: null,
},
{
query: "FnMut() -> P",
elems: [{
name: "fnmut",
fullPath: ["fnmut"],
pathWithoutLast: [],
pathLast: "fnmut",
generics: [],
bindings: [
[
"output",
[{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
typeFilter: -1,
}],
],
],
typeFilter: -1,
}],
foundElems: 1,
original: "FnMut() -> P",
returned: [],
userQuery: "fnmut() -> p",
error: null,
},
{
query: "(FnMut() -> P)",
elems: [{
name: "fnmut",
fullPath: ["fnmut"],
pathWithoutLast: [],
pathLast: "fnmut",
generics: [],
bindings: [
[
"output",
[{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
typeFilter: -1,
}],
],
],
typeFilter: -1,
}],
foundElems: 1,
original: "(FnMut() -> P)",
returned: [],
userQuery: "(fnmut() -> p)",
error: null,
},
{
query: "Fn(F<P>)",
elems: [{
name: "fn",
fullPath: ["fn"],
pathWithoutLast: [],
pathLast: "fn",
generics: [{
name: "f",
fullPath: ["f"],
pathWithoutLast: [],
pathLast: "f",
generics: [
{
name: "p",
fullPath: ["p"],
pathWithoutLast: [],
pathLast: "p",
generics: [],
},
],
typeFilter: -1,
}],
bindings: [
[
"output",
[],
],
],
typeFilter: -1,
}],
foundElems: 1,
original: "Fn(F<P>)",
returned: [],
userQuery: "fn(f<p>)",
error: null,
},
{
query: "primitive:fnonce(aaaaa, b) -> a",
elems: [{
name: "fnonce",
fullPath: ["fnonce"],
pathWithoutLast: [],
pathLast: "fnonce",
generics: [
{
name: "aaaaa",
fullPath: ["aaaaa"],
pathWithoutLast: [],
pathLast: "aaaaa",
generics: [],
typeFilter: -1,
},
{
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "b",
generics: [],
typeFilter: -1,
},
],
bindings: [
[
"output",
[{
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "a",
generics: [],
typeFilter: -1,
}],
],
],
typeFilter: 1,
}],
foundElems: 1,
original: "primitive:fnonce(aaaaa, b) -> a",
returned: [],
userQuery: "primitive:fnonce(aaaaa, b) -> a",
error: null,
},
{
query: "primitive:fnonce(aaaaa, keyword:b) -> trait:a",
elems: [{
name: "fnonce",
fullPath: ["fnonce"],
pathWithoutLast: [],
pathLast: "fnonce",
generics: [
{
name: "aaaaa",
fullPath: ["aaaaa"],
pathWithoutLast: [],
pathLast: "aaaaa",
generics: [],
typeFilter: -1,
},
{
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "b",
generics: [],
typeFilter: 0,
},
],
bindings: [
[
"output",
[{
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "a",
generics: [],
typeFilter: 10,
}],
],
],
typeFilter: 1,
}],
foundElems: 1,
original: "primitive:fnonce(aaaaa, keyword:b) -> trait:a",
returned: [],
userQuery: "primitive:fnonce(aaaaa, keyword:b) -> trait:a",
error: null,
},
{
query: "x, trait:fn(aaaaa, b -> a)",
elems: [
{
name: "x",
fullPath: ["x"],
pathWithoutLast: [],
pathLast: "x",
generics: [],
typeFilter: -1,
},
{
name: "fn",
fullPath: ["fn"],
pathWithoutLast: [],
pathLast: "fn",
generics: [
{
name: "->",
fullPath: ["->"],
pathWithoutLast: [],
pathLast: "->",
generics: [
{
name: "aaaaa",
fullPath: ["aaaaa"],
pathWithoutLast: [],
pathLast: "aaaaa",
generics: [],
typeFilter: -1,
},
{
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "b",
generics: [],
typeFilter: -1,
},
],
bindings: [
[
"output",
[{
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "a",
generics: [],
typeFilter: -1,
}],
],
],
typeFilter: -1,
},
],
bindings: [
[
"output",
[],
]
],
typeFilter: 10,
}
],
foundElems: 2,
original: "x, trait:fn(aaaaa, b -> a)",
returned: [],
userQuery: "x, trait:fn(aaaaa, b -> a)",
error: null,
},
{
query: 'a,b(c)',
elems: [
{
name: "a",
fullPath: ["a"],
pathWithoutLast: [],
pathLast: "a",
generics: [],
typeFilter: -1,
},
{
name: "b",
fullPath: ["b"],
pathWithoutLast: [],
pathLast: "b",
generics: [{
name: "c",
fullPath: ["c"],
pathWithoutLast: [],
pathLast: "c",
generics: [],
typeFilter: -1,
}],
bindings: [
[
"output",
[],
]
],
typeFilter: -1,
}
],
foundElems: 2,
original: "a,b(c)",
returned: [],
userQuery: "a,b(c)",
error: null,
},
]; ];

View file

@ -37,15 +37,6 @@ const PARSED = [
userQuery: "a b", userQuery: "a b",
error: null, error: null,
}, },
{
query: 'a,b(c)',
elems: [],
foundElems: 0,
original: "a,b(c)",
returned: [],
userQuery: "a,b(c)",
error: "Expected `,`, `:` or `->`, found `(`",
},
{ {
query: 'aaa,a', query: 'aaa,a',
elems: [ elems: [

View file

@ -1,6 +1,12 @@
// exact-check // exact-check
const EXPECTED = [ const EXPECTED = [
// not a HOF query
{
'query': 'u32 -> !',
'others': [],
},
// ML-style higher-order function notation // ML-style higher-order function notation
{ {
'query': 'bool, (u32 -> !) -> ()', 'query': 'bool, (u32 -> !) -> ()',
@ -53,11 +59,6 @@ const EXPECTED = [
{"path": "hof", "name": "fn_once"}, {"path": "hof", "name": "fn_once"},
], ],
}, },
{
'query': 'u32 -> !',
// not a HOF query
'others': [],
},
{ {
'query': '(str, str -> i8) -> ()', 'query': '(str, str -> i8) -> ()',
'others': [ 'others': [
@ -91,4 +92,85 @@ const EXPECTED = [
// params and return are not the same // params and return are not the same
'others': [], 'others': [],
}, },
// Rust-style higher-order function notation
{
'query': 'bool, fn(u32) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_ptr"},
],
},
{
'query': 'u8, fnonce(u32) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_once"},
],
},
{
'query': 'u8, fn(u32) -> ! -> ()',
// fnonce != fn
'others': [],
},
{
'query': 'i8, fnmut(u32) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_mut"},
],
},
{
'query': 'i8, fn(u32) -> ! -> ()',
// fnmut != fn
'others': [],
},
{
'query': 'char, fn(u32) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_"},
],
},
{
'query': 'char, fnmut(u32) -> ! -> ()',
// fn != fnmut
'others': [],
},
{
'query': 'fn(first<u32>) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_ptr"},
],
},
{
'query': 'fnonce(second<u32>) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_once"},
],
},
{
'query': 'fnmut(third<u32>) -> ! -> ()',
'others': [
{"path": "hof", "name": "fn_mut"},
],
},
{
'query': 'fn(u32) -> ! -> ()',
'others': [
// fn matches primitive:fn and trait:Fn
{"path": "hof", "name": "fn_"},
{"path": "hof", "name": "fn_ptr"},
],
},
{
'query': 'trait:fn(u32) -> ! -> ()',
'others': [
// fn matches primitive:fn and trait:Fn
{"path": "hof", "name": "fn_"},
],
},
{
'query': 'primitive:fn(u32) -> ! -> ()',
'others': [
// fn matches primitive:fn and trait:Fn
{"path": "hof", "name": "fn_ptr"},
],
},
]; ];