Remove fn/closure #[spirv(unroll_loops)] attribute.

This commit is contained in:
Eduard-Mihai Burtescu 2022-11-24 17:51:13 +02:00 committed by Sylvester Hesp
parent 4452da80f5
commit 5836b83415
14 changed files with 1227 additions and 1580 deletions

View File

@ -5,12 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!-- NOTE(eddyb) sections from the original template:
### Added
- New features go here in a bullet list
### Changed
- Changes to existing functionality go here in a bullet list
### Deprecated
- Mark features soon-to-be removed in a bullet list
### Removed
- Features that have been removed in a bullet list
### Fixed
- Bug fixes in a bullet list
### Security
- Changes/fixes related to security vulnerabilities in a bullet list
-->
## [Unreleased]
### Changed 🛠️
- Applied workspace inheritance to Cargo.toml files
### Removed
- Removed the `fn`/closure `#[spirv(unroll_loops)]` attribute, as it has no users,
is becoming non-trivial to support, and requires redesign for better ergonomics
(e.g. `#[spirv(unroll)]` applied to individual loops, not the whole `fn`/closure)
## [0.4.0-alpha.17]
### Changed 🛠️
@ -29,6 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 🚨BREAKING🚨 Migrated from `register_attr` to `register_tool`. [More information](docs/src/migration-to-register-tool.md).
- Updated toolchain to `nightly-2022-10-01`
- Updated `glam` to `0.22`
### Removed
- Removed `glam::BVec` support (they are no longer `#[repl(simd)]` in `glam`, as Rust doesn't support SIMD vectors with `bool` elements)
## [0.4.0-alpha.15]

View File

@ -89,7 +89,6 @@ pub enum SpirvAttribute {
InputAttachmentIndex(u32),
// `fn`/closure attributes:
UnrollLoops,
BufferLoadIntrinsic,
BufferStoreIntrinsic,
}
@ -124,7 +123,6 @@ pub struct AggregatedSpirvAttributes {
pub input_attachment_index: Option<Spanned<u32>>,
// `fn`/closure attributes:
pub unroll_loops: Option<Spanned<()>>,
pub buffer_load_intrinsic: Option<Spanned<()>>,
pub buffer_store_intrinsic: Option<Spanned<()>>,
}
@ -213,7 +211,6 @@ impl AggregatedSpirvAttributes {
span,
"#[spirv(attachment_index)]",
),
UnrollLoops => try_insert(&mut self.unroll_loops, (), span, "#[spirv(unroll_loops)]"),
BufferLoadIntrinsic => try_insert(
&mut self.buffer_load_intrinsic,
(),
@ -346,15 +343,6 @@ impl CheckSpirvAttrVisitor<'_> {
_ => Err(Expected("function parameter")),
},
SpirvAttribute::UnrollLoops => match target {
Target::Fn
| Target::Closure
| Target::Method(MethodKind::Trait { body: true } | MethodKind::Inherent) => {
Ok(())
}
_ => Err(Expected("function or closure")),
},
SpirvAttribute::BufferLoadIntrinsic | SpirvAttribute::BufferStoreIntrinsic => {
match target {
Target::Fn => Ok(()),

View File

@ -117,9 +117,6 @@ impl<'tcx> CodegenCx<'tcx> {
.map_or_else(|| instance.to_string(), ToString::to_string);
self.entry_stub(&instance, fn_abi, declared, entry_name, entry);
}
if attrs.unroll_loops.is_some() {
self.unroll_loops_decorations.borrow_mut().insert(fn_id);
}
if attrs.buffer_load_intrinsic.is_some() {
let mode = &fn_abi.ret.mode;
self.buffer_load_intrinsic_fn_id

View File

@ -5,9 +5,7 @@ mod type_;
use crate::builder::{ExtInst, InstructionTable};
use crate::builder_spirv::{BuilderCursor, BuilderSpirv, SpirvConst, SpirvValue, SpirvValueKind};
use crate::decorations::{
CustomDecoration, SerializedSpan, UnrollLoopsDecoration, ZombieDecoration,
};
use crate::decorations::{CustomDecoration, SerializedSpan, ZombieDecoration};
use crate::spirv_type::{SpirvType, SpirvTypePrinter, TypeCache};
use crate::symbols::Symbols;
use crate::target::SpirvTarget;
@ -20,7 +18,7 @@ use rustc_codegen_ssa::traits::{
AsmMethods, BackendTypes, CoverageInfoMethods, DebugInfoMethods, GlobalAsmOperandRef,
MiscMethods,
};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_data_structures::fx::FxHashMap;
use rustc_middle::mir::mono::CodegenUnit;
use rustc_middle::mir::Body;
use rustc_middle::ty::layout::{HasParamEnv, HasTyCtxt};
@ -55,10 +53,6 @@ pub struct CodegenCx<'tcx> {
/// each with its own reason and span that should be used for reporting
/// (in the event that the value is actually needed)
zombie_decorations: RefCell<FxHashMap<Word, ZombieDecoration>>,
/// Functions that have `#[spirv(unroll_loops)]`, and therefore should
/// get `LoopControl::UNROLL` applied to all of their loops' `OpLoopMerge`
/// instructions, during structuralization.
unroll_loops_decorations: RefCell<FxHashSet<Word>>,
/// Cache of all the builtin symbols we need
pub sym: Rc<Symbols>,
pub instruction_table: InstructionTable,
@ -121,7 +115,6 @@ impl<'tcx> CodegenCx<'tcx> {
vtables: Default::default(),
ext_inst: Default::default(),
zombie_decorations: Default::default(),
unroll_loops_decorations: Default::default(),
target,
sym,
instruction_table: InstructionTable::new(),
@ -208,13 +201,7 @@ impl<'tcx> CodegenCx<'tcx> {
self.zombie_decorations
.into_inner()
.into_iter()
.map(|(id, zombie)| zombie.encode(id))
.chain(
self.unroll_loops_decorations
.into_inner()
.into_iter()
.map(|id| UnrollLoopsDecoration {}.encode(id)),
),
.map(|(id, zombie)| zombie.encode(id)),
);
result
}

View File

@ -99,16 +99,6 @@ impl<'a, D: Deserialize<'a>> LazilyDeserialized<'a, D> {
}
}
/// An `OpFunction` with `#[spirv(unroll_loops)]` on the Rust `fn` definition,
/// which should get `LoopControl::UNROLL` applied to all of its loops'
/// `OpLoopMerge` instructions, during structuralization.
#[derive(Deserialize, Serialize)]
pub struct UnrollLoopsDecoration {}
impl CustomDecoration for UnrollLoopsDecoration {
const ENCODING_PREFIX: &'static str = "U";
}
#[derive(Deserialize, Serialize)]
pub struct ZombieDecoration {
pub reason: String,

View File

@ -19,11 +19,10 @@ mod zombies;
use std::borrow::Cow;
use crate::codegen_cx::SpirvMetadata;
use crate::decorations::{CustomDecoration, UnrollLoopsDecoration};
use rspirv::binary::{Assemble, Consumer};
use rspirv::dr::{Block, Instruction, Loader, Module, ModuleHeader, Operand};
use rspirv::spirv::{Op, StorageClass, Word};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_data_structures::fx::FxHashMap;
use rustc_errors::ErrorGuaranteed;
use rustc_session::Session;
@ -226,14 +225,9 @@ pub fn link(sess: &Session, mut inputs: Vec<Module>, opts: &Options) -> Result<L
dce::dce(&mut output);
}
let unroll_loops_decorations = UnrollLoopsDecoration::decode_all(&output)
.map(|(id, _)| id)
.collect::<FxHashSet<_>>();
UnrollLoopsDecoration::remove_all(&mut output);
let mut output = if opts.structurize {
let _timer = sess.timer("link_structurize");
structurizer::structurize(output, unroll_loops_decorations)
structurizer::structurize(output)
} else {
output
};

View File

@ -1,7 +1,7 @@
use indexmap::{indexmap, IndexMap};
use rspirv::dr::{Block, Builder, Function, InsertPoint, Module, Operand};
use rspirv::spirv::{LoopControl, Op, SelectionControl, Word};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_data_structures::fx::FxHashMap;
use std::{iter, mem};
/// Cached IDs of `OpTypeBool`, `OpConstantFalse`, and `OpConstantTrue`.
@ -37,7 +37,7 @@ impl FuncBuilder<'_> {
}
}
pub fn structurize(module: Module, unroll_loops_decorations: FxHashSet<Word>) -> Module {
pub fn structurize(module: Module) -> Module {
let mut builder = Builder::new_from_module(module);
// Get the `OpTypeBool` type (it will only be created if it's missing).
@ -72,14 +72,6 @@ pub fn structurize(module: Module, unroll_loops_decorations: FxHashSet<Word>) ->
builder: &mut builder,
};
let func_id = func.function().def_id().unwrap();
let loop_control = if unroll_loops_decorations.contains(&func_id) {
LoopControl::UNROLL
} else {
LoopControl::NONE
};
let block_id_to_idx = func
.blocks()
.iter()
@ -95,7 +87,6 @@ pub fn structurize(module: Module, unroll_loops_decorations: FxHashSet<Word>) ->
},
func,
block_id_to_idx,
loop_control,
incoming_edge_count: vec![],
regions: FxHashMap::default(),
}
@ -140,10 +131,6 @@ struct Structurizer<'a> {
func: FuncBuilder<'a>,
block_id_to_idx: FxHashMap<BlockId, BlockIdx>,
/// `LoopControl` to use in all loops' `OpLoopMerge` instruction.
/// Currently only affected by function-scoped `#[spirv(unroll_loops)]`.
loop_control: LoopControl,
/// Number of edges pointing to each block.
/// Computed by `post_order` and updated when structuring loops
/// (backedge count is subtracted to hide them from outer regions).
@ -390,7 +377,7 @@ impl Structurizer<'_> {
.loop_merge(
while_exit_block_id,
while_body_merge_id,
self.loop_control,
LoopControl::NONE,
iter::empty(),
)
.unwrap();

View File

@ -339,7 +339,6 @@ impl Symbols {
"matrix",
SpirvAttribute::IntrinsicType(IntrinsicType::Matrix),
),
("unroll_loops", SpirvAttribute::UnrollLoops),
("buffer_load_intrinsic", SpirvAttribute::BufferLoadIntrinsic),
(
"buffer_store_intrinsic",

View File

@ -1,18 +0,0 @@
// build-pass
// compile-flags: -C llvm-args=--disassemble-fn=unroll_loops::java_hash_ten_times
use spirv_std::spirv;
#[spirv(unroll_loops)]
fn java_hash_ten_times(mut x: u32, y: u32) -> u32 {
let mut i = 0;
while i < 10 {
x = 31 * x + y;
i += 1;
}
x
}
#[spirv(fragment)]
pub fn main() {
java_hash_ten_times(7, 42);
}

View File

@ -1,40 +0,0 @@
%1 = OpFunction %2 None %3
%4 = OpFunctionParameter %2
%5 = OpFunctionParameter %2
%6 = OpLabel
OpLine %7 9 4
OpBranch %8
%8 = OpLabel
OpBranch %9
%9 = OpLabel
%10 = OpPhi %11 %12 %8 %13 %14
%15 = OpPhi %2 %4 %8 %16 %14
%17 = OpPhi %18 %19 %8 %20 %14
OpLoopMerge %21 %14 Unroll
OpBranchConditional %17 %22 %21
%22 = OpLabel
OpLine %7 9 10
%23 = OpSLessThan %18 %10 %24
OpLine %7 9 10
OpSelectionMerge %25 None
OpBranchConditional %23 %26 %27
%26 = OpLabel
OpLine %7 10 12
%28 = OpIMul %2 %29 %15
OpLine %7 10 8
%16 = OpIAdd %2 %28 %5
OpLine %7 11 8
%13 = OpIAdd %11 %10 %30
OpLine %7 9 4
OpBranch %25
%27 = OpLabel
OpLine %7 14 1
OpReturnValue %15
%25 = OpLabel
%20 = OpPhi %18 %19 %26
OpBranch %14
%14 = OpLabel
OpBranch %9
%21 = OpLabel
OpUnreachable
OpFunctionEnd

View File

@ -17,18 +17,17 @@
// * builtin: `position`
// NOTE(eddyb) accounting for the number of errors this test actually produces:
// * 473 errors, all "attribute is only valid on" (see `invalid-target.stderr`)
// * 437 errors, all "attribute is only valid on" (see `invalid-target.stderr`)
// * 41 uses of `#[rust_gpu::spirv(...)]` in this test
// * at most 12 attributes per `#[rust_gpu::spirv(...)]`, so an upper bound of `41*12 = 492`
// * the difference between 492 and 473 is 19, i.e. valid attributes, made up of:
// * at most 11 attributes per `#[rust_gpu::spirv(...)]`, so an upper bound of `41*11 = 451`
// * the difference between 451 and 437 is 14, i.e. valid attributes, made up of:
// * 4 on `_Struct`
// * 8 on functions, i.e. 2 on each of:
// * 4 on functions, i.e. 1 on each of:
// * `_inherent_method`
// * `_trait_method_with_default`,
// * `_trait_method` (in `impl _Trait for ()`)
// * `_fn`
// * 6 on `_entry_param`
// * 1 on `_closure`
// NOTE(shesp) Directly using `#[rust_gpu::spirv(...)]` because macro attributes are invalid in most contexts
@ -36,7 +35,6 @@
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
macro_rules! _macro {
() => {};
@ -46,7 +44,6 @@ macro_rules! _macro {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
extern crate spirv_std as _;
@ -54,7 +51,6 @@ extern crate spirv_std as _;
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
use spirv_std as _;
@ -62,7 +58,6 @@ use spirv_std as _;
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
mod _mod {}
@ -70,14 +65,12 @@ mod _mod {}
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
extern "C" {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
type _ForeignTy;
@ -85,7 +78,6 @@ extern "C" {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
static _FOREIGN_STATIC: ();
@ -93,7 +85,6 @@ extern "C" {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
fn _foreign_fn();
}
@ -102,7 +93,6 @@ extern "C" {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
static _STATIC: () = ();
@ -110,7 +100,6 @@ static _STATIC: () = ();
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
const _CONST: () = ();
@ -118,7 +107,6 @@ const _CONST: () = ();
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
type _TyAlias = ();
@ -126,7 +114,6 @@ type _TyAlias = ();
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
type _OpaqueTy = impl Copy;
@ -138,21 +125,18 @@ fn _opaque_ty_definer() -> _OpaqueTy {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
enum _Enum {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
_Variant {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
_field: (),
},
@ -162,14 +146,12 @@ enum _Enum {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
union _Union {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
_field: (),
}
@ -177,14 +159,12 @@ union _Union {
#[rust_gpu::spirv(
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
struct _Struct {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
_field: (),
}
@ -193,14 +173,12 @@ struct _Struct {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
impl _Struct {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
const _INHERENT_ASSOC_CONST: () = ();
@ -215,7 +193,6 @@ impl _Struct {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
trait _TraitAlias = Copy;
@ -223,14 +200,12 @@ trait _TraitAlias = Copy;
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
trait _Trait {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
type _AssocTy;
@ -238,7 +213,6 @@ trait _Trait {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
const _TRAIT_ASSOC_CONST: ();
@ -246,7 +220,6 @@ trait _Trait {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
fn _trait_method();
@ -261,14 +234,12 @@ trait _Trait {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
impl _Trait for () {
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
type _AssocTy = ();
@ -276,7 +247,6 @@ impl _Trait for () {
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
const _TRAIT_ASSOC_CONST: () = ();
@ -295,7 +265,6 @@ fn _fn(
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
unroll_loops, // fn/closure-only
)]
_entry_param: (),
) {
@ -303,7 +272,6 @@ fn _fn(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
let _statement = ();
@ -319,7 +287,6 @@ fn _fn(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
(1, 2, 3) // expression
);
@ -329,7 +296,6 @@ fn _fn(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)]
_arm => {}
}
@ -340,19 +306,16 @@ fn _fn_with_generics<
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)] '_lifetime_param,
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)] _TyParam,
#[rust_gpu::spirv(
sampler, block, sampled_image, generic_image_type, // struct-only
vertex, // fn-only
uniform, position, descriptor_set = 0, binding = 0, flat, invariant, // param-only
unroll_loops, // fn/closure-only
)] const _CONST_PARAM: usize,
>() {
}

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,3 @@ fn _entry(
#[spirv(invariant, invariant)] _invariant: (),
) {
}
#[spirv(unroll_loops, unroll_loops)]
fn _unroll_loops() {}

View File

@ -208,17 +208,5 @@ note: previous #[spirv(invariant)] attribute
51 | #[spirv(invariant, invariant)] _invariant: (),
| ^^^^^^^^^
error: only one #[spirv(unroll_loops)] attribute is allowed on a function
--> $DIR/multiple.rs:55:23
|
55 | #[spirv(unroll_loops, unroll_loops)]
| ^^^^^^^^^^^^
|
note: previous #[spirv(unroll_loops)] attribute
--> $DIR/multiple.rs:55:9
|
55 | #[spirv(unroll_loops, unroll_loops)]
| ^^^^^^^^^^^^
error: aborting due to 18 previous errors; 1 warning emitted
error: aborting due to 17 previous errors; 1 warning emitted