Skip to content

Commit d759931

Browse files
authored
Rollup merge of #84339 - alexcrichton:llvm-fptoint-sat, r=nagisa
rustc: Use LLVM's new saturating float-to-int intrinsics This commit updates rustc, with an applicable LLVM version, to use LLVM's new `llvm.fpto{u,s}i.sat.*.*` intrinsics to implement saturating floating-point-to-int conversions. This results in a little bit tighter codegen for x86/x86_64, but the main purpose of this is to prepare for upcoming changes to the WebAssembly backend in LLVM where wasm's saturating float-to-int instructions will now be implemented with these intrinsics. This change allows simplifying a good deal of surrounding code, namely removing a lot of wasm-specific behavior. WebAssembly no longer has any special-casing of saturating arithmetic instructions and the need for `fptoint_may_trap` is gone and all handling code for that is now removed. This means that the only wasm-specific logic is in the `fpto{s,u}i` instructions which only get used for "out of bounds is undefined behavior". This does mean that for the WebAssembly target specifically the Rust compiler will no longer be 100% compatible with pre-LLVM 12 versions, but it seems like that's unlikely to be relied on by too many folks. Note that this change does immediately regress the codegen of saturating float-to-int casts on WebAssembly due to the specialization of the LLVM intrinsic not being present in our LLVM fork just yet. I'll be following up with an LLVM update to pull in those patches, but affects a few other SIMD things in flight for WebAssembly so I wanted to separate this change. Eventually the entire `cast_float_to_int` function can be removed when LLVM 12 is the minimum version, but that will require sinking the complexity of it into other backends such as Cranelfit.
2 parents b06e03c + de2a460 commit d759931

File tree

5 files changed

+86
-207
lines changed

5 files changed

+86
-207
lines changed

compiler/rustc_codegen_llvm/src/builder.rs

+28-65
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::common::Funclet;
22
use crate::context::CodegenCx;
33
use crate::llvm::{self, BasicBlock, False};
44
use crate::llvm::{AtomicOrdering, AtomicRmwBinOp, SynchronizationScope};
5+
use crate::llvm_util;
56
use crate::type_::Type;
67
use crate::type_of::LayoutLlvmExt;
78
use crate::value::Value;
@@ -16,7 +17,7 @@ use rustc_data_structures::small_c_str::SmallCStr;
1617
use rustc_hir::def_id::DefId;
1718
use rustc_middle::ty::layout::TyAndLayout;
1819
use rustc_middle::ty::{self, Ty, TyCtxt};
19-
use rustc_span::{sym, Span};
20+
use rustc_span::Span;
2021
use rustc_target::abi::{self, Align, Size};
2122
use rustc_target::spec::{HasTargetSpec, Target};
2223
use std::borrow::Cow;
@@ -669,81 +670,47 @@ impl BuilderMethods<'a, 'tcx> for Builder<'a, 'll, 'tcx> {
669670
}
670671

671672
fn fptoui_sat(&mut self, val: &'ll Value, dest_ty: &'ll Type) -> Option<&'ll Value> {
672-
// WebAssembly has saturating floating point to integer casts if the
673-
// `nontrapping-fptoint` target feature is activated. We'll use those if
674-
// they are available.
675-
if self.sess().target.arch == "wasm32"
676-
&& self.sess().target_features.contains(&sym::nontrapping_dash_fptoint)
677-
{
673+
if llvm_util::get_version() >= (12, 0, 0) {
678674
let src_ty = self.cx.val_ty(val);
679675
let float_width = self.cx.float_width(src_ty);
680676
let int_width = self.cx.int_width(dest_ty);
681-
let name = match (int_width, float_width) {
682-
(32, 32) => Some("llvm.wasm.trunc.saturate.unsigned.i32.f32"),
683-
(32, 64) => Some("llvm.wasm.trunc.saturate.unsigned.i32.f64"),
684-
(64, 32) => Some("llvm.wasm.trunc.saturate.unsigned.i64.f32"),
685-
(64, 64) => Some("llvm.wasm.trunc.saturate.unsigned.i64.f64"),
686-
_ => None,
687-
};
688-
if let Some(name) = name {
689-
let intrinsic = self.get_intrinsic(name);
690-
return Some(self.call(intrinsic, &[val], None));
691-
}
677+
let name = format!("llvm.fptoui.sat.i{}.f{}", int_width, float_width);
678+
let intrinsic = self.get_intrinsic(&name);
679+
return Some(self.call(intrinsic, &[val], None));
692680
}
681+
693682
None
694683
}
695684

696685
fn fptosi_sat(&mut self, val: &'ll Value, dest_ty: &'ll Type) -> Option<&'ll Value> {
697-
// WebAssembly has saturating floating point to integer casts if the
698-
// `nontrapping-fptoint` target feature is activated. We'll use those if
699-
// they are available.
700-
if self.sess().target.arch == "wasm32"
701-
&& self.sess().target_features.contains(&sym::nontrapping_dash_fptoint)
702-
{
686+
if llvm_util::get_version() >= (12, 0, 0) {
703687
let src_ty = self.cx.val_ty(val);
704688
let float_width = self.cx.float_width(src_ty);
705689
let int_width = self.cx.int_width(dest_ty);
706-
let name = match (int_width, float_width) {
707-
(32, 32) => Some("llvm.wasm.trunc.saturate.signed.i32.f32"),
708-
(32, 64) => Some("llvm.wasm.trunc.saturate.signed.i32.f64"),
709-
(64, 32) => Some("llvm.wasm.trunc.saturate.signed.i64.f32"),
710-
(64, 64) => Some("llvm.wasm.trunc.saturate.signed.i64.f64"),
711-
_ => None,
712-
};
713-
if let Some(name) = name {
714-
let intrinsic = self.get_intrinsic(name);
715-
return Some(self.call(intrinsic, &[val], None));
716-
}
690+
let name = format!("llvm.fptosi.sat.i{}.f{}", int_width, float_width);
691+
let intrinsic = self.get_intrinsic(&name);
692+
return Some(self.call(intrinsic, &[val], None));
717693
}
718-
None
719-
}
720694

721-
fn fptosui_may_trap(&self, val: &'ll Value, dest_ty: &'ll Type) -> bool {
722-
// Most of the time we'll be generating the `fptosi` or `fptoui`
723-
// instruction for floating-point-to-integer conversions. These
724-
// instructions by definition in LLVM do not trap. For the WebAssembly
725-
// target, however, we'll lower in some cases to intrinsic calls instead
726-
// which may trap. If we detect that this is a situation where we'll be
727-
// using the intrinsics then we report that the call map trap, which
728-
// callers might need to handle.
729-
if !self.wasm_and_missing_nontrapping_fptoint() {
730-
return false;
731-
}
732-
let src_ty = self.cx.val_ty(val);
733-
let float_width = self.cx.float_width(src_ty);
734-
let int_width = self.cx.int_width(dest_ty);
735-
matches!((int_width, float_width), (32, 32) | (32, 64) | (64, 32) | (64, 64))
695+
None
736696
}
737697

