Merge commit 'b385428e3d
' into subtree-update_cg_gcc_2024-03-05
This commit is contained in:
commit
0d359efbe6
76 changed files with 7183 additions and 4278 deletions
|
@ -4,6 +4,7 @@
|
|||
* TODO(antoyo): support LTO (gcc's equivalent to Full LTO is -flto -flto-partition=one — https://documentation.suse.com/sbp/all/html/SBP-GCC-10/index.html).
|
||||
* For Thin LTO, this might be helpful:
|
||||
* In gcc 4.6 -fwhopr was removed and became default with -flto. The non-whopr path can still be executed via -flto-partition=none.
|
||||
* Or the new incremental LTO?
|
||||
*
|
||||
* Maybe some missing optizations enabled by rustc's LTO is in there: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
|
||||
* Like -fipa-icf (should be already enabled) and maybe -fdevirtualize-at-ltrans.
|
||||
|
@ -24,9 +25,10 @@
|
|||
hash_raw_entry
|
||||
)]
|
||||
#![allow(broken_intra_doc_links)]
|
||||
#![recursion_limit="256"]
|
||||
#![recursion_limit = "256"]
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![warn(unused_lifetimes)]
|
||||
#![deny(clippy::pattern_type_mismatch)]
|
||||
|
||||
extern crate rustc_apfloat;
|
||||
extern crate rustc_ast;
|
||||
|
@ -37,7 +39,8 @@ extern crate rustc_errors;
|
|||
extern crate rustc_fluent_macro;
|
||||
extern crate rustc_fs_util;
|
||||
extern crate rustc_hir;
|
||||
#[cfg(feature="master")]
|
||||
extern crate rustc_index;
|
||||
#[cfg(feature = "master")]
|
||||
extern crate rustc_interface;
|
||||
extern crate rustc_macros;
|
||||
extern crate rustc_metadata;
|
||||
|
@ -77,36 +80,40 @@ mod type_of;
|
|||
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
#[cfg(not(feature = "master"))]
|
||||
use std::sync::atomic::AtomicBool;
|
||||
#[cfg(not(feature = "master"))]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
#[cfg(not(feature="master"))]
|
||||
use std::sync::atomic::AtomicBool;
|
||||
#[cfg(not(feature="master"))]
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use gccjit::{Context, OptimizationLevel};
|
||||
#[cfg(feature="master")]
|
||||
use gccjit::{TargetInfo, Version};
|
||||
#[cfg(not(feature="master"))]
|
||||
use gccjit::CType;
|
||||
use errors::LTONotSupported;
|
||||
#[cfg(not(feature = "master"))]
|
||||
use gccjit::CType;
|
||||
use gccjit::{Context, OptimizationLevel};
|
||||
#[cfg(feature = "master")]
|
||||
use gccjit::{TargetInfo, Version};
|
||||
use rustc_ast::expand::allocator::AllocatorKind;
|
||||
use rustc_codegen_ssa::{CodegenResults, CompiledModule, ModuleCodegen};
|
||||
use rustc_codegen_ssa::base::codegen_crate;
|
||||
use rustc_codegen_ssa::back::write::{CodegenContext, FatLtoInput, ModuleConfig, TargetMachineFactoryFn};
|
||||
use rustc_codegen_ssa::back::lto::{LtoModuleCodegen, SerializedModule, ThinModule};
|
||||
use rustc_codegen_ssa::back::write::{
|
||||
CodegenContext, FatLtoInput, ModuleConfig, TargetMachineFactoryFn,
|
||||
};
|
||||
use rustc_codegen_ssa::base::codegen_crate;
|
||||
use rustc_codegen_ssa::traits::{
|
||||
CodegenBackend, ExtraBackendMethods, ThinBufferMethods, WriteBackendMethods,
|
||||
};
|
||||
use rustc_codegen_ssa::{CodegenResults, CompiledModule, ModuleCodegen};
|
||||
use rustc_data_structures::fx::FxIndexMap;
|
||||
use rustc_data_structures::sync::IntoDynSyncSend;
|
||||
use rustc_codegen_ssa::traits::{CodegenBackend, ExtraBackendMethods, ThinBufferMethods, WriteBackendMethods};
|
||||
use rustc_errors::{ErrorGuaranteed, DiagCtxt};
|
||||
use rustc_errors::{DiagCtxt, ErrorGuaranteed};
|
||||
use rustc_metadata::EncodedMetadata;
|
||||
use rustc_middle::dep_graph::{WorkProduct, WorkProductId};
|
||||
use rustc_middle::util::Providers;
|
||||
use rustc_middle::ty::TyCtxt;
|
||||
use rustc_middle::util::Providers;
|
||||
use rustc_session::config::{Lto, OptLevel, OutputFilenames};
|
||||
use rustc_session::Session;
|
||||
use rustc_span::Symbol;
|
||||
use rustc_span::fatal_error::FatalError;
|
||||
use rustc_span::Symbol;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::back::lto::ModuleBuffer;
|
||||
|
@ -124,13 +131,13 @@ impl<F: Fn() -> String> Drop for PrintOnPanic<F> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature="master"))]
|
||||
#[cfg(not(feature = "master"))]
|
||||
#[derive(Debug)]
|
||||
pub struct TargetInfo {
|
||||
supports_128bit_integers: AtomicBool,
|
||||
}
|
||||
|
||||
#[cfg(not(feature="master"))]
|
||||
#[cfg(not(feature = "master"))]
|
||||
impl TargetInfo {
|
||||
fn cpu_supports(&self, _feature: &str) -> bool {
|
||||
false
|
||||
|
@ -173,26 +180,26 @@ impl CodegenBackend for GccCodegenBackend {
|
|||
}
|
||||
|
||||
fn init(&self, sess: &Session) {
|
||||
#[cfg(feature="master")]
|
||||
#[cfg(feature = "master")]
|
||||
{
|
||||
let target_cpu = target_cpu(sess);
|
||||
|
||||
// Get the second TargetInfo with the correct CPU features by setting the arch.
|
||||
let context = Context::default();
|
||||
if target_cpu != "generic" {
|
||||
context.add_command_line_option(&format!("-march={}", target_cpu));
|
||||
context.add_command_line_option(format!("-march={}", target_cpu));
|
||||
}
|
||||
|
||||
**self.target_info.info.lock().expect("lock") = context.get_target_info();
|
||||
}
|
||||
|
||||
#[cfg(feature="master")]
|
||||
#[cfg(feature = "master")]
|
||||
gccjit::set_global_personality_function_name(b"rust_eh_personality\0");
|
||||
if sess.lto() == Lto::Thin {
|
||||
sess.dcx().emit_warn(LTONotSupported {});
|
||||
}
|
||||
|
||||
#[cfg(not(feature="master"))]
|
||||
#[cfg(not(feature = "master"))]
|
||||
{
|
||||
let temp_dir = TempDir::new().expect("cannot create temporary directory");
|
||||
let temp_file = temp_dir.into_path().join("result.asm");
|
||||
|
@ -200,39 +207,62 @@ impl CodegenBackend for GccCodegenBackend {
|
|||
check_context.set_print_errors_to_stderr(false);
|
||||
let _int128_ty = check_context.new_c_type(CType::UInt128t);
|
||||
// NOTE: we cannot just call compile() as this would require other files than libgccjit.so.
|
||||
check_context.compile_to_file(gccjit::OutputKind::Assembler, temp_file.to_str().expect("path to str"));
|
||||
self.target_info.info.lock().expect("lock").supports_128bit_integers.store(check_context.get_last_error() == Ok(None), Ordering::SeqCst);
|
||||
check_context.compile_to_file(
|
||||
gccjit::OutputKind::Assembler,
|
||||
temp_file.to_str().expect("path to str"),
|
||||
);
|
||||
self.target_info
|
||||
.info
|
||||
.lock()
|
||||
.expect("lock")
|
||||
.supports_128bit_integers
|
||||
.store(check_context.get_last_error() == Ok(None), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
fn provide(&self, providers: &mut Providers) {
|
||||
providers.global_backend_features =
|
||||
|tcx, ()| gcc_util::global_gcc_features(tcx.sess, true)
|
||||
providers.global_backend_features = |tcx, ()| gcc_util::global_gcc_features(tcx.sess, true)
|
||||
}
|
||||
|
||||
fn codegen_crate<'tcx>(&self, tcx: TyCtxt<'tcx>, metadata: EncodedMetadata, need_metadata_module: bool) -> Box<dyn Any> {
|
||||
fn codegen_crate(
|
||||
&self,
|
||||
tcx: TyCtxt<'_>,
|
||||
metadata: EncodedMetadata,
|
||||
need_metadata_module: bool,
|
||||
) -> Box<dyn Any> {
|
||||
let target_cpu = target_cpu(tcx.sess);
|
||||
let res = codegen_crate(self.clone(), tcx, target_cpu.to_string(), metadata, need_metadata_module);
|
||||
let res = codegen_crate(
|
||||
self.clone(),
|
||||
tcx,
|
||||
target_cpu.to_string(),
|
||||
metadata,
|
||||
need_metadata_module,
|
||||
);
|
||||
|
||||
Box::new(res)
|
||||
}
|
||||
|
||||
fn join_codegen(&self, ongoing_codegen: Box<dyn Any>, sess: &Session, _outputs: &OutputFilenames) -> (CodegenResults, FxIndexMap<WorkProductId, WorkProduct>) {
|
||||
fn join_codegen(
|
||||
&self,
|
||||
ongoing_codegen: Box<dyn Any>,
|
||||
sess: &Session,
|
||||
_outputs: &OutputFilenames,
|
||||
) -> (CodegenResults, FxIndexMap<WorkProductId, WorkProduct>) {
|
||||
ongoing_codegen
|
||||
.downcast::<rustc_codegen_ssa::back::write::OngoingCodegen<GccCodegenBackend>>()
|
||||
.expect("Expected GccCodegenBackend's OngoingCodegen, found Box<Any>")
|
||||
.join(sess)
|
||||
}
|
||||
|
||||
fn link(&self, sess: &Session, codegen_results: CodegenResults, outputs: &OutputFilenames) -> Result<(), ErrorGuaranteed> {
|
||||
fn link(
|
||||
&self,
|
||||
sess: &Session,
|
||||
codegen_results: CodegenResults,
|
||||
outputs: &OutputFilenames,
|
||||
) -> Result<(), ErrorGuaranteed> {
|
||||
use rustc_codegen_ssa::back::link::link_binary;
|
||||
|
||||
link_binary(
|
||||
sess,
|
||||
&crate::archive::ArArchiveBuilderBuilder,
|
||||
&codegen_results,
|
||||
outputs,
|
||||
)
|
||||
link_binary(sess, &crate::archive::ArArchiveBuilderBuilder, &codegen_results, outputs)
|
||||
}
|
||||
|
||||
fn target_features(&self, sess: &Session, allow_unstable: bool) -> Vec<Symbol> {
|
||||
|
@ -245,13 +275,15 @@ fn new_context<'gcc, 'tcx>(tcx: TyCtxt<'tcx>) -> Context<'gcc> {
|
|||
if tcx.sess.target.arch == "x86" || tcx.sess.target.arch == "x86_64" {
|
||||
context.add_command_line_option("-masm=intel");
|
||||
}
|
||||
#[cfg(feature="master")]
|
||||
#[cfg(feature = "master")]
|
||||
{
|
||||
context.set_special_chars_allowed_in_func_names("$.*");
|
||||
let version = Version::get();
|
||||
let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
|
||||
context.set_output_ident(&format!("rustc version {} with libgccjit {}",
|
||||
rustc_interface::util::rustc_version_str().unwrap_or("unknown version"),
|
||||
version,
|
||||
context.set_output_ident(&format!(
|
||||
"rustc version {} with libgccjit {}",
|
||||
rustc_interface::util::rustc_version_str().unwrap_or("unknown version"),
|
||||
version,
|
||||
));
|
||||
}
|
||||
// TODO(antoyo): check if this should only be added when using -Cforce-unwind-tables=n.
|
||||
|
@ -260,26 +292,41 @@ fn new_context<'gcc, 'tcx>(tcx: TyCtxt<'tcx>) -> Context<'gcc> {
|
|||
}
|
||||
|
||||
impl ExtraBackendMethods for GccCodegenBackend {
|
||||
fn codegen_allocator<'tcx>(&self, tcx: TyCtxt<'tcx>, module_name: &str, kind: AllocatorKind, alloc_error_handler_kind: AllocatorKind) -> Self::Module {
|
||||
fn codegen_allocator(
|
||||
&self,
|
||||
tcx: TyCtxt<'_>,
|
||||
module_name: &str,
|
||||
kind: AllocatorKind,
|
||||
alloc_error_handler_kind: AllocatorKind,
|
||||
) -> Self::Module {
|
||||
let mut mods = GccContext {
|
||||
context: new_context(tcx),
|
||||
should_combine_object_files: false,
|
||||
temp_dir: None,
|
||||
};
|
||||
|
||||
unsafe { allocator::codegen(tcx, &mut mods, module_name, kind, alloc_error_handler_kind); }
|
||||
unsafe {
|
||||
allocator::codegen(tcx, &mut mods, module_name, kind, alloc_error_handler_kind);
|
||||
}
|
||||
mods
|
||||
}
|
||||
|
||||
fn compile_codegen_unit(&self, tcx: TyCtxt<'_>, cgu_name: Symbol) -> (ModuleCodegen<Self::Module>, u64) {
|
||||
fn compile_codegen_unit(
|
||||
&self,
|
||||
tcx: TyCtxt<'_>,
|
||||
cgu_name: Symbol,
|
||||
) -> (ModuleCodegen<Self::Module>, u64) {
|
||||
base::compile_codegen_unit(tcx, cgu_name, self.target_info.clone())
|
||||
}
|
||||
|
||||
fn target_machine_factory(&self, _sess: &Session, _opt_level: OptLevel, _features: &[String]) -> TargetMachineFactoryFn<Self> {
|
||||
fn target_machine_factory(
|
||||
&self,
|
||||
_sess: &Session,
|
||||
_opt_level: OptLevel,
|
||||
_features: &[String],
|
||||
) -> TargetMachineFactoryFn<Self> {
|
||||
// TODO(antoyo): set opt level.
|
||||
Arc::new(|_| {
|
||||
Ok(())
|
||||
})
|
||||
Arc::new(|_| Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,11 +357,19 @@ impl WriteBackendMethods for GccCodegenBackend {
|
|||
type ThinData = ();
|
||||
type ThinBuffer = ThinBuffer;
|
||||
|
||||
fn run_fat_lto(cgcx: &CodegenContext<Self>, modules: Vec<FatLtoInput<Self>>, cached_modules: Vec<(SerializedModule<Self::ModuleBuffer>, WorkProduct)>) -> Result<LtoModuleCodegen<Self>, FatalError> {
|
||||
fn run_fat_lto(
|
||||
cgcx: &CodegenContext<Self>,
|
||||
modules: Vec<FatLtoInput<Self>>,
|
||||
cached_modules: Vec<(SerializedModule<Self::ModuleBuffer>, WorkProduct)>,
|
||||
) -> Result<LtoModuleCodegen<Self>, FatalError> {
|
||||
back::lto::run_fat(cgcx, modules, cached_modules)
|
||||
}
|
||||
|
||||
fn run_thin_lto(_cgcx: &CodegenContext<Self>, _modules: Vec<(String, Self::ThinBuffer)>, _cached_modules: Vec<(SerializedModule<Self::ModuleBuffer>, WorkProduct)>) -> Result<(Vec<LtoModuleCodegen<Self>>, Vec<WorkProduct>), FatalError> {
|
||||
fn run_thin_lto(
|
||||
_cgcx: &CodegenContext<Self>,
|
||||
_modules: Vec<(String, Self::ThinBuffer)>,
|
||||
_cached_modules: Vec<(SerializedModule<Self::ModuleBuffer>, WorkProduct)>,
|
||||
) -> Result<(Vec<LtoModuleCodegen<Self>>, Vec<WorkProduct>), FatalError> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
|
@ -326,21 +381,37 @@ impl WriteBackendMethods for GccCodegenBackend {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
unsafe fn optimize(_cgcx: &CodegenContext<Self>, _dcx: &DiagCtxt, module: &ModuleCodegen<Self::Module>, config: &ModuleConfig) -> Result<(), FatalError> {
|
||||
unsafe fn optimize(
|
||||
_cgcx: &CodegenContext<Self>,
|
||||
_dcx: &DiagCtxt,
|
||||
module: &ModuleCodegen<Self::Module>,
|
||||
config: &ModuleConfig,
|
||||
) -> Result<(), FatalError> {
|
||||
module.module_llvm.context.set_optimization_level(to_gcc_opt_level(config.opt_level));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn optimize_fat(_cgcx: &CodegenContext<Self>, _module: &mut ModuleCodegen<Self::Module>) -> Result<(), FatalError> {
|
||||
fn optimize_fat(
|
||||
_cgcx: &CodegenContext<Self>,
|
||||
_module: &mut ModuleCodegen<Self::Module>,
|
||||
) -> Result<(), FatalError> {
|
||||
// TODO(antoyo)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn optimize_thin(_cgcx: &CodegenContext<Self>, _thin: ThinModule<Self>) -> Result<ModuleCodegen<Self::Module>, FatalError> {
|
||||
unsafe fn optimize_thin(
|
||||
_cgcx: &CodegenContext<Self>,
|
||||
_thin: ThinModule<Self>,
|
||||
) -> Result<ModuleCodegen<Self::Module>, FatalError> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
unsafe fn codegen(cgcx: &CodegenContext<Self>, dcx: &DiagCtxt, module: ModuleCodegen<Self::Module>, config: &ModuleConfig) -> Result<CompiledModule, FatalError> {
|
||||
unsafe fn codegen(
|
||||
cgcx: &CodegenContext<Self>,
|
||||
dcx: &DiagCtxt,
|
||||
module: ModuleCodegen<Self::Module>,
|
||||
config: &ModuleConfig,
|
||||
) -> Result<CompiledModule, FatalError> {
|
||||
back::write::codegen(cgcx, dcx, module, config)
|
||||
}
|
||||
|
||||
|
@ -352,7 +423,11 @@ impl WriteBackendMethods for GccCodegenBackend {
|
|||
unimplemented!();
|
||||
}
|
||||
|
||||
fn run_link(cgcx: &CodegenContext<Self>, dcx: &DiagCtxt, modules: Vec<ModuleCodegen<Self::Module>>) -> Result<ModuleCodegen<Self::Module>, FatalError> {
|
||||
fn run_link(
|
||||
cgcx: &CodegenContext<Self>,
|
||||
dcx: &DiagCtxt,
|
||||
modules: Vec<ModuleCodegen<Self::Module>>,
|
||||
) -> Result<ModuleCodegen<Self::Module>, FatalError> {
|
||||
back::write::link(cgcx, dcx, modules)
|
||||
}
|
||||
}
|
||||
|
@ -360,56 +435,57 @@ impl WriteBackendMethods for GccCodegenBackend {
|
|||
/// This is the entrypoint for a hot plugged rustc_codegen_gccjit
|
||||
#[no_mangle]
|
||||
pub fn __rustc_codegen_backend() -> Box<dyn CodegenBackend> {
|
||||
#[cfg(feature="master")]
|
||||
#[cfg(feature = "master")]
|
||||
let info = {
|
||||
// Check whether the target supports 128-bit integers.
|
||||
let context = Context::default();
|
||||
Arc::new(Mutex::new(IntoDynSyncSend(context.get_target_info())))
|
||||
};
|
||||
#[cfg(not(feature="master"))]
|
||||
#[cfg(not(feature = "master"))]
|
||||
let info = Arc::new(Mutex::new(IntoDynSyncSend(TargetInfo {
|
||||
supports_128bit_integers: AtomicBool::new(false),
|
||||
})));
|
||||
|
||||
Box::new(GccCodegenBackend {
|
||||
target_info: LockedTargetInfo { info },
|
||||
})
|
||||
Box::new(GccCodegenBackend { target_info: LockedTargetInfo { info } })
|
||||
}
|
||||
|
||||
fn to_gcc_opt_level(optlevel: Option<OptLevel>) -> OptimizationLevel {
|
||||
match optlevel {
|
||||
None => OptimizationLevel::None,
|
||||
Some(level) => {
|
||||
match level {
|
||||
OptLevel::No => OptimizationLevel::None,
|
||||
OptLevel::Less => OptimizationLevel::Limited,
|
||||
OptLevel::Default => OptimizationLevel::Standard,
|
||||
OptLevel::Aggressive => OptimizationLevel::Aggressive,
|
||||
OptLevel::Size | OptLevel::SizeMin => OptimizationLevel::Limited,
|
||||
}
|
||||
Some(level) => match level {
|
||||
OptLevel::No => OptimizationLevel::None,
|
||||
OptLevel::Less => OptimizationLevel::Limited,
|
||||
OptLevel::Default => OptimizationLevel::Standard,
|
||||
OptLevel::Aggressive => OptimizationLevel::Aggressive,
|
||||
OptLevel::Size | OptLevel::SizeMin => OptimizationLevel::Limited,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_features(sess: &Session, allow_unstable: bool, target_info: &LockedTargetInfo) -> Vec<Symbol> {
|
||||
sess
|
||||
.target
|
||||
pub fn target_features(
|
||||
sess: &Session,
|
||||
allow_unstable: bool,
|
||||
target_info: &LockedTargetInfo,
|
||||
) -> Vec<Symbol> {
|
||||
sess.target
|
||||
.supported_target_features()
|
||||
.iter()
|
||||
.filter_map(
|
||||
|&(feature, gate)| {
|
||||
if sess.is_nightly_build() || allow_unstable || gate.is_stable() { Some(feature) } else { None }
|
||||
},
|
||||
)
|
||||
.filter_map(|&(feature, gate)| {
|
||||
if sess.is_nightly_build() || allow_unstable || gate.is_stable() {
|
||||
Some(feature)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter(|_feature| {
|
||||
target_info.cpu_supports(_feature)
|
||||
/*
|
||||
adx, aes, avx, avx2, avx512bf16, avx512bitalg, avx512bw, avx512cd, avx512dq, avx512er, avx512f, avx512fp16, avx512ifma,
|
||||
avx512pf, avx512vbmi, avx512vbmi2, avx512vl, avx512vnni, avx512vp2intersect, avx512vpopcntdq,
|
||||
bmi1, bmi2, cmpxchg16b, ermsb, f16c, fma, fxsr, gfni, lzcnt, movbe, pclmulqdq, popcnt, rdrand, rdseed, rtm,
|
||||
sha, sse, sse2, sse3, sse4.1, sse4.2, sse4a, ssse3, tbm, vaes, vpclmulqdq, xsave, xsavec, xsaveopt, xsaves
|
||||
*/
|
||||
adx, aes, avx, avx2, avx512bf16, avx512bitalg, avx512bw, avx512cd, avx512dq, avx512er, avx512f, avx512fp16, avx512ifma,
|
||||
avx512pf, avx512vbmi, avx512vbmi2, avx512vl, avx512vnni, avx512vp2intersect, avx512vpopcntdq,
|
||||
bmi1, bmi2, cmpxchg16b, ermsb, f16c, fma, fxsr, gfni, lzcnt, movbe, pclmulqdq, popcnt, rdrand, rdseed, rtm,
|
||||
sha, sse, sse2, sse3, sse4.1, sse4.2, sse4a, ssse3, tbm, vaes, vpclmulqdq, xsave, xsavec, xsaveopt, xsaves
|
||||
*/
|
||||
})
|
||||
.map(|feature| Symbol::intern(feature))
|
||||
.map(Symbol::intern)
|
||||
.collect()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue