From 6ed283bb346ef7da1a476bd84fca53c188d3907e Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Thu, 15 Aug 2024 12:42:57 +0000 Subject: [PATCH 01/19] rustdoc-json: Add test for `Self` type --- tests/rustdoc-json/traits/self.rs | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/rustdoc-json/traits/self.rs diff --git a/tests/rustdoc-json/traits/self.rs b/tests/rustdoc-json/traits/self.rs new file mode 100644 index 0000000000000..c7d952ae567d4 --- /dev/null +++ b/tests/rustdoc-json/traits/self.rs @@ -0,0 +1,58 @@ +// ignore-tidy-linelength + +pub struct Foo; + +// Check that Self is represented uniformly between inherent impls, trait impls, +// and trait definitions, even though it uses both SelfTyParam and SelfTyAlias +// internally. +// +// Each assertion matches 3 times, and should be the same each time. + +impl Foo { + //@ ismany '$.index[*][?(@.name=="by_ref")].inner.function.decl.inputs[0][0]' '"self"' '"self"' '"self"' + //@ ismany '$.index[*][?(@.name=="by_ref")].inner.function.decl.inputs[0][1].borrowed_ref.type.generic' '"Self"' '"Self"' '"Self"' + //@ ismany '$.index[*][?(@.name=="by_ref")].inner.function.decl.inputs[0][1].borrowed_ref.lifetime' null null null + //@ ismany '$.index[*][?(@.name=="by_ref")].inner.function.decl.inputs[0][1].borrowed_ref.mutable' false false false + pub fn by_ref(&self) {} + + //@ ismany '$.index[*][?(@.name=="by_exclusive_ref")].inner.function.decl.inputs[0][0]' '"self"' '"self"' '"self"' + //@ ismany '$.index[*][?(@.name=="by_exclusive_ref")].inner.function.decl.inputs[0][1].borrowed_ref.type.generic' '"Self"' '"Self"' '"Self"' + //@ ismany '$.index[*][?(@.name=="by_exclusive_ref")].inner.function.decl.inputs[0][1].borrowed_ref.lifetime' null null null + //@ ismany '$.index[*][?(@.name=="by_exclusive_ref")].inner.function.decl.inputs[0][1].borrowed_ref.mutable' true true true + pub fn by_exclusive_ref(&mut self) {} + + //@ ismany '$.index[*][?(@.name=="by_value")].inner.function.decl.inputs[0][0]' '"self"' '"self"' '"self"' + //@ ismany '$.index[*][?(@.name=="by_value")].inner.function.decl.inputs[0][1].generic' '"Self"' '"Self"' '"Self"' + pub fn by_value(self) {} + + //@ ismany '$.index[*][?(@.name=="with_lifetime")].inner.function.decl.inputs[0][0]' '"self"' '"self"' '"self"' + //@ ismany '$.index[*][?(@.name=="with_lifetime")].inner.function.decl.inputs[0][1].borrowed_ref.type.generic' '"Self"' '"Self"' '"Self"' + //@ ismany '$.index[*][?(@.name=="with_lifetime")].inner.function.decl.inputs[0][1].borrowed_ref.lifetime' \"\'a\" \"\'a\" \"\'a\" + //@ ismany '$.index[*][?(@.name=="with_lifetime")].inner.function.decl.inputs[0][1].borrowed_ref.mutable' false false false + pub fn with_lifetime<'a>(&'a self) {} + + //@ ismany '$.index[*][?(@.name=="build")].inner.function.decl.output.generic' '"Self"' '"Self"' '"Self"' + pub fn build() -> Self { + Self + } +} + +pub struct Bar; + +pub trait SelfParams { + fn by_ref(&self); + fn by_exclusive_ref(&mut self); + fn by_value(self); + fn with_lifetime<'a>(&'a self); + fn build() -> Self; +} + +impl SelfParams for Bar { + fn by_ref(&self) {} + fn by_exclusive_ref(&mut self) {} + fn by_value(self) {} + fn with_lifetime<'a>(&'a self) {} + fn build() -> Self { + Self + } +} From 42a901acd9c9d3a0c9ca7adf2470b789f9c81a5b Mon Sep 17 00:00:00 2001 From: Michael Goulet Date: Sat, 24 Aug 2024 16:15:43 -0400 Subject: [PATCH 02/19] Don't use TyKind in lint --- compiler/rustc_lint/src/foreign_modules.rs | 25 +++++++++++----------- compiler/rustc_middle/src/ty/sty.rs | 2 +- compiler/rustc_type_ir/src/ty_kind.rs | 7 ------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/compiler/rustc_lint/src/foreign_modules.rs b/compiler/rustc_lint/src/foreign_modules.rs index 5da1cbc2283b6..a60fc0ffbbb3b 100644 --- a/compiler/rustc_lint/src/foreign_modules.rs +++ b/compiler/rustc_lint/src/foreign_modules.rs @@ -265,8 +265,6 @@ fn structurally_same_type_impl<'tcx>( } else { // Do a full, depth-first comparison between the two. use rustc_type_ir::TyKind::*; - let a_kind = a.kind(); - let b_kind = b.kind(); let compare_layouts = |a, b| -> Result> { debug!("compare_layouts({:?}, {:?})", a, b); @@ -281,12 +279,11 @@ fn structurally_same_type_impl<'tcx>( Ok(a_layout == b_layout) }; - #[allow(rustc::usage_of_ty_tykind)] let is_primitive_or_pointer = - |kind: &ty::TyKind<'_>| kind.is_primitive() || matches!(kind, RawPtr(..) | Ref(..)); + |ty: Ty<'tcx>| ty.is_primitive() || matches!(ty.kind(), RawPtr(..) | Ref(..)); ensure_sufficient_stack(|| { - match (a_kind, b_kind) { + match (a.kind(), b.kind()) { (Adt(a_def, _), Adt(b_def, _)) => { // We can immediately rule out these types as structurally same if // their layouts differ. @@ -382,17 +379,21 @@ fn structurally_same_type_impl<'tcx>( // An Adt and a primitive or pointer type. This can be FFI-safe if non-null // enum layout optimisation is being applied. - (Adt(..), other_kind) | (other_kind, Adt(..)) - if is_primitive_or_pointer(other_kind) => - { - let (primitive, adt) = - if is_primitive_or_pointer(a.kind()) { (a, b) } else { (b, a) }; - if let Some(ty) = types::repr_nullable_ptr(tcx, param_env, adt, ckind) { - ty == primitive + (Adt(..), _) if is_primitive_or_pointer(b) => { + if let Some(ty) = types::repr_nullable_ptr(tcx, param_env, a, ckind) { + ty == b } else { compare_layouts(a, b).unwrap_or(false) } } + (_, Adt(..)) if is_primitive_or_pointer(a) => { + if let Some(ty) = types::repr_nullable_ptr(tcx, param_env, b, ckind) { + ty == a + } else { + compare_layouts(a, b).unwrap_or(false) + } + } + // Otherwise, just compare the layouts. This may fail to lint for some // incompatible types, but at the very least, will stop reads into // uninitialised memory. diff --git a/compiler/rustc_middle/src/ty/sty.rs b/compiler/rustc_middle/src/ty/sty.rs index d60bfb9faa1aa..c6621a7a64331 100644 --- a/compiler/rustc_middle/src/ty/sty.rs +++ b/compiler/rustc_middle/src/ty/sty.rs @@ -1000,7 +1000,7 @@ impl<'tcx> Ty<'tcx> { #[inline] pub fn is_primitive(self) -> bool { - self.kind().is_primitive() + matches!(self.kind(), Bool | Char | Int(_) | Uint(_) | Float(_)) } #[inline] diff --git a/compiler/rustc_type_ir/src/ty_kind.rs b/compiler/rustc_type_ir/src/ty_kind.rs index 328b6739d9756..80c3565911e9a 100644 --- a/compiler/rustc_type_ir/src/ty_kind.rs +++ b/compiler/rustc_type_ir/src/ty_kind.rs @@ -254,13 +254,6 @@ pub enum TyKind { Error(I::ErrorGuaranteed), } -impl TyKind { - #[inline] - pub fn is_primitive(&self) -> bool { - matches!(self, Bool | Char | Int(_) | Uint(_) | Float(_)) - } -} - // This is manually implemented because a derive would require `I: Debug` impl fmt::Debug for TyKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { From af05882eb5775d9bf8b2efea5598cd6305f6a33e Mon Sep 17 00:00:00 2001 From: Jubilee Young Date: Sat, 24 Aug 2024 15:14:19 -0700 Subject: [PATCH 03/19] Deny wasm_c_abi lint to nudge the last 25% This shouldn't affect projects indirectly depending on wasm-bindgen because cargo passes `--cap-lints=allow` when building dependencies. --- compiler/rustc_lint_defs/src/builtin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/rustc_lint_defs/src/builtin.rs b/compiler/rustc_lint_defs/src/builtin.rs index a65d30eb817a0..bcd00ac06ba4b 100644 --- a/compiler/rustc_lint_defs/src/builtin.rs +++ b/compiler/rustc_lint_defs/src/builtin.rs @@ -4806,7 +4806,7 @@ declare_lint! { /// version of Rust this will be fixed and therefore dependencies relying /// on the non-spec-compliant C ABI will stop functioning. pub WASM_C_ABI, - Warn, + Deny, "detects dependencies that are incompatible with the Wasm C ABI", @future_incompatible = FutureIncompatibleInfo { reason: FutureIncompatibilityReason::FutureReleaseErrorReportInDeps, From c61f85b6dd47343abe6383ea2eb71f0b3a7d0e2b Mon Sep 17 00:00:00 2001 From: Michael Goulet Date: Mon, 22 Jan 2024 02:29:21 +0000 Subject: [PATCH 04/19] Don't make pattern nonterminals match statement nonterminals --- compiler/rustc_ast/src/token.rs | 57 ++++++++++++------- .../rustc_parse/src/parser/nonterminal.rs | 20 +------ compiler/rustc_parse/src/parser/pat.rs | 6 +- compiler/rustc_parse/src/parser/path.rs | 5 +- .../patterns-dont-match-nt-statement.rs | 19 +++++++ 5 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 tests/ui/pattern/patterns-dont-match-nt-statement.rs diff --git a/compiler/rustc_ast/src/token.rs b/compiler/rustc_ast/src/token.rs index 43d87b96ead90..f1dddb3acacaa 100644 --- a/compiler/rustc_ast/src/token.rs +++ b/compiler/rustc_ast/src/token.rs @@ -486,6 +486,9 @@ impl Token { } /// Returns `true` if the token can appear at the start of an expression. + /// + /// **NB**: Take care when modifying this function, since it will change + /// the stable set of tokens that are allowed to match an expr nonterminal. pub fn can_begin_expr(&self) -> bool { match self.uninterpolate().kind { Ident(name, is_raw) => @@ -504,10 +507,13 @@ impl Token { PathSep | // global path Lifetime(..) | // labeled loop Pound => true, // expression attributes - Interpolated(ref nt) => matches!(&**nt, NtLiteral(..) | - NtExpr(..) | - NtBlock(..) | - NtPath(..)), + Interpolated(ref nt) => + matches!(&**nt, + NtBlock(..) | + NtExpr(..) | + NtLiteral(..) | + NtPath(..) + ), _ => false, } } @@ -515,23 +521,32 @@ impl Token { /// Returns `true` if the token can appear at the start of a pattern. /// /// Shamelessly borrowed from `can_begin_expr`, only used for diagnostics right now. - pub fn can_begin_pattern(&self) -> bool { - match self.uninterpolate().kind { - Ident(name, is_raw) => - ident_can_begin_expr(name, self.span, is_raw), // value name or keyword - | OpenDelim(Delimiter::Bracket | Delimiter::Parenthesis) // tuple or array - | Literal(..) // literal - | BinOp(Minus) // unary minus - | BinOp(And) // reference - | AndAnd // double reference - // DotDotDot is no longer supported - | DotDot | DotDotDot | DotDotEq // ranges - | Lt | BinOp(Shl) // associated path - | PathSep => true, // global path - Interpolated(ref nt) => matches!(&**nt, NtLiteral(..) | - NtPat(..) | - NtBlock(..) | - NtPath(..)), + pub fn can_begin_pattern(&self, pat_kind: NtPatKind) -> bool { + match &self.uninterpolate().kind { + // box, ref, mut, and other identifiers (can stricten) + Ident(..) | NtIdent(..) | + OpenDelim(Delimiter::Parenthesis) | // tuple pattern + OpenDelim(Delimiter::Bracket) | // slice pattern + BinOp(And) | // reference + BinOp(Minus) | // negative literal + AndAnd | // double reference + Literal(_) | // literal + DotDot | // range pattern (future compat) + DotDotDot | // range pattern (future compat) + PathSep | // path + Lt | // path (UFCS constant) + BinOp(Shl) => true, // path (double UFCS) + // leading vert `|` or-pattern + BinOp(Or) => matches!(pat_kind, PatWithOr), + Interpolated(nt) => + matches!(&**nt, + | NtExpr(..) + | NtLiteral(..) + | NtMeta(..) + | NtPat(..) + | NtPath(..) + | NtTy(..) + ), _ => false, } } diff --git a/compiler/rustc_parse/src/parser/nonterminal.rs b/compiler/rustc_parse/src/parser/nonterminal.rs index 999f6f0eeb0ce..e66d0df012bcb 100644 --- a/compiler/rustc_parse/src/parser/nonterminal.rs +++ b/compiler/rustc_parse/src/parser/nonterminal.rs @@ -86,25 +86,7 @@ impl<'a> Parser<'a> { token::Interpolated(nt) => may_be_ident(nt), _ => false, }, - NonterminalKind::Pat(pat_kind) => match &token.kind { - // box, ref, mut, and other identifiers (can stricten) - token::Ident(..) | token::NtIdent(..) | - token::OpenDelim(Delimiter::Parenthesis) | // tuple pattern - token::OpenDelim(Delimiter::Bracket) | // slice pattern - token::BinOp(token::And) | // reference - token::BinOp(token::Minus) | // negative literal - token::AndAnd | // double reference - token::Literal(_) | // literal - token::DotDot | // range pattern (future compat) - token::DotDotDot | // range pattern (future compat) - token::PathSep | // path - token::Lt | // path (UFCS constant) - token::BinOp(token::Shl) => true, // path (double UFCS) - // leading vert `|` or-pattern - token::BinOp(token::Or) => matches!(pat_kind, PatWithOr), - token::Interpolated(nt) => may_be_ident(nt), - _ => false, - }, + NonterminalKind::Pat(pat_kind) => token.can_begin_pattern(pat_kind), NonterminalKind::Lifetime => match &token.kind { token::Lifetime(_) | token::NtLifetime(..) => true, _ => false, diff --git a/compiler/rustc_parse/src/parser/pat.rs b/compiler/rustc_parse/src/parser/pat.rs index cc68ae237ba18..8233f9a79435f 100644 --- a/compiler/rustc_parse/src/parser/pat.rs +++ b/compiler/rustc_parse/src/parser/pat.rs @@ -444,7 +444,11 @@ impl<'a> Parser<'a> { let mut lo = self.token.span; - if self.token.is_keyword(kw::Let) && self.look_ahead(1, |tok| tok.can_begin_pattern()) { + if self.token.is_keyword(kw::Let) + && self.look_ahead(1, |tok| { + tok.can_begin_pattern(token::NtPatKind::PatParam { inferred: false }) + }) + { self.bump(); self.dcx().emit_err(RemoveLet { span: lo }); lo = self.token.span; diff --git a/compiler/rustc_parse/src/parser/path.rs b/compiler/rustc_parse/src/parser/path.rs index d8bf10e6021cc..8ee40ecd77e3f 100644 --- a/compiler/rustc_parse/src/parser/path.rs +++ b/compiler/rustc_parse/src/parser/path.rs @@ -378,7 +378,10 @@ impl<'a> Parser<'a> { if self.may_recover() && prev_token_before_parsing == token::PathSep && (style == PathStyle::Expr && self.token.can_begin_expr() - || style == PathStyle::Pat && self.token.can_begin_pattern()) + || style == PathStyle::Pat + && self.token.can_begin_pattern(token::NtPatKind::PatParam { + inferred: false, + })) { snapshot = Some(self.create_snapshot_for_diagnostic()); } diff --git a/tests/ui/pattern/patterns-dont-match-nt-statement.rs b/tests/ui/pattern/patterns-dont-match-nt-statement.rs new file mode 100644 index 0000000000000..c8d41459383ac --- /dev/null +++ b/tests/ui/pattern/patterns-dont-match-nt-statement.rs @@ -0,0 +1,19 @@ +//@ check-pass + +// Make sure that a `stmt` nonterminal does not eagerly match against +// a `pat`, since this will always cause a parse error... + +macro_rules! m { + ($pat:pat) => {}; + ($stmt:stmt) => {}; +} + +macro_rules! m2 { + ($stmt:stmt) => { + m! { $stmt } + }; +} + +m2! { let x = 1 } + +fn main() {} From 0763a3afc8f7d71cfe3b62c76fd40912f6a6c9ed Mon Sep 17 00:00:00 2001 From: Jubilee Young Date: Mon, 26 Aug 2024 20:11:58 -0700 Subject: [PATCH 05/19] Bump backtrace to rust-lang/backtrace@fc37b22 It should be 0.3.74~ish. This should help with backtraces on Android, QNX NTO 7.0, and Windows. --- library/backtrace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/backtrace b/library/backtrace index 72265bea21089..fc37b22dc8a38 160000 --- a/library/backtrace +++ b/library/backtrace @@ -1 +1 @@ -Subproject commit 72265bea210891ae47bbe6d4f17b493ef0606619 +Subproject commit fc37b22dc8a384d93f6b7b4817266eec6433875e From a1c36c6ae9e6036b89e4c4b9cec2ce2e375b2a85 Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Wed, 14 Aug 2024 20:16:28 +0300 Subject: [PATCH 06/19] linker: Synchronize native library search in rustc and linker --- compiler/rustc_codegen_ssa/src/back/link.rs | 61 ++++---------- compiler/rustc_metadata/src/lib.rs | 5 +- compiler/rustc_metadata/src/native_libs.rs | 92 ++++++++++++++++++--- 3 files changed, 101 insertions(+), 57 deletions(-) diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs index 4d19425255faf..e8143b9a5f38f 100644 --- a/compiler/rustc_codegen_ssa/src/back/link.rs +++ b/compiler/rustc_codegen_ssa/src/back/link.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use std::ffi::OsString; use std::fs::{read, File, OpenOptions}; use std::io::{BufWriter, Write}; -use std::ops::Deref; +use std::ops::{ControlFlow, Deref}; use std::path::{Path, PathBuf}; use std::process::{ExitStatus, Output, Stdio}; use std::{env, fmt, fs, io, mem, str}; @@ -18,8 +18,8 @@ use rustc_data_structures::temp_dir::MaybeTempDir; use rustc_errors::{DiagCtxtHandle, ErrorGuaranteed, FatalError}; use rustc_fs_util::{fix_windows_verbatim_for_gcc, try_canonicalize}; use rustc_hir::def_id::{CrateNum, LOCAL_CRATE}; -use rustc_metadata::find_native_static_library; use rustc_metadata::fs::{copy_to_stdout, emit_wrapper_file, METADATA_FILENAME}; +use rustc_metadata::{find_native_static_library, walk_native_lib_search_dirs}; use rustc_middle::bug; use rustc_middle::middle::debugger_visualizer::DebuggerVisualizerFile; use rustc_middle::middle::dependency_format::Linkage; @@ -2110,50 +2110,19 @@ fn add_library_search_dirs( return; } - // Library search paths explicitly supplied by user (`-L` on the command line). - for search_path in sess.target_filesearch(PathKind::Native).cli_search_paths() { - cmd.include_path(&fix_windows_verbatim_for_gcc(&search_path.dir)); - } - for search_path in sess.target_filesearch(PathKind::Framework).cli_search_paths() { - // Contrary to the `-L` docs only framework-specific paths are considered here. - if search_path.kind != PathKind::All { - cmd.framework_path(&search_path.dir); - } - } - - // The toolchain ships some native library components and self-contained linking was enabled. - // Add the self-contained library directory to search paths. - if self_contained_components.intersects( - LinkSelfContainedComponents::LIBC - | LinkSelfContainedComponents::UNWIND - | LinkSelfContainedComponents::MINGW, - ) { - let lib_path = sess.target_tlib_path.dir.join("self-contained"); - cmd.include_path(&fix_windows_verbatim_for_gcc(&lib_path)); - } - - // Toolchains for some targets may ship `libunwind.a`, but place it into the main sysroot - // library directory instead of the self-contained directories. - // Sanitizer libraries have the same issue and are also linked by name on Apple targets. - // The targets here should be in sync with `copy_third_party_objects` in bootstrap. - // FIXME: implement `-Clink-self-contained=+/-unwind,+/-sanitizers`, move the shipped libunwind - // and sanitizers to self-contained directory, and stop adding this search path. - if sess.target.vendor == "fortanix" - || sess.target.os == "linux" - || sess.target.os == "fuchsia" - || sess.target.is_like_osx && !sess.opts.unstable_opts.sanitizer.is_empty() - { - cmd.include_path(&fix_windows_verbatim_for_gcc(&sess.target_tlib_path.dir)); - } - - // Mac Catalyst uses the macOS SDK, but to link to iOS-specific frameworks - // we must have the support library stubs in the library search path (#121430). - if let Some(sdk_root) = apple_sdk_root - && sess.target.llvm_target.contains("macabi") - { - cmd.include_path(&sdk_root.join("System/iOSSupport/usr/lib")); - cmd.framework_path(&sdk_root.join("System/iOSSupport/System/Library/Frameworks")); - } + walk_native_lib_search_dirs( + sess, + self_contained_components, + apple_sdk_root, + |dir, is_framework| { + if is_framework { + cmd.framework_path(dir); + } else { + cmd.include_path(&fix_windows_verbatim_for_gcc(dir)); + } + ControlFlow::<()>::Continue(()) + }, + ); } /// Add options making relocation sections in the produced ELF files read-only diff --git a/compiler/rustc_metadata/src/lib.rs b/compiler/rustc_metadata/src/lib.rs index acaf9fb0fc32a..d530a7cd9d4bc 100644 --- a/compiler/rustc_metadata/src/lib.rs +++ b/compiler/rustc_metadata/src/lib.rs @@ -3,6 +3,7 @@ #![allow(rustc::potential_query_instability)] #![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")] #![doc(rust_logo)] +#![feature(control_flow_enum)] #![feature(coroutines)] #![feature(decl_macro)] #![feature(error_iter)] @@ -34,7 +35,9 @@ pub mod locator; pub use creader::{load_symbol_from_dylib, DylibError}; pub use fs::{emit_wrapper_file, METADATA_FILENAME}; -pub use native_libs::find_native_static_library; +pub use native_libs::{ + find_native_static_library, try_find_native_static_library, walk_native_lib_search_dirs, +}; pub use rmeta::{encode_metadata, rendered_const, EncodedMetadata, METADATA_HEADER}; rustc_fluent_macro::fluent_messages! { "../messages.ftl" } diff --git a/compiler/rustc_metadata/src/native_libs.rs b/compiler/rustc_metadata/src/native_libs.rs index 34497f5ac53f8..a6ad449cb53e8 100644 --- a/compiler/rustc_metadata/src/native_libs.rs +++ b/compiler/rustc_metadata/src/native_libs.rs @@ -1,4 +1,5 @@ -use std::path::PathBuf; +use std::ops::ControlFlow; +use std::path::{Path, PathBuf}; use rustc_ast::{NestedMetaItem, CRATE_NODE_ID}; use rustc_attr as attr; @@ -16,10 +17,68 @@ use rustc_session::Session; use rustc_span::def_id::{DefId, LOCAL_CRATE}; use rustc_span::symbol::{sym, Symbol}; use rustc_target::spec::abi::Abi; +use rustc_target::spec::LinkSelfContainedComponents; use crate::{errors, fluent_generated}; -pub fn find_native_static_library(name: &str, verbatim: bool, sess: &Session) -> PathBuf { +pub fn walk_native_lib_search_dirs( + sess: &Session, + self_contained_components: LinkSelfContainedComponents, + apple_sdk_root: Option<&Path>, + mut f: impl FnMut(&Path, bool /*is_framework*/) -> ControlFlow, +) -> ControlFlow { + // Library search paths explicitly supplied by user (`-L` on the command line). + for search_path in sess.target_filesearch(PathKind::Native).cli_search_paths() { + f(&search_path.dir, false)?; + } + for search_path in sess.target_filesearch(PathKind::Framework).cli_search_paths() { + // Frameworks are looked up strictly in framework-specific paths. + if search_path.kind != PathKind::All { + f(&search_path.dir, true)?; + } + } + + // The toolchain ships some native library components and self-contained linking was enabled. + // Add the self-contained library directory to search paths. + if self_contained_components.intersects( + LinkSelfContainedComponents::LIBC + | LinkSelfContainedComponents::UNWIND + | LinkSelfContainedComponents::MINGW, + ) { + f(&sess.target_tlib_path.dir.join("self-contained"), false)?; + } + + // Toolchains for some targets may ship `libunwind.a`, but place it into the main sysroot + // library directory instead of the self-contained directories. + // Sanitizer libraries have the same issue and are also linked by name on Apple targets. + // The targets here should be in sync with `copy_third_party_objects` in bootstrap. + // FIXME: implement `-Clink-self-contained=+/-unwind,+/-sanitizers`, move the shipped libunwind + // and sanitizers to self-contained directory, and stop adding this search path. + if sess.target.vendor == "fortanix" + || sess.target.os == "linux" + || sess.target.os == "fuchsia" + || sess.target.is_like_osx && !sess.opts.unstable_opts.sanitizer.is_empty() + { + f(&sess.target_tlib_path.dir, false)?; + } + + // Mac Catalyst uses the macOS SDK, but to link to iOS-specific frameworks + // we must have the support library stubs in the library search path (#121430). + if let Some(sdk_root) = apple_sdk_root + && sess.target.llvm_target.contains("macabi") + { + f(&sdk_root.join("System/iOSSupport/usr/lib"), false)?; + f(&sdk_root.join("System/iOSSupport/System/Library/Frameworks"), true)?; + } + + ControlFlow::Continue(()) +} + +pub fn try_find_native_static_library( + sess: &Session, + name: &str, + verbatim: bool, +) -> Option { let formats = if verbatim { vec![("".into(), "".into())] } else { @@ -30,16 +89,29 @@ pub fn find_native_static_library(name: &str, verbatim: bool, sess: &Session) -> if os == unix { vec![os] } else { vec![os, unix] } }; - for path in sess.target_filesearch(PathKind::Native).search_paths() { - for (prefix, suffix) in &formats { - let test = path.dir.join(format!("{prefix}{name}{suffix}")); - if test.exists() { - return test; + // FIXME: Account for self-contained linking settings and Apple SDK. + walk_native_lib_search_dirs( + sess, + LinkSelfContainedComponents::empty(), + None, + |dir, is_framework| { + if !is_framework { + for (prefix, suffix) in &formats { + let test = dir.join(format!("{prefix}{name}{suffix}")); + if test.exists() { + return ControlFlow::Break(test); + } + } } - } - } + ControlFlow::Continue(()) + }, + ) + .break_value() +} - sess.dcx().emit_fatal(errors::MissingNativeLibrary::new(name, verbatim)); +pub fn find_native_static_library(name: &str, verbatim: bool, sess: &Session) -> PathBuf { + try_find_native_static_library(sess, name, verbatim) + .unwrap_or_else(|| sess.dcx().emit_fatal(errors::MissingNativeLibrary::new(name, verbatim))) } fn find_bundled_library( From 05bd36de5068c8c34a404594926af268e5f4cb13 Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Thu, 22 Aug 2024 01:56:20 +0300 Subject: [PATCH 07/19] linker: Better support alternative static library naming on MSVC Previously `libname.a` naming was supported as a fallback when producing rlibs, but not when producing executables or dynamic libraries --- compiler/rustc_codegen_ssa/src/back/linker.rs | 14 ++++++++++---- tests/run-make/native-lib-alt-naming/native.rs | 2 ++ tests/run-make/native-lib-alt-naming/rmake.rs | 15 +++++++++++++++ tests/run-make/native-lib-alt-naming/rust.rs | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tests/run-make/native-lib-alt-naming/native.rs create mode 100644 tests/run-make/native-lib-alt-naming/rmake.rs create mode 100644 tests/run-make/native-lib-alt-naming/rust.rs diff --git a/compiler/rustc_codegen_ssa/src/back/linker.rs b/compiler/rustc_codegen_ssa/src/back/linker.rs index fbab988a32b08..cb266247e0dde 100644 --- a/compiler/rustc_codegen_ssa/src/back/linker.rs +++ b/compiler/rustc_codegen_ssa/src/back/linker.rs @@ -7,7 +7,7 @@ use std::{env, iter, mem, str}; use cc::windows_registry; use rustc_hir::def_id::{CrateNum, LOCAL_CRATE}; -use rustc_metadata::find_native_static_library; +use rustc_metadata::{find_native_static_library, try_find_native_static_library}; use rustc_middle::bug; use rustc_middle::middle::dependency_format::Linkage; use rustc_middle::middle::exported_symbols; @@ -891,9 +891,15 @@ impl<'a> Linker for MsvcLinker<'a> { } fn link_staticlib_by_name(&mut self, name: &str, verbatim: bool, whole_archive: bool) { - let prefix = if whole_archive { "/WHOLEARCHIVE:" } else { "" }; - let suffix = if verbatim { "" } else { ".lib" }; - self.link_arg(format!("{prefix}{name}{suffix}")); + // On MSVC-like targets rustc supports static libraries using alternative naming + // scheme (`libfoo.a`) unsupported by linker, search for such libraries manually. + if let Some(path) = try_find_native_static_library(self.sess, name, verbatim) { + self.link_staticlib_by_path(&path, whole_archive); + } else { + let prefix = if whole_archive { "/WHOLEARCHIVE:" } else { "" }; + let suffix = if verbatim { "" } else { ".lib" }; + self.link_arg(format!("{prefix}{name}{suffix}")); + } } fn link_staticlib_by_path(&mut self, path: &Path, whole_archive: bool) { diff --git a/tests/run-make/native-lib-alt-naming/native.rs b/tests/run-make/native-lib-alt-naming/native.rs new file mode 100644 index 0000000000000..6c869e74cd269 --- /dev/null +++ b/tests/run-make/native-lib-alt-naming/native.rs @@ -0,0 +1,2 @@ +#[no_mangle] +pub extern "C" fn native_lib_alt_naming() {} diff --git a/tests/run-make/native-lib-alt-naming/rmake.rs b/tests/run-make/native-lib-alt-naming/rmake.rs new file mode 100644 index 0000000000000..d1ea0fc868767 --- /dev/null +++ b/tests/run-make/native-lib-alt-naming/rmake.rs @@ -0,0 +1,15 @@ +// On MSVC the alternative naming format for static libraries (`libfoo.a`) is accepted in addition +// to the default format (`foo.lib`). + +//REMOVE@ only-msvc + +use run_make_support::rustc; + +fn main() { + // Prepare the native library. + rustc().input("native.rs").crate_type("staticlib").output("libnative.a").run(); + + // Try to link to it from both a rlib and a bin. + rustc().input("rust.rs").crate_type("rlib").arg("-lstatic=native").run(); + rustc().input("rust.rs").crate_type("bin").arg("-lstatic=native").run(); +} diff --git a/tests/run-make/native-lib-alt-naming/rust.rs b/tests/run-make/native-lib-alt-naming/rust.rs new file mode 100644 index 0000000000000..da0f5d925d107 --- /dev/null +++ b/tests/run-make/native-lib-alt-naming/rust.rs @@ -0,0 +1 @@ +pub fn main() {} From ac8f1320143baff8f2471985d77270f99ac42d0b Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Tue, 27 Aug 2024 22:12:18 +0300 Subject: [PATCH 08/19] docs: Update docs for the rustc's `-L` option --- src/doc/rustc/src/command-line-arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doc/rustc/src/command-line-arguments.md b/src/doc/rustc/src/command-line-arguments.md index fa23e3e414d90..e5631ba42741a 100644 --- a/src/doc/rustc/src/command-line-arguments.md +++ b/src/doc/rustc/src/command-line-arguments.md @@ -47,7 +47,7 @@ KIND=PATH` where `KIND` may be one of: directory. - `native` — Only search for native libraries in this directory. - `framework` — Only search for macOS frameworks in this directory. -- `all` — Search for all library kinds in this directory. This is the default +- `all` — Search for all library kinds in this directory, except frameworks. This is the default if `KIND` is not specified. From ee05de8e5ea88f2664b9bb414ecc045ecae90955 Mon Sep 17 00:00:00 2001 From: Ben Kimock Date: Mon, 26 Aug 2024 22:44:26 -0400 Subject: [PATCH 09/19] Re-enable android tests/benches in alloc --- library/alloc/benches/lib.rs | 3 --- library/alloc/tests/string.rs | 3 --- library/alloc/tests/vec.rs | 3 --- library/alloc/tests/vec_deque.rs | 3 --- 4 files changed, 12 deletions(-) diff --git a/library/alloc/benches/lib.rs b/library/alloc/benches/lib.rs index 0561f49c967e5..ae9608ec7bd5c 100644 --- a/library/alloc/benches/lib.rs +++ b/library/alloc/benches/lib.rs @@ -1,6 +1,3 @@ -// Disabling on android for the time being -// See https://github.com/rust-lang/rust/issues/73535#event-3477699747 -#![cfg(not(target_os = "android"))] // Disabling in Miri as these would take too long. #![cfg(not(miri))] #![feature(btree_extract_if)] diff --git a/library/alloc/tests/string.rs b/library/alloc/tests/string.rs index c5bc4185a3670..dc03c4860e84b 100644 --- a/library/alloc/tests/string.rs +++ b/library/alloc/tests/string.rs @@ -723,7 +723,6 @@ fn test_reserve_exact() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_with_capacity() { let string = String::try_with_capacity(1000).unwrap(); assert_eq!(0, string.len()); @@ -734,7 +733,6 @@ fn test_try_with_capacity() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve() { // These are the interesting cases: // * exactly isize::MAX should never trigger a CapacityOverflow (can be OOM) @@ -803,7 +801,6 @@ fn test_try_reserve() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve_exact() { // This is exactly the same as test_try_reserve with the method changed. // See that test for comments. diff --git a/library/alloc/tests/vec.rs b/library/alloc/tests/vec.rs index fd2ddbf59e42d..3722fb06a6a8a 100644 --- a/library/alloc/tests/vec.rs +++ b/library/alloc/tests/vec.rs @@ -1695,7 +1695,6 @@ fn test_reserve_exact() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_with_capacity() { let mut vec: Vec = Vec::try_with_capacity(5).unwrap(); assert_eq!(0, vec.len()); @@ -1707,7 +1706,6 @@ fn test_try_with_capacity() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve() { // These are the interesting cases: // * exactly isize::MAX should never trigger a CapacityOverflow (can be OOM) @@ -1803,7 +1801,6 @@ fn test_try_reserve() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve_exact() { // This is exactly the same as test_try_reserve with the method changed. // See that test for comments. diff --git a/library/alloc/tests/vec_deque.rs b/library/alloc/tests/vec_deque.rs index db972122fef2a..f32ba8d5aa461 100644 --- a/library/alloc/tests/vec_deque.rs +++ b/library/alloc/tests/vec_deque.rs @@ -1185,7 +1185,6 @@ fn test_reserve_exact_2() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_with_capacity() { let vec: VecDeque = VecDeque::try_with_capacity(5).unwrap(); assert_eq!(0, vec.len()); @@ -1196,7 +1195,6 @@ fn test_try_with_capacity() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve() { // These are the interesting cases: // * exactly isize::MAX should never trigger a CapacityOverflow (can be OOM) @@ -1292,7 +1290,6 @@ fn test_try_reserve() { #[test] #[cfg_attr(miri, ignore)] // Miri does not support signalling OOM -#[cfg_attr(target_os = "android", ignore)] // Android used in CI has a broken dlmalloc fn test_try_reserve_exact() { // This is exactly the same as test_try_reserve with the method changed. // See that test for comments. From 83de14c4ffeaa58874b0bbdb2da1ce3b32798b90 Mon Sep 17 00:00:00 2001 From: Ben Kimock Date: Mon, 26 Aug 2024 23:00:21 -0400 Subject: [PATCH 10/19] Enable some ilog2 tests as well --- library/core/tests/num/int_log.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/library/core/tests/num/int_log.rs b/library/core/tests/num/int_log.rs index 2320a7acc35ac..60902752dab64 100644 --- a/library/core/tests/num/int_log.rs +++ b/library/core/tests/num/int_log.rs @@ -1,7 +1,4 @@ -//! This tests the `Integer::{ilog,log2,log10}` methods. These tests are in a -//! separate file because there's both a large number of them, and not all tests -//! can be run on Android. This is because in Android `ilog2` uses an imprecise -//! approximation:https://github.com/rust-lang/rust/blob/4825e12fc9c79954aa0fe18f5521efa6c19c7539/src/libstd/sys/unix/android.rs#L27-L53 +//! Tests for the `Integer::{ilog,log2,log10}` methods. #[test] fn checked_ilog() { @@ -48,6 +45,10 @@ fn checked_ilog2() { assert_eq!(0i8.checked_ilog2(), None); assert_eq!(0i16.checked_ilog2(), None); + assert_eq!(8192u16.checked_ilog2(), Some((8192f32).log2() as u32)); + assert_eq!(32768u16.checked_ilog2(), Some((32768f32).log2() as u32)); + assert_eq!(8192i16.checked_ilog2(), Some((8192f32).log2() as u32)); + for i in 1..=u8::MAX { assert_eq!(i.checked_ilog2(), Some((i as f32).log2() as u32), "checking {i}"); } @@ -77,15 +78,6 @@ fn checked_ilog2() { } } -// Validate cases that fail on Android's imprecise float ilog2 implementation. -#[test] -#[cfg(not(target_os = "android"))] -fn checked_ilog2_not_android() { - assert_eq!(8192u16.checked_ilog2(), Some((8192f32).log2() as u32)); - assert_eq!(32768u16.checked_ilog2(), Some((32768f32).log2() as u32)); - assert_eq!(8192i16.checked_ilog2(), Some((8192f32).log2() as u32)); -} - #[test] fn checked_ilog10() { assert_eq!(0u8.checked_ilog10(), None); From 1eb4cb452b62f82037e192cfb201e8795ce342ff Mon Sep 17 00:00:00 2001 From: Folyd Date: Sat, 8 Jun 2024 23:59:28 -0700 Subject: [PATCH 11/19] Separate core search logic with search ui --- src/librustdoc/html/static/js/search.js | 6807 ++++++++++++----------- src/tools/rustdoc-js/tester.js | 7 +- 2 files changed, 3426 insertions(+), 3388 deletions(-) diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index be0ec425946c2..6f575e60ed42b 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -16,6 +16,7 @@ if (!Array.prototype.toSpliced) { } (function() { +// ==================== Core search logic begin ==================== // This mapping table should match the discriminants of // `rustdoc::formats::item_type::ItemType` type in Rust. const itemTypes = [ @@ -48,35 +49,6 @@ const itemTypes = [ "generic", ]; -const longItemTypes = [ - "keyword", - "primitive type", - "module", - "extern crate", - "re-export", - "struct", - "enum", - "function", - "type alias", - "static", - "trait", - "", - "trait method", - "method", - "struct field", - "enum variant", - "macro", - "assoc type", - "constant", - "assoc const", - "union", - "foreign type", - "existential type", - "attribute macro", - "derive macro", - "trait alias", -]; - // used for special search precedence const TY_GENERIC = itemTypes.indexOf("generic"); const TY_IMPORT = itemTypes.indexOf("import"); @@ -93,44 +65,8 @@ const UNBOXING_LIMIT = 5; const REGEX_IDENT = /\p{ID_Start}\p{ID_Continue}*|_\p{ID_Continue}+/uy; const REGEX_INVALID_TYPE_FILTER = /[^a-z]/ui; -// In the search display, allows to switch between tabs. -function printTab(nb) { - let iter = 0; - let foundCurrentTab = false; - let foundCurrentResultSet = false; - onEachLazy(document.getElementById("search-tabs").childNodes, elem => { - if (nb === iter) { - addClass(elem, "selected"); - foundCurrentTab = true; - } else { - removeClass(elem, "selected"); - } - iter += 1; - }); - const isTypeSearch = (nb > 0 || iter === 1); - iter = 0; - onEachLazy(document.getElementById("results").childNodes, elem => { - if (nb === iter) { - addClass(elem, "active"); - foundCurrentResultSet = true; - } else { - removeClass(elem, "active"); - } - iter += 1; - }); - if (foundCurrentTab && foundCurrentResultSet) { - searchState.currentTab = nb; - // Corrections only kick in on type-based searches. - const correctionsElem = document.getElementsByClassName("search-corrections"); - if (isTypeSearch) { - removeClass(correctionsElem[0], "hidden"); - } else { - addClass(correctionsElem[0], "hidden"); - } - } else if (nb !== 0) { - printTab(0); - } -} +const MAX_RESULTS = 200; +const NO_TYPE_FILTER = -1; /** * The [edit distance] is a metric for measuring the difference between two strings. @@ -240,3651 +176,3756 @@ function editDistance(a, b, limit) { return editDistanceState.calculate(a, b, limit); } -function initSearch(rawSearchIndex) { - const MAX_RESULTS = 200; - const NO_TYPE_FILTER = -1; - /** - * @type {Array} - */ - let searchIndex; - /** - * @type {Map} - */ - let searchIndexDeprecated; - /** - * @type {Map} - */ - let searchIndexEmptyDesc; - /** - * @type {Uint32Array} - */ - let functionTypeFingerprint; - let currentResults; - /** - * Map from normalized type names to integers. Used to make type search - * more efficient. - * - * @type {Map} - */ - const typeNameIdMap = new Map(); - const ALIASES = new Map(); - - /** - * Special type name IDs for searching by array. - */ - const typeNameIdOfArray = buildTypeMapIndex("array"); - /** - * Special type name IDs for searching by slice. - */ - const typeNameIdOfSlice = buildTypeMapIndex("slice"); - /** - * Special type name IDs for searching by both array and slice (`[]` syntax). - */ - const typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]"); - /** - * Special type name IDs for searching by tuple. - */ - const typeNameIdOfTuple = buildTypeMapIndex("tuple"); - /** - * Special type name IDs for searching by unit. - */ - const typeNameIdOfUnit = buildTypeMapIndex("unit"); - /** - * Special type name IDs for searching by both tuple and unit (`()` syntax). - */ - const typeNameIdOfTupleOrUnit = buildTypeMapIndex("()"); - /** - * Special type name IDs for searching `fn`. - */ - const typeNameIdOfFn = buildTypeMapIndex("fn"); - /** - * Special type name IDs for searching `fnmut`. - */ - const typeNameIdOfFnMut = buildTypeMapIndex("fnmut"); - /** - * Special type name IDs for searching `fnonce`. - */ - const typeNameIdOfFnOnce = buildTypeMapIndex("fnonce"); - /** - * Special type name IDs for searching higher order functions (`->` syntax). - */ - const typeNameIdOfHof = buildTypeMapIndex("->"); - - /** - * Add an item to the type Name->ID map, or, if one already exists, use it. - * Returns the number. If name is "" or null, return null (pure generic). - * - * This is effectively string interning, so that function matching can be - * done more quickly. Two types with the same name but different item kinds - * get the same ID. - * - * @param {string} name - * @param {boolean} isAssocType - True if this is an assoc type - * - * @returns {integer} - */ - function buildTypeMapIndex(name, isAssocType) { - if (name === "" || name === null) { - return null; - } - - if (typeNameIdMap.has(name)) { - const obj = typeNameIdMap.get(name); - obj.assocOnly = isAssocType && obj.assocOnly; - return obj.id; - } else { - const id = typeNameIdMap.size; - typeNameIdMap.set(name, {id, assocOnly: isAssocType}); - return id; - } - } - - function isSpecialStartCharacter(c) { - return "<\"".indexOf(c) !== -1; - } +function isEndCharacter(c) { + return "=,>-])".indexOf(c) !== -1; +} - function isEndCharacter(c) { - return "=,>-])".indexOf(c) !== -1; - } +/** + * Returns `true` if the given `c` character is a separator. + * + * @param {string} c + * + * @return {boolean} + */ +function isSeparatorCharacter(c) { + return c === "," || c === "="; +} - function itemTypeFromName(typename) { - const index = itemTypes.findIndex(i => i === typename); - if (index < 0) { - throw ["Unknown type filter ", typename]; - } - return index; - } +/** + * Returns `true` if the current parser position is starting with "->". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ +function isReturnArrow(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "->"; +} - /** - * If we encounter a `"`, then we try to extract the string from it until we find another `"`. - * - * This function will throw an error in the following cases: - * * There is already another string element. - * * We are parsing a generic argument. - * * There is more than one element. - * * There is no closing `"`. - * - * @param {ParsedQuery} query - * @param {ParserState} parserState - * @param {boolean} isInGenerics - */ - function getStringElem(query, parserState, isInGenerics) { - if (isInGenerics) { - throw ["Unexpected ", "\"", " in generics"]; - } else if (query.literalSearch) { - throw ["Cannot have more than one literal search element"]; - } else if (parserState.totalElems - parserState.genericsElems > 0) { - throw ["Cannot use literal search when there is more than one element"]; +/** + * Increase current parser position until it doesn't find a whitespace anymore. + * + * @param {ParserState} parserState + */ +function skipWhitespace(parserState) { + while (parserState.pos < parserState.userQuery.length) { + const c = parserState.userQuery[parserState.pos]; + if (c !== " ") { + break; } parserState.pos += 1; - const start = parserState.pos; - const end = getIdentEndPosition(parserState); - if (parserState.pos >= parserState.length) { - throw ["Unclosed ", "\""]; - } else if (parserState.userQuery[end] !== "\"") { - throw ["Unexpected ", parserState.userQuery[end], " in a string element"]; - } else if (start === end) { - throw ["Cannot have empty string element"]; - } - // To skip the quote at the end. - parserState.pos += 1; - query.literalSearch = true; - } - - /** - * Returns `true` if the current parser position is starting with "::". - * - * @param {ParserState} parserState - * - * @return {boolean} - */ - function isPathStart(parserState) { - return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "::"; - } - - /** - * Returns `true` if the current parser position is starting with "->". - * - * @param {ParserState} parserState - * - * @return {boolean} - */ - function isReturnArrow(parserState) { - return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "->"; } +} - /** - * If the current parser position is at the beginning of an identifier, - * move the position to the end of it and return `true`. Otherwise, return `false`. - * - * @param {ParserState} parserState - * - * @return {boolean} - */ - function consumeIdent(parserState) { - REGEX_IDENT.lastIndex = parserState.pos; - const match = parserState.userQuery.match(REGEX_IDENT); - if (match) { - parserState.pos += match[0].length; +/** + * Returns `true` if the previous character is `lookingFor`. + * + * @param {ParserState} parserState + * @param {String} lookingFor + * + * @return {boolean} + */ +function prevIs(parserState, lookingFor) { + let pos = parserState.pos; + while (pos > 0) { + const c = parserState.userQuery[pos - 1]; + if (c === lookingFor) { return true; + } else if (c !== " ") { + break; } - return false; + pos -= 1; } + return false; +} - /** - * Returns `true` if the given `c` character is a separator. - * - * @param {string} c - * - * @return {boolean} - */ - function isSeparatorCharacter(c) { - return c === "," || c === "="; - } +/** + * Returns `true` if the last element in the `elems` argument has generics. + * + * @param {Array} elems + * @param {ParserState} parserState + * + * @return {boolean} + */ +function isLastElemGeneric(elems, parserState) { + return (elems.length > 0 && elems[elems.length - 1].generics.length > 0) || + prevIs(parserState, ">"); +} - /** - * Returns `true` if the given `c` character is a path separator. For example - * `:` in `a::b` or a whitespace in `a b`. - * - * @param {string} c - * - * @return {boolean} - */ - function isPathSeparator(c) { - return c === ":" || c === " "; +function getFilteredNextElem(query, parserState, elems, isInGenerics) { + const start = parserState.pos; + if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) { + throw ["Expected type filter before ", ":"]; } - - /** - * Returns `true` if the previous character is `lookingFor`. - * - * @param {ParserState} parserState - * @param {String} lookingFor - * - * @return {boolean} - */ - function prevIs(parserState, lookingFor) { - let pos = parserState.pos; - while (pos > 0) { - const c = parserState.userQuery[pos - 1]; - if (c === lookingFor) { - return true; - } else if (c !== " ") { - break; - } - pos -= 1; + 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 + ":", + ")", + ]; } - return false; + 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); } +} - /** - * Returns `true` if the last element in the `elems` argument has generics. - * - * @param {Array} elems - * @param {ParserState} parserState - * - * @return {boolean} - */ - function isLastElemGeneric(elems, parserState) { - return (elems.length > 0 && elems[elems.length - 1].generics.length > 0) || - prevIs(parserState, ">"); +/** + * This function parses the next query element until it finds `endChar`, + * calling `getNextElem` to collect each element. + * + * If there is no `endChar`, this function will implicitly stop at the end + * without raising an error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array} elems - This is where the new {QueryElement} will be added. + * @param {string} endChar - This function will stop when it'll encounter this + * character. + * @returns {{foundSeparator: bool}} + */ +function getItemsBefore(query, parserState, elems, endChar) { + let foundStopChar = true; + let foundSeparator = false; + + // If this is a generic, keep the outer item's type filter around. + const oldTypeFilter = parserState.typeFilter; + parserState.typeFilter = null; + const oldIsInBinding = parserState.isInBinding; + parserState.isInBinding = null; + + // ML-style Higher Order Function notation + // + // a way to search for any closure or fn pointer regardless of + // which closure trait is used + // + // Looks like this: + // + // `option, (t -> u) -> option` + // ^^^^^^ + // + // The Rust-style closure notation is implemented in getNextElem + let hofParameters = null; + + let extra = ""; + if (endChar === ">") { + extra = "<"; + } else if (endChar === "]") { + extra = "["; + } else if (endChar === ")") { + extra = "("; + } else if (endChar === "") { + extra = "->"; + } else { + extra = endChar; } - /** - * Increase current parser position until it doesn't find a whitespace anymore. - * - * @param {ParserState} parserState - */ - function skipWhitespace(parserState) { - while (parserState.pos < parserState.userQuery.length) { - const c = parserState.userQuery[parserState.pos]; - if (c !== " ") { - break; + while (parserState.pos < parserState.length) { + const c = parserState.userQuery[parserState.pos]; + if (c === endChar) { + if (parserState.isInBinding) { + throw ["Unexpected ", endChar, " after ", "="]; + } + break; + } else if (endChar !== "" && isReturnArrow(parserState)) { + // ML-style HOF notation only works when delimited in something, + // otherwise a function arrow starts the return type of the top + if (parserState.isInBinding) { + throw ["Unexpected ", "->", " after ", "="]; + } + hofParameters = [...elems]; + elems.length = 0; + parserState.pos += 2; + foundStopChar = true; + foundSeparator = false; + continue; + } else if (c === " ") { + parserState.pos += 1; + continue; + } else if (isSeparatorCharacter(c)) { + parserState.pos += 1; + foundStopChar = true; + foundSeparator = true; + continue; + } else if (c === ":" && isPathStart(parserState)) { + throw ["Unexpected ", "::", ": paths cannot start with ", "::"]; + } else if (isEndCharacter(c)) { + throw ["Unexpected ", c, " after ", extra]; + } + if (!foundStopChar) { + let extra = []; + if (isLastElemGeneric(query.elems, parserState)) { + extra = [" after ", ">"]; + } else if (prevIs(parserState, "\"")) { + throw ["Cannot have more than one element if you use quotes"]; + } + if (endChar !== "") { + throw [ + "Expected ", + ",", + ", ", + "=", + ", or ", + endChar, + ...extra, + ", found ", + c, + ]; } + throw [ + "Expected ", + ",", + " or ", + "=", + ...extra, + ", found ", + c, + ]; + } + const posBefore = parserState.pos; + getFilteredNextElem(query, parserState, elems, endChar !== ""); + if (endChar !== "" && parserState.pos >= parserState.length) { + throw ["Unclosed ", extra]; + } + // This case can be encountered if `getNextElem` encountered a "stop character" + // right from the start. For example if you have `,,` or `<>`. In this case, + // we simply move up the current position to continue the parsing. + if (posBefore === parserState.pos) { parserState.pos += 1; } + foundStopChar = false; } - - function makePrimitiveElement(name, extra) { - return Object.assign({ - name, - id: null, - fullPath: [name], - pathWithoutLast: [], - pathLast: name, - normalizedPathLast: name, - generics: [], - bindings: new Map(), - typeFilter: "primitive", - bindingName: null, - }, extra); + if (parserState.pos >= parserState.length && endChar !== "") { + throw ["Unclosed ", extra]; + } + // We are either at the end of the string or on the `endChar` character, let's move + // forward in any case. + parserState.pos += 1; + + if (hofParameters) { + // Commas in a HOF don't cause wrapping parens to become a tuple. + // If you want a one-tuple with a HOF in it, write `((a -> b),)`. + foundSeparator = false; + // HOFs can't have directly nested bindings. + if ([...elems, ...hofParameters].some(x => x.bindingName) + || parserState.isInBinding) { + throw ["Unexpected ", "=", " within ", "->"]; + } + // HOFs are represented the same way closures are. + // The arguments are wrapped in a tuple, and the output + // is a binding, even though the compiler doesn't technically + // represent fn pointers that way. + const hofElem = makePrimitiveElement("->", { + generics: hofParameters, + bindings: new Map([["output", [...elems]]]), + typeFilter: null, + }); + elems.length = 0; + elems[0] = hofElem; } - /** - * @param {ParsedQuery} query - * @param {ParserState} parserState - * @param {string} name - Name of the query element. - * @param {Array} generics - List of generics of this query element. - * - * @return {QueryElement} - The newly created `QueryElement`. - */ - function createQueryElement(query, parserState, name, generics, isInGenerics) { - const path = name.trim(); - if (path.length === 0 && generics.length === 0) { - throw ["Unexpected ", parserState.userQuery[parserState.pos]]; - } - if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { - throw ["Cannot have more than one element if you use quotes"]; + parserState.typeFilter = oldTypeFilter; + parserState.isInBinding = oldIsInBinding; + + return { foundSeparator }; +} + +/** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array} elems - This is where the new {QueryElement} will be added. + * @param {boolean} isInGenerics + */ +function getNextElem(query, parserState, elems, isInGenerics) { + const generics = []; + + skipWhitespace(parserState); + let start = parserState.pos; + let end; + if ("[(".indexOf(parserState.userQuery[parserState.pos]) !== -1) { + let endChar = ")"; + let name = "()"; + let friendlyName = "tuple"; + + if (parserState.userQuery[parserState.pos] === "[") { + endChar = "]"; + name = "[]"; + friendlyName = "slice"; } + parserState.pos += 1; + const { foundSeparator } = getItemsBefore(query, parserState, generics, endChar); const typeFilter = parserState.typeFilter; + const bindingName = parserState.isInBinding; parserState.typeFilter = null; - if (name === "!") { - if (typeFilter !== null && typeFilter !== "primitive") { + parserState.isInBinding = null; + for (const gen of generics) { + if (gen.bindingName !== null) { + throw ["Type parameter ", "=", ` cannot be within ${friendlyName} `, name]; + } + } + if (name === "()" && !foundSeparator && generics.length === 1 + && typeFilter === null) { + elems.push(generics[0]); + } else if (name === "()" && generics.length === 1 && generics[0].name === "->") { + // `primitive:(a -> b)` parser to `primitive:"->"` + // not `primitive:"()"<"->">` + generics[0].typeFilter = typeFilter; + elems.push(generics[0]); + } else { + if (typeFilter !== null && typeFilter !== "primitive") { throw [ - "Invalid search type: primitive never type ", - "!", + "Invalid search type: primitive ", + name, " and ", typeFilter, " both specified", ]; } - if (generics.length !== 0) { - throw [ - "Never type ", - "!", - " does not accept generic parameters", - ]; - } - const bindingName = parserState.isInBinding; - parserState.isInBinding = null; - return makePrimitiveElement("never", { bindingName }); - } - const quadcolon = /::\s*::/.exec(path); - if (path.startsWith("::")) { - throw ["Paths cannot start with ", "::"]; - } else if (path.endsWith("::")) { - throw ["Paths cannot end with ", "::"]; - } else if (quadcolon !== null) { - throw ["Unexpected ", quadcolon[0]]; - } - const pathSegments = path.split(/(?:::\s*)|(?:\s+(?:::\s*)?)/); - // In case we only have something like `