738698
fn fptoui(&mut self, val: &'ll Value, dest_ty: &'ll Type) -> &'ll Value {
739-
// When we can, use the native wasm intrinsics which have tighter
740-
// codegen. Note that this has a semantic difference in that the
741-
// intrinsic can trap whereas `fptoui` never traps. That difference,
742-
// however, is handled by `fptosui_may_trap` above.
699+
// On WebAssembly the `fptoui` and `fptosi` instructions currently have
700+
// poor codegen. The reason for this is that the corresponding wasm
701+
// instructions, `i32.trunc_f32_s` for example, will trap when the float
702+
// is out-of-bounds, infinity, or nan. This means that LLVM
703+
// automatically inserts control flow around `fptoui` and `fptosi`
704+
// because the LLVM instruction `fptoui` is defined as producing a
705+
// poison value, not having UB on out-of-bounds values.
743706
//
744-
// Note that we skip the wasm intrinsics for vector types where `fptoui`
745-
// must be used instead.
746-
if self.wasm_and_missing_nontrapping_fptoint() {
707+
// This method, however, is only used with non-saturating casts that
708+
// have UB on out-of-bounds values. This means that it's ok if we use
709+
// the raw wasm instruction since out-of-bounds values can do whatever
710+
// we like. To ensure that LLVM picks the right instruction we choose
711+
// the raw wasm intrinsic functions which avoid LLVM inserting all the
712+
// other control flow automatically.
713+
if self.sess().target.arch == "wasm32" {
747714
let src_ty = self.cx.val_ty(val);
748715
if self.cx.type_kind(src_ty) != TypeKind::Vector {
749716
let float_width = self.cx.float_width(src_ty);
@@ -765,7 +732,8 @@ impl BuilderMethods<'a, 'tcx> for Builder<'a, 'll, 'tcx> {
765732
}
766733

767734
fn fptosi(&mut self, val: &'ll Value, dest_ty: &'ll Type) -> &'ll Value {
768-
if self.wasm_and_missing_nontrapping_fptoint() {
735+
// see `fptoui` above for why wasm is different here
736+
if self.sess().target.arch == "wasm32" {
769737
let src_ty = self.cx.val_ty(val);
770738
if self.cx.type_kind(src_ty) != TypeKind::Vector {
771739
let float_width = self.cx.float_width(src_ty);
@@ -1419,9 +1387,4 @@ impl Builder<'a, 'll, 'tcx> {
14191387
llvm::LLVMAddIncoming(phi, &val, &bb, 1 as c_uint);
14201388
}
14211389
}
1422-
1423-
fn wasm_and_missing_nontrapping_fptoint(&self) -> bool {
1424-
self.sess().target.arch == "wasm32"
1425-
&& !self.sess().target_features.contains(&sym::nontrapping_dash_fptoint)
1426-
}
14271390
}

compiler/rustc_codegen_llvm/src/context.rs

+22-8
Original file line numberDiff line numberDiff line change
@@ -503,14 +503,6 @@ impl CodegenCx<'b, 'tcx> {
503503
let t_f32 = self.type_f32();
504504
let t_f64 = self.type_f64();
505505

506-
ifn!("llvm.wasm.trunc.saturate.unsigned.i32.f32", fn(t_f32) -> t_i32);
507-
ifn!("llvm.wasm.trunc.saturate.unsigned.i32.f64", fn(t_f64) -> t_i32);
508-
ifn!("llvm.wasm.trunc.saturate.unsigned.i64.f32", fn(t_f32) -> t_i64);
509-
ifn!("llvm.wasm.trunc.saturate.unsigned.i64.f64", fn(t_f64) -> t_i64);
510-
ifn!("llvm.wasm.trunc.saturate.signed.i32.f32", fn(t_f32) -> t_i32);
511-
ifn!("llvm.wasm.trunc.saturate.signed.i32.f64", fn(t_f64) -> t_i32);
512-
ifn!("llvm.wasm.trunc.saturate.signed.i64.f32", fn(t_f32) -> t_i64);
513-
ifn!("llvm.wasm.trunc.saturate.signed.i64.f64", fn(t_f64) -> t_i64);
514506
ifn!("llvm.wasm.trunc.unsigned.i32.f32", fn(t_f32) -> t_i32);
515507
ifn!("llvm.wasm.trunc.unsigned.i32.f64", fn(t_f64) -> t_i32);
516508
ifn!("llvm.wasm.trunc.unsigned.i64.f32", fn(t_f32) -> t_i64);
@@ -520,6 +512,28 @@ impl CodegenCx<'b, 'tcx> {
520512
ifn!("llvm.wasm.trunc.signed.i64.f32", fn(t_f32) -> t_i64);
521513
ifn!("llvm.wasm.trunc.signed.i64.f64", fn(t_f64) -> t_i64);
522514

515+
ifn!("llvm.fptosi.sat.i8.f32", fn(t_f32) -> t_i8);
516+
ifn!("llvm.fptosi.sat.i16.f32", fn(t_f32) -> t_i16);
517+
ifn!("llvm.fptosi.sat.i32.f32", fn(t_f32) -> t_i32);
518+
ifn!("llvm.fptosi.sat.i64.f32", fn(t_f32) -> t_i64);
519+
ifn!("llvm.fptosi.sat.i128.f32", fn(t_f32) -> t_i128);
520+
ifn!("llvm.fptosi.sat.i8.f64", fn(t_f64) -> t_i8);
521+
ifn!("llvm.fptosi.sat.i16.f64", fn(t_f64) -> t_i16);
522+
ifn!("llvm.fptosi.sat.i32.f64", fn(t_f64) -> t_i32);
523+
ifn!("llvm.fptosi.sat.i64.f64", fn(t_f64) -> t_i64);
524+
ifn!("llvm.fptosi.sat.i128.f64", fn(t_f64) -> t_i128);
525+
526+
ifn!("llvm.fptoui.sat.i8.f32", fn(t_f32) -> t_i8);
527+
ifn!("llvm.fptoui.sat.i16.f32", fn(t_f32) -> t_i16);
528+
ifn!("llvm.fptoui.sat.i32.f32", fn(t_f32) -> t_i32);
529+
ifn!("llvm.fptoui.sat.i64.f32", fn(t_f32) -> t_i64);
530+
ifn!("llvm.fptoui.sat.i128.f32", fn(t_f32) -> t_i128);
531+
ifn!("llvm.fptoui.sat.i8.f64", fn(t_f64) -> t_i8);
532+
ifn!("llvm.fptoui.sat.i16.f64", fn(t_f64) -> t_i16);
533+
ifn!("llvm.fptoui.sat.i32.f64", fn(t_f64) -> t_i32);
534+
ifn!("llvm.fptoui.sat.i64.f64", fn(t_f64) -> t_i64);
535+
ifn!("llvm.fptoui.sat.i128.f64", fn(t_f64) -> t_i128);
536+
523537
ifn!("llvm.trap", fn() -> void);
524538
ifn!("llvm.debugtrap", fn() -> void);
525539
ifn!("llvm.frameaddress", fn(t_i32) -> i8p);

compiler/rustc_codegen_ssa/src/mir/rvalue.rs

+36-132
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use rustc_apfloat::{ieee, Float, Round, Status};
1111
use rustc_hir::lang_items::LangItem;
1212
use rustc_middle::mir;
1313
use rustc_middle::ty::cast::{CastTy, IntTy};
14-
use rustc_middle::ty::layout::{HasTyCtxt, TyAndLayout};
14+
use rustc_middle::ty::layout::HasTyCtxt;
1515
use rustc_middle::ty::{self, adjustment::PointerCast, Instance, Ty, TyCtxt};
1616
use rustc_span::source_map::{Span, DUMMY_SP};
1717
use rustc_span::symbol::sym;
@@ -385,10 +385,10 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
385385
bx.inttoptr(usize_llval, ll_t_out)
386386
}
387387
(CastTy::Float, CastTy::Int(IntTy::I)) => {
388-
cast_float_to_int(&mut bx, true, llval, ll_t_in, ll_t_out, cast)
388+
cast_float_to_int(&mut bx, true, llval, ll_t_in, ll_t_out)
389389
}
390390
(CastTy::Float, CastTy::Int(_)) => {
391-
cast_float_to_int(&mut bx, false, llval, ll_t_in, ll_t_out, cast)
391+
cast_float_to_int(&mut bx, false, llval, ll_t_in, ll_t_out)
392392
}
393393
_ => bug!("unsupported cast: {:?} to {:?}", operand.layout.ty, cast.ty),
394394
};
@@ -790,7 +790,6 @@ fn cast_float_to_int<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
790790
x: Bx::Value,
791791
float_ty: Bx::Type,
792792
int_ty: Bx::Type,
793-
int_layout: TyAndLayout<'tcx>,
794793
) -> Bx::Value {
795794
if let Some(false) = bx.cx().sess().opts.debugging_opts.saturating_float_casts {
796795
return if signed { bx.fptosi(x, int_ty) } else { bx.fptoui(x, int_ty) };
@@ -891,134 +890,39 @@ fn cast_float_to_int<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
891890
let int_min = bx.cx().const_uint_big(int_ty, int_min(signed, int_width) as u128);
892891
let zero = bx.cx().const_uint(int_ty, 0);
893892

894-
// The codegen here differs quite a bit depending on whether our builder's
895-
// `fptosi` and `fptoui` instructions may trap for out-of-bounds values. If
896-
// they don't trap then we can start doing everything inline with a
897-
// `select` instruction because it's ok to execute `fptosi` and `fptoui`
898-
// even if we don't use the results.
899-
if !bx.fptosui_may_trap(x, int_ty) {
900-
// Step 1 ...
901-
let fptosui_result = if signed { bx.fptosi(x, int_ty) } else { bx.fptoui(x, int_ty) };
902-
let less_or_nan = bx.fcmp(RealPredicate::RealULT, x, f_min);
903-
let greater = bx.fcmp(RealPredicate::RealOGT, x, f_max);
904-
905-
// Step 2: We use two comparisons and two selects, with %s1 being the
906-
// result:
907-
// %less_or_nan = fcmp ult %x, %f_min
908-
// %greater = fcmp olt %x, %f_max
909-
// %s0 = select %less_or_nan, int_ty::MIN, %fptosi_result
910-
// %s1 = select %greater, int_ty::MAX, %s0
911-
// Note that %less_or_nan uses an *unordered* comparison. This
912-
// comparison is true if the operands are not comparable (i.e., if x is
913-
// NaN). The unordered comparison ensures that s1 becomes int_ty::MIN if
914-
// x is NaN.
915-
//
916-
// Performance note: Unordered comparison can be lowered to a "flipped"
917-
// comparison and a negation, and the negation can be merged into the
918-
// select. Therefore, it not necessarily any more expensive than a
919-
// ordered ("normal") comparison. Whether these optimizations will be
920-
// performed is ultimately up to the backend, but at least x86 does
921-
// perform them.
922-
let s0 = bx.select(less_or_nan, int_min, fptosui_result);
923-
let s1 = bx.select(greater, int_max, s0);
924-
925-
// Step 3: NaN replacement.
926-
// For unsigned types, the above step already yielded int_ty::MIN == 0 if x is NaN.
927-
// Therefore we only need to execute this step for signed integer types.
928-
if signed {
929-
// LLVM has no isNaN predicate, so we use (x == x) instead
930-
let cmp = bx.fcmp(RealPredicate::RealOEQ, x, x);
931-
bx.select(cmp, s1, zero)
932-
} else {
933-
s1
934-
}
893+
// Step 1 ...
894+
let fptosui_result = if signed { bx.fptosi(x, int_ty) } else { bx.fptoui(x, int_ty) };
895+
let less_or_nan = bx.fcmp(RealPredicate::RealULT, x, f_min);
896+
let greater = bx.fcmp(RealPredicate::RealOGT, x, f_max);
897+
898+
// Step 2: We use two comparisons and two selects, with %s1 being the
899+
// result:
900+
// %less_or_nan = fcmp ult %x, %f_min
901+
// %greater = fcmp olt %x, %f_max
902+
// %s0 = select %less_or_nan, int_ty::MIN, %fptosi_result
903+
// %s1 = select %greater, int_ty::MAX, %s0
904+
// Note that %less_or_nan uses an *unordered* comparison. This
905+
// comparison is true if the operands are not comparable (i.e., if x is
906+
// NaN). The unordered comparison ensures that s1 becomes int_ty::MIN if
907+
// x is NaN.
908+
//
909+
// Performance note: Unordered comparison can be lowered to a "flipped"
910+
// comparison and a negation, and the negation can be merged into the
911+
// select. Therefore, it not necessarily any more expensive than a
912+
// ordered ("normal") comparison. Whether these optimizations will be
913+
// performed is ultimately up to the backend, but at least x86 does
914+
// perform them.
915+
let s0 = bx.select(less_or_nan, int_min, fptosui_result);
916+
let s1 = bx.select(greater, int_max, s0);
917+
918+
// Step 3: NaN replacement.
919+
// For unsigned types, the above step already yielded int_ty::MIN == 0 if x is NaN.
920+
// Therefore we only need to execute this step for signed integer types.
921+
if signed {
922+
// LLVM has no isNaN predicate, so we use (x == x) instead
923+
let cmp = bx.fcmp(RealPredicate::RealOEQ, x, x);
924+
bx.select(cmp, s1, zero)
935925
} else {
936-
// In this case we cannot execute `fptosi` or `fptoui` and then later
937-
// discard the result. The builder is telling us that these instructions
938-
// will trap on out-of-bounds values, so we need to use basic blocks and
939-
// control flow to avoid executing the `fptosi` and `fptoui`
940-
// instructions.
941-
//
942-
// The general idea of what we're constructing here is, for f64 -> i32:
943-
//
944-
// ;; block so far... %0 is the argument
945-
// %result = alloca i32, align 4
946-
// %inbound_lower = fcmp oge double %0, 0xC1E0000000000000
947-
// %inbound_upper = fcmp ole double %0, 0x41DFFFFFFFC00000
948-
// ;; match (inbound_lower, inbound_upper) {
949-
// ;; (true, true) => %0 can be converted without trapping
950-
// ;; (false, false) => %0 is a NaN
951-
// ;; (true, false) => %0 is too large
952-
// ;; (false, true) => %0 is too small
953-
// ;; }
954-
// ;;
955-
// ;; The (true, true) check, go to %convert if so.
956-
// %inbounds = and i1 %inbound_lower, %inbound_upper
957-
// br i1 %inbounds, label %convert, label %specialcase
958-
//
959-
// convert:
960-
// %cvt = call i32 @llvm.wasm.trunc.signed.i32.f64(double %0)
961-
// store i32 %cvt, i32* %result, align 4
962-
// br label %done
963-
//
964-
// specialcase:
965-
// ;; Handle the cases where the number is NaN, too large or too small
966-
//
967-
// ;; Either (true, false) or (false, true)
968-
// %is_not_nan = or i1 %inbound_lower, %inbound_upper
969-
// ;; Figure out which saturated value we are interested in if not `NaN`
970-
// %saturated = select i1 %inbound_lower, i32 2147483647, i32 -2147483648
971-
// ;; Figure out between saturated and NaN representations
972-
// %result_nan = select i1 %is_not_nan, i32 %saturated, i32 0
973-
// store i32 %result_nan, i32* %result, align 4
974-
// br label %done
975-
//
976-
// done:
977-
// %r = load i32, i32* %result, align 4
978-
// ;; ...
979-
let done = bx.build_sibling_block("float_cast_done");
980-
let mut convert = bx.build_sibling_block("float_cast_convert");
981-
let mut specialcase = bx.build_sibling_block("float_cast_specialcase");
982-
983-
let result = PlaceRef::alloca(bx, int_layout);
984-
result.storage_live(bx);
985-
986-
// Use control flow to figure out whether we can execute `fptosi` in a
987-
// basic block, or whether we go to a different basic block to implement
988-
// the saturating logic.
989-
let inbound_lower = bx.fcmp(RealPredicate::RealOGE, x, f_min);
990-
let inbound_upper = bx.fcmp(RealPredicate::RealOLE, x, f_max);
991-
let inbounds = bx.and(inbound_lower, inbound_upper);
992-
bx.cond_br(inbounds, convert.llbb(), specialcase.llbb());
993-
994-
// Translation of the `convert` basic block
995-
let cvt = if signed { convert.fptosi(x, int_ty) } else { convert.fptoui(x, int_ty) };
996-
convert.store(cvt, result.llval, result.align);
997-
convert.br(done.llbb());
998-
999-
// Translation of the `specialcase` basic block. Note that like above
1000-
// we try to be a bit clever here for unsigned conversions. In those
1001-
// cases the `int_min` is zero so we don't need two select instructions,
1002-
// just one to choose whether we need `int_max` or not. If
1003-
// `inbound_lower` is true then we're guaranteed to not be `NaN` and
1004-
// since we're greater than zero we must be saturating to `int_max`. If
1005-
// `inbound_lower` is false then we're either NaN or less than zero, so
1006-
// we saturate to zero.
1007-
let result_nan = if signed {
1008-
let is_not_nan = specialcase.or(inbound_lower, inbound_upper);
1009-
let saturated = specialcase.select(inbound_lower, int_max, int_min);
1010-
specialcase.select(is_not_nan, saturated, zero)
1011-
} else {
1012-
specialcase.select(inbound_lower, int_max, int_min)
1013-
};
1014-
specialcase.store(result_nan, result.llval, result.align);
1015-
specialcase.br(done.llbb());
1016-
1017-
// Translation of the `done` basic block, positioning ourselves to
1018-
// continue from that point as well.
1019-
*bx = done;
1020-
let ret = bx.load(result.llval, result.align);
1021-
result.storage_dead(bx);
1022-
ret
926+
s1
1023927
}
1024928
}

0 commit comments

Comments
 (0)