`, there is no name. - if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { - if (generics.length > 0 || prevIs(parserState, ">")) { - throw ["Found generics without a path"]; - } else { - throw ["Unexpected ", parserState.userQuery[parserState.pos]]; - } - } - for (const [i, pathSegment] of pathSegments.entries()) { - if (pathSegment === "!") { - if (i !== 0) { - throw ["Never type ", "!", " is not associated item"]; - } - pathSegments[i] = "never"; + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; } + elems.push(makePrimitiveElement(name, { bindingName, generics })); } - parserState.totalElems += 1; - if (isInGenerics) { - parserState.genericsElems += 1; + } else if (parserState.userQuery[parserState.pos] === "&") { + if (parserState.typeFilter !== null && parserState.typeFilter !== "primitive") { + throw [ + "Invalid search type: primitive ", + "&", + " and ", + parserState.typeFilter, + " both specified", + ]; } - const bindingName = parserState.isInBinding; - parserState.isInBinding = null; - const bindings = new Map(); - const pathLast = pathSegments[pathSegments.length - 1]; - return { - name: name.trim(), - id: null, - fullPath: pathSegments, - pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), - pathLast, - normalizedPathLast: pathLast.replace(/_/g, ""), - generics: generics.filter(gen => { - // Syntactically, bindings are parsed as generics, - // but the query engine treats them differently. - if (gen.bindingName !== null) { - if (gen.name !== null) { - gen.bindingName.generics.unshift(gen); - } - bindings.set(gen.bindingName.name, gen.bindingName.generics); - return false; - } - return true; - }), - bindings, - typeFilter, - bindingName, - }; - } - - /** - * This function goes through all characters until it reaches an invalid ident character or the - * end of the query. It returns the position of the last character of the ident. - * - * @param {ParserState} parserState - * - * @return {integer} - */ - function getIdentEndPosition(parserState) { - let afterIdent = consumeIdent(parserState); - let end = parserState.pos; - let macroExclamation = -1; - while (parserState.pos < parserState.length) { - const c = parserState.userQuery[parserState.pos]; - if (c === "!") { - if (macroExclamation !== -1) { - throw ["Cannot have more than one ", "!", " in an ident"]; - } else if (parserState.pos + 1 < parserState.length) { - const pos = parserState.pos; - parserState.pos++; - const beforeIdent = consumeIdent(parserState); - parserState.pos = pos; - if (beforeIdent) { - throw ["Unexpected ", "!", ": it can only be at the end of an ident"]; - } - } - if (afterIdent) macroExclamation = parserState.pos; - } else if (isPathSeparator(c)) { - if (c === ":") { - if (!isPathStart(parserState)) { - break; - } - // Skip current ":". - parserState.pos += 1; - } else { - while (parserState.pos + 1 < parserState.length) { - const next_c = parserState.userQuery[parserState.pos + 1]; - if (next_c !== " ") { - break; - } - parserState.pos += 1; - } - } - if (macroExclamation !== -1) { - throw ["Cannot have associated items in macros"]; - } - } else if ( - c === "[" || - c === "(" || - isEndCharacter(c) || - isSpecialStartCharacter(c) || - isSeparatorCharacter(c) - ) { - break; - } else if (parserState.pos > 0) { - throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1], - " (not a valid identifier)"]; - } else { - throw ["Unexpected ", c, " (not a valid identifier)"]; - } + parserState.typeFilter = null; + parserState.pos += 1; + let c = parserState.userQuery[parserState.pos]; + while (c === " " && parserState.pos < parserState.length) { parserState.pos += 1; - afterIdent = consumeIdent(parserState); - end = parserState.pos; + c = parserState.userQuery[parserState.pos]; } - if (macroExclamation !== -1) { - if (parserState.typeFilter === null) { - parserState.typeFilter = "macro"; - } else if (parserState.typeFilter !== "macro") { - throw [ - "Invalid search type: macro ", - "!", - " and ", - parserState.typeFilter, - " both specified", - ]; - } - end = macroExclamation; + const generics = []; + if (parserState.userQuery.slice(parserState.pos, parserState.pos + 3) === "mut") { + generics.push(makePrimitiveElement("mut", { typeFilter: "keyword" })); + parserState.pos += 3; + c = parserState.userQuery[parserState.pos]; } - return end; - } - - function getFilteredNextElem(query, parserState, elems, isInGenerics) { - const start = parserState.pos; - if (parserState.userQuery[parserState.pos] === ":" && !isPathStart(parserState)) { - throw ["Expected type filter before ", ":"]; + while (c === " " && parserState.pos < parserState.length) { + parserState.pos += 1; + c = parserState.userQuery[parserState.pos]; + } + if (!isEndCharacter(c) && parserState.pos < parserState.length) { + getFilteredNextElem(query, parserState, generics, isInGenerics); + } + elems.push(makePrimitiveElement("reference", { generics })); + } else { + const isStringElem = parserState.userQuery[start] === "\""; + // We handle the strings on their own mostly to make code easier to follow. + if (isStringElem) { + start += 1; + getStringElem(query, parserState, isInGenerics); + end = parserState.pos - 1; + } else { + end = getIdentEndPosition(parserState); } - 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"]; + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<" + ) { + if (start >= end) { + throw ["Found generics without a path"]; } - // 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 {ParserState} parserState - * @param {Array} elems - This is where the new {QueryElement} will be added. - * @param {boolean} isInGenerics - */ - function getNextElem(query, parserState, elems, isInGenerics) { - const generics = []; - - skipWhitespace(parserState); - let start = parserState.pos; - let end; - if ("[(".indexOf(parserState.userQuery[parserState.pos]) !== -1) { - let endChar = ")"; - let name = "()"; - let friendlyName = "tuple"; - - if (parserState.userQuery[parserState.pos] === "[") { - endChar = "]"; - name = "[]"; - friendlyName = "slice"; + 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 { foundSeparator } = getItemsBefore(query, parserState, generics, endChar); const typeFilter = parserState.typeFilter; - const bindingName = parserState.isInBinding; parserState.typeFilter = null; - parserState.isInBinding = null; - for (const gen of generics) { - if (gen.bindingName !== null) { - throw ["Type parameter ", "=", ` cannot be within ${friendlyName} `, name]; - } - } - if (name === "()" && !foundSeparator && generics.length === 1 && typeFilter === null) { - elems.push(generics[0]); - } else if (name === "()" && generics.length === 1 && generics[0].name === "->") { - // `primitive:(a -> b)` parser to `primitive:"->"` - // not `primitive:"()"<"->">` - generics[0].typeFilter = typeFilter; - elems.push(generics[0]); + 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 { - if (typeFilter !== null && typeFilter !== "primitive") { - throw [ - "Invalid search type: primitive ", - name, - " and ", - typeFilter, - " both specified", - ]; - } - parserState.totalElems += 1; - if (isInGenerics) { - parserState.genericsElems += 1; - } - elems.push(makePrimitiveElement(name, { bindingName, generics })); - } - } else if (parserState.userQuery[parserState.pos] === "&") { - if (parserState.typeFilter !== null && parserState.typeFilter !== "primitive") { - throw [ - "Invalid search type: primitive ", - "&", - " and ", - parserState.typeFilter, - " both specified", - ]; - } - parserState.typeFilter = null; - parserState.pos += 1; - let c = parserState.userQuery[parserState.pos]; - while (c === " " && parserState.pos < parserState.length) { - parserState.pos += 1; - c = parserState.userQuery[parserState.pos]; - } - const generics = []; - if (parserState.userQuery.slice(parserState.pos, parserState.pos + 3) === "mut") { - generics.push(makePrimitiveElement("mut", { typeFilter: "keyword"})); - parserState.pos += 3; - c = parserState.userQuery[parserState.pos]; - } - while (c === " " && parserState.pos < parserState.length) { - parserState.pos += 1; - c = parserState.userQuery[parserState.pos]; + generics.push(makePrimitiveElement(null, { + bindingName: makePrimitiveElement("output"), + typeFilter: null, + })); } - if (!isEndCharacter(c) && parserState.pos < parserState.length) { - getFilteredNextElem(query, parserState, generics, isInGenerics); + parserState.typeFilter = typeFilter; + } + if (isStringElem) { + skipWhitespace(parserState); + } + if (start >= end && generics.length === 0) { + return; + } + if (parserState.userQuery[parserState.pos] === "=") { + if (parserState.isInBinding) { + throw ["Cannot write ", "=", " twice in a binding"]; } - elems.push(makePrimitiveElement("reference", { generics })); - } else { - const isStringElem = parserState.userQuery[start] === "\""; - // We handle the strings on their own mostly to make code easier to follow. - if (isStringElem) { - start += 1; - getStringElem(query, parserState, isInGenerics); - end = parserState.pos - 1; - } else { - end = getIdentEndPosition(parserState); + if (!isInGenerics) { + throw ["Type parameter ", "=", " must be within generics list"]; } - if (parserState.pos < parserState.length && - parserState.userQuery[parserState.pos] === "<" - ) { - if (start >= end) { - throw ["Found generics without a path"]; - } - parserState.pos += 1; - 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; + const name = parserState.userQuery.slice(start, end).trim(); + if (name === "!") { + throw ["Type parameter ", "=", " key cannot be ", "!", " never type"]; } - if (isStringElem) { - skipWhitespace(parserState); + if (name.includes("!")) { + throw ["Type parameter ", "=", " key cannot be ", "!", " macro"]; } - if (start >= end && generics.length === 0) { - return; + if (name.includes("::")) { + throw ["Type parameter ", "=", " key cannot contain ", "::", " path"]; } - if (parserState.userQuery[parserState.pos] === "=") { - if (parserState.isInBinding) { - throw ["Cannot write ", "=", " twice in a binding"]; - } - if (!isInGenerics) { - throw ["Type parameter ", "=", " must be within generics list"]; - } - const name = parserState.userQuery.slice(start, end).trim(); - if (name === "!") { - throw ["Type parameter ", "=", " key cannot be ", "!", " never type"]; - } - if (name.includes("!")) { - throw ["Type parameter ", "=", " key cannot be ", "!", " macro"]; - } - if (name.includes("::")) { - throw ["Type parameter ", "=", " key cannot contain ", "::", " path"]; - } - if (name.includes(":")) { - throw ["Type parameter ", "=", " key cannot contain ", ":", " type"]; - } - parserState.isInBinding = { name, generics }; - } else { - elems.push( - createQueryElement( - query, - parserState, - parserState.userQuery.slice(start, end), - generics, - isInGenerics, - ), - ); + if (name.includes(":")) { + throw ["Type parameter ", "=", " key cannot contain ", ":", " type"]; } + parserState.isInBinding = { name, generics }; + } else { + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics, + ), + ); } } +} - /** - * This function parses the next query element until it finds `endChar`, calling `getNextElem` - * to collect each element. - * - * If there is no `endChar`, this function will implicitly stop at the end without raising an - * error. - * - * @param {ParsedQuery} query - * @param {ParserState} parserState - * @param {Array} elems - This is where the new {QueryElement} will be added. - * @param {string} endChar - This function will stop when it'll encounter this - * character. - * @returns {{foundSeparator: bool}} - */ - function getItemsBefore(query, parserState, elems, endChar) { - let foundStopChar = true; - let foundSeparator = false; +/** + * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored + * if empty). + * + * @param {ParserState} parserState + */ +function checkExtraTypeFilterCharacters(start, parserState) { + const query = parserState.userQuery.slice(start, parserState.pos).trim(); + + const match = query.match(REGEX_INVALID_TYPE_FILTER); + if (match) { + throw [ + "Unexpected ", + match[0], + " in type filter (before ", + ":", + ")", + ]; + } +} - // If this is a generic, keep the outer item's type filter around. - const oldTypeFilter = parserState.typeFilter; - parserState.typeFilter = null; - const oldIsInBinding = parserState.isInBinding; +/** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {string} name - Name of the query element. + * @param {Array} generics - List of generics of this query element. + * + * @return {QueryElement} - The newly created `QueryElement`. + */ +function createQueryElement(query, parserState, name, generics, isInGenerics) { + const path = name.trim(); + if (path.length === 0 && generics.length === 0) { + throw ["Unexpected ", parserState.userQuery[parserState.pos]]; + } + if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { + throw ["Cannot have more than one element if you use quotes"]; + } + const typeFilter = parserState.typeFilter; + parserState.typeFilter = null; + if (name === "!") { + if (typeFilter !== null && typeFilter !== "primitive") { + throw [ + "Invalid search type: primitive never type ", + "!", + " and ", + typeFilter, + " both specified", + ]; + } + if (generics.length !== 0) { + throw [ + "Never type ", + "!", + " does not accept generic parameters", + ]; + } + const bindingName = parserState.isInBinding; parserState.isInBinding = null; - - // ML-style Higher Order Function notation - // - // a way to search for any closure or fn pointer regardless of - // which closure trait is used - // - // Looks like this: - // - // `option, (t -> u) -> option` - // ^^^^^^ - // - // The Rust-style closure notation is implemented in getNextElem - let hofParameters = null; - - let extra = ""; - if (endChar === ">") { - extra = "<"; - } else if (endChar === "]") { - extra = "["; - } else if (endChar === ")") { - extra = "("; - } else if (endChar === "") { - extra = "->"; + return makePrimitiveElement("never", { bindingName }); + } + const quadcolon = /::\s*::/.exec(path); + if (path.startsWith("::")) { + throw ["Paths cannot start with ", "::"]; + } else if (path.endsWith("::")) { + throw ["Paths cannot end with ", "::"]; + } else if (quadcolon !== null) { + throw ["Unexpected ", quadcolon[0]]; + } + const pathSegments = path.split(/(?:::\s*)|(?:\s+(?:::\s*)?)/); + // In case we only have something like `

`, there is no name. + if (pathSegments.length === 0 + || (pathSegments.length === 1 && pathSegments[0] === "")) { + if (generics.length > 0 || prevIs(parserState, ">")) { + throw ["Found generics without a path"]; } else { - extra = endChar; + throw ["Unexpected ", parserState.userQuery[parserState.pos]]; + } + } + for (const [i, pathSegment] of pathSegments.entries()) { + if (pathSegment === "!") { + if (i !== 0) { + throw ["Never type ", "!", " is not associated item"]; + } + pathSegments[i] = "never"; } + } + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + const bindingName = parserState.isInBinding; + parserState.isInBinding = null; + const bindings = new Map(); + const pathLast = pathSegments[pathSegments.length - 1]; + return { + name: name.trim(), + id: null, + fullPath: pathSegments, + pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), + pathLast, + normalizedPathLast: pathLast.replace(/_/g, ""), + generics: generics.filter(gen => { + // Syntactically, bindings are parsed as generics, + // but the query engine treats them differently. + if (gen.bindingName !== null) { + if (gen.name !== null) { + gen.bindingName.generics.unshift(gen); + } + bindings.set(gen.bindingName.name, gen.bindingName.generics); + return false; + } + return true; + }), + bindings, + typeFilter, + bindingName, + }; +} - while (parserState.pos < parserState.length) { - const c = parserState.userQuery[parserState.pos]; - if (c === endChar) { - if (parserState.isInBinding) { - throw ["Unexpected ", endChar, " after ", "="]; - } - break; - } else if (endChar !== "" && isReturnArrow(parserState)) { - // ML-style HOF notation only works when delimited in something, - // otherwise a function arrow starts the return type of the top - if (parserState.isInBinding) { - throw ["Unexpected ", "->", " after ", "="]; - } - hofParameters = [...elems]; - elems.length = 0; - parserState.pos += 2; - foundStopChar = true; - foundSeparator = false; - continue; - } else if (c === " ") { - parserState.pos += 1; - continue; - } else if (isSeparatorCharacter(c)) { +function makePrimitiveElement(name, extra) { + return Object.assign({ + name, + id: null, + fullPath: [name], + pathWithoutLast: [], + pathLast: name, + normalizedPathLast: name, + generics: [], + bindings: new Map(), + typeFilter: "primitive", + bindingName: null, + }, extra); +} + +/** + * If we encounter a `"`, then we try to extract the string + * from it until we find another `"`. + * + * This function will throw an error in the following cases: + * * There is already another string element. + * * We are parsing a generic argument. + * * There is more than one element. + * * There is no closing `"`. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {boolean} isInGenerics + */ +function getStringElem(query, parserState, isInGenerics) { + if (isInGenerics) { + throw ["Unexpected ", "\"", " in generics"]; + } else if (query.literalSearch) { + throw ["Cannot have more than one literal search element"]; + } else if (parserState.totalElems - parserState.genericsElems > 0) { + throw ["Cannot use literal search when there is more than one element"]; + } + parserState.pos += 1; + const start = parserState.pos; + const end = getIdentEndPosition(parserState); + if (parserState.pos >= parserState.length) { + throw ["Unclosed ", "\""]; + } else if (parserState.userQuery[end] !== "\"") { + throw ["Unexpected ", parserState.userQuery[end], " in a string element"]; + } else if (start === end) { + throw ["Cannot have empty string element"]; + } + // To skip the quote at the end. + parserState.pos += 1; + query.literalSearch = true; +} + +/** + * This function goes through all characters until it reaches an invalid ident + * character or the end of the query. It returns the position of the last + * character of the ident. + * + * @param {ParserState} parserState + * + * @return {integer} + */ +function getIdentEndPosition(parserState) { + let afterIdent = consumeIdent(parserState); + let end = parserState.pos; + let macroExclamation = -1; + while (parserState.pos < parserState.length) { + const c = parserState.userQuery[parserState.pos]; + if (c === "!") { + if (macroExclamation !== -1) { + throw ["Cannot have more than one ", "!", " in an ident"]; + } else if (parserState.pos + 1 < parserState.length) { + const pos = parserState.pos; + parserState.pos++; + const beforeIdent = consumeIdent(parserState); + parserState.pos = pos; + if (beforeIdent) { + throw ["Unexpected ", "!", ": it can only be at the end of an ident"]; + } + } + if (afterIdent) macroExclamation = parserState.pos; + } else if (isPathSeparator(c)) { + if (c === ":") { + if (!isPathStart(parserState)) { + break; + } + // Skip current ":". parserState.pos += 1; - foundStopChar = true; - foundSeparator = true; - continue; - } else if (c === ":" && isPathStart(parserState)) { - throw ["Unexpected ", "::", ": paths cannot start with ", "::"]; - } else if (isEndCharacter(c)) { - throw ["Unexpected ", c, " after ", extra]; - } - if (!foundStopChar) { - let extra = []; - if (isLastElemGeneric(query.elems, parserState)) { - extra = [" after ", ">"]; - } else if (prevIs(parserState, "\"")) { - throw ["Cannot have more than one element if you use quotes"]; - } - if (endChar !== "") { - throw [ - "Expected ", - ",", - ", ", - "=", - ", or ", - endChar, - ...extra, - ", found ", - c, - ]; + } else { + while (parserState.pos + 1 < parserState.length) { + const next_c = parserState.userQuery[parserState.pos + 1]; + if (next_c !== " ") { + break; + } + parserState.pos += 1; } - throw [ - "Expected ", - ",", - " or ", - "=", - ...extra, - ", found ", - c, - ]; - } - const posBefore = parserState.pos; - getFilteredNextElem(query, parserState, elems, endChar !== ""); - if (endChar !== "" && parserState.pos >= parserState.length) { - throw ["Unclosed ", extra]; } - // This case can be encountered if `getNextElem` encountered a "stop character" right - // from the start. For example if you have `,,` or `<>`. In this case, we simply move up - // the current position to continue the parsing. - if (posBefore === parserState.pos) { - parserState.pos += 1; + if (macroExclamation !== -1) { + throw ["Cannot have associated items in macros"]; } - foundStopChar = false; - } - if (parserState.pos >= parserState.length && endChar !== "") { - throw ["Unclosed ", extra]; + } else if ( + c === "[" || + c === "(" || + isEndCharacter(c) || + isSpecialStartCharacter(c) || + isSeparatorCharacter(c) + ) { + break; + } else if (parserState.pos > 0) { + throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1], + " (not a valid identifier)"]; + } else { + throw ["Unexpected ", c, " (not a valid identifier)"]; } - // We are either at the end of the string or on the `endChar` character, let's move forward - // in any case. parserState.pos += 1; - - if (hofParameters) { - // Commas in a HOF don't cause wrapping parens to become a tuple. - // If you want a one-tuple with a HOF in it, write `((a -> b),)`. - foundSeparator = false; - // HOFs can't have directly nested bindings. - if ([...elems, ...hofParameters].some(x => x.bindingName) || parserState.isInBinding) { - throw ["Unexpected ", "=", " within ", "->"]; - } - // HOFs are represented the same way closures are. - // The arguments are wrapped in a tuple, and the output - // is a binding, even though the compiler doesn't technically - // represent fn pointers that way. - const hofElem = makePrimitiveElement("->", { - generics: hofParameters, - bindings: new Map([["output", [...elems]]]), - typeFilter: null, - }); - elems.length = 0; - elems[0] = hofElem; + afterIdent = consumeIdent(parserState); + end = parserState.pos; + } + if (macroExclamation !== -1) { + if (parserState.typeFilter === null) { + parserState.typeFilter = "macro"; + } else if (parserState.typeFilter !== "macro") { + throw [ + "Invalid search type: macro ", + "!", + " and ", + parserState.typeFilter, + " both specified", + ]; } + end = macroExclamation; + } + return end; +} - parserState.typeFilter = oldTypeFilter; - parserState.isInBinding = oldIsInBinding; +function isSpecialStartCharacter(c) { + return "<\"".indexOf(c) !== -1; +} + +/** + * Returns `true` if the current parser position is starting with "::". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ +function isPathStart(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "::"; +} - return { foundSeparator }; +/** + * If the current parser position is at the beginning of an identifier, + * move the position to the end of it and return `true`. Otherwise, return `false`. + * + * @param {ParserState} parserState + * + * @return {boolean} + */ +function consumeIdent(parserState) { + REGEX_IDENT.lastIndex = parserState.pos; + const match = parserState.userQuery.match(REGEX_IDENT); + if (match) { + parserState.pos += match[0].length; + return true; } + return false; +} - /** - * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored - * if empty). - * - * @param {ParserState} parserState - */ - function checkExtraTypeFilterCharacters(start, parserState) { - const query = parserState.userQuery.slice(start, parserState.pos).trim(); +/** + * Returns `true` if the given `c` character is a path separator. For example + * `:` in `a::b` or a whitespace in `a b`. + * + * @param {string} c + * + * @return {boolean} + */ +function isPathSeparator(c) { + return c === ":" || c === " "; +} - const match = query.match(REGEX_INVALID_TYPE_FILTER); - if (match) { - throw [ - "Unexpected ", - match[0], - " in type filter (before ", - ":", - ")", - ]; - } +class VlqHexDecoder { + constructor(string, cons) { + this.string = string; + this.cons = cons; + this.offset = 0; + this.backrefQueue = []; } - - /** - * Parses the provided `query` input to fill `parserState`. If it encounters an error while - * parsing `query`, it'll throw an error. - * - * @param {ParsedQuery} query - * @param {ParserState} parserState - */ - function parseInput(query, parserState) { - let foundStopChar = true; - - while (parserState.pos < parserState.length) { - const c = parserState.userQuery[parserState.pos]; - if (isEndCharacter(c)) { - foundStopChar = true; - if (isSeparatorCharacter(c)) { - parserState.pos += 1; - continue; - } else if (c === "-" || c === ">") { - if (isReturnArrow(parserState)) { - break; - } - throw ["Unexpected ", c, " (did you mean ", "->", "?)"]; - } else if (parserState.pos > 0) { - throw ["Unexpected ", c, " after ", parserState.userQuery[parserState.pos - 1]]; - } - throw ["Unexpected ", c]; - } else if (c === " ") { - skipWhitespace(parserState); - continue; + // call after consuming `{` + decodeList() { + let c = this.string.charCodeAt(this.offset); + const ret = []; + while (c !== 125) { // 125 = "}" + ret.push(this.decode()); + c = this.string.charCodeAt(this.offset); + } + this.offset += 1; // eat cb + return ret; + } + // consumes and returns a list or integer + decode() { + let n = 0; + let c = this.string.charCodeAt(this.offset); + if (c === 123) { // 123 = "{" + this.offset += 1; + return this.decodeList(); + } + while (c < 96) { // 96 = "`" + n = (n << 4) | (c & 0xF); + this.offset += 1; + c = this.string.charCodeAt(this.offset); + } + // last character >= la + n = (n << 4) | (c & 0xF); + const [sign, value] = [n & 1, n >> 1]; + this.offset += 1; + return sign ? -value : value; + } + next() { + const c = this.string.charCodeAt(this.offset); + // sixteen characters after "0" are backref + if (c >= 48 && c < 64) { // 48 = "0", 64 = "@" + this.offset += 1; + return this.backrefQueue[c - 48]; + } + // special exception: 0 doesn't use backref encoding + // it's already one character, and it's always nullish + if (c === 96) { // 96 = "`" + this.offset += 1; + return this.cons(0); + } + const result = this.cons(this.decode()); + this.backrefQueue.unshift(result); + if (this.backrefQueue.length > 16) { + this.backrefQueue.pop(); + } + return result; + } +} +class RoaringBitmap { + constructor(str) { + const strdecoded = atob(str); + const u8array = new Uint8Array(strdecoded.length); + for (let j = 0; j < strdecoded.length; ++j) { + u8array[j] = strdecoded.charCodeAt(j); + } + const has_runs = u8array[0] === 0x3b; + const size = has_runs ? + ((u8array[2] | (u8array[3] << 8)) + 1) : + ((u8array[4] | (u8array[5] << 8) | (u8array[6] << 16) | (u8array[7] << 24))); + let i = has_runs ? 4 : 8; + let is_run; + if (has_runs) { + const is_run_len = Math.floor((size + 7) / 8); + is_run = u8array.slice(i, i + is_run_len); + i += is_run_len; + } else { + is_run = new Uint8Array(); + } + this.keys = []; + this.cardinalities = []; + for (let j = 0; j < size; ++j) { + this.keys.push(u8array[i] | (u8array[i + 1] << 8)); + i += 2; + this.cardinalities.push((u8array[i] | (u8array[i + 1] << 8)) + 1); + i += 2; + } + this.containers = []; + let offsets = null; + if (!has_runs || this.keys.length >= 4) { + offsets = []; + for (let j = 0; j < size; ++j) { + offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) | + (u8array[i + 3] << 24)); + i += 4; } - if (!foundStopChar) { - let extra = ""; - if (isLastElemGeneric(query.elems, parserState)) { - extra = [" after ", ">"]; - } else if (prevIs(parserState, "\"")) { - throw ["Cannot have more than one element if you use quotes"]; - } - if (parserState.typeFilter !== null) { - throw [ - "Expected ", - ",", - " or ", - "->", - ...extra, - ", found ", - c, - ]; - } - throw [ - "Expected ", - ",", - ", ", - ":", - " or ", - "->", - ...extra, - ", found ", - c, - ]; + } + for (let j = 0; j < size; ++j) { + if (offsets && offsets[j] !== i) { + console.log(this.containers); + throw new Error(`corrupt bitmap ${j}: ${i} / ${offsets[j]}`); } - const before = query.elems.length; - getFilteredNextElem(query, parserState, query.elems, false); - if (query.elems.length === before) { - // Nothing was added, weird... Let's increase the position to not remain stuck. - parserState.pos += 1; + if (is_run[j >> 3] & (1 << (j & 0x7))) { + const runcount = (u8array[i] | (u8array[i + 1] << 8)); + i += 2; + this.containers.push(new RoaringBitmapRun( + runcount, + u8array.slice(i, i + (runcount * 4)), + )); + i += runcount * 4; + } else if (this.cardinalities[j] >= 4096) { + this.containers.push(new RoaringBitmapBits(u8array.slice(i, i + 8192))); + i += 8192; + } else { + const end = this.cardinalities[j] * 2; + this.containers.push(new RoaringBitmapArray( + this.cardinalities[j], + u8array.slice(i, i + end), + )); + i += end; } - foundStopChar = false; - } - if (parserState.typeFilter !== null) { - throw [ - "Unexpected ", - ":", - " (expected path after type filter ", - parserState.typeFilter + ":", - ")", - ]; } - while (parserState.pos < parserState.length) { - if (isReturnArrow(parserState)) { - parserState.pos += 2; - skipWhitespace(parserState); - // Get returned elements. - getItemsBefore(query, parserState, query.returned, ""); - // Nothing can come afterward! - if (query.returned.length === 0) { - throw ["Expected at least one item after ", "->"]; - } - break; - } else { - parserState.pos += 1; + } + contains(keyvalue) { + const key = keyvalue >> 16; + const value = keyvalue & 0xFFFF; + for (let i = 0; i < this.keys.length; ++i) { + if (this.keys[i] === key) { + return this.containers[i].contains(value); } } + return false; } +} - /** - * Takes the user search input and returns an empty `ParsedQuery`. - * - * @param {string} userQuery - * - * @return {ParsedQuery} - */ - function newParsedQuery(userQuery) { - return { - original: userQuery, - userQuery: userQuery.toLowerCase(), - elems: [], - returned: [], - // Total number of "top" elements (does not include generics). - foundElems: 0, - // Total number of elements (includes generics). - totalElems: 0, - literalSearch: false, - error: null, - correction: null, - proposeCorrectionFrom: null, - proposeCorrectionTo: null, - // bloom filter build from type ids - typeFingerprint: new Uint32Array(4), - }; +class RoaringBitmapRun { + constructor(runcount, array) { + this.runcount = runcount; + this.array = array; } - - /** - * Build an URL with search parameters. - * - * @param {string} search - The current search being performed. - * @param {string|null} filterCrates - The current filtering crate (if any). - * - * @return {string} - */ - function buildUrl(search, filterCrates) { - let extra = "?search=" + encodeURIComponent(search); - - if (filterCrates !== null) { - extra += "&filter-crate=" + encodeURIComponent(filterCrates); + contains(value) { + const l = this.runcount * 4; + for (let i = 0; i < l; i += 4) { + const start = this.array[i] | (this.array[i + 1] << 8); + const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8); + if (value >= start && value <= (start + lenm1)) { + return true; + } + } + return false; + } +} +class RoaringBitmapArray { + constructor(cardinality, array) { + this.cardinality = cardinality; + this.array = array; + } + contains(value) { + const l = this.cardinality * 2; + for (let i = 0; i < l; i += 2) { + const start = this.array[i] | (this.array[i + 1] << 8); + if (value === start) { + return true; + } } - return getNakedUrl() + extra + window.location.hash; + return false; } +} +class RoaringBitmapBits { + constructor(array) { + this.array = array; + } + contains(value) { + return !!(this.array[value >> 3] & (1 << (value & 7))); + } +} - /** - * Return the filtering crate or `null` if there is none. - * - * @return {string|null} - */ - function getFilterCrates() { - const elem = document.getElementById("crate-search"); - if (elem && - elem.value !== "all crates" && - rawSearchIndex.has(elem.value) - ) { - return elem.value; - } - return null; +class DocSearch { + constructor(rawSearchIndex, rootPath, searchState) { + /** + * @type {Map} + */ + this.searchIndexDeprecated = new Map(); + /** + * @type {Map} + */ + this.searchIndexEmptyDesc = new Map(); + /** + * @type {Uint32Array} + */ + this.functionTypeFingerprint = null; + /** + * Map from normalized type names to integers. Used to make type search + * more efficient. + * + * @type {Map} + */ + this.typeNameIdMap = new Map(); + this.ALIASES = new Map(); + this.rootPath = rootPath; + this.searchState = searchState; + + /** + * Special type name IDs for searching by array. + */ + this.typeNameIdOfArray = this.buildTypeMapIndex("array"); + /** + * Special type name IDs for searching by slice. + */ + this.typeNameIdOfSlice = this.buildTypeMapIndex("slice"); + /** + * Special type name IDs for searching by both array and slice (`[]` syntax). + */ + this.typeNameIdOfArrayOrSlice = this.buildTypeMapIndex("[]"); + /** + * Special type name IDs for searching by tuple. + */ + this.typeNameIdOfTuple = this.buildTypeMapIndex("tuple"); + /** + * Special type name IDs for searching by unit. + */ + this.typeNameIdOfUnit = this.buildTypeMapIndex("unit"); + /** + * Special type name IDs for searching by both tuple and unit (`()` syntax). + */ + this.typeNameIdOfTupleOrUnit = this.buildTypeMapIndex("()"); + /** + * Special type name IDs for searching `fn`. + */ + this.typeNameIdOfFn = this.buildTypeMapIndex("fn"); + /** + * Special type name IDs for searching `fnmut`. + */ + this.typeNameIdOfFnMut = this.buildTypeMapIndex("fnmut"); + /** + * Special type name IDs for searching `fnonce`. + */ + this.typeNameIdOfFnOnce = this.buildTypeMapIndex("fnonce"); + /** + * Special type name IDs for searching higher order functions (`->` syntax). + */ + this.typeNameIdOfHof = this.buildTypeMapIndex("->"); + + /** + * Empty, immutable map used in item search types with no bindings. + * + * @type {Map>} + */ + this.EMPTY_BINDINGS_MAP = new Map(); + + /** + * Empty, immutable map used in item search types with no bindings. + * + * @type {Array} + */ + this.EMPTY_GENERICS_ARRAY = []; + + /** + * Object pool for function types with no bindings or generics. + * This is reset after loading the index. + * + * @type {Map} + */ + this.TYPES_POOL = new Map(); + + /** + * @type {Array} + */ + this.searchIndex = this.buildIndex(rawSearchIndex); } /** - * Parses the query. - * - * The supported syntax by this parser is given in the rustdoc book chapter - * /src/doc/rustdoc/src/read-documentation/search.md + * Add an item to the type Name->ID map, or, if one already exists, use it. + * Returns the number. If name is "" or null, return null (pure generic). * - * When adding new things to the parser, add them there, too! + * This is effectively string interning, so that function matching can be + * done more quickly. Two types with the same name but different item kinds + * get the same ID. * - * @param {string} val - The user query + * @param {string} name + * @param {boolean} isAssocType - True if this is an assoc type * - * @return {ParsedQuery} - The parsed query + * @returns {integer} */ - function parseQuery(userQuery) { - function convertTypeFilterOnElem(elem) { - if (elem.typeFilter !== null) { - let typeFilter = elem.typeFilter; - if (typeFilter === "const") { - typeFilter = "constant"; - } - elem.typeFilter = itemTypeFromName(typeFilter); - } else { - elem.typeFilter = NO_TYPE_FILTER; - } - for (const elem2 of elem.generics) { - convertTypeFilterOnElem(elem2); - } - for (const constraints of elem.bindings.values()) { - for (const constraint of constraints) { - convertTypeFilterOnElem(constraint); - } - } + buildTypeMapIndex(name, isAssocType) { + if (name === "" || name === null) { + return null; } - userQuery = userQuery.trim().replace(/\r|\n|\t/g, " "); - const parserState = { - length: userQuery.length, - pos: 0, - // Total number of elements (includes generics). - totalElems: 0, - genericsElems: 0, - typeFilter: null, - isInBinding: null, - userQuery: userQuery.toLowerCase(), - }; - let query = newParsedQuery(userQuery); - try { - parseInput(query, parserState); - for (const elem of query.elems) { - convertTypeFilterOnElem(elem); - } - for (const elem of query.returned) { - convertTypeFilterOnElem(elem); - } - } catch (err) { - query = newParsedQuery(userQuery); - query.error = err; - return query; - } - if (!query.literalSearch) { - // If there is more than one element in the query, we switch to literalSearch in any - // case. - query.literalSearch = parserState.totalElems > 1; + if (this.typeNameIdMap.has(name)) { + const obj = this.typeNameIdMap.get(name); + obj.assocOnly = isAssocType && obj.assocOnly; + return obj.id; + } else { + const id = this.typeNameIdMap.size; + this.typeNameIdMap.set(name, { id, assocOnly: isAssocType }); + return id; } - query.foundElems = query.elems.length + query.returned.length; - query.totalElems = parserState.totalElems; - return query; } /** - * Creates the query results. + * Convert a list of RawFunctionType / ID to object-based FunctionType. + * + * Crates often have lots of functions in them, and it's common to have a large number of + * functions that operate on a small set of data types, so the search index compresses them + * by encoding function parameter and return types as indexes into an array of names. * - * @param {Array} results_in_args - * @param {Array} results_returned - * @param {Array} results_others - * @param {ParsedQuery} parsedQuery + * Even when a general-purpose compression algorithm is used, this is still a win. + * I checked. https://github.com/rust-lang/rust/pull/98475#issue-1284395985 * - * @return {ResultsTable} + * The format for individual function types is encoded in + * librustdoc/html/render/mod.rs: impl Serialize for RenderType + * + * @param {null|Array} types + * @param {Array<{name: string, ty: number}>} lowercasePaths + * + * @return {Array} */ - function createQueryResults(results_in_args, results_returned, results_others, parsedQuery) { - return { - "in_args": results_in_args, - "returned": results_returned, - "others": results_others, - "query": parsedQuery, - }; + buildItemSearchTypeAll(types, lowercasePaths) { + return types.length > 0 ? + types.map(type => this.buildItemSearchType(type, lowercasePaths)) : + this.EMPTY_GENERICS_ARRAY; } /** - * Executes the parsed query and builds a {ResultsTable}. - * - * @param {ParsedQuery} parsedQuery - The parsed user query - * @param {Object} [filterCrates] - Crate to search in if defined - * @param {Object} [currentCrate] - Current crate, to rank results from this crate higher + * Converts a single type. * - * @return {ResultsTable} + * @param {RawFunctionType} type */ - async function execQuery(parsedQuery, filterCrates, currentCrate) { - const results_others = new Map(), results_in_args = new Map(), - results_returned = new Map(); - - /** - * Add extra data to result objects, and filter items that have been - * marked for removal. - * - * @param {[ResultObject]} results - * @returns {[ResultObject]} - */ - function transformResults(results) { - const duplicates = new Set(); - const out = []; - - for (const result of results) { - if (result.id !== -1) { - const obj = searchIndex[result.id]; - obj.dist = result.dist; - const res = buildHrefAndPath(obj); - obj.displayPath = pathSplitter(res[0]); - - // To be sure than it some items aren't considered as duplicate. - obj.fullPath = res[2] + "|" + obj.ty; - if (duplicates.has(obj.fullPath)) { - continue; - } - - // Exports are specifically not shown if the items they point at - // are already in the results. - if (obj.ty === TY_IMPORT && duplicates.has(res[2])) { - continue; - } - if (duplicates.has(res[2] + "|" + TY_IMPORT)) { - continue; + buildItemSearchType(type, lowercasePaths, isAssocType) { + const PATH_INDEX_DATA = 0; + const GENERICS_DATA = 1; + const BINDINGS_DATA = 2; + let pathIndex, generics, bindings; + if (typeof type === "number") { + pathIndex = type; + generics = this.EMPTY_GENERICS_ARRAY; + bindings = this.EMPTY_BINDINGS_MAP; + } else { + pathIndex = type[PATH_INDEX_DATA]; + generics = this.buildItemSearchTypeAll( + type[GENERICS_DATA], + lowercasePaths, + ); + if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) { + bindings = new Map(type[BINDINGS_DATA].map(binding => { + const [assocType, constraints] = binding; + // Associated type constructors are represented sloppily in rustdoc's + // type search, to make the engine simpler. + // + // MyType=Result> is equivalent to MyType>=T> + // and both are, essentially + // MyType)>, except the tuple isn't actually there. + // It's more like the value of a type binding is naturally an array, + // which rustdoc calls "constraints". + // + // As a result, the key should never have generics on it. + return [ + this.buildItemSearchType(assocType, lowercasePaths, true).id, + this.buildItemSearchTypeAll(constraints, lowercasePaths), + ]; + })); + } else { + bindings = this.EMPTY_BINDINGS_MAP; + } + } + /** + * @type {FunctionType} + */ + let result; + if (pathIndex < 0) { + // types less than 0 are generic parameters + // the actual names of generic parameters aren't stored, since they aren't API + result = { + id: pathIndex, + ty: TY_GENERIC, + path: null, + exactPath: null, + generics, + bindings, + }; + } else if (pathIndex === 0) { + // `0` is used as a sentinel because it's fewer bytes than `null` + result = { + id: null, + ty: null, + path: null, + exactPath: null, + generics, + bindings, + }; + } else { + const item = lowercasePaths[pathIndex - 1]; + result = { + id: this.buildTypeMapIndex(item.name, isAssocType), + ty: item.ty, + path: item.path, + exactPath: item.exactPath, + generics, + bindings, + }; + } + const cr = this.TYPES_POOL.get(result.id); + if (cr) { + // Shallow equality check. Since this function is used + // to construct every type object, this should be mostly + // equivalent to a deep equality check, except if there's + // a conflict, we don't keep the old one around, so it's + // not a fully precise implementation of hashcons. + if (cr.generics.length === result.generics.length && + cr.generics !== result.generics && + cr.generics.every((x, i) => result.generics[i] === x) + ) { + result.generics = cr.generics; + } + if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) { + let ok = true; + for (const [k, v] of cr.bindings.entries()) { + const v2 = result.bindings.get(v); + if (!v2) { + ok = false; + break; } - duplicates.add(obj.fullPath); - duplicates.add(res[2]); - - obj.href = res[1]; - out.push(obj); - if (out.length >= MAX_RESULTS) { + if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) { + result.bindings.set(k, v); + } else if (v !== v2) { + ok = false; break; } } + if (ok) { + result.bindings = cr.bindings; + } } - return out; + if (cr.ty === result.ty && cr.path === result.path + && cr.bindings === result.bindings && cr.generics === result.generics + && cr.ty === result.ty + ) { + return cr; + } + } + this.TYPES_POOL.set(result.id, result); + return result; + } + + /** + * Type fingerprints allow fast, approximate matching of types. + * + * This algo creates a compact representation of the type set using a Bloom filter. + * This fingerprint is used three ways: + * + * - It accelerates the matching algorithm by checking the function fingerprint against the + * query fingerprint. If any bits are set in the query but not in the function, it can't + * match. + * + * - The fourth section has the number of distinct items in the set. + * This is the distance function, used for filtering and for sorting. + * + * [^1]: Distance is the relatively naive metric of counting the number of distinct items in + * the function that are not present in the query. + * + * @param {FunctionType|QueryElement} type - a single type + * @param {Uint32Array} output - write the fingerprint to this data structure: uses 128 bits + * @param {Set} fps - Set of distinct items + */ + buildFunctionTypeFingerprint(type, output, fps) { + let input = type.id; + // All forms of `[]`/`()`/`->` get collapsed down to one thing in the bloom filter. + // Differentiating between arrays and slices, if the user asks for it, is + // still done in the matching algorithm. + if (input === this.typeNameIdOfArray || input === this.typeNameIdOfSlice) { + input = this.typeNameIdOfArrayOrSlice; + } + if (input === this.typeNameIdOfTuple || input === this.typeNameIdOfUnit) { + input = this.typeNameIdOfTupleOrUnit; + } + if (input === this.typeNameIdOfFn || input === this.typeNameIdOfFnMut || + input === this.typeNameIdOfFnOnce) { + input = this.typeNameIdOfHof; + } + // http://burtleburtle.net/bob/hash/integer.html + // ~~ is toInt32. It's used before adding, so + // the number stays in safe integer range. + const hashint1 = k => { + k = (~~k + 0x7ed55d16) + (k << 12); + k = (k ^ 0xc761c23c) ^ (k >>> 19); + k = (~~k + 0x165667b1) + (k << 5); + k = (~~k + 0xd3a2646c) ^ (k << 9); + k = (~~k + 0xfd7046c5) + (k << 3); + return (k ^ 0xb55a4f09) ^ (k >>> 16); + }; + const hashint2 = k => { + k = ~k + (k << 15); + k ^= k >>> 12; + k += k << 2; + k ^= k >>> 4; + k = Math.imul(k, 2057); + return k ^ (k >> 16); + }; + if (input !== null) { + const h0a = hashint1(input); + const h0b = hashint2(input); + // Less Hashing, Same Performance: Building a Better Bloom Filter + // doi=10.1.1.72.2442 + const h1a = ~~(h0a + Math.imul(h0b, 2)); + const h1b = ~~(h0a + Math.imul(h0b, 3)); + const h2a = ~~(h0a + Math.imul(h0b, 4)); + const h2b = ~~(h0a + Math.imul(h0b, 5)); + output[0] |= (1 << (h0a % 32)) | (1 << (h1b % 32)); + output[1] |= (1 << (h1a % 32)) | (1 << (h2b % 32)); + output[2] |= (1 << (h2a % 32)) | (1 << (h0b % 32)); + fps.add(input); + } + for (const g of type.generics) { + this.buildFunctionTypeFingerprint(g, output, fps); + } + const fb = { + id: null, + ty: 0, + generics: this.EMPTY_GENERICS_ARRAY, + bindings: this.EMPTY_BINDINGS_MAP, + }; + for (const [k, v] of type.bindings.entries()) { + fb.id = k; + fb.generics = v; + this.buildFunctionTypeFingerprint(fb, output, fps); } + output[3] = fps.size; + } + /** + * Convert raw search index into in-memory search index. + * + * @param {[string, RawSearchIndexCrate][]} rawSearchIndex + */ + buildIndex(rawSearchIndex) { /** - * This function takes a result map, and sorts it by various criteria, including edit - * distance, substring match, and the crate it comes from. + * Convert from RawFunctionSearchType to FunctionSearchType. * - * @param {Results} results - * @param {boolean} isType - * @param {string} preferredCrate - * @returns {Promise<[ResultObject]>} + * Crates often have lots of functions in them, and function signatures are sometimes + * complex, so rustdoc uses a pretty tight encoding for them. This function converts it + * to a simpler, object-based encoding so that the actual search code is more readable + * and easier to debug. + * + * The raw function search type format is generated using serde in + * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string + * + * @param {Array<{name: string, ty: number}>} lowercasePaths + * + * @return {null|FunctionSearchType} */ - async function sortResults(results, isType, preferredCrate) { - const userQuery = parsedQuery.userQuery; - const casedUserQuery = parsedQuery.original; - const result_list = []; - for (const result of results.values()) { - result.item = searchIndex[result.id]; - result.word = searchIndex[result.id].word; - result_list.push(result); - } - - result_list.sort((aaa, bbb) => { - let a, b; - - // sort by exact case-sensitive match - a = (aaa.item.name !== casedUserQuery); - b = (bbb.item.name !== casedUserQuery); - if (a !== b) { - return a - b; + const buildFunctionSearchTypeCallback = lowercasePaths => { + return functionSearchType => { + if (functionSearchType === 0) { + return null; + } + const INPUTS_DATA = 0; + const OUTPUT_DATA = 1; + let inputs, output; + if (typeof functionSearchType[INPUTS_DATA] === "number") { + inputs = [ + this.buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths), + ]; + } else { + inputs = this.buildItemSearchTypeAll( + functionSearchType[INPUTS_DATA], + lowercasePaths, + ); } - - // sort by exact match with regard to the last word (mismatch goes later) - a = (aaa.word !== userQuery); - b = (bbb.word !== userQuery); - if (a !== b) { - return a - b; + if (functionSearchType.length > 1) { + if (typeof functionSearchType[OUTPUT_DATA] === "number") { + output = [ + this.buildItemSearchType( + functionSearchType[OUTPUT_DATA], + lowercasePaths, + ), + ]; + } else { + output = this.buildItemSearchTypeAll( + functionSearchType[OUTPUT_DATA], + lowercasePaths, + ); + } + } else { + output = []; } - - // sort by index of keyword in item name (no literal occurrence goes later) - a = (aaa.index < 0); - b = (bbb.index < 0); - if (a !== b) { - return a - b; + const where_clause = []; + const l = functionSearchType.length; + for (let i = 2; i < l; ++i) { + where_clause.push(typeof functionSearchType[i] === "number" + ? [this.buildItemSearchType(functionSearchType[i], lowercasePaths)] + : this.buildItemSearchTypeAll(functionSearchType[i], lowercasePaths)); } + return { + inputs, output, where_clause, + }; + }; + }; - // Sort by distance in the path part, if specified - // (less changes required to match means higher rankings) - a = aaa.path_dist; - b = bbb.path_dist; - if (a !== b) { - return a - b; - } + const searchIndex = []; + let currentIndex = 0; + let id = 0; - // (later literal occurrence, if any, goes later) - a = aaa.index; - b = bbb.index; - if (a !== b) { - return a - b; - } - - // Sort by distance in the name part, the last part of the path - // (less changes required to match means higher rankings) - a = (aaa.dist); - b = (bbb.dist); - if (a !== b) { - return a - b; - } + // Function type fingerprints are 128-bit bloom filters that are used to + // estimate the distance between function and query. + // This loop counts the number of items to allocate a fingerprint for. + for (const crate of rawSearchIndex.values()) { + // Each item gets an entry in the fingerprint array, and the crate + // does, too + id += crate.t.length + 1; + } + this.functionTypeFingerprint = new Uint32Array((id + 1) * 4); + // This loop actually generates the search item indexes, including + // normalized names, type signature objects and fingerprints, and aliases. + id = 0; - // sort deprecated items later - a = searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex); - b = searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex); - if (a !== b) { - return a - b; - } + for (const [crate, crateCorpus] of rawSearchIndex) { + // a string representing the lengths of each description shard + // a string representing the list of function types + const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => noop); + let descShard = { + crate, + shard: 0, + start: 0, + len: itemDescShardDecoder.next(), + promise: null, + resolve: null, + }; + const descShardList = [descShard]; - // sort by crate (current crate comes first) - a = (aaa.item.crate !== preferredCrate); - b = (bbb.item.crate !== preferredCrate); - if (a !== b) { - return a - b; - } + // Deprecated items and items with no description + this.searchIndexDeprecated.set(crate, new RoaringBitmap(crateCorpus.c)); + this.searchIndexEmptyDesc.set(crate, new RoaringBitmap(crateCorpus.e)); + let descIndex = 0; - // sort by item name length (longer goes later) - a = aaa.word.length; - b = bbb.word.length; - if (a !== b) { - return a - b; - } + // This object should have exactly the same set of fields as the "row" + // object defined below. Your JavaScript runtime will thank you. + // https://mathiasbynens.be/notes/shapes-ics + const crateRow = { + crate, + ty: 3, // == ExternCrate + name: crate, + path: "", + descShard, + descIndex, + exactPath: "", + desc: crateCorpus.doc, + parent: undefined, + type: null, + id, + word: crate, + normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), + bitIndex: 0, + implDisambiguator: null, + }; + id += 1; + searchIndex.push(crateRow); + currentIndex += 1; + if (!this.searchIndexEmptyDesc.get(crate).contains(0)) { + descIndex += 1; + } - // sort by item name (lexicographically larger goes later) - a = aaa.word; - b = bbb.word; - if (a !== b) { - return (a > b ? +1 : -1); - } + // a String of one character item type codes + const itemTypes = crateCorpus.t; + // an array of (String) item names + const itemNames = crateCorpus.n; + // an array of [(Number) item index, + // (String) full path] + // an item whose index is not present will fall back to the previous present path + // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present, + // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11 + const itemPaths = new Map(crateCorpus.q); + // An array of [(Number) item index, (Number) path index] + // Used to de-duplicate inlined and re-exported stuff + const itemReexports = new Map(crateCorpus.r); + // an array of (Number) the parent path index + 1 to `paths`, or 0 if none + const itemParentIdxDecoder = new VlqHexDecoder(crateCorpus.i, noop => noop); + // a map Number, string for impl disambiguators + const implDisambiguator = new Map(crateCorpus.b); + // an array of [(Number) item type, + // (String) name] + const paths = crateCorpus.p; + // an array of [(String) alias name + // [Number] index to items] + const aliases = crateCorpus.a; - // sort by description (no description goes later) - a = searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex); - b = searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex); - if (a !== b) { - return a - b; - } + // an array of [{name: String, ty: Number}] + const lowercasePaths = []; - // sort by type (later occurrence in `itemTypes` goes later) - a = aaa.item.ty; - b = bbb.item.ty; - if (a !== b) { - return a - b; - } + // a string representing the list of function types + const itemFunctionDecoder = new VlqHexDecoder( + crateCorpus.f, + buildFunctionSearchTypeCallback(lowercasePaths), + ); - // sort by path (lexicographically larger goes later) - a = aaa.item.path; - b = bbb.item.path; - if (a !== b) { - return (a > b ? +1 : -1); + // convert `rawPaths` entries into object form + // generate normalizedPaths for function search mode + let len = paths.length; + let lastPath = itemPaths.get(0); + for (let i = 0; i < len; ++i) { + const elem = paths[i]; + const ty = elem[0]; + const name = elem[1]; + let path = null; + if (elem.length > 2) { + path = itemPaths.has(elem[2]) ? itemPaths.get(elem[2]) : lastPath; + lastPath = path; } + const exactPath = elem.length > 3 ? itemPaths.get(elem[3]) : path; - // que sera, sera - return 0; - }); - - return transformResults(result_list); - } - - /** - * This function checks if a list of search query `queryElems` can all be found in the - * search index (`fnTypes`). - * - * This function returns `true` on a match, or `false` if none. If `solutionCb` is - * supplied, it will call that function with mgens, and that callback can accept or - * reject the result bu returning `true` or `false`. If the callback returns false, - * then this function will try with a different solution, or bail with false if it - * runs out of candidates. - * - * @param {Array} fnTypesIn - The objects to check. - * @param {Array} queryElems - The elements from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensIn - * - Map functions generics to query generics (never modified). - * @param {null|Map -> bool} solutionCb - Called for each `mgens` solution. - * @param {number} unboxingDepth - * - Limit checks that Ty matches Vec, - * but not Vec>>>> - * - * @return {boolean} - Returns true if a match, false otherwise. - */ - function unifyFunctionTypes( - fnTypesIn, - queryElems, - whereClause, - mgensIn, - solutionCb, - unboxingDepth, - ) { - if (unboxingDepth >= UNBOXING_LIMIT) { - return false; - } - /** - * @type Map|null - */ - const mgens = mgensIn === null ? null : new Map(mgensIn); - if (queryElems.length === 0) { - return !solutionCb || solutionCb(mgens); - } - if (!fnTypesIn || fnTypesIn.length === 0) { - return false; + lowercasePaths.push({ ty, name: name.toLowerCase(), path, exactPath }); + paths[i] = { ty, name, path, exactPath }; } - const ql = queryElems.length; - const fl = fnTypesIn.length; - // One element fast path / base case - if (ql === 1 && queryElems[0].generics.length === 0 - && queryElems[0].bindings.size === 0) { - const queryElem = queryElems[0]; - for (const fnType of fnTypesIn) { - if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) { - continue; - } - if (fnType.id < 0 && queryElem.id < 0) { - if (mgens && mgens.has(fnType.id) && - mgens.get(fnType.id) !== queryElem.id) { - continue; + // convert `item*` into an object form, and construct word indices. + // + // before any analysis is performed lets gather the search terms to + // search against apart from the rest of the data. This is a quick + // operation that is cached for the life of the page state so that + // all other search operations have access to this cached data for + // faster analysis operations + lastPath = ""; + len = itemTypes.length; + let lastName = ""; + let lastWord = ""; + for (let i = 0; i < len; ++i) { + const bitIndex = i + 1; + if (descIndex >= descShard.len && + !this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) { + descShard = { + crate, + shard: descShard.shard + 1, + start: descShard.start + descShard.len, + len: itemDescShardDecoder.next(), + promise: null, + resolve: null, + }; + descIndex = 0; + descShardList.push(descShard); + } + const name = itemNames[i] === "" ? lastName : itemNames[i]; + const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase(); + const path = itemPaths.has(i) ? itemPaths.get(i) : lastPath; + const type = itemFunctionDecoder.next(); + if (type !== null) { + if (type) { + const fp = this.functionTypeFingerprint.subarray(id * 4, (id + 1) * 4); + const fps = new Set(); + for (const t of type.inputs) { + this.buildFunctionTypeFingerprint(t, fp, fps); } - const mgensScratch = new Map(mgens); - mgensScratch.set(fnType.id, queryElem.id); - if (!solutionCb || solutionCb(mgensScratch)) { - return true; + for (const t of type.output) { + this.buildFunctionTypeFingerprint(t, fp, fps); + } + for (const w of type.where_clause) { + for (const t of w) { + this.buildFunctionTypeFingerprint(t, fp, fps); + } } - } else if (!solutionCb || solutionCb(mgens ? new Map(mgens) : null)) { - // unifyFunctionTypeIsMatchCandidate already checks that ids match - return true; } } - for (const fnType of fnTypesIn) { - if (!unifyFunctionTypeIsUnboxCandidate( - fnType, - queryElem, - whereClause, - mgens, - unboxingDepth + 1, - )) { + // This object should have exactly the same set of fields as the "crateRow" + // object defined above. + const itemParentIdx = itemParentIdxDecoder.next(); + const row = { + crate, + ty: itemTypes.charCodeAt(i) - 65, // 65 = "A" + name, + path, + descShard, + descIndex, + exactPath: itemReexports.has(i) ? + itemPaths.get(itemReexports.get(i)) : path, + parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined, + type, + id, + word, + normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), + bitIndex, + implDisambiguator: implDisambiguator.has(i) ? + implDisambiguator.get(i) : null, + }; + id += 1; + searchIndex.push(row); + lastPath = row.path; + if (!this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) { + descIndex += 1; + } + lastName = name; + lastWord = word; + } + + if (aliases) { + const currentCrateAliases = new Map(); + this.ALIASES.set(crate, currentCrateAliases); + for (const alias_name in aliases) { + if (!Object.prototype.hasOwnProperty.call(aliases, alias_name)) { continue; } - if (fnType.id < 0) { - if (mgens && mgens.has(fnType.id) && - mgens.get(fnType.id) !== 0) { - continue; - } - const mgensScratch = new Map(mgens); - mgensScratch.set(fnType.id, 0); - if (unifyFunctionTypes( - whereClause[(-fnType.id) - 1], - queryElems, - whereClause, - mgensScratch, - solutionCb, - unboxingDepth + 1, - )) { - return true; - } - } else if (unifyFunctionTypes( - [...fnType.generics, ...Array.from(fnType.bindings.values()).flat() ], - queryElems, - whereClause, - mgens ? new Map(mgens) : null, - solutionCb, - unboxingDepth + 1, - )) { - return true; + + let currentNameAliases; + if (currentCrateAliases.has(alias_name)) { + currentNameAliases = currentCrateAliases.get(alias_name); + } else { + currentNameAliases = []; + currentCrateAliases.set(alias_name, currentNameAliases); + } + for (const local_alias of aliases[alias_name]) { + currentNameAliases.push(local_alias + currentIndex); } } - return false; } + currentIndex += itemTypes.length; + this.searchState.descShards.set(crate, descShardList); + } + // Drop the (rather large) hash table used for reusing function items + this.TYPES_POOL = new Map(); + return searchIndex; + } - // Multiple element recursive case - /** - * @type Array - */ - const fnTypes = fnTypesIn.slice(); - /** - * Algorithm works by building up a solution set in the working arrays - * fnTypes gets mutated in place to make this work, while queryElems - * is left alone. - * - * It works backwards, because arrays can be cheaply truncated that way. - * - * vvvvvvv `queryElem` - * queryElems = [ unknown, unknown, good, good, good ] - * fnTypes = [ unknown, unknown, good, good, good ] - * ^^^^^^^^^^^^^^^^ loop over these elements to find candidates - * - * Everything in the current working solution is known to be a good - * match, but it might not be the match we wind up going with, because - * there might be more than one candidate match, and we need to try them all - * before giving up. So, to handle this, it backtracks on failure. - */ - const flast = fl - 1; - const qlast = ql - 1; - const queryElem = queryElems[qlast]; - let queryElemsTmp = null; - for (let i = flast; i >= 0; i -= 1) { - const fnType = fnTypes[i]; - if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) { - continue; + /** + * Parses the query. + * + * The supported syntax by this parser is given in the rustdoc book chapter + * /src/doc/rustdoc/src/read-documentation/search.md + * + * When adding new things to the parser, add them there, too! + * + * @param {string} val - The user query + * + * @return {ParsedQuery} - The parsed query + */ + static parseQuery(userQuery) { + function itemTypeFromName(typename) { + const index = itemTypes.findIndex(i => i === typename); + if (index < 0) { + throw ["Unknown type filter ", typename]; + } + return index; + } + + function convertTypeFilterOnElem(elem) { + if (elem.typeFilter !== null) { + let typeFilter = elem.typeFilter; + if (typeFilter === "const") { + typeFilter = "constant"; } - let mgensScratch; - if (fnType.id < 0) { - mgensScratch = new Map(mgens); - if (mgensScratch.has(fnType.id) - && mgensScratch.get(fnType.id) !== queryElem.id) { + elem.typeFilter = itemTypeFromName(typeFilter); + } else { + elem.typeFilter = NO_TYPE_FILTER; + } + for (const elem2 of elem.generics) { + convertTypeFilterOnElem(elem2); + } + for (const constraints of elem.bindings.values()) { + for (const constraint of constraints) { + convertTypeFilterOnElem(constraint); + } + } + } + + /** + * Takes the user search input and returns an empty `ParsedQuery`. + * + * @param {string} userQuery + * + * @return {ParsedQuery} + */ + function newParsedQuery(userQuery) { + return { + original: userQuery, + userQuery: userQuery.toLowerCase(), + elems: [], + returned: [], + // Total number of "top" elements (does not include generics). + foundElems: 0, + // Total number of elements (includes generics). + totalElems: 0, + literalSearch: false, + error: null, + correction: null, + proposeCorrectionFrom: null, + proposeCorrectionTo: null, + // bloom filter build from type ids + typeFingerprint: new Uint32Array(4), + }; + } + + /** + * Parses the provided `query` input to fill `parserState`. If it encounters an error while + * parsing `query`, it'll throw an error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + */ + function parseInput(query, parserState) { + let foundStopChar = true; + + while (parserState.pos < parserState.length) { + const c = parserState.userQuery[parserState.pos]; + if (isEndCharacter(c)) { + foundStopChar = true; + if (isSeparatorCharacter(c)) { + parserState.pos += 1; continue; + } else if (c === "-" || c === ">") { + if (isReturnArrow(parserState)) { + break; + } + throw ["Unexpected ", c, " (did you mean ", "->", "?)"]; + } else if (parserState.pos > 0) { + throw ["Unexpected ", c, " after ", + parserState.userQuery[parserState.pos - 1]]; } - mgensScratch.set(fnType.id, queryElem.id); - } else { - mgensScratch = mgens; + throw ["Unexpected ", c]; + } else if (c === " ") { + skipWhitespace(parserState); + continue; } - // fnTypes[i] is a potential match - // fnTypes[flast] is the last item in the list - // swap them, and drop the potential match from the list - // check if the remaining function types also match - fnTypes[i] = fnTypes[flast]; - fnTypes.length = flast; - if (!queryElemsTmp) { - queryElemsTmp = queryElems.slice(0, qlast); + if (!foundStopChar) { + let extra = ""; + if (isLastElemGeneric(query.elems, parserState)) { + extra = [" after ", ">"]; + } else if (prevIs(parserState, "\"")) { + throw ["Cannot have more than one element if you use quotes"]; + } + if (parserState.typeFilter !== null) { + throw [ + "Expected ", + ",", + " or ", + "->", + ...extra, + ", found ", + c, + ]; + } + throw [ + "Expected ", + ",", + ", ", + ":", + " or ", + "->", + ...extra, + ", found ", + c, + ]; } - const passesUnification = unifyFunctionTypes( - fnTypes, - queryElemsTmp, - whereClause, - mgensScratch, - mgensScratch => { - if (fnType.generics.length === 0 && queryElem.generics.length === 0 - && fnType.bindings.size === 0 && queryElem.bindings.size === 0) { - return !solutionCb || solutionCb(mgensScratch); - } - const solution = unifyFunctionTypeCheckBindings( - fnType, - queryElem, - whereClause, - mgensScratch, - unboxingDepth, - ); - if (!solution) { - return false; - } - const simplifiedGenerics = solution.simplifiedGenerics; - for (const simplifiedMgens of solution.mgens) { - const passesUnification = unifyFunctionTypes( - simplifiedGenerics, - queryElem.generics, - whereClause, - simplifiedMgens, - solutionCb, - unboxingDepth, - ); - if (passesUnification) { - return true; - } - } - return false; - }, - unboxingDepth, - ); - if (passesUnification) { - return true; + const before = query.elems.length; + getFilteredNextElem(query, parserState, query.elems, false); + if (query.elems.length === before) { + // Nothing was added, weird... Let's increase the position to not remain stuck. + parserState.pos += 1; } - // backtrack - fnTypes[flast] = fnTypes[i]; - fnTypes[i] = fnType; - fnTypes.length = fl; + foundStopChar = false; } - for (let i = flast; i >= 0; i -= 1) { - const fnType = fnTypes[i]; - if (!unifyFunctionTypeIsUnboxCandidate( - fnType, - queryElem, - whereClause, - mgens, - unboxingDepth + 1, - )) { - continue; - } - let mgensScratch; - if (fnType.id < 0) { - mgensScratch = new Map(mgens); - if (mgensScratch.has(fnType.id) && mgensScratch.get(fnType.id) !== 0) { - continue; + if (parserState.typeFilter !== null) { + throw [ + "Unexpected ", + ":", + " (expected path after type filter ", + parserState.typeFilter + ":", + ")", + ]; + } + while (parserState.pos < parserState.length) { + if (isReturnArrow(parserState)) { + parserState.pos += 2; + skipWhitespace(parserState); + // Get returned elements. + getItemsBefore(query, parserState, query.returned, ""); + // Nothing can come afterward! + if (query.returned.length === 0) { + throw ["Expected at least one item after ", "->"]; } - mgensScratch.set(fnType.id, 0); + break; } else { - mgensScratch = mgens; - } - const generics = fnType.id < 0 ? - whereClause[(-fnType.id) - 1] : - fnType.generics; - const bindings = fnType.bindings ? - Array.from(fnType.bindings.values()).flat() : - []; - const passesUnification = unifyFunctionTypes( - fnTypes.toSpliced(i, 1, ...generics, ...bindings), - queryElems, - whereClause, - mgensScratch, - solutionCb, - unboxingDepth + 1, - ); - if (passesUnification) { - return true; + parserState.pos += 1; } } - return false; } + + + userQuery = userQuery.trim().replace(/\r|\n|\t/g, " "); + const parserState = { + length: userQuery.length, + pos: 0, + // Total number of elements (includes generics). + totalElems: 0, + genericsElems: 0, + typeFilter: null, + isInBinding: null, + userQuery: userQuery.toLowerCase(), + }; + let query = newParsedQuery(userQuery); + + try { + parseInput(query, parserState); + for (const elem of query.elems) { + convertTypeFilterOnElem(elem); + } + for (const elem of query.returned) { + convertTypeFilterOnElem(elem); + } + } catch (err) { + query = newParsedQuery(userQuery); + query.error = err; + return query; + } + if (!query.literalSearch) { + // If there is more than one element in the query, we switch to literalSearch in any + // case. + query.literalSearch = parserState.totalElems > 1; + } + query.foundElems = query.elems.length + query.returned.length; + query.totalElems = parserState.totalElems; + return query; + } + + /** + * Executes the parsed query and builds a {ResultsTable}. + * + * @param {ParsedQuery} parsedQuery - The parsed user query + * @param {Object} [filterCrates] - Crate to search in if defined + * @param {Object} [currentCrate] - Current crate, to rank results from this crate higher + * + * @return {ResultsTable} + */ + async execQuery(parsedQuery, filterCrates, currentCrate) { + const results_others = new Map(), results_in_args = new Map(), + results_returned = new Map(); + /** - * Check if this function is a match candidate. + * Creates the query results. * - * This function is all the fast checks that don't require backtracking. - * It checks that two items are not named differently, and is load-bearing for that. - * It also checks that, if the query has generics, the function type must have generics - * or associated type bindings: that's not load-bearing, but it prevents unnecessary - * backtracking later. + * @param {Array} results_in_args + * @param {Array} results_returned + * @param {Array} results_others + * @param {ParsedQuery} parsedQuery * - * @param {FunctionType} fnType - * @param {QueryElement} queryElem - * @param {Map|null} mgensIn - Map functions generics to query generics. - * @returns {boolean} + * @return {ResultsTable} */ - function unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgensIn) { - // type filters look like `trait:Read` or `enum:Result` - if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { - return false; - } - // fnType.id < 0 means generic - // queryElem.id < 0 does too - // mgensIn[fnType.id] = queryElem.id - // or, if mgensIn[fnType.id] = 0, then we've matched this generic with a bare trait - // and should make that same decision everywhere it appears - if (fnType.id < 0 && queryElem.id < 0) { - if (mgensIn) { - if (mgensIn.has(fnType.id) && mgensIn.get(fnType.id) !== queryElem.id) { - return false; - } - for (const [fid, qid] of mgensIn.entries()) { - if (fnType.id !== fid && queryElem.id === qid) { - return false; - } - if (fnType.id === fid && queryElem.id !== qid) { - return false; - } - } - } - return true; - } else { - if (queryElem.id === typeNameIdOfArrayOrSlice && - (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) - ) { - // [] matches primitive:array or primitive:slice - // if it matches, then we're fine, and this is an appropriate match candidate - } else if (queryElem.id === typeNameIdOfTupleOrUnit && - (fnType.id === typeNameIdOfTuple || fnType.id === typeNameIdOfUnit) - ) { - // () matches primitive:tuple or primitive:unit - // if it matches, then we're fine, and this is an appropriate match candidate - } else if (queryElem.id === typeNameIdOfHof && - (fnType.id === typeNameIdOfFn || fnType.id === typeNameIdOfFnMut || - fnType.id === typeNameIdOfFnOnce) - ) { - // -> matches fn, fnonce, and fnmut - // if it matches, then we're fine, and this is an appropriate match candidate - } else if (fnType.id !== queryElem.id || queryElem.id === null) { - return false; - } - // If the query elem has generics, and the function doesn't, - // it can't match. - if ((fnType.generics.length + fnType.bindings.size) === 0 && - queryElem.generics.length !== 0 - ) { - return false; + function createQueryResults( + results_in_args, + results_returned, + results_others, + parsedQuery) { + return { + "in_args": results_in_args, + "returned": results_returned, + "others": results_others, + "query": parsedQuery, + }; + } + + const buildHrefAndPath = item => { + let displayPath; + let href; + const type = itemTypes[item.ty]; + const name = item.name; + let path = item.path; + let exactPath = item.exactPath; + + if (type === "mod") { + displayPath = path + "::"; + href = this.rootPath + path.replace(/::/g, "/") + "/" + + name + "/index.html"; + } else if (type === "import") { + displayPath = item.path + "::"; + href = this.rootPath + item.path.replace(/::/g, "/") + + "/index.html#reexport." + name; + } else if (type === "primitive" || type === "keyword") { + displayPath = ""; + href = this.rootPath + path.replace(/::/g, "/") + + "/" + type + "." + name + ".html"; + } else if (type === "externcrate") { + displayPath = ""; + href = this.rootPath + name + "/index.html"; + } else if (item.parent !== undefined) { + const myparent = item.parent; + let anchor = type + "." + name; + const parentType = itemTypes[myparent.ty]; + let pageType = parentType; + let pageName = myparent.name; + exactPath = `${myparent.exactPath}::${myparent.name}`; + + if (parentType === "primitive") { + displayPath = myparent.name + "::"; + } else if (type === "structfield" && parentType === "variant") { + // Structfields belonging to variants are special: the + // final path element is the enum name. + const enumNameIdx = item.path.lastIndexOf("::"); + const enumName = item.path.substr(enumNameIdx + 2); + path = item.path.substr(0, enumNameIdx); + displayPath = path + "::" + enumName + "::" + myparent.name + "::"; + anchor = "variant." + myparent.name + ".field." + name; + pageType = "enum"; + pageName = enumName; + } else { + displayPath = path + "::" + myparent.name + "::"; } - if (fnType.bindings.size < queryElem.bindings.size) { - return false; + if (item.implDisambiguator !== null) { + anchor = item.implDisambiguator + "/" + anchor; } - // If the query element is a path (it contains `::`), we need to check if this - // path is compatible with the target type. - const queryElemPathLength = queryElem.pathWithoutLast.length; - if (queryElemPathLength > 0) { - const fnTypePath = fnType.path !== undefined && fnType.path !== null ? - fnType.path.split("::") : []; - // If the path provided in the query element is longer than this type, - // no need to check it since it won't match in any case. - if (queryElemPathLength > fnTypePath.length) { - return false; + href = this.rootPath + path.replace(/::/g, "/") + + "/" + pageType + + "." + pageName + + ".html#" + anchor; + } else { + displayPath = item.path + "::"; + href = this.rootPath + item.path.replace(/::/g, "/") + + "/" + type + "." + name + ".html"; + } + return [displayPath, href, `${exactPath}::${name}`]; + }; + + function pathSplitter(path) { + const tmp = "" + path.replace(/::/g, "::"); + if (tmp.endsWith("")) { + return tmp.slice(0, tmp.length - 6); + } + return tmp; + } + + /** + * Add extra data to result objects, and filter items that have been + * marked for removal. + * + * @param {[ResultObject]} results + * @returns {[ResultObject]} + */ + const transformResults = results => { + const duplicates = new Set(); + const out = []; + + for (const result of results) { + if (result.id !== -1) { + const obj = this.searchIndex[result.id]; + obj.dist = result.dist; + const res = buildHrefAndPath(obj); + obj.displayPath = pathSplitter(res[0]); + + // To be sure than it some items aren't considered as duplicate. + obj.fullPath = res[2] + "|" + obj.ty; + if (duplicates.has(obj.fullPath)) { + continue; } - let i = 0; - for (const path of fnTypePath) { - if (path === queryElem.pathWithoutLast[i]) { - i += 1; - if (i >= queryElemPathLength) { - break; - } - } + + // Exports are specifically not shown if the items they point at + // are already in the results. + if (obj.ty === TY_IMPORT && duplicates.has(res[2])) { + continue; } - if (i < queryElemPathLength) { - // If we didn't find all parts of the path of the query element inside - // the fn type, then it's not the right one. - return false; + if (duplicates.has(res[2] + "|" + TY_IMPORT)) { + continue; + } + duplicates.add(obj.fullPath); + duplicates.add(res[2]); + + obj.href = res[1]; + out.push(obj); + if (out.length >= MAX_RESULTS) { + break; } } - return true; } - } + return out; + }; + /** - * This function checks the associated type bindings. Any that aren't matched get converted - * to generics, and this function returns an array of the function's generics with these - * simplified bindings added to them. That is, it takes a path like this: - * - * Iterator - * - * ... if queryElem itself has an `Item=` in it, then this function returns an empty array. - * But if queryElem contains no Item=, then this function returns a one-item array with the - * ID of u32 in it, and the rest of the matching engine acts as if `Iterator` were - * the type instead. + * This function takes a result map, and sorts it by various criteria, including edit + * distance, substring match, and the crate it comes from. * - * @param {FunctionType} fnType - * @param {QueryElement} queryElem - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map} mgensIn - Map functions generics to query generics. - * Never modified. - * @param {number} unboxingDepth - * @returns {false|{mgens: [Map], simplifiedGenerics: [FunctionType]}} + * @param {Results} results + * @param {boolean} isType + * @param {string} preferredCrate + * @returns {Promise<[ResultObject]>} */ - function unifyFunctionTypeCheckBindings( - fnType, - queryElem, - whereClause, - mgensIn, - unboxingDepth, - ) { - if (fnType.bindings.size < queryElem.bindings.size) { - return false; + const sortResults = async(results, isType, preferredCrate) => { + const userQuery = parsedQuery.userQuery; + const casedUserQuery = parsedQuery.original; + const result_list = []; + for (const result of results.values()) { + result.item = this.searchIndex[result.id]; + result.word = this.searchIndex[result.id].word; + result_list.push(result); } - let simplifiedGenerics = fnType.generics || []; - if (fnType.bindings.size > 0) { - let mgensSolutionSet = [mgensIn]; - for (const [name, constraints] of queryElem.bindings.entries()) { - if (mgensSolutionSet.length === 0) { - return false; - } - if (!fnType.bindings.has(name)) { - return false; - } - const fnTypeBindings = fnType.bindings.get(name); - mgensSolutionSet = mgensSolutionSet.flatMap(mgens => { - const newSolutions = []; - unifyFunctionTypes( - fnTypeBindings, - constraints, - whereClause, - mgens, - newMgens => { - newSolutions.push(newMgens); - // return `false` makes unifyFunctionTypes return the full set of - // possible solutions - return false; - }, - unboxingDepth, - ); - return newSolutions; - }); + + result_list.sort((aaa, bbb) => { + let a, b; + + // sort by exact case-sensitive match + a = (aaa.item.name !== casedUserQuery); + b = (bbb.item.name !== casedUserQuery); + if (a !== b) { + return a - b; } - if (mgensSolutionSet.length === 0) { - return false; + + // sort by exact match with regard to the last word (mismatch goes later) + a = (aaa.word !== userQuery); + b = (bbb.word !== userQuery); + if (a !== b) { + return a - b; } - const binds = Array.from(fnType.bindings.entries()).flatMap(entry => { - const [name, constraints] = entry; - if (queryElem.bindings.has(name)) { - return []; - } else { - return constraints; - } - }); - if (simplifiedGenerics.length > 0) { - simplifiedGenerics = [...simplifiedGenerics, ...binds]; - } else { - simplifiedGenerics = binds; + + // sort by index of keyword in item name (no literal occurrence goes later) + a = (aaa.index < 0); + b = (bbb.index < 0); + if (a !== b) { + return a - b; } - return { simplifiedGenerics, mgens: mgensSolutionSet }; - } - return { simplifiedGenerics, mgens: [mgensIn] }; - } + + // Sort by distance in the path part, if specified + // (less changes required to match means higher rankings) + a = aaa.path_dist; + b = bbb.path_dist; + if (a !== b) { + return a - b; + } + + // (later literal occurrence, if any, goes later) + a = aaa.index; + b = bbb.index; + if (a !== b) { + return a - b; + } + + // Sort by distance in the name part, the last part of the path + // (less changes required to match means higher rankings) + a = (aaa.dist); + b = (bbb.dist); + if (a !== b) { + return a - b; + } + + // sort deprecated items later + a = this.searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex); + b = this.searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex); + if (a !== b) { + return a - b; + } + + // sort by crate (current crate comes first) + a = (aaa.item.crate !== preferredCrate); + b = (bbb.item.crate !== preferredCrate); + if (a !== b) { + return a - b; + } + + // sort by item name length (longer goes later) + a = aaa.word.length; + b = bbb.word.length; + if (a !== b) { + return a - b; + } + + // sort by item name (lexicographically larger goes later) + a = aaa.word; + b = bbb.word; + if (a !== b) { + return (a > b ? +1 : -1); + } + + // sort by description (no description goes later) + a = this.searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex); + b = this.searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex); + if (a !== b) { + return a - b; + } + + // sort by type (later occurrence in `itemTypes` goes later) + a = aaa.item.ty; + b = bbb.item.ty; + if (a !== b) { + return a - b; + } + + // sort by path (lexicographically larger goes later) + a = aaa.item.path; + b = bbb.item.path; + if (a !== b) { + return (a > b ? +1 : -1); + } + + // que sera, sera + return 0; + }); + + return transformResults(result_list); + }; + /** - * @param {FunctionType} fnType - * @param {QueryElement} queryElem + * This function checks if a list of search query `queryElems` can all be found in the + * search index (`fnTypes`). + * + * This function returns `true` on a match, or `false` if none. If `solutionCb` is + * supplied, it will call that function with mgens, and that callback can accept or + * reject the result bu returning `true` or `false`. If the callback returns false, + * then this function will try with a different solution, or bail with false if it + * runs out of candidates. + * + * @param {Array} fnTypesIn - The objects to check. + * @param {Array} queryElems - The elements from the parsed query. * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgens - Map functions generics to query generics. + * @param {Map|null} mgensIn + * - Map functions generics to query generics (never modified). + * @param {null|Map -> bool} solutionCb - Called for each `mgens` solution. * @param {number} unboxingDepth - * @returns {boolean} + * - Limit checks that Ty matches Vec, + * but not Vec>>>> + * + * @return {boolean} - Returns true if a match, false otherwise. */ - function unifyFunctionTypeIsUnboxCandidate( - fnType, - queryElem, + function unifyFunctionTypes( + fnTypesIn, + queryElems, whereClause, - mgens, + mgensIn, + solutionCb, unboxingDepth, ) { if (unboxingDepth >= UNBOXING_LIMIT) { return false; } - if (fnType.id < 0 && queryElem.id >= 0) { - if (!whereClause) { - return false; - } - // mgens[fnType.id] === 0 indicates that we committed to unboxing this generic - // mgens[fnType.id] === null indicates that we haven't decided yet - if (mgens && mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { - return false; - } - // Where clauses can represent cyclical data. - // `null` prevents it from trying to unbox in an infinite loop - const mgensTmp = new Map(mgens); - mgensTmp.set(fnType.id, null); - // This is only a potential unbox if the search query appears in the where clause - // for example, searching `Read -> usize` should find - // `fn read_all(R) -> Result` - // generic `R` is considered "unboxed" - return checkIfInList( - whereClause[(-fnType.id) - 1], - queryElem, - whereClause, - mgensTmp, - unboxingDepth, - ); - } else if (fnType.generics.length > 0 || fnType.bindings.size > 0) { - const simplifiedGenerics = [ - ...fnType.generics, - ...Array.from(fnType.bindings.values()).flat(), - ]; - return checkIfInList( - simplifiedGenerics, - queryElem, - whereClause, - mgens, - unboxingDepth, - ); - } - return false; - } - - /** - * This function checks if the object (`row`) matches the given type (`elem`) and its - * generics (if any). - * - * @param {Array} list - * @param {QueryElement} elem - The element from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgens - Map functions generics to query generics. - * @param {number} unboxingDepth - * - * @return {boolean} - Returns true if found, false otherwise. - */ - function checkIfInList(list, elem, whereClause, mgens, unboxingDepth) { - for (const entry of list) { - if (checkType(entry, elem, whereClause, mgens, unboxingDepth)) { - return true; - } + /** + * @type Map|null + */ + const mgens = mgensIn === null ? null : new Map(mgensIn); + if (queryElems.length === 0) { + return !solutionCb || solutionCb(mgens); } - return false; - } - - /** - * This function checks if the object (`row`) matches the given type (`elem`) and its - * generics (if any). - * - * @param {Row} row - * @param {QueryElement} elem - The element from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgens - Map functions generics to query generics. - * - * @return {boolean} - Returns true if the type matches, false otherwise. - */ - function checkType(row, elem, whereClause, mgens, unboxingDepth) { - if (unboxingDepth >= UNBOXING_LIMIT) { + if (!fnTypesIn || fnTypesIn.length === 0) { return false; } - if (row.bindings.size === 0 && elem.bindings.size === 0) { - if (elem.id < 0 && mgens === null) { - return row.id < 0 || checkIfInList( - row.generics, - elem, + const ql = queryElems.length; + const fl = fnTypesIn.length; + + // One element fast path / base case + if (ql === 1 && queryElems[0].generics.length === 0 + && queryElems[0].bindings.size === 0) { + const queryElem = queryElems[0]; + for (const fnType of fnTypesIn) { + if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) { + continue; + } + if (fnType.id < 0 && queryElem.id < 0) { + if (mgens && mgens.has(fnType.id) && + mgens.get(fnType.id) !== queryElem.id) { + continue; + } + const mgensScratch = new Map(mgens); + mgensScratch.set(fnType.id, queryElem.id); + if (!solutionCb || solutionCb(mgensScratch)) { + return true; + } + } else if (!solutionCb || solutionCb(mgens ? new Map(mgens) : null)) { + // unifyFunctionTypeIsMatchCandidate already checks that ids match + return true; + } + } + for (const fnType of fnTypesIn) { + if (!unifyFunctionTypeIsUnboxCandidate( + fnType, + queryElem, whereClause, mgens, unboxingDepth + 1, - ); - } - if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 && - typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 && - // special case - elem.id !== typeNameIdOfArrayOrSlice && elem.id !== typeNameIdOfTupleOrUnit - && elem.id !== typeNameIdOfHof - ) { - return row.id === elem.id || checkIfInList( - row.generics, - elem, - whereClause, - mgens, - unboxingDepth, - ); - } - } - return unifyFunctionTypes([row], [elem], whereClause, mgens, null, unboxingDepth); - } - - /** - * Compute an "edit distance" that ignores missing path elements. - * @param {string[]} contains search query path - * @param {Row} ty indexed item - * @returns {null|number} edit distance - */ - function checkPath(contains, ty) { - if (contains.length === 0) { - return 0; - } - const maxPathEditDistance = Math.floor( - contains.reduce((acc, next) => acc + next.length, 0) / 3, - ); - let ret_dist = maxPathEditDistance + 1; - const path = ty.path.split("::"); - - if (ty.parent && ty.parent.name) { - path.push(ty.parent.name.toLowerCase()); - } - - const length = path.length; - const clength = contains.length; - pathiter: for (let i = length - clength; i >= 0; i -= 1) { - let dist_total = 0; - for (let x = 0; x < clength; ++x) { - const [p, c] = [path[i + x], contains[x]]; - if (Math.floor((p.length - c.length) / 3) <= maxPathEditDistance && - p.indexOf(c) !== -1 - ) { - // discount distance on substring match - dist_total += Math.floor((p.length - c.length) / 3); - } else { - const dist = editDistance(p, c, maxPathEditDistance); - if (dist > maxPathEditDistance) { - continue pathiter; + )) { + continue; + } + if (fnType.id < 0) { + if (mgens && mgens.has(fnType.id) && + mgens.get(fnType.id) !== 0) { + continue; } - dist_total += dist; + const mgensScratch = new Map(mgens); + mgensScratch.set(fnType.id, 0); + if (unifyFunctionTypes( + whereClause[(-fnType.id) - 1], + queryElems, + whereClause, + mgensScratch, + solutionCb, + unboxingDepth + 1, + )) { + return true; + } + } else if (unifyFunctionTypes( + [...fnType.generics, ...Array.from(fnType.bindings.values()).flat()], + queryElems, + whereClause, + mgens ? new Map(mgens) : null, + solutionCb, + unboxingDepth + 1, + )) { + return true; } } - ret_dist = Math.min(ret_dist, Math.round(dist_total / clength)); - } - return ret_dist > maxPathEditDistance ? null : ret_dist; - } - - function typePassesFilter(filter, type) { - // No filter or Exact mach - if (filter <= NO_TYPE_FILTER || filter === type) return true; - - // Match related items - const name = itemTypes[type]; - switch (itemTypes[filter]) { - case "constant": - return name === "associatedconstant"; - case "fn": - return name === "method" || name === "tymethod"; - case "type": - return name === "primitive" || name === "associatedtype"; - case "trait": - return name === "traitalias"; + return false; } - // No match - return false; - } - - function createAliasFromItem(item) { - return { - crate: item.crate, - name: item.name, - path: item.path, - descShard: item.descShard, - descIndex: item.descIndex, - exactPath: item.exactPath, - ty: item.ty, - parent: item.parent, - type: item.type, - is_alias: true, - bitIndex: item.bitIndex, - implDisambiguator: item.implDisambiguator, - }; - } - - async function handleAliases(ret, query, filterCrates, currentCrate) { - const lowerQuery = query.toLowerCase(); - // We separate aliases and crate aliases because we want to have current crate - // aliases to be before the others in the displayed results. - const aliases = []; - const crateAliases = []; - if (filterCrates !== null) { - if (ALIASES.has(filterCrates) && ALIASES.get(filterCrates).has(lowerQuery)) { - const query_aliases = ALIASES.get(filterCrates).get(lowerQuery); - for (const alias of query_aliases) { - aliases.push(createAliasFromItem(searchIndex[alias])); + // Multiple element recursive case + /** + * @type Array + */ + const fnTypes = fnTypesIn.slice(); + /** + * Algorithm works by building up a solution set in the working arrays + * fnTypes gets mutated in place to make this work, while queryElems + * is left alone. + * + * It works backwards, because arrays can be cheaply truncated that way. + * + * vvvvvvv `queryElem` + * queryElems = [ unknown, unknown, good, good, good ] + * fnTypes = [ unknown, unknown, good, good, good ] + * ^^^^^^^^^^^^^^^^ loop over these elements to find candidates + * + * Everything in the current working solution is known to be a good + * match, but it might not be the match we wind up going with, because + * there might be more than one candidate match, and we need to try them all + * before giving up. So, to handle this, it backtracks on failure. + */ + const flast = fl - 1; + const qlast = ql - 1; + const queryElem = queryElems[qlast]; + let queryElemsTmp = null; + for (let i = flast; i >= 0; i -= 1) { + const fnType = fnTypes[i]; + if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) { + continue; + } + let mgensScratch; + if (fnType.id < 0) { + mgensScratch = new Map(mgens); + if (mgensScratch.has(fnType.id) + && mgensScratch.get(fnType.id) !== queryElem.id) { + continue; } + mgensScratch.set(fnType.id, queryElem.id); + } else { + mgensScratch = mgens; } - } else { - for (const [crate, crateAliasesIndex] of ALIASES) { - if (crateAliasesIndex.has(lowerQuery)) { - const pushTo = crate === currentCrate ? crateAliases : aliases; - const query_aliases = crateAliasesIndex.get(lowerQuery); - for (const alias of query_aliases) { - pushTo.push(createAliasFromItem(searchIndex[alias])); + // fnTypes[i] is a potential match + // fnTypes[flast] is the last item in the list + // swap them, and drop the potential match from the list + // check if the remaining function types also match + fnTypes[i] = fnTypes[flast]; + fnTypes.length = flast; + if (!queryElemsTmp) { + queryElemsTmp = queryElems.slice(0, qlast); + } + const passesUnification = unifyFunctionTypes( + fnTypes, + queryElemsTmp, + whereClause, + mgensScratch, + mgensScratch => { + if (fnType.generics.length === 0 && queryElem.generics.length === 0 + && fnType.bindings.size === 0 && queryElem.bindings.size === 0) { + return !solutionCb || solutionCb(mgensScratch); } - } + const solution = unifyFunctionTypeCheckBindings( + fnType, + queryElem, + whereClause, + mgensScratch, + unboxingDepth, + ); + if (!solution) { + return false; + } + const simplifiedGenerics = solution.simplifiedGenerics; + for (const simplifiedMgens of solution.mgens) { + const passesUnification = unifyFunctionTypes( + simplifiedGenerics, + queryElem.generics, + whereClause, + simplifiedMgens, + solutionCb, + unboxingDepth, + ); + if (passesUnification) { + return true; + } + } + return false; + }, + unboxingDepth, + ); + if (passesUnification) { + return true; } + // backtrack + fnTypes[flast] = fnTypes[i]; + fnTypes[i] = fnType; + fnTypes.length = fl; } - - const sortFunc = (aaa, bbb) => { - if (aaa.path < bbb.path) { - return 1; - } else if (aaa.path === bbb.path) { - return 0; - } - return -1; - }; - crateAliases.sort(sortFunc); - aliases.sort(sortFunc); - - const fetchDesc = alias => { - return searchIndexEmptyDesc.get(alias.crate).contains(alias.bitIndex) ? - "" : searchState.loadDesc(alias); - }; - const [crateDescs, descs] = await Promise.all([ - Promise.all(crateAliases.map(fetchDesc)), - Promise.all(aliases.map(fetchDesc)), - ]); - - const pushFunc = alias => { - alias.alias = query; - const res = buildHrefAndPath(alias); - alias.displayPath = pathSplitter(res[0]); - alias.fullPath = alias.displayPath + alias.name; - alias.href = res[1]; - - ret.others.unshift(alias); - if (ret.others.length > MAX_RESULTS) { - ret.others.pop(); + for (let i = flast; i >= 0; i -= 1) { + const fnType = fnTypes[i]; + if (!unifyFunctionTypeIsUnboxCandidate( + fnType, + queryElem, + whereClause, + mgens, + unboxingDepth + 1, + )) { + continue; } - }; - - aliases.forEach((alias, i) => { - alias.desc = descs[i]; - }); - aliases.forEach(pushFunc); - crateAliases.forEach((alias, i) => { - alias.desc = crateDescs[i]; - }); - crateAliases.forEach(pushFunc); - } - - /** - * This function adds the given result into the provided `results` map if it matches the - * following condition: - * - * * If it is a "literal search" (`parsedQuery.literalSearch`), then `dist` must be 0. - * * If it is not a "literal search", `dist` must be <= `maxEditDistance`. - * - * The `results` map contains information which will be used to sort the search results: - * - * * `fullId` is a `string`` used as the key of the object we use for the `results` map. - * * `id` is the index in the `searchIndex` array for this element. - * * `index` is an `integer`` used to sort by the position of the word in the item's name. - * * `dist` is the main metric used to sort the search results. - * * `path_dist` is zero if a single-component search query is used, otherwise it's the - * distance computed for everything other than the last path component. - * - * @param {Results} results - * @param {string} fullId - * @param {integer} id - * @param {integer} index - * @param {integer} dist - * @param {integer} path_dist - */ - function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) { - if (dist <= maxEditDistance || index !== -1) { - if (results.has(fullId)) { - const result = results.get(fullId); - if (result.dontValidate || result.dist <= dist) { - return; + let mgensScratch; + if (fnType.id < 0) { + mgensScratch = new Map(mgens); + if (mgensScratch.has(fnType.id) && mgensScratch.get(fnType.id) !== 0) { + continue; } + mgensScratch.set(fnType.id, 0); + } else { + mgensScratch = mgens; + } + const generics = fnType.id < 0 ? + whereClause[(-fnType.id) - 1] : + fnType.generics; + const bindings = fnType.bindings ? + Array.from(fnType.bindings.values()).flat() : + []; + const passesUnification = unifyFunctionTypes( + fnTypes.toSpliced(i, 1, ...generics, ...bindings), + queryElems, + whereClause, + mgensScratch, + solutionCb, + unboxingDepth + 1, + ); + if (passesUnification) { + return true; } - results.set(fullId, { - id: id, - index: index, - dontValidate: parsedQuery.literalSearch, - dist: dist, - path_dist: path_dist, - }); } + return false; } - /** - * This function is called in case the query is only one element (with or without generics). - * This element will be compared to arguments' and returned values' items and also to items. + * Check if this function is a match candidate. * - * Other important thing to note: since there is only one element, we use edit - * distance for name comparisons. + * This function is all the fast checks that don't require backtracking. + * It checks that two items are not named differently, and is load-bearing for that. + * It also checks that, if the query has generics, the function type must have generics + * or associated type bindings: that's not load-bearing, but it prevents unnecessary + * backtracking later. * - * @param {Row} row - * @param {integer} pos - Position in the `searchIndex`. - * @param {QueryElement} elem - The element from the parsed query. - * @param {Results} results_others - Unqualified results (not in arguments nor in - * returned values). - * @param {Results} results_in_args - Matching arguments results. - * @param {Results} results_returned - Matching returned arguments results. + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {Map|null} mgensIn - Map functions generics to query generics. + * @returns {boolean} */ - function handleSingleArg( - row, - pos, - elem, - results_others, - results_in_args, - results_returned, - maxEditDistance, - ) { - if (!row || (filterCrates !== null && row.crate !== filterCrates)) { - return; + const unifyFunctionTypeIsMatchCandidate = (fnType, queryElem, mgensIn) => { + // type filters look like `trait:Read` or `enum:Result` + if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { + return false; } - let path_dist = 0; - const fullId = row.id; - - // fpDist is a minimum possible type distance, where "type distance" is the number of - // atoms in the function not present in the query - const tfpDist = compareTypeFingerprints( - fullId, - parsedQuery.typeFingerprint, - ); - if (tfpDist !== null) { - const in_args = row.type && row.type.inputs - && checkIfInList(row.type.inputs, elem, row.type.where_clause, null, 0); - const returned = row.type && row.type.output - && checkIfInList(row.type.output, elem, row.type.where_clause, null, 0); - if (in_args) { - results_in_args.max_dist = Math.max(results_in_args.max_dist || 0, tfpDist); - const maxDist = results_in_args.size < MAX_RESULTS ? - (tfpDist + 1) : - results_in_args.max_dist; - addIntoResults(results_in_args, fullId, pos, -1, tfpDist, 0, maxDist); + // fnType.id < 0 means generic + // queryElem.id < 0 does too + // mgensIn[fnType.id] = queryElem.id + // or, if mgensIn[fnType.id] = 0, then we've matched this generic with a bare trait + // and should make that same decision everywhere it appears + if (fnType.id < 0 && queryElem.id < 0) { + if (mgensIn) { + if (mgensIn.has(fnType.id) && mgensIn.get(fnType.id) !== queryElem.id) { + return false; + } + for (const [fid, qid] of mgensIn.entries()) { + if (fnType.id !== fid && queryElem.id === qid) { + return false; + } + if (fnType.id === fid && queryElem.id !== qid) { + return false; + } + } } - if (returned) { - results_returned.max_dist = Math.max(results_returned.max_dist || 0, tfpDist); - const maxDist = results_returned.size < MAX_RESULTS ? - (tfpDist + 1) : - results_returned.max_dist; - addIntoResults(results_returned, fullId, pos, -1, tfpDist, 0, maxDist); + return true; + } else { + if (queryElem.id === this.typeNameIdOfArrayOrSlice && + (fnType.id === this.typeNameIdOfSlice || fnType.id === this.typeNameIdOfArray) + ) { + // [] matches primitive:array or primitive:slice + // if it matches, then we're fine, and this is an appropriate match candidate + } else if (queryElem.id === this.typeNameIdOfTupleOrUnit && + (fnType.id === this.typeNameIdOfTuple || fnType.id === this.typeNameIdOfUnit) + ) { + // () matches primitive:tuple or primitive:unit + // if it matches, then we're fine, and this is an appropriate match candidate + } else if (queryElem.id === this.typeNameIdOfHof && + (fnType.id === this.typeNameIdOfFn || fnType.id === this.typeNameIdOfFnMut || + fnType.id === this.typeNameIdOfFnOnce) + ) { + // -> matches fn, fnonce, and fnmut + // if it matches, then we're fine, and this is an appropriate match candidate + } else if (fnType.id !== queryElem.id || queryElem.id === null) { + return false; } - } - - if (!typePassesFilter(elem.typeFilter, row.ty)) { - return; - } - - let index = row.word.indexOf(elem.pathLast); - const normalizedIndex = row.normalizedName.indexOf(elem.pathLast); - if (index === -1 || (index > normalizedIndex && normalizedIndex !== -1)) { - index = normalizedIndex; - } - - if (elem.fullPath.length > 1) { - path_dist = checkPath(elem.pathWithoutLast, row); - if (path_dist === null) { - return; + // If the query elem has generics, and the function doesn't, + // it can't match. + if ((fnType.generics.length + fnType.bindings.size) === 0 && + queryElem.generics.length !== 0 + ) { + return false; } - } - - if (parsedQuery.literalSearch) { - if (row.word === elem.pathLast) { - addIntoResults(results_others, fullId, pos, index, 0, path_dist); + if (fnType.bindings.size < queryElem.bindings.size) { + return false; } - return; - } - - const dist = editDistance(row.normalizedName, elem.normalizedPathLast, maxEditDistance); - - if (index === -1 && dist > maxEditDistance) { - return; + // If the query element is a path (it contains `::`), we need to check if this + // path is compatible with the target type. + const queryElemPathLength = queryElem.pathWithoutLast.length; + if (queryElemPathLength > 0) { + const fnTypePath = fnType.path !== undefined && fnType.path !== null ? + fnType.path.split("::") : []; + // If the path provided in the query element is longer than this type, + // no need to check it since it won't match in any case. + if (queryElemPathLength > fnTypePath.length) { + return false; + } + let i = 0; + for (const path of fnTypePath) { + if (path === queryElem.pathWithoutLast[i]) { + i += 1; + if (i >= queryElemPathLength) { + break; + } + } + } + if (i < queryElemPathLength) { + // If we didn't find all parts of the path of the query element inside + // the fn type, then it's not the right one. + return false; + } + } + return true; } - - addIntoResults(results_others, fullId, pos, index, dist, path_dist, maxEditDistance); - } - - /** - * This function is called in case the query has more than one element. In this case, it'll - * try to match the items which validates all the elements. For `aa -> bb` will look for - * functions which have a parameter `aa` and has `bb` in its returned values. + }; + /** + * This function checks the associated type bindings. Any that aren't matched get converted + * to generics, and this function returns an array of the function's generics with these + * simplified bindings added to them. That is, it takes a path like this: * - * @param {Row} row - * @param {integer} pos - Position in the `searchIndex`. - * @param {Object} results + * Iterator + * + * ... if queryElem itself has an `Item=` in it, then this function returns an empty array. + * But if queryElem contains no Item=, then this function returns a one-item array with the + * ID of u32 in it, and the rest of the matching engine acts as if `Iterator` were + * the type instead. + * + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map} mgensIn - Map functions generics to query generics. + * Never modified. + * @param {number} unboxingDepth + * @returns {false|{mgens: [Map], simplifiedGenerics: [FunctionType]}} */ - function handleArgs(row, pos, results) { - if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) { - return; - } - - const tfpDist = compareTypeFingerprints( - row.id, - parsedQuery.typeFingerprint, - ); - if (tfpDist === null) { - return; - } - if (results.size >= MAX_RESULTS && tfpDist > results.max_dist) { - return; - } - - // If the result is too "bad", we return false and it ends this search. - if (!unifyFunctionTypes( - row.type.inputs, - parsedQuery.elems, - row.type.where_clause, - null, - mgens => { - return unifyFunctionTypes( - row.type.output, - parsedQuery.returned, - row.type.where_clause, - mgens, - null, - 0, // unboxing depth - ); - }, - 0, // unboxing depth - )) { - return; + function unifyFunctionTypeCheckBindings( + fnType, + queryElem, + whereClause, + mgensIn, + unboxingDepth, + ) { + if (fnType.bindings.size < queryElem.bindings.size) { + return false; } - - results.max_dist = Math.max(results.max_dist || 0, tfpDist); - addIntoResults(results, row.id, pos, 0, tfpDist, 0, Number.MAX_VALUE); - } - - function innerRunQuery() { - const queryLen = - parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) + - parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0); - const maxEditDistance = Math.floor(queryLen / 3); - - /** - * @type {Map} - */ - const genericSymbols = new Map(); - - /** - * Convert names to ids in parsed query elements. - * This is not used for the "In Names" tab, but is used for the - * "In Params", "In Returns", and "In Function Signature" tabs. - * - * If there is no matching item, but a close-enough match, this - * function also that correction. - * - * See `buildTypeMapIndex` for more information. - * - * @param {QueryElement} elem - * @param {boolean} isAssocType - */ - function convertNameToId(elem, isAssocType) { - const loweredName = elem.pathLast.toLowerCase(); - if (typeNameIdMap.has(loweredName) && - (isAssocType || !typeNameIdMap.get(loweredName).assocOnly)) { - elem.id = typeNameIdMap.get(loweredName).id; - } else if (!parsedQuery.literalSearch) { - let match = null; - let matchDist = maxEditDistance + 1; - let matchName = ""; - for (const [name, {id, assocOnly}] of typeNameIdMap) { - const dist = Math.min( - editDistance(name, loweredName, maxEditDistance), - editDistance(name, elem.normalizedPathLast, maxEditDistance), - ); - if (dist <= matchDist && dist <= maxEditDistance && - (isAssocType || !assocOnly)) { - if (dist === matchDist && matchName > name) { - continue; - } - match = id; - matchDist = dist; - matchName = name; - } + let simplifiedGenerics = fnType.generics || []; + if (fnType.bindings.size > 0) { + let mgensSolutionSet = [mgensIn]; + for (const [name, constraints] of queryElem.bindings.entries()) { + if (mgensSolutionSet.length === 0) { + return false; } - if (match !== null) { - parsedQuery.correction = matchName; + if (!fnType.bindings.has(name)) { + return false; } - elem.id = match; + const fnTypeBindings = fnType.bindings.get(name); + mgensSolutionSet = mgensSolutionSet.flatMap(mgens => { + const newSolutions = []; + unifyFunctionTypes( + fnTypeBindings, + constraints, + whereClause, + mgens, + newMgens => { + newSolutions.push(newMgens); + // return `false` makes unifyFunctionTypes return the full set of + // possible solutions + return false; + }, + unboxingDepth, + ); + return newSolutions; + }); } - if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 - && elem.generics.length === 0 && elem.bindings.size === 0) - || elem.typeFilter === TY_GENERIC) { - if (genericSymbols.has(elem.name)) { - elem.id = genericSymbols.get(elem.name); + if (mgensSolutionSet.length === 0) { + return false; + } + const binds = Array.from(fnType.bindings.entries()).flatMap(entry => { + const [name, constraints] = entry; + if (queryElem.bindings.has(name)) { + return []; } else { - elem.id = -(genericSymbols.size + 1); - genericSymbols.set(elem.name, elem.id); - } - if (elem.typeFilter === -1 && elem.name.length >= 3) { - // Silly heuristic to catch if the user probably meant - // to not write a generic parameter. We don't use it, - // just bring it up. - const maxPartDistance = Math.floor(elem.name.length / 3); - let matchDist = maxPartDistance + 1; - let matchName = ""; - for (const name of typeNameIdMap.keys()) { - const dist = editDistance(name, elem.name, maxPartDistance); - if (dist <= matchDist && dist <= maxPartDistance) { - if (dist === matchDist && matchName > name) { - continue; - } - matchDist = dist; - matchName = name; - } - } - if (matchName !== "") { - parsedQuery.proposeCorrectionFrom = elem.name; - parsedQuery.proposeCorrectionTo = matchName; - } + return constraints; } - elem.typeFilter = TY_GENERIC; + }); + if (simplifiedGenerics.length > 0) { + simplifiedGenerics = [...simplifiedGenerics, ...binds]; + } else { + simplifiedGenerics = binds; } - if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) { - // Rust does not have HKT - parsedQuery.error = [ - "Generic type parameter ", - elem.name, - " does not accept generic parameters", - ]; + return { simplifiedGenerics, mgens: mgensSolutionSet }; + } + return { simplifiedGenerics, mgens: [mgensIn] }; + } + /** + * @param {FunctionType} fnType + * @param {QueryElement} queryElem + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgens - Map functions generics to query generics. + * @param {number} unboxingDepth + * @returns {boolean} + */ + function unifyFunctionTypeIsUnboxCandidate( + fnType, + queryElem, + whereClause, + mgens, + unboxingDepth, + ) { + if (unboxingDepth >= UNBOXING_LIMIT) { + return false; + } + if (fnType.id < 0 && queryElem.id >= 0) { + if (!whereClause) { + return false; } - for (const elem2 of elem.generics) { - convertNameToId(elem2); + // mgens[fnType.id] === 0 indicates that we committed to unboxing this generic + // mgens[fnType.id] === null indicates that we haven't decided yet + if (mgens && mgens.has(fnType.id) && mgens.get(fnType.id) !== 0) { + return false; } - elem.bindings = new Map(Array.from(elem.bindings.entries()) - .map(entry => { - const [name, constraints] = entry; - if (!typeNameIdMap.has(name)) { - parsedQuery.error = [ - "Type parameter ", - name, - " does not exist", - ]; - return [null, []]; - } - for (const elem2 of constraints) { - convertNameToId(elem2); - } - - return [typeNameIdMap.get(name).id, constraints]; - }), + // Where clauses can represent cyclical data. + // `null` prevents it from trying to unbox in an infinite loop + const mgensTmp = new Map(mgens); + mgensTmp.set(fnType.id, null); + // This is only a potential unbox if the search query appears in the where clause + // for example, searching `Read -> usize` should find + // `fn read_all(R) -> Result` + // generic `R` is considered "unboxed" + return checkIfInList( + whereClause[(-fnType.id) - 1], + queryElem, + whereClause, + mgensTmp, + unboxingDepth, + ); + } else if (fnType.generics.length > 0 || fnType.bindings.size > 0) { + const simplifiedGenerics = [ + ...fnType.generics, + ...Array.from(fnType.bindings.values()).flat(), + ]; + return checkIfInList( + simplifiedGenerics, + queryElem, + whereClause, + mgens, + unboxingDepth, ); } + return false; + } - const fps = new Set(); - for (const elem of parsedQuery.elems) { - convertNameToId(elem); - buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint, fps); - } - for (const elem of parsedQuery.returned) { - convertNameToId(elem); - buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint, fps); + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Array} list + * @param {QueryElement} elem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgens - Map functions generics to query generics. + * @param {number} unboxingDepth + * + * @return {boolean} - Returns true if found, false otherwise. + */ + function checkIfInList(list, elem, whereClause, mgens, unboxingDepth) { + for (const entry of list) { + if (checkType(entry, elem, whereClause, mgens, unboxingDepth)) { + return true; + } } + return false; + } - if (parsedQuery.foundElems === 1 && parsedQuery.returned.length === 0) { - if (parsedQuery.elems.length === 1) { - const elem = parsedQuery.elems[0]; - for (let i = 0, nSearchIndex = searchIndex.length; i < nSearchIndex; ++i) { - // It means we want to check for this element everywhere (in names, args and - // returned). - handleSingleArg( - searchIndex[i], - i, - elem, - results_others, - results_in_args, - results_returned, - maxEditDistance, - ); - } + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {[FunctionType]} whereClause - Trait bounds for generic items. + * @param {Map|null} mgens - Map functions generics to query generics. + * + * @return {boolean} - Returns true if the type matches, false otherwise. + */ + const checkType = (row, elem, whereClause, mgens, unboxingDepth) => { + if (unboxingDepth >= UNBOXING_LIMIT) { + return false; + } + if (row.bindings.size === 0 && elem.bindings.size === 0) { + if (elem.id < 0 && mgens === null) { + return row.id < 0 || checkIfInList( + row.generics, + elem, + whereClause, + mgens, + unboxingDepth + 1, + ); } - } else if (parsedQuery.foundElems > 0) { - // Sort input and output so that generic type variables go first and - // types with generic parameters go last. - // That's because of the way unification is structured: it eats off - // the end, and hits a fast path if the last item is a simple atom. - const sortQ = (a, b) => { - const ag = a.generics.length === 0 && a.bindings.size === 0; - const bg = b.generics.length === 0 && b.bindings.size === 0; - if (ag !== bg) { - return ag - bg; - } - const ai = a.id > 0; - const bi = b.id > 0; - return ai - bi; - }; - parsedQuery.elems.sort(sortQ); - parsedQuery.returned.sort(sortQ); - for (let i = 0, nSearchIndex = searchIndex.length; i < nSearchIndex; ++i) { - handleArgs(searchIndex[i], i, results_others); + if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 && + typePassesFilter(elem.typeFilter, row.ty) && elem.generics.length === 0 && + // special case + elem.id !== this.typeNameIdOfArrayOrSlice + && elem.id !== this.typeNameIdOfTupleOrUnit + && elem.id !== this.typeNameIdOfHof + ) { + return row.id === elem.id || checkIfInList( + row.generics, + elem, + whereClause, + mgens, + unboxingDepth, + ); } } - } - - if (parsedQuery.error === null) { - innerRunQuery(); - } + return unifyFunctionTypes([row], [elem], whereClause, mgens, null, unboxingDepth); + }; - const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([ - sortResults(results_in_args, true, currentCrate), - sortResults(results_returned, true, currentCrate), - sortResults(results_others, false, currentCrate), - ]); - const ret = createQueryResults( - sorted_in_args, - sorted_returned, - sorted_others, - parsedQuery); - await handleAliases(ret, parsedQuery.original.replace(/"/g, ""), - filterCrates, currentCrate); - await Promise.all([ret.others, ret.returned, ret.in_args].map(async list => { - const descs = await Promise.all(list.map(result => { - return searchIndexEmptyDesc.get(result.crate).contains(result.bitIndex) ? - "" : - searchState.loadDesc(result); - })); - for (const [i, result] of list.entries()) { - result.desc = descs[i]; + /** + * Compute an "edit distance" that ignores missing path elements. + * @param {string[]} contains search query path + * @param {Row} ty indexed item + * @returns {null|number} edit distance + */ + function checkPath(contains, ty) { + if (contains.length === 0) { + return 0; } - })); - if (parsedQuery.error !== null && ret.others.length !== 0) { - // It means some doc aliases were found so let's "remove" the error! - ret.query.error = null; - } - return ret; - } + const maxPathEditDistance = Math.floor( + contains.reduce((acc, next) => acc + next.length, 0) / 3, + ); + let ret_dist = maxPathEditDistance + 1; + const path = ty.path.split("::"); - function nextTab(direction) { - const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; - searchState.focusedByTab[searchState.currentTab] = document.activeElement; - printTab(next); - focusSearchResult(); - } - - // Focus the first search result on the active tab, or the result that - // was focused last time this tab was active. - function focusSearchResult() { - const target = searchState.focusedByTab[searchState.currentTab] || - document.querySelectorAll(".search-results.active a").item(0) || - document.querySelectorAll("#search-tabs button").item(searchState.currentTab); - searchState.focusedByTab[searchState.currentTab] = null; - if (target) { - target.focus(); - } - } - - function buildHrefAndPath(item) { - let displayPath; - let href; - const type = itemTypes[item.ty]; - const name = item.name; - let path = item.path; - let exactPath = item.exactPath; - - if (type === "mod") { - displayPath = path + "::"; - href = ROOT_PATH + path.replace(/::/g, "/") + "/" + - name + "/index.html"; - } else if (type === "import") { - displayPath = item.path + "::"; - href = ROOT_PATH + item.path.replace(/::/g, "/") + "/index.html#reexport." + name; - } else if (type === "primitive" || type === "keyword") { - displayPath = ""; - href = ROOT_PATH + path.replace(/::/g, "/") + - "/" + type + "." + name + ".html"; - } else if (type === "externcrate") { - displayPath = ""; - href = ROOT_PATH + name + "/index.html"; - } else if (item.parent !== undefined) { - const myparent = item.parent; - let anchor = type + "." + name; - const parentType = itemTypes[myparent.ty]; - let pageType = parentType; - let pageName = myparent.name; - exactPath = `${myparent.exactPath}::${myparent.name}`; - - if (parentType === "primitive") { - displayPath = myparent.name + "::"; - } else if (type === "structfield" && parentType === "variant") { - // Structfields belonging to variants are special: the - // final path element is the enum name. - const enumNameIdx = item.path.lastIndexOf("::"); - const enumName = item.path.substr(enumNameIdx + 2); - path = item.path.substr(0, enumNameIdx); - displayPath = path + "::" + enumName + "::" + myparent.name + "::"; - anchor = "variant." + myparent.name + ".field." + name; - pageType = "enum"; - pageName = enumName; - } else { - displayPath = path + "::" + myparent.name + "::"; - } - if (item.implDisambiguator !== null) { - anchor = item.implDisambiguator + "/" + anchor; + if (ty.parent && ty.parent.name) { + path.push(ty.parent.name.toLowerCase()); } - href = ROOT_PATH + path.replace(/::/g, "/") + - "/" + pageType + - "." + pageName + - ".html#" + anchor; - } else { - displayPath = item.path + "::"; - href = ROOT_PATH + item.path.replace(/::/g, "/") + - "/" + type + "." + name + ".html"; - } - return [displayPath, href, `${exactPath}::${name}`]; - } - - function pathSplitter(path) { - const tmp = "" + path.replace(/::/g, "::"); - if (tmp.endsWith("")) { - return tmp.slice(0, tmp.length - 6); - } - return tmp; - } - - /** - * Render a set of search results for a single tab. - * @param {Array} array - The search results for this tab - * @param {ParsedQuery} query - * @param {boolean} display - True if this is the active tab - */ - async function addTab(array, query, display) { - const extraClass = display ? " active" : ""; - - const output = document.createElement("div"); - if (array.length > 0) { - output.className = "search-results " + extraClass; - - for (const item of array) { - const name = item.name; - const type = itemTypes[item.ty]; - const longType = longItemTypes[item.ty]; - const typeName = longType.length !== 0 ? `${longType}` : "?"; - - const link = document.createElement("a"); - link.className = "result-" + type; - link.href = item.href; - - const resultName = document.createElement("div"); - resultName.className = "result-name"; - - resultName.insertAdjacentHTML( - "beforeend", - `${typeName}`); - link.appendChild(resultName); - - let alias = " "; - if (item.is_alias) { - alias = `

\ -${item.alias} - see \ -
`; - } - resultName.insertAdjacentHTML( - "beforeend", - `
${alias}\ -${item.displayPath}${name}\ -
`); - - const description = document.createElement("div"); - description.className = "desc"; - description.insertAdjacentHTML("beforeend", item.desc); - - link.appendChild(description); - output.appendChild(link); - } - } else if (query.error === null) { - output.className = "search-failed" + extraClass; - output.innerHTML = "No results :(
" + - "Try on DuckDuckGo?

" + - "Or try looking in one of these:"; - } - return [output, array.length]; - } - - function makeTabHeader(tabNb, text, nbElems) { - // https://blog.horizon-eda.org/misc/2020/02/19/ui.html - // - // CSS runs with `font-variant-numeric: tabular-nums` to ensure all - // digits are the same width. \u{2007} is a Unicode space character - // that is defined to be the same width as a digit. - const fmtNbElems = - nbElems < 10 ? `\u{2007}(${nbElems})\u{2007}\u{2007}` : - nbElems < 100 ? `\u{2007}(${nbElems})\u{2007}` : - `\u{2007}(${nbElems})`; - if (searchState.currentTab === tabNb) { - return ""; - } - return ""; - } - - /** - * @param {ResultsTable} results - * @param {boolean} go_to_first - * @param {string} filterCrates - */ - async function showResults(results, go_to_first, filterCrates) { - const search = searchState.outputElement(); - if (go_to_first || (results.others.length === 1 - && getSettingValue("go-to-only-result") === "true") - ) { - // Needed to force re-execution of JS when coming back to a page. Let's take this - // scenario as example: - // - // 1. You have the "Directly go to item in search if there is only one result" option - // enabled. - // 2. You make a search which results only one result, leading you automatically to - // this result. - // 3. You go back to previous page. - // - // Now, without the call below, the JS will not be re-executed and the previous state - // will be used, starting search again since the search input is not empty, leading you - // back to the previous page again. - window.onunload = () => {}; - searchState.removeQueryParameters(); - const elem = document.createElement("a"); - elem.href = results.others[0].href; - removeClass(elem, "active"); - // For firefox, we need the element to be in the DOM so it can be clicked. - document.body.appendChild(elem); - elem.click(); - return; - } - if (results.query === undefined) { - results.query = parseQuery(searchState.input.value); - } - - currentResults = results.query.userQuery; - - const [ret_others, ret_in_args, ret_returned] = await Promise.all([ - addTab(results.others, results.query, true), - addTab(results.in_args, results.query, false), - addTab(results.returned, results.query, false), - ]); - // Navigate to the relevant tab if the current tab is empty, like in case users search - // for "-> String". If they had selected another tab previously, they have to click on - // it again. - let currentTab = searchState.currentTab; - if ((currentTab === 0 && ret_others[1] === 0) || - (currentTab === 1 && ret_in_args[1] === 0) || - (currentTab === 2 && ret_returned[1] === 0)) { - if (ret_others[1] !== 0) { - currentTab = 0; - } else if (ret_in_args[1] !== 0) { - currentTab = 1; - } else if (ret_returned[1] !== 0) { - currentTab = 2; - } - } - - let crates = ""; - if (rawSearchIndex.size > 1) { - crates = " in 
"; - } - - let output = `

Results${crates}

`; - if (results.query.error !== null) { - const error = results.query.error; - error.forEach((value, index) => { - value = value.split("<").join("<").split(">").join(">"); - if (index % 2 !== 0) { - error[index] = `${value.replaceAll(" ", " ")}`; - } else { - error[index] = value; + const length = path.length; + const clength = contains.length; + pathiter: for (let i = length - clength; i >= 0; i -= 1) { + let dist_total = 0; + for (let x = 0; x < clength; ++x) { + const [p, c] = [path[i + x], contains[x]]; + if (Math.floor((p.length - c.length) / 3) <= maxPathEditDistance && + p.indexOf(c) !== -1 + ) { + // discount distance on substring match + dist_total += Math.floor((p.length - c.length) / 3); + } else { + const dist = editDistance(p, c, maxPathEditDistance); + if (dist > maxPathEditDistance) { + continue pathiter; + } + dist_total += dist; + } } - }); - output += `

Query parser error: "${error.join("")}".

`; - output += "
" + - makeTabHeader(0, "In Names", ret_others[1]) + - "
"; - currentTab = 0; - } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { - output += "
" + - makeTabHeader(0, "In Names", ret_others[1]) + - makeTabHeader(1, "In Parameters", ret_in_args[1]) + - makeTabHeader(2, "In Return Types", ret_returned[1]) + - "
"; - } else { - const signatureTabTitle = - results.query.elems.length === 0 ? "In Function Return Types" : - results.query.returned.length === 0 ? "In Function Parameters" : - "In Function Signatures"; - output += "
" + - makeTabHeader(0, signatureTabTitle, ret_others[1]) + - "
"; - currentTab = 0; - } - - if (results.query.correction !== null) { - const orig = results.query.returned.length > 0 - ? results.query.returned[0].name - : results.query.elems[0].name; - output += "

" + - `Type "${orig}" not found. ` + - "Showing results for closest type name " + - `"${results.query.correction}" instead.

`; - } - if (results.query.proposeCorrectionFrom !== null) { - const orig = results.query.proposeCorrectionFrom; - const targ = results.query.proposeCorrectionTo; - output += "

" + - `Type "${orig}" not found and used as generic parameter. ` + - `Consider searching for "${targ}" instead.

`; - } - - const resultsElem = document.createElement("div"); - resultsElem.id = "results"; - resultsElem.appendChild(ret_others[0]); - resultsElem.appendChild(ret_in_args[0]); - resultsElem.appendChild(ret_returned[0]); - - search.innerHTML = output; - const crateSearch = document.getElementById("crate-search"); - if (crateSearch) { - crateSearch.addEventListener("input", updateCrate); - } - search.appendChild(resultsElem); - // Reset focused elements. - searchState.showResults(search); - const elems = document.getElementById("search-tabs").childNodes; - searchState.focusedByTab = []; - let i = 0; - for (const elem of elems) { - const j = i; - elem.onclick = () => printTab(j); - searchState.focusedByTab.push(null); - i += 1; - } - printTab(currentTab); - } - - function updateSearchHistory(url) { - if (!browserSupportsHistoryApi()) { - return; - } - const params = searchState.getQueryStringParams(); - if (!history.state && !params.search) { - history.pushState(null, "", url); - } else { - history.replaceState(null, "", url); - } - } - - /** - * Perform a search based on the current state of the search input element - * and display the results. - * @param {boolean} [forced] - */ - async function search(forced) { - const query = parseQuery(searchState.input.value.trim()); - let filterCrates = getFilterCrates(); - - if (!forced && query.userQuery === currentResults) { - if (query.userQuery.length > 0) { - putBackSearch(); + ret_dist = Math.min(ret_dist, Math.round(dist_total / clength)); } - return; + return ret_dist > maxPathEditDistance ? null : ret_dist; } - searchState.setLoadingSearch(); + function typePassesFilter(filter, type) { + // No filter or Exact mach + if (filter <= NO_TYPE_FILTER || filter === type) return true; - const params = searchState.getQueryStringParams(); + // Match related items + const name = itemTypes[type]; + switch (itemTypes[filter]) { + case "constant": + return name === "associatedconstant"; + case "fn": + return name === "method" || name === "tymethod"; + case "type": + return name === "primitive" || name === "associatedtype"; + case "trait": + return name === "traitalias"; + } - // In case we have no information about the saved crate and there is a URL query parameter, - // we override it with the URL query parameter. - if (filterCrates === null && params["filter-crate"] !== undefined) { - filterCrates = params["filter-crate"]; + // No match + return false; } - // Update document title to maintain a meaningful browser history - searchState.title = "\"" + query.original + "\" Search - Rust"; + function createAliasFromItem(item) { + return { + crate: item.crate, + name: item.name, + path: item.path, + descShard: item.descShard, + descIndex: item.descIndex, + exactPath: item.exactPath, + ty: item.ty, + parent: item.parent, + type: item.type, + is_alias: true, + bitIndex: item.bitIndex, + implDisambiguator: item.implDisambiguator, + }; + } - // Because searching is incremental by character, only the most - // recent search query is added to the browser history. - updateSearchHistory(buildUrl(query.original, filterCrates)); + const handleAliases = async(ret, query, filterCrates, currentCrate) => { + const lowerQuery = query.toLowerCase(); + // We separate aliases and crate aliases because we want to have current crate + // aliases to be before the others in the displayed results. + const aliases = []; + const crateAliases = []; + if (filterCrates !== null) { + if (this.ALIASES.has(filterCrates) + && this.ALIASES.get(filterCrates).has(lowerQuery)) { + const query_aliases = this.ALIASES.get(filterCrates).get(lowerQuery); + for (const alias of query_aliases) { + aliases.push(createAliasFromItem(this.searchIndex[alias])); + } + } + } else { + for (const [crate, crateAliasesIndex] of this.ALIASES) { + if (crateAliasesIndex.has(lowerQuery)) { + const pushTo = crate === currentCrate ? crateAliases : aliases; + const query_aliases = crateAliasesIndex.get(lowerQuery); + for (const alias of query_aliases) { + pushTo.push(createAliasFromItem(this.searchIndex[alias])); + } + } + } + } - await showResults( - await execQuery(query, filterCrates, window.currentCrate), - params.go_to_first, - filterCrates); - } + const sortFunc = (aaa, bbb) => { + if (aaa.path < bbb.path) { + return 1; + } else if (aaa.path === bbb.path) { + return 0; + } + return -1; + }; + crateAliases.sort(sortFunc); + aliases.sort(sortFunc); - /** - * Convert a list of RawFunctionType / ID to object-based FunctionType. - * - * Crates often have lots of functions in them, and it's common to have a large number of - * functions that operate on a small set of data types, so the search index compresses them - * by encoding function parameter and return types as indexes into an array of names. - * - * Even when a general-purpose compression algorithm is used, this is still a win. I checked. - * https://github.com/rust-lang/rust/pull/98475#issue-1284395985 - * - * The format for individual function types is encoded in - * librustdoc/html/render/mod.rs: impl Serialize for RenderType - * - * @param {null|Array} types - * @param {Array<{name: string, ty: number}>} lowercasePaths - * - * @return {Array} - */ - function buildItemSearchTypeAll(types, lowercasePaths) { - return types.length > 0 ? - types.map(type => buildItemSearchType(type, lowercasePaths)) : - EMPTY_GENERICS_ARRAY; - } + const fetchDesc = alias => { + return this.searchIndexEmptyDesc.get(alias.crate).contains(alias.bitIndex) ? + "" : this.searchState.loadDesc(alias); + }; + const [crateDescs, descs] = await Promise.all([ + Promise.all(crateAliases.map(fetchDesc)), + Promise.all(aliases.map(fetchDesc)), + ]); - /** - * Empty, immutable map used in item search types with no bindings. - * - * @type {Map>} - */ - const EMPTY_BINDINGS_MAP = new Map(); + const pushFunc = alias => { + alias.alias = query; + const res = buildHrefAndPath(alias); + alias.displayPath = pathSplitter(res[0]); + alias.fullPath = alias.displayPath + alias.name; + alias.href = res[1]; - /** - * Empty, immutable map used in item search types with no bindings. - * - * @type {Array} - */ - const EMPTY_GENERICS_ARRAY = []; + ret.others.unshift(alias); + if (ret.others.length > MAX_RESULTS) { + ret.others.pop(); + } + }; - /** - * Object pool for function types with no bindings or generics. - * This is reset after loading the index. - * - * @type {Map} - */ - let TYPES_POOL = new Map(); + aliases.forEach((alias, i) => { + alias.desc = descs[i]; + }); + aliases.forEach(pushFunc); + crateAliases.forEach((alias, i) => { + alias.desc = crateDescs[i]; + }); + crateAliases.forEach(pushFunc); + }; - /** - * Converts a single type. - * - * @param {RawFunctionType} type - */ - function buildItemSearchType(type, lowercasePaths, isAssocType) { - const PATH_INDEX_DATA = 0; - const GENERICS_DATA = 1; - const BINDINGS_DATA = 2; - let pathIndex, generics, bindings; - if (typeof type === "number") { - pathIndex = type; - generics = EMPTY_GENERICS_ARRAY; - bindings = EMPTY_BINDINGS_MAP; - } else { - pathIndex = type[PATH_INDEX_DATA]; - generics = buildItemSearchTypeAll( - type[GENERICS_DATA], - lowercasePaths, - ); - if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) { - bindings = new Map(type[BINDINGS_DATA].map(binding => { - const [assocType, constraints] = binding; - // Associated type constructors are represented sloppily in rustdoc's - // type search, to make the engine simpler. - // - // MyType=Result> is equivalent to MyType>=T> - // and both are, essentially - // MyType)>, except the tuple isn't actually there. - // It's more like the value of a type binding is naturally an array, - // which rustdoc calls "constraints". - // - // As a result, the key should never have generics on it. - return [ - buildItemSearchType(assocType, lowercasePaths, true).id, - buildItemSearchTypeAll(constraints, lowercasePaths), - ]; - })); - } else { - bindings = EMPTY_BINDINGS_MAP; + /** + * This function adds the given result into the provided `results` map if it matches the + * following condition: + * + * * If it is a "literal search" (`parsedQuery.literalSearch`), then `dist` must be 0. + * * If it is not a "literal search", `dist` must be <= `maxEditDistance`. + * + * The `results` map contains information which will be used to sort the search results: + * + * * `fullId` is a `string`` used as the key of the object we use for the `results` map. + * * `id` is the index in the `searchIndex` array for this element. + * * `index` is an `integer`` used to sort by the position of the word in the item's name. + * * `dist` is the main metric used to sort the search results. + * * `path_dist` is zero if a single-component search query is used, otherwise it's the + * distance computed for everything other than the last path component. + * + * @param {Results} results + * @param {string} fullId + * @param {integer} id + * @param {integer} index + * @param {integer} dist + * @param {integer} path_dist + */ + function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) { + if (dist <= maxEditDistance || index !== -1) { + if (results.has(fullId)) { + const result = results.get(fullId); + if (result.dontValidate || result.dist <= dist) { + return; + } + } + results.set(fullId, { + id: id, + index: index, + dontValidate: parsedQuery.literalSearch, + dist: dist, + path_dist: path_dist, + }); } } + /** - * @type {FunctionType} + * This function is called in case the query is only one element (with or without generics). + * This element will be compared to arguments' and returned values' items and also to items. + * + * Other important thing to note: since there is only one element, we use edit + * distance for name comparisons. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {QueryElement} elem - The element from the parsed query. + * @param {Results} results_others - Unqualified results (not in arguments nor in + * returned values). + * @param {Results} results_in_args - Matching arguments results. + * @param {Results} results_returned - Matching returned arguments results. */ - let result; - if (pathIndex < 0) { - // types less than 0 are generic parameters - // the actual names of generic parameters aren't stored, since they aren't API - result = { - id: pathIndex, - ty: TY_GENERIC, - path: null, - exactPath: null, - generics, - bindings, - }; - } else if (pathIndex === 0) { - // `0` is used as a sentinel because it's fewer bytes than `null` - result = { - id: null, - ty: null, - path: null, - exactPath: null, - generics, - bindings, - }; - } else { - const item = lowercasePaths[pathIndex - 1]; - result = { - id: buildTypeMapIndex(item.name, isAssocType), - ty: item.ty, - path: item.path, - exactPath: item.exactPath, - generics, - bindings, - }; - } - const cr = TYPES_POOL.get(result.id); - if (cr) { - // Shallow equality check. Since this function is used - // to construct every type object, this should be mostly - // equivalent to a deep equality check, except if there's - // a conflict, we don't keep the old one around, so it's - // not a fully precise implementation of hashcons. - if (cr.generics.length === result.generics.length && - cr.generics !== result.generics && - cr.generics.every((x, i) => result.generics[i] === x) - ) { - result.generics = cr.generics; + function handleSingleArg( + row, + pos, + elem, + results_others, + results_in_args, + results_returned, + maxEditDistance, + ) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; } - if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) { - let ok = true; - for (const [k, v] of cr.bindings.entries()) { - const v2 = result.bindings.get(v); - if (!v2) { - ok = false; - break; - } - if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) { - result.bindings.set(k, v); - } else if (v !== v2) { - ok = false; - break; - } + let path_dist = 0; + const fullId = row.id; + + // fpDist is a minimum possible type distance, where "type distance" is the number of + // atoms in the function not present in the query + const tfpDist = compareTypeFingerprints( + fullId, + parsedQuery.typeFingerprint, + ); + if (tfpDist !== null) { + const in_args = row.type && row.type.inputs + && checkIfInList(row.type.inputs, elem, row.type.where_clause, null, 0); + const returned = row.type && row.type.output + && checkIfInList(row.type.output, elem, row.type.where_clause, null, 0); + if (in_args) { + results_in_args.max_dist = Math.max(results_in_args.max_dist || 0, tfpDist); + const maxDist = results_in_args.size < MAX_RESULTS ? + (tfpDist + 1) : + results_in_args.max_dist; + addIntoResults(results_in_args, fullId, pos, -1, tfpDist, 0, maxDist); + } + if (returned) { + results_returned.max_dist = Math.max(results_returned.max_dist || 0, tfpDist); + const maxDist = results_returned.size < MAX_RESULTS ? + (tfpDist + 1) : + results_returned.max_dist; + addIntoResults(results_returned, fullId, pos, -1, tfpDist, 0, maxDist); + } + } + + if (!typePassesFilter(elem.typeFilter, row.ty)) { + return; + } + + let index = row.word.indexOf(elem.pathLast); + const normalizedIndex = row.normalizedName.indexOf(elem.pathLast); + if (index === -1 || (index > normalizedIndex && normalizedIndex !== -1)) { + index = normalizedIndex; + } + + if (elem.fullPath.length > 1) { + path_dist = checkPath(elem.pathWithoutLast, row); + if (path_dist === null) { + return; } - if (ok) { - result.bindings = cr.bindings; + } + + if (parsedQuery.literalSearch) { + if (row.word === elem.pathLast) { + addIntoResults(results_others, fullId, pos, index, 0, path_dist); } + return; } - if (cr.ty === result.ty && cr.path === result.path - && cr.bindings === result.bindings && cr.generics === result.generics - && cr.ty === result.ty - ) { - return cr; + + const dist = editDistance(row.normalizedName, elem.normalizedPathLast, maxEditDistance); + + if (index === -1 && dist > maxEditDistance) { + return; } + + addIntoResults(results_others, fullId, pos, index, dist, path_dist, maxEditDistance); } - TYPES_POOL.set(result.id, result); - return result; - } - /** - * Convert from RawFunctionSearchType to FunctionSearchType. - * - * Crates often have lots of functions in them, and function signatures are sometimes complex, - * so rustdoc uses a pretty tight encoding for them. This function converts it to a simpler, - * object-based encoding so that the actual search code is more readable and easier to debug. - * - * The raw function search type format is generated using serde in - * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string - * - * @param {Array<{name: string, ty: number}>} lowercasePaths - * - * @return {null|FunctionSearchType} - */ - function buildFunctionSearchTypeCallback(lowercasePaths) { - return functionSearchType => { - if (functionSearchType === 0) { - return null; + /** + * This function is called in case the query has more than one element. In this case, it'll + * try to match the items which validates all the elements. For `aa -> bb` will look for + * functions which have a parameter `aa` and has `bb` in its returned values. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {Object} results + */ + function handleArgs(row, pos, results) { + if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) { + return; } - const INPUTS_DATA = 0; - const OUTPUT_DATA = 1; - let inputs, output; - if (typeof functionSearchType[INPUTS_DATA] === "number") { - inputs = [buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths)]; - } else { - inputs = buildItemSearchTypeAll( - functionSearchType[INPUTS_DATA], - lowercasePaths, - ); + + const tfpDist = compareTypeFingerprints( + row.id, + parsedQuery.typeFingerprint, + ); + if (tfpDist === null) { + return; } - if (functionSearchType.length > 1) { - if (typeof functionSearchType[OUTPUT_DATA] === "number") { - output = [buildItemSearchType(functionSearchType[OUTPUT_DATA], lowercasePaths)]; - } else { - output = buildItemSearchTypeAll( - functionSearchType[OUTPUT_DATA], - lowercasePaths, - ); - } - } else { - output = []; + if (results.size >= MAX_RESULTS && tfpDist > results.max_dist) { + return; } - const where_clause = []; - const l = functionSearchType.length; - for (let i = 2; i < l; ++i) { - where_clause.push(typeof functionSearchType[i] === "number" - ? [buildItemSearchType(functionSearchType[i], lowercasePaths)] - : buildItemSearchTypeAll(functionSearchType[i], lowercasePaths)); + + // If the result is too "bad", we return false and it ends this search. + if (!unifyFunctionTypes( + row.type.inputs, + parsedQuery.elems, + row.type.where_clause, + null, + mgens => { + return unifyFunctionTypes( + row.type.output, + parsedQuery.returned, + row.type.where_clause, + mgens, + null, + 0, // unboxing depth + ); + }, + 0, // unboxing depth + )) { + return; } - return { - inputs, output, where_clause, - }; - }; - } - /** - * Type fingerprints allow fast, approximate matching of types. - * - * This algo creates a compact representation of the type set using a Bloom filter. - * This fingerprint is used three ways: - * - * - It accelerates the matching algorithm by checking the function fingerprint against the - * query fingerprint. If any bits are set in the query but not in the function, it can't - * match. - * - * - The fourth section has the number of distinct items in the set. - * This is the distance function, used for filtering and for sorting. - * - * [^1]: Distance is the relatively naive metric of counting the number of distinct items in - * the function that are not present in the query. - * - * @param {FunctionType|QueryElement} type - a single type - * @param {Uint32Array} output - write the fingerprint to this data structure: uses 128 bits - * @param {Set} fps - Set of distinct items - */ - function buildFunctionTypeFingerprint(type, output, fps) { - let input = type.id; - // All forms of `[]`/`()`/`->` get collapsed down to one thing in the bloom filter. - // Differentiating between arrays and slices, if the user asks for it, is - // still done in the matching algorithm. - if (input === typeNameIdOfArray || input === typeNameIdOfSlice) { - input = typeNameIdOfArrayOrSlice; - } - if (input === typeNameIdOfTuple || input === typeNameIdOfUnit) { - input = typeNameIdOfTupleOrUnit; - } - if (input === typeNameIdOfFn || input === typeNameIdOfFnMut || - input === typeNameIdOfFnOnce) { - input = typeNameIdOfHof; - } - // http://burtleburtle.net/bob/hash/integer.html - // ~~ is toInt32. It's used before adding, so - // the number stays in safe integer range. - const hashint1 = k => { - k = (~~k + 0x7ed55d16) + (k << 12); - k = (k ^ 0xc761c23c) ^ (k >>> 19); - k = (~~k + 0x165667b1) + (k << 5); - k = (~~k + 0xd3a2646c) ^ (k << 9); - k = (~~k + 0xfd7046c5) + (k << 3); - return (k ^ 0xb55a4f09) ^ (k >>> 16); - }; - const hashint2 = k => { - k = ~k + (k << 15); - k ^= k >>> 12; - k += k << 2; - k ^= k >>> 4; - k = Math.imul(k, 2057); - return k ^ (k >> 16); - }; - if (input !== null) { - const h0a = hashint1(input); - const h0b = hashint2(input); - // Less Hashing, Same Performance: Building a Better Bloom Filter - // doi=10.1.1.72.2442 - const h1a = ~~(h0a + Math.imul(h0b, 2)); - const h1b = ~~(h0a + Math.imul(h0b, 3)); - const h2a = ~~(h0a + Math.imul(h0b, 4)); - const h2b = ~~(h0a + Math.imul(h0b, 5)); - output[0] |= (1 << (h0a % 32)) | (1 << (h1b % 32)); - output[1] |= (1 << (h1a % 32)) | (1 << (h2b % 32)); - output[2] |= (1 << (h2a % 32)) | (1 << (h0b % 32)); - fps.add(input); - } - for (const g of type.generics) { - buildFunctionTypeFingerprint(g, output, fps); - } - const fb = { - id: null, - ty: 0, - generics: EMPTY_GENERICS_ARRAY, - bindings: EMPTY_BINDINGS_MAP, - }; - for (const [k, v] of type.bindings.entries()) { - fb.id = k; - fb.generics = v; - buildFunctionTypeFingerprint(fb, output, fps); + results.max_dist = Math.max(results.max_dist || 0, tfpDist); + addIntoResults(results, row.id, pos, 0, tfpDist, 0, Number.MAX_VALUE); } - output[3] = fps.size; - } - /** - * Compare the query fingerprint with the function fingerprint. - * - * @param {{number}} fullId - The function - * @param {{Uint32Array}} queryFingerprint - The query - * @returns {number|null} - Null if non-match, number if distance - * This function might return 0! - */ - function compareTypeFingerprints(fullId, queryFingerprint) { - const fh0 = functionTypeFingerprint[fullId * 4]; - const fh1 = functionTypeFingerprint[(fullId * 4) + 1]; - const fh2 = functionTypeFingerprint[(fullId * 4) + 2]; - const [qh0, qh1, qh2] = queryFingerprint; - // Approximate set intersection with bloom filters. - // This can be larger than reality, not smaller, because hashes have - // the property that if they've got the same value, they hash to the - // same thing. False positives exist, but not false negatives. - const [in0, in1, in2] = [fh0 & qh0, fh1 & qh1, fh2 & qh2]; - // Approximate the set of items in the query but not the function. - // This might be smaller than reality, but cannot be bigger. - // - // | in_ | qh_ | XOR | Meaning | - // | --- | --- | --- | ------------------------------------------------ | - // | 0 | 0 | 0 | Not present | - // | 1 | 0 | 1 | IMPOSSIBLE because `in_` is `fh_ & qh_` | - // | 1 | 1 | 0 | If one or both is false positive, false negative | - // | 0 | 1 | 1 | Since in_ has no false negatives, must be real | - if ((in0 ^ qh0) || (in1 ^ qh1) || (in2 ^ qh2)) { - return null; - } - return functionTypeFingerprint[(fullId * 4) + 3]; - } - - class VlqHexDecoder { - constructor(string, cons) { - this.string = string; - this.cons = cons; - this.offset = 0; - this.backrefQueue = []; - } - // call after consuming `{` - decodeList() { - let c = this.string.charCodeAt(this.offset); - const ret = []; - while (c !== 125) { // 125 = "}" - ret.push(this.decode()); - c = this.string.charCodeAt(this.offset); - } - this.offset += 1; // eat cb - return ret; - } - // consumes and returns a list or integer - decode() { - let n = 0; - let c = this.string.charCodeAt(this.offset); - if (c === 123) { // 123 = "{" - this.offset += 1; - return this.decodeList(); - } - while (c < 96) { // 96 = "`" - n = (n << 4) | (c & 0xF); - this.offset += 1; - c = this.string.charCodeAt(this.offset); - } - // last character >= la - n = (n << 4) | (c & 0xF); - const [sign, value] = [n & 1, n >> 1]; - this.offset += 1; - return sign ? -value : value; - } - next() { - const c = this.string.charCodeAt(this.offset); - // sixteen characters after "0" are backref - if (c >= 48 && c < 64) { // 48 = "0", 64 = "@" - this.offset += 1; - return this.backrefQueue[c - 48]; - } - // special exception: 0 doesn't use backref encoding - // it's already one character, and it's always nullish - if (c === 96) { // 96 = "`" - this.offset += 1; - return this.cons(0); - } - const result = this.cons(this.decode()); - this.backrefQueue.unshift(result); - if (this.backrefQueue.length > 16) { - this.backrefQueue.pop(); - } - return result; - } - } - class RoaringBitmap { - constructor(str) { - const strdecoded = atob(str); - const u8array = new Uint8Array(strdecoded.length); - for (let j = 0; j < strdecoded.length; ++j) { - u8array[j] = strdecoded.charCodeAt(j); - } - const has_runs = u8array[0] === 0x3b; - const size = has_runs ? - ((u8array[2] | (u8array[3] << 8)) + 1) : - ((u8array[4] | (u8array[5] << 8) | (u8array[6] << 16) | (u8array[7] << 24))); - let i = has_runs ? 4 : 8; - let is_run; - if (has_runs) { - const is_run_len = Math.floor((size + 7) / 8); - is_run = u8array.slice(i, i + is_run_len); - i += is_run_len; - } else { - is_run = new Uint8Array(); - } - this.keys = []; - this.cardinalities = []; - for (let j = 0; j < size; ++j) { - this.keys.push(u8array[i] | (u8array[i + 1] << 8)); - i += 2; - this.cardinalities.push((u8array[i] | (u8array[i + 1] << 8)) + 1); - i += 2; + /** + * Compare the query fingerprint with the function fingerprint. + * + * @param {{number}} fullId - The function + * @param {{Uint32Array}} queryFingerprint - The query + * @returns {number|null} - Null if non-match, number if distance + * This function might return 0! + */ + const compareTypeFingerprints = (fullId, queryFingerprint) => { + const fh0 = this.functionTypeFingerprint[fullId * 4]; + const fh1 = this.functionTypeFingerprint[(fullId * 4) + 1]; + const fh2 = this.functionTypeFingerprint[(fullId * 4) + 2]; + const [qh0, qh1, qh2] = queryFingerprint; + // Approximate set intersection with bloom filters. + // This can be larger than reality, not smaller, because hashes have + // the property that if they've got the same value, they hash to the + // same thing. False positives exist, but not false negatives. + const [in0, in1, in2] = [fh0 & qh0, fh1 & qh1, fh2 & qh2]; + // Approximate the set of items in the query but not the function. + // This might be smaller than reality, but cannot be bigger. + // + // | in_ | qh_ | XOR | Meaning | + // | --- | --- | --- | ------------------------------------------------ | + // | 0 | 0 | 0 | Not present | + // | 1 | 0 | 1 | IMPOSSIBLE because `in_` is `fh_ & qh_` | + // | 1 | 1 | 0 | If one or both is false positive, false negative | + // | 0 | 1 | 1 | Since in_ has no false negatives, must be real | + if ((in0 ^ qh0) || (in1 ^ qh1) || (in2 ^ qh2)) { + return null; } - this.containers = []; - let offsets = null; - if (!has_runs || this.keys.length >= 4) { - offsets = []; - for (let j = 0; j < size; ++j) { - offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) | - (u8array[i + 3] << 24)); - i += 4; + return this.functionTypeFingerprint[(fullId * 4) + 3]; + }; + + + const innerRunQuery = () => { + const queryLen = + parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) + + parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0); + const maxEditDistance = Math.floor(queryLen / 3); + + /** + * @type {Map} + */ + const genericSymbols = new Map(); + + /** + * Convert names to ids in parsed query elements. + * This is not used for the "In Names" tab, but is used for the + * "In Params", "In Returns", and "In Function Signature" tabs. + * + * If there is no matching item, but a close-enough match, this + * function also that correction. + * + * See `buildTypeMapIndex` for more information. + * + * @param {QueryElement} elem + * @param {boolean} isAssocType + */ + const convertNameToId = (elem, isAssocType) => { + const loweredName = elem.pathLast.toLowerCase(); + if (this.typeNameIdMap.has(loweredName) && + (isAssocType || !this.typeNameIdMap.get(loweredName).assocOnly)) { + elem.id = this.typeNameIdMap.get(loweredName).id; + } else if (!parsedQuery.literalSearch) { + let match = null; + let matchDist = maxEditDistance + 1; + let matchName = ""; + for (const [name, { id, assocOnly }] of this.typeNameIdMap) { + const dist = Math.min( + editDistance(name, loweredName, maxEditDistance), + editDistance(name, elem.normalizedPathLast, maxEditDistance), + ); + if (dist <= matchDist && dist <= maxEditDistance && + (isAssocType || !assocOnly)) { + if (dist === matchDist && matchName > name) { + continue; + } + match = id; + matchDist = dist; + matchName = name; + } + } + if (match !== null) { + parsedQuery.correction = matchName; + } + elem.id = match; + } + if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 + && elem.generics.length === 0 && elem.bindings.size === 0) + || elem.typeFilter === TY_GENERIC) { + if (genericSymbols.has(elem.name)) { + elem.id = genericSymbols.get(elem.name); + } else { + elem.id = -(genericSymbols.size + 1); + genericSymbols.set(elem.name, elem.id); + } + if (elem.typeFilter === -1 && elem.name.length >= 3) { + // Silly heuristic to catch if the user probably meant + // to not write a generic parameter. We don't use it, + // just bring it up. + const maxPartDistance = Math.floor(elem.name.length / 3); + let matchDist = maxPartDistance + 1; + let matchName = ""; + for (const name of this.typeNameIdMap.keys()) { + const dist = editDistance(name, elem.name, maxPartDistance); + if (dist <= matchDist && dist <= maxPartDistance) { + if (dist === matchDist && matchName > name) { + continue; + } + matchDist = dist; + matchName = name; + } + } + if (matchName !== "") { + parsedQuery.proposeCorrectionFrom = elem.name; + parsedQuery.proposeCorrectionTo = matchName; + } + } + elem.typeFilter = TY_GENERIC; } - } - for (let j = 0; j < size; ++j) { - if (offsets && offsets[j] !== i) { - console.log(this.containers); - throw new Error(`corrupt bitmap ${j}: ${i} / ${offsets[j]}`); - } - if (is_run[j >> 3] & (1 << (j & 0x7))) { - const runcount = (u8array[i] | (u8array[i + 1] << 8)); - i += 2; - this.containers.push(new RoaringBitmapRun( - runcount, - u8array.slice(i, i + (runcount * 4)), - )); - i += runcount * 4; - } else if (this.cardinalities[j] >= 4096) { - this.containers.push(new RoaringBitmapBits(u8array.slice(i, i + 8192))); - i += 8192; - } else { - const end = this.cardinalities[j] * 2; - this.containers.push(new RoaringBitmapArray( - this.cardinalities[j], - u8array.slice(i, i + end), - )); - i += end; + if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) { + // Rust does not have HKT + parsedQuery.error = [ + "Generic type parameter ", + elem.name, + " does not accept generic parameters", + ]; } - } - } - contains(keyvalue) { - const key = keyvalue >> 16; - const value = keyvalue & 0xFFFF; - for (let i = 0; i < this.keys.length; ++i) { - if (this.keys[i] === key) { - return this.containers[i].contains(value); + for (const elem2 of elem.generics) { + convertNameToId(elem2); } + elem.bindings = new Map(Array.from(elem.bindings.entries()) + .map(entry => { + const [name, constraints] = entry; + if (!this.typeNameIdMap.has(name)) { + parsedQuery.error = [ + "Type parameter ", + name, + " does not exist", + ]; + return [null, []]; + } + for (const elem2 of constraints) { + convertNameToId(elem2); + } + + return [this.typeNameIdMap.get(name).id, constraints]; + }), + ); + }; + + const fps = new Set(); + for (const elem of parsedQuery.elems) { + convertNameToId(elem); + this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint, fps); + } + for (const elem of parsedQuery.returned) { + convertNameToId(elem); + this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint, fps); } - return false; - } - } - class RoaringBitmapRun { - constructor(runcount, array) { - this.runcount = runcount; - this.array = array; - } - contains(value) { - const l = this.runcount * 4; - for (let i = 0; i < l; i += 4) { - const start = this.array[i] | (this.array[i + 1] << 8); - const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8); - if (value >= start && value <= (start + lenm1)) { - return true; + if (parsedQuery.foundElems === 1 && parsedQuery.returned.length === 0) { + if (parsedQuery.elems.length === 1) { + const elem = parsedQuery.elems[0]; + const length = this.searchIndex.length; + for (let i = 0, nSearchIndex = length; i < nSearchIndex; ++i) { + // It means we want to check for this element everywhere (in names, args and + // returned). + handleSingleArg( + this.searchIndex[i], + i, + elem, + results_others, + results_in_args, + results_returned, + maxEditDistance, + ); + } } - } - return false; - } - } - class RoaringBitmapArray { - constructor(cardinality, array) { - this.cardinality = cardinality; - this.array = array; - } - contains(value) { - const l = this.cardinality * 2; - for (let i = 0; i < l; i += 2) { - const start = this.array[i] | (this.array[i + 1] << 8); - if (value === start) { - return true; + } else if (parsedQuery.foundElems > 0) { + // Sort input and output so that generic type variables go first and + // types with generic parameters go last. + // That's because of the way unification is structured: it eats off + // the end, and hits a fast path if the last item is a simple atom. + const sortQ = (a, b) => { + const ag = a.generics.length === 0 && a.bindings.size === 0; + const bg = b.generics.length === 0 && b.bindings.size === 0; + if (ag !== bg) { + return ag - bg; + } + const ai = a.id > 0; + const bi = b.id > 0; + return ai - bi; + }; + parsedQuery.elems.sort(sortQ); + parsedQuery.returned.sort(sortQ); + for (let i = 0, nSearchIndex = this.searchIndex.length; i < nSearchIndex; ++i) { + handleArgs(this.searchIndex[i], i, results_others); } } - return false; - } - } - class RoaringBitmapBits { - constructor(array) { - this.array = array; - } - contains(value) { - return !!(this.array[value >> 3] & (1 << (value & 7))); - } - } - - /** - * Convert raw search index into in-memory search index. - * - * @param {[string, RawSearchIndexCrate][]} rawSearchIndex - */ - function buildIndex(rawSearchIndex) { - searchIndex = []; - searchIndexDeprecated = new Map(); - searchIndexEmptyDesc = new Map(); - let currentIndex = 0; - let id = 0; + }; - // Function type fingerprints are 128-bit bloom filters that are used to - // estimate the distance between function and query. - // This loop counts the number of items to allocate a fingerprint for. - for (const crate of rawSearchIndex.values()) { - // Each item gets an entry in the fingerprint array, and the crate - // does, too - id += crate.t.length + 1; + if (parsedQuery.error === null) { + innerRunQuery(); } - functionTypeFingerprint = new Uint32Array((id + 1) * 4); - - // This loop actually generates the search item indexes, including - // normalized names, type signature objects and fingerprints, and aliases. - id = 0; - for (const [crate, crateCorpus] of rawSearchIndex) { - // a string representing the lengths of each description shard - // a string representing the list of function types - const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => noop); - let descShard = { - crate, - shard: 0, - start: 0, - len: itemDescShardDecoder.next(), - promise: null, - resolve: null, - }; - const descShardList = [ descShard ]; + const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([ + sortResults(results_in_args, true, currentCrate), + sortResults(results_returned, true, currentCrate), + sortResults(results_others, false, currentCrate), + ]); + const ret = createQueryResults( + sorted_in_args, + sorted_returned, + sorted_others, + parsedQuery); + await handleAliases(ret, parsedQuery.original.replace(/"/g, ""), + filterCrates, currentCrate); + await Promise.all([ret.others, ret.returned, ret.in_args].map(async list => { + const descs = await Promise.all(list.map(result => { + return this.searchIndexEmptyDesc.get(result.crate).contains(result.bitIndex) ? + "" : + this.searchState.loadDesc(result); + })); + for (const [i, result] of list.entries()) { + result.desc = descs[i]; + } + })); + if (parsedQuery.error !== null && ret.others.length !== 0) { + // It means some doc aliases were found so let's "remove" the error! + ret.query.error = null; + } + return ret; + } +} - // Deprecated items and items with no description - searchIndexDeprecated.set(crate, new RoaringBitmap(crateCorpus.c)); - searchIndexEmptyDesc.set(crate, new RoaringBitmap(crateCorpus.e)); - let descIndex = 0; +// ==================== Core search logic end ==================== - // This object should have exactly the same set of fields as the "row" - // object defined below. Your JavaScript runtime will thank you. - // https://mathiasbynens.be/notes/shapes-ics - const crateRow = { - crate, - ty: 3, // == ExternCrate - name: crate, - path: "", - descShard, - descIndex, - exactPath: "", - desc: crateCorpus.doc, - parent: undefined, - type: null, - id, - word: crate, - normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), - bitIndex: 0, - implDisambiguator: null, - }; - id += 1; - searchIndex.push(crateRow); - currentIndex += 1; - if (!searchIndexEmptyDesc.get(crate).contains(0)) { - descIndex += 1; - } +let rawSearchIndex; +let docSearch; +const longItemTypes = [ + "keyword", + "primitive type", + "module", + "extern crate", + "re-export", + "struct", + "enum", + "function", + "type alias", + "static", + "trait", + "", + "trait method", + "method", + "struct field", + "enum variant", + "macro", + "assoc type", + "constant", + "assoc const", + "union", + "foreign type", + "existential type", + "attribute macro", + "derive macro", + "trait alias", +]; +let currentResults; - // a String of one character item type codes - const itemTypes = crateCorpus.t; - // an array of (String) item names - const itemNames = crateCorpus.n; - // an array of [(Number) item index, - // (String) full path] - // an item whose index is not present will fall back to the previous present path - // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present, - // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11 - const itemPaths = new Map(crateCorpus.q); - // An array of [(Number) item index, (Number) path index] - // Used to de-duplicate inlined and re-exported stuff - const itemReexports = new Map(crateCorpus.r); - // an array of (Number) the parent path index + 1 to `paths`, or 0 if none - const itemParentIdxDecoder = new VlqHexDecoder(crateCorpus.i, noop => noop); - // a map Number, string for impl disambiguators - const implDisambiguator = new Map(crateCorpus.b); - // an array of [(Number) item type, - // (String) name] - const paths = crateCorpus.p; - // an array of [(String) alias name - // [Number] index to items] - const aliases = crateCorpus.a; +// In the search display, allows to switch between tabs. +function printTab(nb) { + let iter = 0; + let foundCurrentTab = false; + let foundCurrentResultSet = false; + onEachLazy(document.getElementById("search-tabs").childNodes, elem => { + if (nb === iter) { + addClass(elem, "selected"); + foundCurrentTab = true; + } else { + removeClass(elem, "selected"); + } + iter += 1; + }); + const isTypeSearch = (nb > 0 || iter === 1); + iter = 0; + onEachLazy(document.getElementById("results").childNodes, elem => { + if (nb === iter) { + addClass(elem, "active"); + foundCurrentResultSet = true; + } else { + removeClass(elem, "active"); + } + iter += 1; + }); + if (foundCurrentTab && foundCurrentResultSet) { + searchState.currentTab = nb; + // Corrections only kick in on type-based searches. + const correctionsElem = document.getElementsByClassName("search-corrections"); + if (isTypeSearch) { + removeClass(correctionsElem[0], "hidden"); + } else { + addClass(correctionsElem[0], "hidden"); + } + } else if (nb !== 0) { + printTab(0); + } +} - // an array of [{name: String, ty: Number}] - const lowercasePaths = []; +/** + * Build an URL with search parameters. + * + * @param {string} search - The current search being performed. + * @param {string|null} filterCrates - The current filtering crate (if any). + * + * @return {string} + */ +function buildUrl(search, filterCrates) { + let extra = "?search=" + encodeURIComponent(search); - // a string representing the list of function types - const itemFunctionDecoder = new VlqHexDecoder( - crateCorpus.f, - buildFunctionSearchTypeCallback(lowercasePaths), - ); + if (filterCrates !== null) { + extra += "&filter-crate=" + encodeURIComponent(filterCrates); + } + return getNakedUrl() + extra + window.location.hash; +} - // convert `rawPaths` entries into object form - // generate normalizedPaths for function search mode - let len = paths.length; - let lastPath = itemPaths.get(0); - for (let i = 0; i < len; ++i) { - const elem = paths[i]; - const ty = elem[0]; - const name = elem[1]; - let path = null; - if (elem.length > 2) { - path = itemPaths.has(elem[2]) ? itemPaths.get(elem[2]) : lastPath; - lastPath = path; - } - const exactPath = elem.length > 3 ? itemPaths.get(elem[3]) : path; +/** + * Return the filtering crate or `null` if there is none. + * + * @return {string|null} + */ +function getFilterCrates() { + const elem = document.getElementById("crate-search"); + + if (elem && + elem.value !== "all crates" && + window.searchIndex.has(elem.value) + ) { + return elem.value; + } + return null; +} - lowercasePaths.push({ty, name: name.toLowerCase(), path, exactPath}); - paths[i] = {ty, name, path, exactPath}; - } +function nextTab(direction) { + const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; + searchState.focusedByTab[searchState.currentTab] = document.activeElement; + printTab(next); + focusSearchResult(); +} - // convert `item*` into an object form, and construct word indices. - // - // before any analysis is performed lets gather the search terms to - // search against apart from the rest of the data. This is a quick - // operation that is cached for the life of the page state so that - // all other search operations have access to this cached data for - // faster analysis operations - lastPath = ""; - len = itemTypes.length; - let lastName = ""; - let lastWord = ""; - for (let i = 0; i < len; ++i) { - const bitIndex = i + 1; - if (descIndex >= descShard.len && - !searchIndexEmptyDesc.get(crate).contains(bitIndex)) { - descShard = { - crate, - shard: descShard.shard + 1, - start: descShard.start + descShard.len, - len: itemDescShardDecoder.next(), - promise: null, - resolve: null, - }; - descIndex = 0; - descShardList.push(descShard); - } - const name = itemNames[i] === "" ? lastName : itemNames[i]; - const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase(); - const path = itemPaths.has(i) ? itemPaths.get(i) : lastPath; - const type = itemFunctionDecoder.next(); - if (type !== null) { - if (type) { - const fp = functionTypeFingerprint.subarray(id * 4, (id + 1) * 4); - const fps = new Set(); - for (const t of type.inputs) { - buildFunctionTypeFingerprint(t, fp, fps); - } - for (const t of type.output) { - buildFunctionTypeFingerprint(t, fp, fps); - } - for (const w of type.where_clause) { - for (const t of w) { - buildFunctionTypeFingerprint(t, fp, fps); - } - } - } - } - // This object should have exactly the same set of fields as the "crateRow" - // object defined above. - const itemParentIdx = itemParentIdxDecoder.next(); - const row = { - crate, - ty: itemTypes.charCodeAt(i) - 65, // 65 = "A" - name, - path, - descShard, - descIndex, - exactPath: itemReexports.has(i) ? itemPaths.get(itemReexports.get(i)) : path, - parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined, - type, - id, - word, - normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), - bitIndex, - implDisambiguator: implDisambiguator.has(i) ? implDisambiguator.get(i) : null, - }; - id += 1; - searchIndex.push(row); - lastPath = row.path; - if (!searchIndexEmptyDesc.get(crate).contains(bitIndex)) { - descIndex += 1; - } - lastName = name; - lastWord = word; +// Focus the first search result on the active tab, or the result that +// was focused last time this tab was active. +function focusSearchResult() { + const target = searchState.focusedByTab[searchState.currentTab] || + document.querySelectorAll(".search-results.active a").item(0) || + document.querySelectorAll("#search-tabs button").item(searchState.currentTab); + searchState.focusedByTab[searchState.currentTab] = null; + if (target) { + target.focus(); + } +} + +/** + * Render a set of search results for a single tab. + * @param {Array} array - The search results for this tab + * @param {ParsedQuery} query + * @param {boolean} display - True if this is the active tab + */ +async function addTab(array, query, display) { + const extraClass = display ? " active" : ""; + + const output = document.createElement("div"); + if (array.length > 0) { + output.className = "search-results " + extraClass; + + for (const item of array) { + const name = item.name; + const type = itemTypes[item.ty]; + const longType = longItemTypes[item.ty]; + const typeName = longType.length !== 0 ? `${longType}` : "?"; + + const link = document.createElement("a"); + link.className = "result-" + type; + link.href = item.href; + + const resultName = document.createElement("div"); + resultName.className = "result-name"; + + resultName.insertAdjacentHTML( + "beforeend", + `${typeName}`); + link.appendChild(resultName); + + let alias = " "; + if (item.is_alias) { + alias = `
\ +${item.alias} - see \ +
`; } + resultName.insertAdjacentHTML( + "beforeend", + `
${alias}\ +${item.displayPath}${name}\ +
`); - if (aliases) { - const currentCrateAliases = new Map(); - ALIASES.set(crate, currentCrateAliases); - for (const alias_name in aliases) { - if (!Object.prototype.hasOwnProperty.call(aliases, alias_name)) { - continue; - } + const description = document.createElement("div"); + description.className = "desc"; + description.insertAdjacentHTML("beforeend", item.desc); + + link.appendChild(description); + output.appendChild(link); + } + } else if (query.error === null) { + output.className = "search-failed" + extraClass; + output.innerHTML = "No results :(
" + + "Try on DuckDuckGo?

" + + "Or try looking in one of these:"; + } + return [output, array.length]; +} - let currentNameAliases; - if (currentCrateAliases.has(alias_name)) { - currentNameAliases = currentCrateAliases.get(alias_name); - } else { - currentNameAliases = []; - currentCrateAliases.set(alias_name, currentNameAliases); - } - for (const local_alias of aliases[alias_name]) { - currentNameAliases.push(local_alias + currentIndex); - } - } - } - currentIndex += itemTypes.length; - searchState.descShards.set(crate, descShardList); - } - // Drop the (rather large) hash table used for reusing function items - TYPES_POOL = new Map(); +function makeTabHeader(tabNb, text, nbElems) { + // https://blog.horizon-eda.org/misc/2020/02/19/ui.html + // + // CSS runs with `font-variant-numeric: tabular-nums` to ensure all + // digits are the same width. \u{2007} is a Unicode space character + // that is defined to be the same width as a digit. + const fmtNbElems = + nbElems < 10 ? `\u{2007}(${nbElems})\u{2007}\u{2007}` : + nbElems < 100 ? `\u{2007}(${nbElems})\u{2007}` : `\u{2007}(${nbElems})`; + if (searchState.currentTab === tabNb) { + return ""; } + return ""; +} - /** - * Callback for when the search form is submitted. - * @param {Event} [e] - The event that triggered this call, if any - */ - function onSearchSubmit(e) { - e.preventDefault(); - searchState.clearInputTimeout(); - search(); +/** + * @param {ResultsTable} results + * @param {boolean} go_to_first + * @param {string} filterCrates + */ +async function showResults(results, go_to_first, filterCrates) { + const search = searchState.outputElement(); + if (go_to_first || (results.others.length === 1 + && getSettingValue("go-to-only-result") === "true") + ) { + // Needed to force re-execution of JS when coming back to a page. Let's take this + // scenario as example: + // + // 1. You have the "Directly go to item in search if there is only one result" option + // enabled. + // 2. You make a search which results only one result, leading you automatically to + // this result. + // 3. You go back to previous page. + // + // Now, without the call below, the JS will not be re-executed and the previous state + // will be used, starting search again since the search input is not empty, leading you + // back to the previous page again. + window.onunload = () => { }; + searchState.removeQueryParameters(); + const elem = document.createElement("a"); + elem.href = results.others[0].href; + removeClass(elem, "active"); + // For firefox, we need the element to be in the DOM so it can be clicked. + document.body.appendChild(elem); + elem.click(); + return; + } + if (results.query === undefined) { + results.query = DocSearch.parseQuery(searchState.input.value); } - function putBackSearch() { - const search_input = searchState.input; - if (!searchState.input) { - return; - } - if (search_input.value !== "" && !searchState.isDisplayed()) { - searchState.showResults(); - if (browserSupportsHistoryApi()) { - history.replaceState(null, "", - buildUrl(search_input.value, getFilterCrates())); - } - document.title = searchState.title; + currentResults = results.query.userQuery; + + const [ret_others, ret_in_args, ret_returned] = await Promise.all([ + addTab(results.others, results.query, true), + addTab(results.in_args, results.query, false), + addTab(results.returned, results.query, false), + ]); + + // Navigate to the relevant tab if the current tab is empty, like in case users search + // for "-> String". If they had selected another tab previously, they have to click on + // it again. + let currentTab = searchState.currentTab; + if ((currentTab === 0 && ret_others[1] === 0) || + (currentTab === 1 && ret_in_args[1] === 0) || + (currentTab === 2 && ret_returned[1] === 0)) { + if (ret_others[1] !== 0) { + currentTab = 0; + } else if (ret_in_args[1] !== 0) { + currentTab = 1; + } else if (ret_returned[1] !== 0) { + currentTab = 2; } } - function registerSearchEvents() { - const params = searchState.getQueryStringParams(); - - // Populate search bar with query string search term when provided, - // but only if the input bar is empty. This avoid the obnoxious issue - // where you start trying to do a search, and the index loads, and - // suddenly your search is gone! - if (searchState.input.value === "") { - searchState.input.value = params.search || ""; + let crates = ""; + if (rawSearchIndex.size > 1) { + crates = " in 
"; + } - const searchAfter500ms = () => { - searchState.clearInputTimeout(); - if (searchState.input.value.length === 0) { - searchState.hideResults(); + let output = `

Results${crates}

`; + if (results.query.error !== null) { + const error = results.query.error; + error.forEach((value, index) => { + value = value.split("<").join("<").split(">").join(">"); + if (index % 2 !== 0) { + error[index] = `${value.replaceAll(" ", " ")}`; } else { - searchState.timeout = setTimeout(search, 500); - } - }; - searchState.input.onkeyup = searchAfter500ms; - searchState.input.oninput = searchAfter500ms; - document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit; - searchState.input.onchange = e => { - if (e.target !== document.activeElement) { - // To prevent doing anything when it's from a blur event. - return; - } - // Do NOT e.preventDefault() here. It will prevent pasting. - searchState.clearInputTimeout(); - // zero-timeout necessary here because at the time of event handler execution the - // pasted content is not in the input field yet. Shouldn’t make any difference for - // change, though. - setTimeout(search, 0); - }; - searchState.input.onpaste = searchState.input.onchange; - - searchState.outputElement().addEventListener("keydown", e => { - // We only handle unmodified keystrokes here. We don't want to interfere with, - // for instance, alt-left and alt-right for history navigation. - if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { - return; - } - // up and down arrow select next/previous search result, or the - // search box if we're already at the top. - if (e.which === 38) { // up - const previous = document.activeElement.previousElementSibling; - if (previous) { - previous.focus(); - } else { - searchState.focus(); - } - e.preventDefault(); - } else if (e.which === 40) { // down - const next = document.activeElement.nextElementSibling; - if (next) { - next.focus(); - } - const rect = document.activeElement.getBoundingClientRect(); - if (window.innerHeight - rect.bottom < rect.height) { - window.scrollBy(0, rect.height); - } - e.preventDefault(); - } else if (e.which === 37) { // left - nextTab(-1); - e.preventDefault(); - } else if (e.which === 39) { // right - nextTab(1); - e.preventDefault(); + error[index] = value; } }); + output += `

Query parser error: "${error.join("")}".

`; + output += "
" + + makeTabHeader(0, "In Names", ret_others[1]) + + "
"; + currentTab = 0; + } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { + output += "
" + + makeTabHeader(0, "In Names", ret_others[1]) + + makeTabHeader(1, "In Parameters", ret_in_args[1]) + + makeTabHeader(2, "In Return Types", ret_returned[1]) + + "
"; + } else { + const signatureTabTitle = + results.query.elems.length === 0 ? "In Function Return Types" : + results.query.returned.length === 0 ? "In Function Parameters" : + "In Function Signatures"; + output += "
" + + makeTabHeader(0, signatureTabTitle, ret_others[1]) + + "
"; + currentTab = 0; + } - searchState.input.addEventListener("keydown", e => { - if (e.which === 40) { // down - focusSearchResult(); - e.preventDefault(); - } - }); + if (results.query.correction !== null) { + const orig = results.query.returned.length > 0 + ? results.query.returned[0].name + : results.query.elems[0].name; + output += "

" + + `Type "${orig}" not found. ` + + "Showing results for closest type name " + + `"${results.query.correction}" instead.

`; + } + if (results.query.proposeCorrectionFrom !== null) { + const orig = results.query.proposeCorrectionFrom; + const targ = results.query.proposeCorrectionTo; + output += "

" + + `Type "${orig}" not found and used as generic parameter. ` + + `Consider searching for "${targ}" instead.

`; + } + + const resultsElem = document.createElement("div"); + resultsElem.id = "results"; + resultsElem.appendChild(ret_others[0]); + resultsElem.appendChild(ret_in_args[0]); + resultsElem.appendChild(ret_returned[0]); + + search.innerHTML = output; + const crateSearch = document.getElementById("crate-search"); + if (crateSearch) { + crateSearch.addEventListener("input", updateCrate); + } + search.appendChild(resultsElem); + // Reset focused elements. + searchState.showResults(search); + const elems = document.getElementById("search-tabs").childNodes; + searchState.focusedByTab = []; + let i = 0; + for (const elem of elems) { + const j = i; + elem.onclick = () => printTab(j); + searchState.focusedByTab.push(null); + i += 1; + } + printTab(currentTab); +} + +function updateSearchHistory(url) { + if (!browserSupportsHistoryApi()) { + return; + } + const params = searchState.getQueryStringParams(); + if (!history.state && !params.search) { + history.pushState(null, "", url); + } else { + history.replaceState(null, "", url); + } +} - searchState.input.addEventListener("focus", () => { +/** + * Perform a search based on the current state of the search input element + * and display the results. + * @param {boolean} [forced] + */ +async function search(forced) { + const query = DocSearch.parseQuery(searchState.input.value.trim()); + let filterCrates = getFilterCrates(); + + if (!forced && query.userQuery === currentResults) { + if (query.userQuery.length > 0) { putBackSearch(); - }); + } + return; + } - searchState.input.addEventListener("blur", () => { - searchState.input.placeholder = searchState.input.origPlaceholder; - }); + searchState.setLoadingSearch(); + + const params = searchState.getQueryStringParams(); + + // In case we have no information about the saved crate and there is a URL query parameter, + // we override it with the URL query parameter. + if (filterCrates === null && params["filter-crate"] !== undefined) { + filterCrates = params["filter-crate"]; + } + + // Update document title to maintain a meaningful browser history + searchState.title = "\"" + query.original + "\" Search - Rust"; + + // Because searching is incremental by character, only the most + // recent search query is added to the browser history. + updateSearchHistory(buildUrl(query.original, filterCrates)); + + await showResults( + await docSearch.execQuery(query, filterCrates, window.currentCrate), + params.go_to_first, + filterCrates); +} + +/** + * Callback for when the search form is submitted. + * @param {Event} [e] - The event that triggered this call, if any + */ +function onSearchSubmit(e) { + e.preventDefault(); + searchState.clearInputTimeout(); + search(); +} - // Push and pop states are used to add search results to the browser - // history. +function putBackSearch() { + const search_input = searchState.input; + if (!searchState.input) { + return; + } + if (search_input.value !== "" && !searchState.isDisplayed()) { + searchState.showResults(); if (browserSupportsHistoryApi()) { - // Store the previous so we can revert back to it later. - const previousTitle = document.title; - - window.addEventListener("popstate", e => { - const params = searchState.getQueryStringParams(); - // Revert to the previous title manually since the History - // API ignores the title parameter. - document.title = previousTitle; - // When browsing forward to search results the previous - // search will be repeated, so the currentResults are - // cleared to ensure the search is successful. - currentResults = null; - // Synchronize search bar with query string state and - // perform the search. This will empty the bar if there's - // nothing there, which lets you really go back to a - // previous state with nothing in the bar. - if (params.search && params.search.length > 0) { - searchState.input.value = params.search; - // Some browsers fire "onpopstate" for every page load - // (Chrome), while others fire the event only when actually - // popping a state (Firefox), which is why search() is - // called both here and at the end of the startSearch() - // function. - e.preventDefault(); - search(); - } else { - searchState.input.value = ""; - // When browsing back from search results the main page - // visibility must be reset. - searchState.hideResults(); - } - }); + history.replaceState(null, "", + buildUrl(search_input.value, getFilterCrates())); + } + document.title = searchState.title; + } +} + +function registerSearchEvents() { + const params = searchState.getQueryStringParams(); + + // Populate search bar with query string search term when provided, + // but only if the input bar is empty. This avoid the obnoxious issue + // where you start trying to do a search, and the index loads, and + // suddenly your search is gone! + if (searchState.input.value === "") { + searchState.input.value = params.search || ""; + } + + const searchAfter500ms = () => { + searchState.clearInputTimeout(); + if (searchState.input.value.length === 0) { + searchState.hideResults(); + } else { + searchState.timeout = setTimeout(search, 500); + } + }; + searchState.input.onkeyup = searchAfter500ms; + searchState.input.oninput = searchAfter500ms; + document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit; + searchState.input.onchange = e => { + if (e.target !== document.activeElement) { + // To prevent doing anything when it's from a blur event. + return; + } + // Do NOT e.preventDefault() here. It will prevent pasting. + searchState.clearInputTimeout(); + // zero-timeout necessary here because at the time of event handler execution the + // pasted content is not in the input field yet. Shouldn’t make any difference for + // change, though. + setTimeout(search, 0); + }; + searchState.input.onpaste = searchState.input.onchange; + + searchState.outputElement().addEventListener("keydown", e => { + // We only handle unmodified keystrokes here. We don't want to interfere with, + // for instance, alt-left and alt-right for history navigation. + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + // up and down arrow select next/previous search result, or the + // search box if we're already at the top. + if (e.which === 38) { // up + const previous = document.activeElement.previousElementSibling; + if (previous) { + previous.focus(); + } else { + searchState.focus(); + } + e.preventDefault(); + } else if (e.which === 40) { // down + const next = document.activeElement.nextElementSibling; + if (next) { + next.focus(); + } + const rect = document.activeElement.getBoundingClientRect(); + if (window.innerHeight - rect.bottom < rect.height) { + window.scrollBy(0, rect.height); + } + e.preventDefault(); + } else if (e.which === 37) { // left + nextTab(-1); + e.preventDefault(); + } else if (e.which === 39) { // right + nextTab(1); + e.preventDefault(); + } + }); + + searchState.input.addEventListener("keydown", e => { + if (e.which === 40) { // down + focusSearchResult(); + e.preventDefault(); } + }); + + searchState.input.addEventListener("focus", () => { + putBackSearch(); + }); + + searchState.input.addEventListener("blur", () => { + searchState.input.placeholder = searchState.input.origPlaceholder; + }); - // This is required in firefox to avoid this problem: Navigating to a search result - // with the keyboard, hitting enter, and then hitting back would take you back to - // the doc page, rather than the search that should overlay it. - // This was an interaction between the back-forward cache and our handlers - // that try to sync state between the URL and the search input. To work around it, - // do a small amount of re-init on page show. - window.onpageshow = () => { - const qSearch = searchState.getQueryStringParams().search; - if (searchState.input.value === "" && qSearch) { - searchState.input.value = qSearch; + // Push and pop states are used to add search results to the browser + // history. + if (browserSupportsHistoryApi()) { + // Store the previous <title> so we can revert back to it later. + const previousTitle = document.title; + + window.addEventListener("popstate", e => { + const params = searchState.getQueryStringParams(); + // Revert to the previous title manually since the History + // API ignores the title parameter. + document.title = previousTitle; + // When browsing forward to search results the previous + // search will be repeated, so the currentResults are + // cleared to ensure the search is successful. + currentResults = null; + // Synchronize search bar with query string state and + // perform the search. This will empty the bar if there's + // nothing there, which lets you really go back to a + // previous state with nothing in the bar. + if (params.search && params.search.length > 0) { + searchState.input.value = params.search; + // Some browsers fire "onpopstate" for every page load + // (Chrome), while others fire the event only when actually + // popping a state (Firefox), which is why search() is + // called both here and at the end of the startSearch() + // function. + e.preventDefault(); + search(); + } else { + searchState.input.value = ""; + // When browsing back from search results the main page + // visibility must be reset. + searchState.hideResults(); } - search(); - }; + }); } - function updateCrate(ev) { - if (ev.target.value === "all crates") { - // If we don't remove it from the URL, it'll be picked up again by the search. - const query = searchState.input.value.trim(); - updateSearchHistory(buildUrl(query, null)); + // This is required in firefox to avoid this problem: Navigating to a search result + // with the keyboard, hitting enter, and then hitting back would take you back to + // the doc page, rather than the search that should overlay it. + // This was an interaction between the back-forward cache and our handlers + // that try to sync state between the URL and the search input. To work around it, + // do a small amount of re-init on page show. + window.onpageshow = () => { + const qSearch = searchState.getQueryStringParams().search; + if (searchState.input.value === "" && qSearch) { + searchState.input.value = qSearch; } - // In case you "cut" the entry from the search input, then change the crate filter - // before paste back the previous search, you get the old search results without - // the filter. To prevent this, we need to remove the previous results. - currentResults = null; - search(true); + search(); + }; +} + +function updateCrate(ev) { + if (ev.target.value === "all crates") { + // If we don't remove it from the URL, it'll be picked up again by the search. + const query = searchState.input.value.trim(); + updateSearchHistory(buildUrl(query, null)); } + // In case you "cut" the entry from the search input, then change the crate filter + // before paste back the previous search, you get the old search results without + // the filter. To prevent this, we need to remove the previous results. + currentResults = null; + search(true); +} - buildIndex(rawSearchIndex); +function initSearch(searchIndx) { + rawSearchIndex = searchIndx; if (typeof window !== "undefined") { + docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState); registerSearchEvents(); // If there's a search term in the URL, execute the search now. if (window.searchState.getQueryStringParams().search) { search(); } + } else if (typeof exports !== "undefined") { + docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState); + exports.docSearch = docSearch; + exports.parseQuery = DocSearch.parseQuery; } +} - if (typeof exports !== "undefined") { - exports.initSearch = initSearch; - exports.execQuery = execQuery; - exports.parseQuery = parseQuery; - } +if (typeof exports !== "undefined") { + exports.initSearch = initSearch; } if (typeof window !== "undefined") { @@ -3897,6 +3938,4 @@ if (typeof window !== "undefined") { // exports. initSearch(new Map()); } - - })(); diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index 43a22f358c31f..e162ba033cc55 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -427,7 +427,6 @@ function loadSearchJS(doc_folder, resource_suffix) { return list[descIndex]; }, loadedDescShard: function(crate, shard, data) { - //console.log(this.descShards); this.descShards.get(crate)[shard].resolve(data.split("\n")); }, }; @@ -436,15 +435,15 @@ function loadSearchJS(doc_folder, resource_suffix) { const searchJs = fs.readdirSync(staticFiles).find(f => f.match(/search.*\.js$/)); const searchModule = require(path.join(staticFiles, searchJs)); searchModule.initSearch(searchIndex.searchIndex); - + const docSearch = searchModule.docSearch; return { doSearch: function(queryStr, filterCrate, currentCrate) { - return searchModule.execQuery(searchModule.parseQuery(queryStr), + return docSearch.execQuery(searchModule.parseQuery(queryStr), filterCrate, currentCrate); }, getCorrections: function(queryStr, filterCrate, currentCrate) { const parsedQuery = searchModule.parseQuery(queryStr); - searchModule.execQuery(parsedQuery, filterCrate, currentCrate); + docSearch.execQuery(parsedQuery, filterCrate, currentCrate); return parsedQuery.correction; }, parseQuery: searchModule.parseQuery, From ae6f8a7764bd69217f3d0f2ea5e98b9b8a18ad7e Mon Sep 17 00:00:00 2001 From: binarycat <binarycat@envs.net> Date: Tue, 27 Aug 2024 21:51:37 -0400 Subject: [PATCH 12/19] allow BufReader::peek to be called on unsized types --- library/std/src/io/buffered/bufreader.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/std/src/io/buffered/bufreader.rs b/library/std/src/io/buffered/bufreader.rs index 0b12e5777c840..cf226bd28d005 100644 --- a/library/std/src/io/buffered/bufreader.rs +++ b/library/std/src/io/buffered/bufreader.rs @@ -94,7 +94,9 @@ impl<R: Read> BufReader<R> { pub fn with_capacity(capacity: usize, inner: R) -> BufReader<R> { BufReader { inner, buf: Buffer::with_capacity(capacity) } } +} +impl<R: Read + ?Sized> BufReader<R> { /// Attempt to look ahead `n` bytes. /// /// `n` must be less than `capacity`. From 92004523dbcb0336d2f752cd30c300ae6d8df8b9 Mon Sep 17 00:00:00 2001 From: Michael Goulet <michael@errs.io> Date: Thu, 29 Aug 2024 00:17:40 -0400 Subject: [PATCH 13/19] Stop using ty::GenericPredicates for non-predicates_of queries --- compiler/rustc_hir_analysis/src/collect.rs | 2 +- .../src/collect/predicates_of.rs | 59 +++++++++---------- .../src/collect/resolve_bound_vars.rs | 2 +- .../src/hir_ty_lowering/mod.rs | 6 +- compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs | 25 ++++---- compiler/rustc_infer/src/traits/util.rs | 2 +- .../src/multiple_supertrait_upcastable.rs | 3 +- .../src/rmeta/decoder/cstore_impl.rs | 18 ++++++ compiler/rustc_metadata/src/rmeta/encoder.rs | 12 ++-- compiler/rustc_metadata/src/rmeta/mod.rs | 4 +- compiler/rustc_middle/src/query/mod.rs | 12 ++-- compiler/rustc_middle/src/ty/context.rs | 6 +- .../src/traits/object_safety.rs | 10 ++-- .../src/traits/select/confirmation.rs | 12 ++-- .../rustc_trait_selection/src/traits/util.rs | 2 +- .../src/traits/vtable.rs | 3 +- src/librustdoc/clean/simplify.rs | 14 +---- .../src/implied_bounds_in_impls.rs | 2 +- .../src/methods/type_id_on_box.rs | 3 +- .../clippy_lints/src/needless_maybe_sized.rs | 2 +- 20 files changed, 101 insertions(+), 98 deletions(-) diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs index 3acf2c6314592..b8fbe0e99ef79 100644 --- a/compiler/rustc_hir_analysis/src/collect.rs +++ b/compiler/rustc_hir_analysis/src/collect.rs @@ -420,7 +420,7 @@ impl<'tcx> HirTyLowerer<'tcx> for ItemCtxt<'tcx> { span: Span, def_id: LocalDefId, assoc_name: Ident, - ) -> ty::GenericPredicates<'tcx> { + ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { self.tcx.at(span).type_param_predicates((self.item_def_id, def_id, assoc_name)) } diff --git a/compiler/rustc_hir_analysis/src/collect/predicates_of.rs b/compiler/rustc_hir_analysis/src/collect/predicates_of.rs index bba8b0497be55..1bff91b1fac87 100644 --- a/compiler/rustc_hir_analysis/src/collect/predicates_of.rs +++ b/compiler/rustc_hir_analysis/src/collect/predicates_of.rs @@ -580,24 +580,24 @@ pub(super) fn explicit_predicates_of<'tcx>( /// Ensures that the super-predicates of the trait with a `DefId` /// of `trait_def_id` are lowered and stored. This also ensures that /// the transitive super-predicates are lowered. -pub(super) fn explicit_super_predicates_of( - tcx: TyCtxt<'_>, +pub(super) fn explicit_super_predicates_of<'tcx>( + tcx: TyCtxt<'tcx>, trait_def_id: LocalDefId, -) -> ty::GenericPredicates<'_> { +) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { implied_predicates_with_filter(tcx, trait_def_id.to_def_id(), PredicateFilter::SelfOnly) } -pub(super) fn explicit_supertraits_containing_assoc_item( - tcx: TyCtxt<'_>, +pub(super) fn explicit_supertraits_containing_assoc_item<'tcx>( + tcx: TyCtxt<'tcx>, (trait_def_id, assoc_name): (DefId, Ident), -) -> ty::GenericPredicates<'_> { +) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { implied_predicates_with_filter(tcx, trait_def_id, PredicateFilter::SelfThatDefines(assoc_name)) } -pub(super) fn explicit_implied_predicates_of( - tcx: TyCtxt<'_>, +pub(super) fn explicit_implied_predicates_of<'tcx>( + tcx: TyCtxt<'tcx>, trait_def_id: LocalDefId, -) -> ty::GenericPredicates<'_> { +) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { implied_predicates_with_filter( tcx, trait_def_id.to_def_id(), @@ -612,11 +612,11 @@ pub(super) fn explicit_implied_predicates_of( /// Ensures that the super-predicates of the trait with a `DefId` /// of `trait_def_id` are lowered and stored. This also ensures that /// the transitive super-predicates are lowered. -pub(super) fn implied_predicates_with_filter( - tcx: TyCtxt<'_>, +pub(super) fn implied_predicates_with_filter<'tcx>( + tcx: TyCtxt<'tcx>, trait_def_id: DefId, filter: PredicateFilter, -) -> ty::GenericPredicates<'_> { +) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { let Some(trait_def_id) = trait_def_id.as_local() else { // if `assoc_name` is None, then the query should've been redirected to an // external provider @@ -679,20 +679,16 @@ pub(super) fn implied_predicates_with_filter( _ => {} } - ty::GenericPredicates { - parent: None, - predicates: implied_bounds, - effects_min_tys: ty::List::empty(), - } + ty::EarlyBinder::bind(implied_bounds) } /// Returns the predicates defined on `item_def_id` of the form /// `X: Foo` where `X` is the type parameter `def_id`. #[instrument(level = "trace", skip(tcx))] -pub(super) fn type_param_predicates( - tcx: TyCtxt<'_>, +pub(super) fn type_param_predicates<'tcx>( + tcx: TyCtxt<'tcx>, (item_def_id, def_id, assoc_name): (LocalDefId, LocalDefId, Ident), -) -> ty::GenericPredicates<'_> { +) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { use rustc_hir::*; use rustc_middle::ty::Ty; @@ -713,18 +709,20 @@ pub(super) fn type_param_predicates( tcx.generics_of(item_def_id).parent.map(|def_id| def_id.expect_local()) }; - let mut result = parent - .map(|parent| { - let icx = ItemCtxt::new(tcx, parent); - icx.probe_ty_param_bounds(DUMMY_SP, def_id, assoc_name) - }) - .unwrap_or_default(); + let result = if let Some(parent) = parent { + let icx = ItemCtxt::new(tcx, parent); + icx.probe_ty_param_bounds(DUMMY_SP, def_id, assoc_name) + } else { + ty::EarlyBinder::bind(&[] as &[_]) + }; let mut extend = None; let item_hir_id = tcx.local_def_id_to_hir_id(item_def_id); let hir_node = tcx.hir_node(item_hir_id); - let Some(hir_generics) = hir_node.generics() else { return result }; + let Some(hir_generics) = hir_node.generics() else { + return result; + }; if let Node::Item(item) = hir_node && let ItemKind::Trait(..) = item.kind // Implied `Self: Trait` and supertrait bounds. @@ -748,9 +746,10 @@ pub(super) fn type_param_predicates( _ => false, }), ); - result.predicates = - tcx.arena.alloc_from_iter(result.predicates.iter().copied().chain(extra_predicates)); - result + + ty::EarlyBinder::bind( + tcx.arena.alloc_from_iter(result.skip_binder().iter().copied().chain(extra_predicates)), + ) } impl<'tcx> ItemCtxt<'tcx> { diff --git a/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs b/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs index e38492d9e6497..cb203e04f0c65 100644 --- a/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs +++ b/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs @@ -1761,7 +1761,7 @@ impl<'a, 'tcx> BoundVarContext<'a, 'tcx> { break Some((bound_vars.into_iter().collect(), assoc_item)); } let predicates = tcx.explicit_supertraits_containing_assoc_item((def_id, assoc_name)); - let obligations = predicates.predicates.iter().filter_map(|&(pred, _)| { + let obligations = predicates.iter_identity_copied().filter_map(|(pred, _)| { let bound_predicate = pred.kind(); match bound_predicate.skip_binder() { ty::ClauseKind::Trait(data) => { diff --git a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs index 0cdd3e4a1c6c9..98e1297ed0694 100644 --- a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs +++ b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs @@ -136,7 +136,7 @@ pub trait HirTyLowerer<'tcx> { span: Span, def_id: LocalDefId, assoc_name: Ident, - ) -> ty::GenericPredicates<'tcx>; + ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]>; /// Lower an associated type to a projection. /// @@ -831,13 +831,13 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ { debug!(?ty_param_def_id, ?assoc_name, ?span); let tcx = self.tcx(); - let predicates = &self.probe_ty_param_bounds(span, ty_param_def_id, assoc_name).predicates; + let predicates = &self.probe_ty_param_bounds(span, ty_param_def_id, assoc_name); debug!("predicates={:#?}", predicates); self.probe_single_bound_for_assoc_item( || { let trait_refs = predicates - .iter() + .iter_identity_copied() .filter_map(|(p, _)| Some(p.as_trait_clause()?.map_bound(|t| t.trait_ref))); traits::transitive_bounds_that_define_assoc_item(tcx, trait_refs, assoc_name) }, diff --git a/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs b/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs index 8e69a075030be..a43d7aa31a522 100644 --- a/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs +++ b/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs @@ -263,27 +263,24 @@ impl<'tcx> HirTyLowerer<'tcx> for FnCtxt<'_, 'tcx> { _: Span, def_id: LocalDefId, _: Ident, - ) -> ty::GenericPredicates<'tcx> { + ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { let tcx = self.tcx; let item_def_id = tcx.hir().ty_param_owner(def_id); let generics = tcx.generics_of(item_def_id); let index = generics.param_def_id_to_index[&def_id.to_def_id()]; // HACK(eddyb) should get the original `Span`. let span = tcx.def_span(def_id); - ty::GenericPredicates { - parent: None, - predicates: tcx.arena.alloc_from_iter( - self.param_env.caller_bounds().iter().filter_map(|predicate| { - match predicate.kind().skip_binder() { - ty::ClauseKind::Trait(data) if data.self_ty().is_param(index) => { - Some((predicate, span)) - } - _ => None, + + ty::EarlyBinder::bind(tcx.arena.alloc_from_iter( + self.param_env.caller_bounds().iter().filter_map(|predicate| { + match predicate.kind().skip_binder() { + ty::ClauseKind::Trait(data) if data.self_ty().is_param(index) => { + Some((predicate, span)) } - }), - ), - effects_min_tys: ty::List::empty(), - } + _ => None, + } + }), + )) } fn lower_assoc_ty( diff --git a/compiler/rustc_infer/src/traits/util.rs b/compiler/rustc_infer/src/traits/util.rs index 335c65da054c3..3e4f9b4816660 100644 --- a/compiler/rustc_infer/src/traits/util.rs +++ b/compiler/rustc_infer/src/traits/util.rs @@ -123,7 +123,7 @@ pub fn transitive_bounds_that_define_assoc_item<'tcx>( stack.extend( tcx.explicit_supertraits_containing_assoc_item((trait_ref.def_id(), assoc_name)) - .instantiate_own_identity() + .iter_identity_copied() .map(|(clause, _)| clause.instantiate_supertrait(tcx, trait_ref)) .filter_map(|clause| clause.as_trait_clause()) // FIXME: Negative supertraits are elaborated here lol diff --git a/compiler/rustc_lint/src/multiple_supertrait_upcastable.rs b/compiler/rustc_lint/src/multiple_supertrait_upcastable.rs index 978109aba5f92..78468020c4d56 100644 --- a/compiler/rustc_lint/src/multiple_supertrait_upcastable.rs +++ b/compiler/rustc_lint/src/multiple_supertrait_upcastable.rs @@ -45,8 +45,7 @@ impl<'tcx> LateLintPass<'tcx> for MultipleSupertraitUpcastable { let direct_super_traits_iter = cx .tcx .explicit_super_predicates_of(def_id) - .predicates - .into_iter() + .iter_identity_copied() .filter_map(|(pred, _)| pred.as_trait_clause()); if direct_super_traits_iter.count() > 1 { cx.emit_span_lint( diff --git a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs index 46039f6e5f67c..2815c5c239080 100644 --- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs +++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs @@ -69,6 +69,24 @@ impl<'a, 'tcx, T: Copy + Decodable<DecodeContext<'a, 'tcx>>> ProcessQueryValue<' } } +impl<'a, 'tcx, T: Copy + Decodable<DecodeContext<'a, 'tcx>>> + ProcessQueryValue<'tcx, ty::EarlyBinder<'tcx, &'tcx [T]>> + for Option<DecodeIterator<'a, 'tcx, T>> +{ + #[inline(always)] + fn process_decoded( + self, + tcx: TyCtxt<'tcx>, + _err: impl Fn() -> !, + ) -> ty::EarlyBinder<'tcx, &'tcx [T]> { + ty::EarlyBinder::bind(if let Some(iter) = self { + tcx.arena.alloc_from_iter(iter) + } else { + &[] + }) + } +} + impl<'a, 'tcx, T: Copy + Decodable<DecodeContext<'a, 'tcx>>> ProcessQueryValue<'tcx, Option<&'tcx [T]>> for Option<DecodeIterator<'a, 'tcx, T>> { diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs index 9c93726ca37f5..3fd6abbb36361 100644 --- a/compiler/rustc_metadata/src/rmeta/encoder.rs +++ b/compiler/rustc_metadata/src/rmeta/encoder.rs @@ -1443,8 +1443,10 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { } if let DefKind::Trait = def_kind { record!(self.tables.trait_def[def_id] <- self.tcx.trait_def(def_id)); - record!(self.tables.explicit_super_predicates_of[def_id] <- self.tcx.explicit_super_predicates_of(def_id)); - record!(self.tables.explicit_implied_predicates_of[def_id] <- self.tcx.explicit_implied_predicates_of(def_id)); + record_array!(self.tables.explicit_super_predicates_of[def_id] <- + self.tcx.explicit_super_predicates_of(def_id).skip_binder()); + record_array!(self.tables.explicit_implied_predicates_of[def_id] <- + self.tcx.explicit_implied_predicates_of(def_id).skip_binder()); let module_children = self.tcx.module_children_local(local_id); record_array!(self.tables.module_children_non_reexports[def_id] <- @@ -1452,8 +1454,10 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> { } if let DefKind::TraitAlias = def_kind { record!(self.tables.trait_def[def_id] <- self.tcx.trait_def(def_id)); - record!(self.tables.explicit_super_predicates_of[def_id] <- self.tcx.explicit_super_predicates_of(def_id)); - record!(self.tables.explicit_implied_predicates_of[def_id] <- self.tcx.explicit_implied_predicates_of(def_id)); + record_array!(self.tables.explicit_super_predicates_of[def_id] <- + self.tcx.explicit_super_predicates_of(def_id).skip_binder()); + record_array!(self.tables.explicit_implied_predicates_of[def_id] <- + self.tcx.explicit_implied_predicates_of(def_id).skip_binder()); } if let DefKind::Trait | DefKind::Impl { .. } = def_kind { let associated_item_def_ids = self.tcx.associated_item_def_ids(def_id); diff --git a/compiler/rustc_metadata/src/rmeta/mod.rs b/compiler/rustc_metadata/src/rmeta/mod.rs index c1b77172983fb..a84923130c3ab 100644 --- a/compiler/rustc_metadata/src/rmeta/mod.rs +++ b/compiler/rustc_metadata/src/rmeta/mod.rs @@ -419,10 +419,10 @@ define_tables! { lookup_deprecation_entry: Table<DefIndex, LazyValue<attr::Deprecation>>, explicit_predicates_of: Table<DefIndex, LazyValue<ty::GenericPredicates<'static>>>, generics_of: Table<DefIndex, LazyValue<ty::Generics>>, - explicit_super_predicates_of: Table<DefIndex, LazyValue<ty::GenericPredicates<'static>>>, + explicit_super_predicates_of: Table<DefIndex, LazyArray<(ty::Clause<'static>, Span)>>, // As an optimization, we only store this for trait aliases, // since it's identical to explicit_super_predicates_of for traits. - explicit_implied_predicates_of: Table<DefIndex, LazyValue<ty::GenericPredicates<'static>>>, + explicit_implied_predicates_of: Table<DefIndex, LazyArray<(ty::Clause<'static>, Span)>>, type_of: Table<DefIndex, LazyValue<ty::EarlyBinder<'static, Ty<'static>>>>, variances_of: Table<DefIndex, LazyArray<ty::Variance>>, fn_sig: Table<DefIndex, LazyValue<ty::EarlyBinder<'static, ty::PolyFnSig<'static>>>>, diff --git a/compiler/rustc_middle/src/query/mod.rs b/compiler/rustc_middle/src/query/mod.rs index c785b235f618b..d6bdc1af0d2ca 100644 --- a/compiler/rustc_middle/src/query/mod.rs +++ b/compiler/rustc_middle/src/query/mod.rs @@ -651,7 +651,7 @@ rustc_queries! { /// is a subset of the full list of predicates. We store these in a separate map /// because we must evaluate them even during type conversion, often before the full /// predicates are available (note that super-predicates must not be cyclic). - query explicit_super_predicates_of(key: DefId) -> ty::GenericPredicates<'tcx> { + query explicit_super_predicates_of(key: DefId) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { desc { |tcx| "computing the super predicates of `{}`", tcx.def_path_str(key) } cache_on_disk_if { key.is_local() } separate_provide_extern @@ -662,7 +662,7 @@ rustc_queries! { /// of the trait. For regular traits, this includes all super-predicates and their /// associated type bounds. For trait aliases, currently, this includes all of the /// predicates of the trait alias. - query explicit_implied_predicates_of(key: DefId) -> ty::GenericPredicates<'tcx> { + query explicit_implied_predicates_of(key: DefId) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { desc { |tcx| "computing the implied predicates of `{}`", tcx.def_path_str(key) } cache_on_disk_if { key.is_local() } separate_provide_extern @@ -671,7 +671,9 @@ rustc_queries! { /// The Ident is the name of an associated type.The query returns only the subset /// of supertraits that define the given associated type. This is used to avoid /// cycles in resolving type-dependent associated item paths like `T::Item`. - query explicit_supertraits_containing_assoc_item(key: (DefId, rustc_span::symbol::Ident)) -> ty::GenericPredicates<'tcx> { + query explicit_supertraits_containing_assoc_item( + key: (DefId, rustc_span::symbol::Ident) + ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { desc { |tcx| "computing the super traits of `{}` with associated type name `{}`", tcx.def_path_str(key.0), key.1 @@ -680,7 +682,9 @@ rustc_queries! { /// To avoid cycles within the predicates of a single item we compute /// per-type-parameter predicates for resolving `T::AssocTy`. - query type_param_predicates(key: (LocalDefId, LocalDefId, rustc_span::symbol::Ident)) -> ty::GenericPredicates<'tcx> { + query type_param_predicates( + key: (LocalDefId, LocalDefId, rustc_span::symbol::Ident) + ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { desc { |tcx| "computing the bounds for type parameter `{}`", tcx.hir().ty_param_name(key.1) } } diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs index 8effb67a1f695..59cacc5ef4181 100644 --- a/compiler/rustc_middle/src/ty/context.rs +++ b/compiler/rustc_middle/src/ty/context.rs @@ -349,16 +349,14 @@ impl<'tcx> Interner for TyCtxt<'tcx> { self, def_id: DefId, ) -> ty::EarlyBinder<'tcx, impl IntoIterator<Item = (ty::Clause<'tcx>, Span)>> { - ty::EarlyBinder::bind(self.explicit_super_predicates_of(def_id).instantiate_identity(self)) + self.explicit_super_predicates_of(def_id).map_bound(|preds| preds.into_iter().copied()) } fn explicit_implied_predicates_of( self, def_id: DefId, ) -> ty::EarlyBinder<'tcx, impl IntoIterator<Item = (ty::Clause<'tcx>, Span)>> { - ty::EarlyBinder::bind( - self.explicit_implied_predicates_of(def_id).instantiate_identity(self), - ) + self.explicit_implied_predicates_of(def_id).map_bound(|preds| preds.into_iter().copied()) } fn has_target_features(self, def_id: DefId) -> bool { diff --git a/compiler/rustc_trait_selection/src/traits/object_safety.rs b/compiler/rustc_trait_selection/src/traits/object_safety.rs index 8e1fc0d7fe687..eb01151baae10 100644 --- a/compiler/rustc_trait_selection/src/traits/object_safety.rs +++ b/compiler/rustc_trait_selection/src/traits/object_safety.rs @@ -185,12 +185,11 @@ fn predicates_reference_self( ) -> SmallVec<[Span; 1]> { let trait_ref = ty::Binder::dummy(ty::TraitRef::identity(tcx, trait_def_id)); let predicates = if supertraits_only { - tcx.explicit_super_predicates_of(trait_def_id) + tcx.explicit_super_predicates_of(trait_def_id).skip_binder() } else { - tcx.predicates_of(trait_def_id) + tcx.predicates_of(trait_def_id).predicates }; predicates - .predicates .iter() .map(|&(predicate, sp)| (predicate.instantiate_supertrait(tcx, trait_ref), sp)) .filter_map(|(clause, sp)| { @@ -266,9 +265,8 @@ fn super_predicates_have_non_lifetime_binders( return SmallVec::new(); } tcx.explicit_super_predicates_of(trait_def_id) - .predicates - .iter() - .filter_map(|(pred, span)| pred.has_non_region_bound_vars().then_some(*span)) + .iter_identity_copied() + .filter_map(|(pred, span)| pred.has_non_region_bound_vars().then_some(span)) .collect() } diff --git a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs index f19cd19c99a83..ceffc1d45c5ca 100644 --- a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs +++ b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs @@ -600,21 +600,19 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> { // Check supertraits hold. This is so that their associated type bounds // will be checked in the code below. - for super_trait in tcx + for (supertrait, _) in tcx .explicit_super_predicates_of(trait_predicate.def_id()) - .instantiate(tcx, trait_predicate.trait_ref.args) - .predicates - .into_iter() + .iter_instantiated_copied(tcx, trait_predicate.trait_ref.args) { - let normalized_super_trait = normalize_with_depth_to( + let normalized_supertrait = normalize_with_depth_to( self, obligation.param_env, obligation.cause.clone(), obligation.recursion_depth + 1, - super_trait, + supertrait, &mut nested, ); - nested.push(obligation.with(tcx, normalized_super_trait)); + nested.push(obligation.with(tcx, normalized_supertrait)); } let assoc_types: Vec<_> = tcx diff --git a/compiler/rustc_trait_selection/src/traits/util.rs b/compiler/rustc_trait_selection/src/traits/util.rs index 52f87699b164f..279f13896f48f 100644 --- a/compiler/rustc_trait_selection/src/traits/util.rs +++ b/compiler/rustc_trait_selection/src/traits/util.rs @@ -131,7 +131,7 @@ impl<'tcx> TraitAliasExpander<'tcx> { let predicates = tcx.explicit_super_predicates_of(trait_ref.def_id()); debug!(?predicates); - let items = predicates.predicates.iter().rev().filter_map(|(pred, span)| { + let items = predicates.skip_binder().iter().rev().filter_map(|(pred, span)| { pred.instantiate_supertrait(tcx, trait_ref) .as_trait_clause() .map(|trait_ref| item.clone_and_push(trait_ref.map_bound(|t| t.trait_ref), *span)) diff --git a/compiler/rustc_trait_selection/src/traits/vtable.rs b/compiler/rustc_trait_selection/src/traits/vtable.rs index 1729d8d307a51..fcd7371e2aa7a 100644 --- a/compiler/rustc_trait_selection/src/traits/vtable.rs +++ b/compiler/rustc_trait_selection/src/traits/vtable.rs @@ -120,8 +120,7 @@ fn prepare_vtable_segments_inner<'tcx, T>( let mut direct_super_traits_iter = tcx .explicit_super_predicates_of(inner_most_trait_ref.def_id()) - .predicates - .into_iter() + .iter_identity_copied() .filter_map(move |(pred, _)| { pred.instantiate_supertrait(tcx, inner_most_trait_ref).as_trait_clause() }); diff --git a/src/librustdoc/clean/simplify.rs b/src/librustdoc/clean/simplify.rs index 1d81ae3eb8ba7..c5e3aaab166da 100644 --- a/src/librustdoc/clean/simplify.rs +++ b/src/librustdoc/clean/simplify.rs @@ -14,7 +14,6 @@ use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::unord::UnordSet; use rustc_hir::def_id::DefId; -use rustc_middle::ty; use thin_vec::ThinVec; use crate::clean; @@ -113,18 +112,9 @@ fn trait_is_same_or_supertrait(cx: &DocContext<'_>, child: DefId, trait_: DefId) return true; } let predicates = cx.tcx.explicit_super_predicates_of(child); - debug_assert!(cx.tcx.generics_of(child).has_self); - let self_ty = cx.tcx.types.self_param; predicates - .predicates - .iter() - .filter_map(|(pred, _)| { - if let ty::ClauseKind::Trait(pred) = pred.kind().skip_binder() { - if pred.trait_ref.self_ty() == self_ty { Some(pred.def_id()) } else { None } - } else { - None - } - }) + .iter_identity_copied() + .filter_map(|(pred, _)| Some(pred.as_trait_clause()?.def_id())) .any(|did| trait_is_same_or_supertrait(cx, did, trait_)) } diff --git a/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs b/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs index 67b48878ca513..6794c6cabfeef 100644 --- a/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs +++ b/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs @@ -246,7 +246,7 @@ fn collect_supertrait_bounds<'tcx>(cx: &LateContext<'tcx>, bounds: GenericBounds && let [.., path] = poly_trait.trait_ref.path.segments && poly_trait.bound_generic_params.is_empty() && let Some(trait_def_id) = path.res.opt_def_id() - && let predicates = cx.tcx.explicit_super_predicates_of(trait_def_id).predicates + && let predicates = cx.tcx.explicit_super_predicates_of(trait_def_id).skip_binder() // If the trait has no supertrait, there is no need to collect anything from that bound && !predicates.is_empty() { diff --git a/src/tools/clippy/clippy_lints/src/methods/type_id_on_box.rs b/src/tools/clippy/clippy_lints/src/methods/type_id_on_box.rs index b62ecef0069af..db8cc4595d4df 100644 --- a/src/tools/clippy/clippy_lints/src/methods/type_id_on_box.rs +++ b/src/tools/clippy/clippy_lints/src/methods/type_id_on_box.rs @@ -25,8 +25,7 @@ fn is_subtrait_of_any(cx: &LateContext<'_>, ty: Ty<'_>) -> bool { || cx .tcx .explicit_super_predicates_of(tr.def_id) - .predicates - .iter() + .iter_identity_copied() .any(|(clause, _)| { matches!(clause.kind().skip_binder(), ty::ClauseKind::Trait(super_tr) if cx.tcx.is_diagnostic_item(sym::Any, super_tr.def_id())) diff --git a/src/tools/clippy/clippy_lints/src/needless_maybe_sized.rs b/src/tools/clippy/clippy_lints/src/needless_maybe_sized.rs index a1d8ec3b32ec9..62e41088f3708 100644 --- a/src/tools/clippy/clippy_lints/src/needless_maybe_sized.rs +++ b/src/tools/clippy/clippy_lints/src/needless_maybe_sized.rs @@ -91,7 +91,7 @@ fn path_to_sized_bound(cx: &LateContext<'_>, trait_bound: &PolyTraitRef<'_>) -> return true; } - for &(predicate, _) in cx.tcx.explicit_super_predicates_of(trait_def_id).predicates { + for (predicate, _) in cx.tcx.explicit_super_predicates_of(trait_def_id).iter_identity_copied() { if let ClauseKind::Trait(trait_predicate) = predicate.kind().skip_binder() && trait_predicate.polarity == PredicatePolarity::Positive && !path.contains(&trait_predicate.def_id()) From e20a888f8cb91d36634942fe5c1d6d38167d8fa3 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukastw97@gmail.com> Date: Thu, 29 Aug 2024 09:24:53 +0200 Subject: [PATCH 14/19] Allow running `./x.py test compiler` --- src/bootstrap/src/core/build_steps/test.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index cc01afd4c18c6..84a6b26a491ed 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -14,9 +14,8 @@ use crate::core::build_steps::synthetic_targets::MirOptPanicAbortSyntheticTarget use crate::core::build_steps::tool::{self, SourceType, Tool}; use crate::core::build_steps::toolstate::ToolState; use crate::core::build_steps::{compile, dist, llvm}; -use crate::core::builder; use crate::core::builder::{ - crate_description, Builder, Compiler, Kind, RunConfig, ShouldRun, Step, + self, crate_description, Alias, Builder, Compiler, Kind, RunConfig, ShouldRun, Step, }; use crate::core::config::flags::{get_completion, Subcommand}; use crate::core::config::TargetSelection; @@ -2435,18 +2434,14 @@ impl Step for CrateLibrustc { const ONLY_HOSTS: bool = true; fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { - run.crate_or_deps("rustc-main") + run.crate_or_deps("rustc-main").path("compiler") } fn make_run(run: RunConfig<'_>) { let builder = run.builder; let host = run.build_triple(); let compiler = builder.compiler_for(builder.top_stage, host, host); - let crates = run - .paths - .iter() - .map(|p| builder.crate_paths[&p.assert_single_path().path].clone()) - .collect(); + let crates = run.make_run_crates(Alias::Compiler); builder.ensure(CrateLibrustc { compiler, target: run.target, crates }); } From de34a9135069a559e3a2249b481fe551a5c1a8dd Mon Sep 17 00:00:00 2001 From: Ralf Jung <post@ralfj.de> Date: Thu, 29 Aug 2024 16:53:07 +0200 Subject: [PATCH 15/19] interpret/visitor: make memory order iteration slightly more efficient --- .../rustc_const_eval/src/interpret/visitor.rs | 19 ++++++++++--------- src/tools/miri/src/helpers.rs | 13 +++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/compiler/rustc_const_eval/src/interpret/visitor.rs b/compiler/rustc_const_eval/src/interpret/visitor.rs index fd649d608c691..b02f12e3c7f0b 100644 --- a/compiler/rustc_const_eval/src/interpret/visitor.rs +++ b/compiler/rustc_const_eval/src/interpret/visitor.rs @@ -25,14 +25,15 @@ pub trait ValueVisitor<'tcx, M: Machine<'tcx>>: Sized { } /// This function provides the chance to reorder the order in which fields are visited for - /// `FieldsShape::Aggregate`: The order of fields will be - /// `(0..num_fields).map(aggregate_field_order)`. + /// `FieldsShape::Aggregate`. /// - /// The default means we iterate in source declaration order; alternative this can do an inverse - /// lookup in `memory_index` to use memory field order instead. + /// The default means we iterate in source declaration order; alternatively this can do some + /// work with `memory_index` to iterate in memory order. #[inline(always)] - fn aggregate_field_order(_memory_index: &IndexVec<FieldIdx, u32>, idx: usize) -> usize { - idx + fn aggregate_field_iter( + memory_index: &IndexVec<FieldIdx, u32>, + ) -> impl Iterator<Item = FieldIdx> + 'static { + memory_index.indices() } // Recursive actions, ready to be overloaded. @@ -172,9 +173,9 @@ pub trait ValueVisitor<'tcx, M: Machine<'tcx>>: Sized { &FieldsShape::Union(fields) => { self.visit_union(v, fields)?; } - FieldsShape::Arbitrary { offsets, memory_index } => { - for idx in 0..offsets.len() { - let idx = Self::aggregate_field_order(memory_index, idx); + FieldsShape::Arbitrary { memory_index, .. } => { + for idx in Self::aggregate_field_iter(memory_index) { + let idx = idx.as_usize(); let field = self.ecx().project_field(v, idx)?; self.visit_field(v, idx, &field)?; } diff --git a/src/tools/miri/src/helpers.rs b/src/tools/miri/src/helpers.rs index 0483745621252..a546ad20fef98 100644 --- a/src/tools/miri/src/helpers.rs +++ b/src/tools/miri/src/helpers.rs @@ -630,14 +630,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { self.ecx } - fn aggregate_field_order(memory_index: &IndexVec<FieldIdx, u32>, idx: usize) -> usize { - // We need to do an *inverse* lookup: find the field that has position `idx` in memory order. - for (src_field, &mem_pos) in memory_index.iter_enumerated() { - if mem_pos as usize == idx { - return src_field.as_usize(); - } - } - panic!("invalid `memory_index`, could not find {}-th field in memory order", idx); + fn aggregate_field_iter( + memory_index: &IndexVec<FieldIdx, u32>, + ) -> impl Iterator<Item = FieldIdx> + 'static { + let inverse_memory_index = memory_index.invert_bijective_mapping(); + inverse_memory_index.into_iter() } // Hook to detect `UnsafeCell`. From 8c798c89dc6ba2c19a46390c5a3756f0f675350e Mon Sep 17 00:00:00 2001 From: Michael Goulet <michael@errs.io> Date: Wed, 28 Aug 2024 23:22:40 -0400 Subject: [PATCH 16/19] Simplify some extern providers --- compiler/rustc_metadata/src/rmeta/decoder.rs | 32 ------------------- .../src/rmeta/decoder/cstore_impl.rs | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/compiler/rustc_metadata/src/rmeta/decoder.rs b/compiler/rustc_metadata/src/rmeta/decoder.rs index 7321e2c760cef..d6227324125a2 100644 --- a/compiler/rustc_metadata/src/rmeta/decoder.rs +++ b/compiler/rustc_metadata/src/rmeta/decoder.rs @@ -1070,34 +1070,6 @@ impl<'a> CrateMetadataRef<'a> { ) } - fn get_explicit_item_bounds<'tcx>( - self, - index: DefIndex, - tcx: TyCtxt<'tcx>, - ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { - let lazy = self.root.tables.explicit_item_bounds.get(self, index); - let output = if lazy.is_default() { - &mut [] - } else { - tcx.arena.alloc_from_iter(lazy.decode((self, tcx))) - }; - ty::EarlyBinder::bind(&*output) - } - - fn get_explicit_item_super_predicates<'tcx>( - self, - index: DefIndex, - tcx: TyCtxt<'tcx>, - ) -> ty::EarlyBinder<'tcx, &'tcx [(ty::Clause<'tcx>, Span)]> { - let lazy = self.root.tables.explicit_item_super_predicates.get(self, index); - let output = if lazy.is_default() { - &mut [] - } else { - tcx.arena.alloc_from_iter(lazy.decode((self, tcx))) - }; - ty::EarlyBinder::bind(&*output) - } - fn get_variant( self, kind: DefKind, @@ -1323,10 +1295,6 @@ impl<'a> CrateMetadataRef<'a> { self.root.tables.optimized_mir.get(self, id).is_some() } - fn cross_crate_inlinable(self, id: DefIndex) -> bool { - self.root.tables.cross_crate_inlinable.get(self, id) - } - fn get_fn_has_self_parameter(self, id: DefIndex, sess: &'a Session) -> bool { self.root .tables diff --git a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs index 27625f791082d..f7be26d21fc96 100644 --- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs +++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs @@ -32,13 +32,20 @@ trait ProcessQueryValue<'tcx, T> { fn process_decoded(self, _tcx: TyCtxt<'tcx>, _err: impl Fn() -> !) -> T; } -impl<T> ProcessQueryValue<'_, Option<T>> for Option<T> { +impl<T> ProcessQueryValue<'_, T> for T { #[inline(always)] - fn process_decoded(self, _tcx: TyCtxt<'_>, _err: impl Fn() -> !) -> Option<T> { + fn process_decoded(self, _tcx: TyCtxt<'_>, _err: impl Fn() -> !) -> T { self } } +impl<'tcx, T> ProcessQueryValue<'tcx, ty::EarlyBinder<'tcx, T>> for T { + #[inline(always)] + fn process_decoded(self, _tcx: TyCtxt<'_>, _err: impl Fn() -> !) -> ty::EarlyBinder<'tcx, T> { + ty::EarlyBinder::bind(self) + } +} + impl<T> ProcessQueryValue<'_, T> for Option<T> { #[inline(always)] fn process_decoded(self, _tcx: TyCtxt<'_>, err: impl Fn() -> !) -> T { @@ -103,7 +110,12 @@ macro_rules! provide_one { provide_one! { $tcx, $def_id, $other, $cdata, $name => { let lazy = $cdata.root.tables.$name.get($cdata, $def_id.index); - if lazy.is_default() { &[] } else { $tcx.arena.alloc_from_iter(lazy.decode(($cdata, $tcx))) } + let value = if lazy.is_default() { + &[] as &[_] + } else { + $tcx.arena.alloc_from_iter(lazy.decode(($cdata, $tcx))) + }; + value.process_decoded($tcx, || panic!("{:?} does not have a {:?}", $def_id, stringify!($name))) } } }; @@ -212,15 +224,15 @@ impl IntoArgs for (CrateNum, SimplifiedType) { } provide! { tcx, def_id, other, cdata, - explicit_item_bounds => { cdata.get_explicit_item_bounds(def_id.index, tcx) } - explicit_item_super_predicates => { cdata.get_explicit_item_super_predicates(def_id.index, tcx) } + explicit_item_bounds => { table_defaulted_array } + explicit_item_super_predicates => { table_defaulted_array } explicit_predicates_of => { table } generics_of => { table } inferred_outlives_of => { table_defaulted_array } explicit_super_predicates_of => { table } explicit_implied_predicates_of => { table } type_of => { table } - type_alias_is_lazy => { cdata.root.tables.type_alias_is_lazy.get(cdata, def_id.index) } + type_alias_is_lazy => { table_direct } variances_of => { table } fn_sig => { table } codegen_fn_attrs => { table } @@ -241,7 +253,7 @@ provide! { tcx, def_id, other, cdata, lookup_default_body_stability => { table } lookup_deprecation_entry => { table } params_in_repr => { table } - unused_generic_params => { cdata.root.tables.unused_generic_params.get(cdata, def_id.index) } + unused_generic_params => { table_direct } def_kind => { cdata.def_kind(def_id.index) } impl_parent => { table } defaultness => { table_direct } @@ -287,9 +299,7 @@ provide! { tcx, def_id, other, cdata, .process_decoded(tcx, || panic!("{def_id:?} does not have trait_impl_trait_tys"))) } - associated_type_for_effects => { - table - } + associated_type_for_effects => { table } associated_types_for_impl_traits_in_associated_fn => { table_defaulted_array } visibility => { cdata.get_visibility(def_id.index) } @@ -310,7 +320,7 @@ provide! { tcx, def_id, other, cdata, item_attrs => { tcx.arena.alloc_from_iter(cdata.get_item_attrs(def_id.index, tcx.sess)) } is_mir_available => { cdata.is_item_mir_available(def_id.index) } is_ctfe_mir_available => { cdata.is_ctfe_mir_available(def_id.index) } - cross_crate_inlinable => { cdata.cross_crate_inlinable(def_id.index) } + cross_crate_inlinable => { table_direct } dylib_dependency_formats => { cdata.get_dylib_dependency_formats(tcx) } is_private_dep => { cdata.private_dep } From c824c1ada7278ac508143b21dc23aeb1ed7e05a5 Mon Sep 17 00:00:00 2001 From: Alex Crichton <alex@alexcrichton.com> Date: Thu, 29 Aug 2024 10:31:17 -0700 Subject: [PATCH 17/19] wasi: Fix sleeping for `Duration::MAX` This commit fixes an assert in the WASI-specific implementation of thread sleep to ensure that sleeping for a very large period of time blocks instead of panicking. This can come up when testing programs that sleep "forever", for example. --- library/std/src/sys/pal/wasi/thread.rs | 61 +++++++++++++------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/library/std/src/sys/pal/wasi/thread.rs b/library/std/src/sys/pal/wasi/thread.rs index 31c9cbd4699bd..4b83870fdea6c 100644 --- a/library/std/src/sys/pal/wasi/thread.rs +++ b/library/std/src/sys/pal/wasi/thread.rs @@ -136,36 +136,37 @@ impl Thread { } pub fn sleep(dur: Duration) { - let nanos = dur.as_nanos(); - assert!(nanos <= u64::MAX as u128); - - const USERDATA: wasi::Userdata = 0x0123_45678; - - let clock = wasi::SubscriptionClock { - id: wasi::CLOCKID_MONOTONIC, - timeout: nanos as u64, - precision: 0, - flags: 0, - }; - - let in_ = wasi::Subscription { - userdata: USERDATA, - u: wasi::SubscriptionU { tag: 0, u: wasi::SubscriptionUU { clock } }, - }; - unsafe { - let mut event: wasi::Event = mem::zeroed(); - let res = wasi::poll_oneoff(&in_, &mut event, 1); - match (res, event) { - ( - Ok(1), - wasi::Event { - userdata: USERDATA, - error: wasi::ERRNO_SUCCESS, - type_: wasi::EVENTTYPE_CLOCK, - .. - }, - ) => {} - _ => panic!("thread::sleep(): unexpected result of poll_oneoff"), + let mut nanos = dur.as_nanos(); + while nanos > 0 { + const USERDATA: wasi::Userdata = 0x0123_45678; + + let clock = wasi::SubscriptionClock { + id: wasi::CLOCKID_MONOTONIC, + timeout: u64::try_from(nanos).unwrap_or(u64::MAX), + precision: 0, + flags: 0, + }; + nanos -= u128::from(clock.timeout); + + let in_ = wasi::Subscription { + userdata: USERDATA, + u: wasi::SubscriptionU { tag: 0, u: wasi::SubscriptionUU { clock } }, + }; + unsafe { + let mut event: wasi::Event = mem::zeroed(); + let res = wasi::poll_oneoff(&in_, &mut event, 1); + match (res, event) { + ( + Ok(1), + wasi::Event { + userdata: USERDATA, + error: wasi::ERRNO_SUCCESS, + type_: wasi::EVENTTYPE_CLOCK, + .. + }, + ) => {} + _ => panic!("thread::sleep(): unexpected result of poll_oneoff"), + } } } } From 518b41c2bdaaf7d6a511802d212a6dc6e72ff7d0 Mon Sep 17 00:00:00 2001 From: Jubilee Young <workingjubilee@gmail.com> Date: Thu, 29 Aug 2024 12:13:19 -0700 Subject: [PATCH 18/19] Try latest backtrace --- library/backtrace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/backtrace b/library/backtrace index fc37b22dc8a38..230570f2dac80 160000 --- a/library/backtrace +++ b/library/backtrace @@ -1 +1 @@ -Subproject commit fc37b22dc8a384d93f6b7b4817266eec6433875e +Subproject commit 230570f2dac80a601f5c0b30da00cc9480bd35eb From fa4f8925f1170ccd86bc242f8566b6a74f63be16 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote <n.nethercote@gmail.com> Date: Thu, 29 Aug 2024 12:28:55 +1000 Subject: [PATCH 19/19] Remove `Option<!>` return types. Several compiler functions have `Option<!>` for their return type. That's odd. The only valid return value is `None`, so why is this type used? Because it lets you write certain patterns slightly more concisely. E.g. if you have these common patterns: ``` let Some(a) = f() else { return }; let Ok(b) = g() else { return }; ``` you can shorten them to these: ``` let a = f()?; let b = g().ok()?; ``` Huh. An `Option` return type typically designates success/failure. How should I interpret the type signature of a function that always returns (i.e. doesn't panic), does useful work (modifying `&mut` arguments), and yet only ever fails? This idiom subverts the type system for a cute syntactic trick. Furthermore, returning `Option<!>` from a function F makes things syntactically more convenient within F, but makes things worse at F's callsites. The callsites can themselves use `?` with F but should not, because they will get an unconditional early return, which is almost certainly not desirable. Instead the return value should be ignored. (Note that some of callsites of `process_operand`, `process_immedate`, `process_assign` actually do use `?`, though the early return doesn't matter in these cases because nothing of significance comes after those calls. Ugh.) When I first saw this pattern I had no idea how to interpret it, and it took me several minutes of close reading to understand everything I've written above. I even started a Zulip thread about it to make sure I understood it properly. "Save a few characters by introducing types so weird that compiler devs have to discuss it on Zulip" feels like a bad trade-off to me. This commit replaces all the `Option<!>` return values and uses `else`/`return` (or something similar) to replace the relevant `?` uses. The result is slightly more verbose but much easier to understand. --- .../src/dataflow_const_prop.rs | 12 +- .../rustc_mir_transform/src/jump_threading.rs | 110 +++++++++--------- .../src/known_panics_lint.rs | 12 +- 3 files changed, 68 insertions(+), 66 deletions(-) diff --git a/compiler/rustc_mir_transform/src/dataflow_const_prop.rs b/compiler/rustc_mir_transform/src/dataflow_const_prop.rs index f207216d6f423..03b1d426df4ad 100644 --- a/compiler/rustc_mir_transform/src/dataflow_const_prop.rs +++ b/compiler/rustc_mir_transform/src/dataflow_const_prop.rs @@ -382,7 +382,7 @@ impl<'a, 'tcx> ConstAnalysis<'a, 'tcx> { place: PlaceIndex, mut operand: OpTy<'tcx>, projection: &[PlaceElem<'tcx>], - ) -> Option<!> { + ) { for &(mut proj_elem) in projection { if let PlaceElem::Index(index) = proj_elem { if let FlatSet::Elem(index) = state.get(index.into(), &self.map) @@ -391,10 +391,14 @@ impl<'a, 'tcx> ConstAnalysis<'a, 'tcx> { { proj_elem = PlaceElem::ConstantIndex { offset, min_length, from_end: false }; } else { - return None; + return; } } - operand = self.ecx.project(&operand, proj_elem).ok()?; + operand = if let Ok(operand) = self.ecx.project(&operand, proj_elem) { + operand + } else { + return; + } } self.map.for_each_projection_value( @@ -426,8 +430,6 @@ impl<'a, 'tcx> ConstAnalysis<'a, 'tcx> { } }, ); - - None } fn binary_op( diff --git a/compiler/rustc_mir_transform/src/jump_threading.rs b/compiler/rustc_mir_transform/src/jump_threading.rs index 96c52845a4a39..8997b1d57cd91 100644 --- a/compiler/rustc_mir_transform/src/jump_threading.rs +++ b/compiler/rustc_mir_transform/src/jump_threading.rs @@ -191,26 +191,26 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { /// Recursion entry point to find threading opportunities. #[instrument(level = "trace", skip(self))] - fn start_from_switch(&mut self, bb: BasicBlock) -> Option<!> { + fn start_from_switch(&mut self, bb: BasicBlock) { let bbdata = &self.body[bb]; if bbdata.is_cleanup || self.loop_headers.contains(bb) { - return None; + return; } - let (discr, targets) = bbdata.terminator().kind.as_switch()?; - let discr = discr.place()?; + let Some((discr, targets)) = bbdata.terminator().kind.as_switch() else { return }; + let Some(discr) = discr.place() else { return }; debug!(?discr, ?bb); let discr_ty = discr.ty(self.body, self.tcx).ty; - let discr_layout = self.ecx.layout_of(discr_ty).ok()?; + let Ok(discr_layout) = self.ecx.layout_of(discr_ty) else { return }; - let discr = self.map.find(discr.as_ref())?; + let Some(discr) = self.map.find(discr.as_ref()) else { return }; debug!(?discr); let cost = CostChecker::new(self.tcx, self.param_env, None, self.body); let mut state = State::new_reachable(); let conds = if let Some((value, then, else_)) = targets.as_static_if() { - let value = ScalarInt::try_from_uint(value, discr_layout.size)?; + let Some(value) = ScalarInt::try_from_uint(value, discr_layout.size) else { return }; self.arena.alloc_from_iter([ Condition { value, polarity: Polarity::Eq, target: then }, Condition { value, polarity: Polarity::Ne, target: else_ }, @@ -225,7 +225,6 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { state.insert_value_idx(discr, conds, self.map); self.find_opportunity(bb, state, cost, 0); - None } /// Recursively walk statements backwards from this bb's terminator to find threading @@ -364,18 +363,17 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { lhs: PlaceIndex, rhs: ImmTy<'tcx>, state: &mut State<ConditionSet<'a>>, - ) -> Option<!> { + ) { let register_opportunity = |c: Condition| { debug!(?bb, ?c.target, "register"); self.opportunities.push(ThreadingOpportunity { chain: vec![bb], target: c.target }) }; - let conditions = state.try_get_idx(lhs, self.map)?; - if let Immediate::Scalar(Scalar::Int(int)) = *rhs { + if let Some(conditions) = state.try_get_idx(lhs, self.map) + && let Immediate::Scalar(Scalar::Int(int)) = *rhs + { conditions.iter_matches(int).for_each(register_opportunity); } - - None } /// If we expect `lhs ?= A`, we have an opportunity if we assume `constant == A`. @@ -428,22 +426,23 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { lhs: PlaceIndex, rhs: &Operand<'tcx>, state: &mut State<ConditionSet<'a>>, - ) -> Option<!> { + ) { match rhs { // If we expect `lhs ?= A`, we have an opportunity if we assume `constant == A`. Operand::Constant(constant) => { - let constant = - self.ecx.eval_mir_constant(&constant.const_, constant.span, None).ok()?; + let Ok(constant) = + self.ecx.eval_mir_constant(&constant.const_, constant.span, None) + else { + return; + }; self.process_constant(bb, lhs, constant, state); } // Transfer the conditions on the copied rhs. Operand::Move(rhs) | Operand::Copy(rhs) => { - let rhs = self.map.find(rhs.as_ref())?; + let Some(rhs) = self.map.find(rhs.as_ref()) else { return }; state.insert_place_idx(rhs, lhs, self.map); } } - - None } #[instrument(level = "trace", skip(self))] @@ -453,16 +452,14 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { lhs_place: &Place<'tcx>, rhs: &Rvalue<'tcx>, state: &mut State<ConditionSet<'a>>, - ) -> Option<!> { - let lhs = self.map.find(lhs_place.as_ref())?; + ) { + let Some(lhs) = self.map.find(lhs_place.as_ref()) else { return }; match rhs { - Rvalue::Use(operand) => self.process_operand(bb, lhs, operand, state)?, + Rvalue::Use(operand) => self.process_operand(bb, lhs, operand, state), // Transfer the conditions on the copy rhs. - Rvalue::CopyForDeref(rhs) => { - self.process_operand(bb, lhs, &Operand::Copy(*rhs), state)? - } + Rvalue::CopyForDeref(rhs) => self.process_operand(bb, lhs, &Operand::Copy(*rhs), state), Rvalue::Discriminant(rhs) => { - let rhs = self.map.find_discr(rhs.as_ref())?; + let Some(rhs) = self.map.find_discr(rhs.as_ref()) else { return }; state.insert_place_idx(rhs, lhs, self.map); } // If we expect `lhs ?= A`, we have an opportunity if we assume `constant == A`. @@ -470,7 +467,7 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { let agg_ty = lhs_place.ty(self.body, self.tcx).ty; let lhs = match kind { // Do not support unions. - AggregateKind::Adt(.., Some(_)) => return None, + AggregateKind::Adt(.., Some(_)) => return, AggregateKind::Adt(_, variant_index, ..) if agg_ty.is_enum() => { if let Some(discr_target) = self.map.apply(lhs, TrackElem::Discriminant) && let Ok(discr_value) = @@ -478,7 +475,11 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { { self.process_immediate(bb, discr_target, discr_value, state); } - self.map.apply(lhs, TrackElem::Variant(*variant_index))? + if let Some(idx) = self.map.apply(lhs, TrackElem::Variant(*variant_index)) { + idx + } else { + return; + } } _ => lhs, }; @@ -490,8 +491,8 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { } // Transfer the conditions on the copy rhs, after inversing polarity. Rvalue::UnaryOp(UnOp::Not, Operand::Move(place) | Operand::Copy(place)) => { - let conditions = state.try_get_idx(lhs, self.map)?; - let place = self.map.find(place.as_ref())?; + let Some(conditions) = state.try_get_idx(lhs, self.map) else { return }; + let Some(place) = self.map.find(place.as_ref()) else { return }; let conds = conditions.map(self.arena, Condition::inv); state.insert_value_idx(place, conds, self.map); } @@ -502,21 +503,25 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { box (Operand::Move(place) | Operand::Copy(place), Operand::Constant(value)) | box (Operand::Constant(value), Operand::Move(place) | Operand::Copy(place)), ) => { - let conditions = state.try_get_idx(lhs, self.map)?; - let place = self.map.find(place.as_ref())?; + let Some(conditions) = state.try_get_idx(lhs, self.map) else { return }; + let Some(place) = self.map.find(place.as_ref()) else { return }; let equals = match op { BinOp::Eq => ScalarInt::TRUE, BinOp::Ne => ScalarInt::FALSE, - _ => return None, + _ => return, }; if value.const_.ty().is_floating_point() { // Floating point equality does not follow bit-patterns. // -0.0 and NaN both have special rules for equality, // and therefore we cannot use integer comparisons for them. // Avoid handling them, though this could be extended in the future. - return None; + return; } - let value = value.const_.normalize(self.tcx, self.param_env).try_to_scalar_int()?; + let Some(value) = + value.const_.normalize(self.tcx, self.param_env).try_to_scalar_int() + else { + return; + }; let conds = conditions.map(self.arena, |c| Condition { value, polarity: if c.matches(equals) { Polarity::Eq } else { Polarity::Ne }, @@ -527,8 +532,6 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { _ => {} } - - None } #[instrument(level = "trace", skip(self))] @@ -537,7 +540,7 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { bb: BasicBlock, stmt: &Statement<'tcx>, state: &mut State<ConditionSet<'a>>, - ) -> Option<!> { + ) { let register_opportunity = |c: Condition| { debug!(?bb, ?c.target, "register"); self.opportunities.push(ThreadingOpportunity { chain: vec![bb], target: c.target }) @@ -550,12 +553,12 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { // If we expect `discriminant(place) ?= A`, // we have an opportunity if `variant_index ?= A`. StatementKind::SetDiscriminant { box place, variant_index } => { - let discr_target = self.map.find_discr(place.as_ref())?; + let Some(discr_target) = self.map.find_discr(place.as_ref()) else { return }; let enum_ty = place.ty(self.body, self.tcx).ty; // `SetDiscriminant` may be a no-op if the assigned variant is the untagged variant // of a niche encoding. If we cannot ensure that we write to the discriminant, do // nothing. - let enum_layout = self.ecx.layout_of(enum_ty).ok()?; + let Ok(enum_layout) = self.ecx.layout_of(enum_ty) else { return }; let writes_discriminant = match enum_layout.variants { Variants::Single { index } => { assert_eq!(index, *variant_index); @@ -568,24 +571,25 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { } => *variant_index != untagged_variant, }; if writes_discriminant { - let discr = self.ecx.discriminant_for_variant(enum_ty, *variant_index).ok()?; - self.process_immediate(bb, discr_target, discr, state)?; + let Ok(discr) = self.ecx.discriminant_for_variant(enum_ty, *variant_index) + else { + return; + }; + self.process_immediate(bb, discr_target, discr, state); } } // If we expect `lhs ?= true`, we have an opportunity if we assume `lhs == true`. StatementKind::Intrinsic(box NonDivergingIntrinsic::Assume( Operand::Copy(place) | Operand::Move(place), )) => { - let conditions = state.try_get(place.as_ref(), self.map)?; + let Some(conditions) = state.try_get(place.as_ref(), self.map) else { return }; conditions.iter_matches(ScalarInt::TRUE).for_each(register_opportunity); } StatementKind::Assign(box (lhs_place, rhs)) => { - self.process_assign(bb, lhs_place, rhs, state)?; + self.process_assign(bb, lhs_place, rhs, state); } _ => {} } - - None } #[instrument(level = "trace", skip(self, state, cost))] @@ -638,17 +642,17 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { targets: &SwitchTargets, target_bb: BasicBlock, state: &mut State<ConditionSet<'a>>, - ) -> Option<!> { + ) { debug_assert_ne!(target_bb, START_BLOCK); debug_assert_eq!(self.body.basic_blocks.predecessors()[target_bb].len(), 1); - let discr = discr.place()?; + let Some(discr) = discr.place() else { return }; let discr_ty = discr.ty(self.body, self.tcx).ty; - let discr_layout = self.ecx.layout_of(discr_ty).ok()?; - let conditions = state.try_get(discr.as_ref(), self.map)?; + let Ok(discr_layout) = self.ecx.layout_of(discr_ty) else { return }; + let Some(conditions) = state.try_get(discr.as_ref(), self.map) else { return }; if let Some((value, _)) = targets.iter().find(|&(_, target)| target == target_bb) { - let value = ScalarInt::try_from_uint(value, discr_layout.size)?; + let Some(value) = ScalarInt::try_from_uint(value, discr_layout.size) else { return }; debug_assert_eq!(targets.iter().filter(|&(_, target)| target == target_bb).count(), 1); // We are inside `target_bb`. Since we have a single predecessor, we know we passed @@ -662,7 +666,7 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { } else if let Some((value, _, else_bb)) = targets.as_static_if() && target_bb == else_bb { - let value = ScalarInt::try_from_uint(value, discr_layout.size)?; + let Some(value) = ScalarInt::try_from_uint(value, discr_layout.size) else { return }; // We only know that `discr != value`. That's much weaker information than // the equality we had in the previous arm. All we can conclude is that @@ -675,8 +679,6 @@ impl<'tcx, 'a> TOFinder<'tcx, 'a> { } } } - - None } } diff --git a/compiler/rustc_mir_transform/src/known_panics_lint.rs b/compiler/rustc_mir_transform/src/known_panics_lint.rs index 7eed47cf2398e..3427b93c1f6fe 100644 --- a/compiler/rustc_mir_transform/src/known_panics_lint.rs +++ b/compiler/rustc_mir_transform/src/known_panics_lint.rs @@ -469,12 +469,12 @@ impl<'mir, 'tcx> ConstPropagator<'mir, 'tcx> { msg: &AssertKind<Operand<'tcx>>, cond: &Operand<'tcx>, location: Location, - ) -> Option<!> { - let value = &self.eval_operand(cond)?; + ) { + let Some(value) = &self.eval_operand(cond) else { return }; trace!("assertion on {:?} should be {:?}", value, expected); let expected = Scalar::from_bool(expected); - let value_const = self.use_ecx(|this| this.ecx.read_scalar(value))?; + let Some(value_const) = self.use_ecx(|this| this.ecx.read_scalar(value)) else { return }; if expected != value_const { // Poison all places this operand references so that further code @@ -516,14 +516,12 @@ impl<'mir, 'tcx> ConstPropagator<'mir, 'tcx> { AssertKind::BoundsCheck { len, index } } // Remaining overflow errors are already covered by checks on the binary operators. - AssertKind::Overflow(..) | AssertKind::OverflowNeg(_) => return None, + AssertKind::Overflow(..) | AssertKind::OverflowNeg(_) => return, // Need proper const propagator for these. - _ => return None, + _ => return, }; self.report_assert_as_lint(location, AssertLintKind::UnconditionalPanic, msg); } - - None } fn ensure_not_propagated(&self, local: Local) {