Skip to content

Commit 490fa90

Browse files
dkolsen-pgilanza
authored andcommitted
[CIR] Vector constants (llvm#700)
Implement vector constants in ClangIR. Resolves issue llvm#498 - Add a `cir.const_vec`, simlar to `cir.const_array` and `cir.const_struct` Create a new kind of attribute, `cir::ConstVectorAttr` in the code or `#cir.const_vector` in the assembly, which represents a compile-time value of a `cir::VectorType`. The values for the elements within the vector are stored as attributes within an `mlir::ArrayAttr`. When doing CodeGen for a prvalue of vector type, try to represent it as `cir.const #cir.const_vector` first. If that fails, most likely because some of the elements are not compile-time values, fall back to the existing code that uses a `cir.vec.create` operation. When lowering directly to LLVM IR, lower `cir.const #cir.const_vector` as `llvm.mlir.constant(dense<[...]> : _type_) : _type_`. When lowering through other MLIR dialects, lower `cir.const #cir.const_vector` as `arith.constant dense<[...]> : _type_`. No new tests were added, but the expected results of the existing tests that use vector constants were updated.
1 parent 2197ebe commit 490fa90

File tree

10 files changed

+255
-76
lines changed

10 files changed

+255
-76
lines changed

clang/include/clang/CIR/Dialect/IR/CIRAttrs.td

+31
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,37 @@ def ConstArrayAttr : CIR_Attr<"ConstArray", "const_array", [TypedAttrInterface]>
146146
}];
147147
}
148148

149+
//===----------------------------------------------------------------------===//
150+
// ConstVectorAttr
151+
//===----------------------------------------------------------------------===//
152+
153+
def ConstVectorAttr : CIR_Attr<"ConstVector", "const_vector",
154+
[TypedAttrInterface]> {
155+
let summary = "A constant vector from ArrayAttr";
156+
let description = [{
157+
A CIR vector attribute is an array of literals of the specified attribute
158+
types.
159+
}];
160+
161+
let parameters = (ins AttributeSelfTypeParameter<"">:$type,
162+
"ArrayAttr":$elts);
163+
164+
// Define a custom builder for the type; that removes the need to pass in an
165+
// MLIRContext instance, as it can be inferred from the `type`.
166+
let builders = [
167+
AttrBuilderWithInferredContext<(ins "mlir::cir::VectorType":$type,
168+
"ArrayAttr":$elts), [{
169+
return $_get(type.getContext(), type, elts);
170+
}]>
171+
];
172+
173+
// Printing and parsing available in CIRDialect.cpp
174+
let hasCustomAssemblyFormat = 1;
175+
176+
// Enable verifier.
177+
let genVerifyDecl = 1;
178+
}
179+
149180
//===----------------------------------------------------------------------===//
150181
// ConstStructAttr
151182
//===----------------------------------------------------------------------===//

clang/lib/CIR/CodeGen/CIRGenDecl.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,16 @@ CIRGenFunction::buildAutoVarAlloca(const VarDecl &D,
6666
if (getLangOpts().OpenMP && openMPLocalAddr.isValid()) {
6767
llvm_unreachable("NYI");
6868
} else if (Ty->isConstantSizeType()) {
69-
// If this value is an array or struct with a statically determinable
70-
// constant initializer, there are optimizations we can do.
69+
// If this value is an array, struct, or vector with a statically
70+
// determinable constant initializer, there are optimizations we can do.
7171
//
7272
// TODO: We should constant-evaluate the initializer of any variable,
7373
// as long as it is initialized by a constant expression. Currently,
7474
// isConstantInitializer produces wrong answers for structs with
7575
// reference or bitfield members, and a few other cases, and checking
7676
// for POD-ness protects us from some of these.
77-
if (D.getInit() && (Ty->isArrayType() || Ty->isRecordType()) &&
77+
if (D.getInit() &&
78+
(Ty->isArrayType() || Ty->isRecordType() || Ty->isVectorType()) &&
7879
(D.isConstexpr() ||
7980
((Ty.isPODType(getContext()) ||
8081
getContext().getBaseElementType(Ty)->isObjCObjectPointerType()) &&

clang/lib/CIR/CodeGen/CIRGenExprConst.cpp

+43-1
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,29 @@ class ConstExprEmitter
10391039
return ConstStructBuilder::BuildStruct(Emitter, ILE, T);
10401040
}
10411041

1042+
mlir::Attribute EmitVectorInitialization(InitListExpr *ILE, QualType T) {
1043+
mlir::cir::VectorType VecTy =
1044+
mlir::cast<mlir::cir::VectorType>(CGM.getTypes().ConvertType(T));
1045+
unsigned NumElements = VecTy.getSize();
1046+
unsigned NumInits = ILE->getNumInits();
1047+
assert(NumElements >= NumInits && "Too many initializers for a vector");
1048+
QualType EltTy = T->castAs<VectorType>()->getElementType();
1049+
SmallVector<mlir::Attribute, 8> Elts;
1050+
// Process the explicit initializers
1051+
for (unsigned i = 0; i < NumInits; ++i) {
1052+
auto Value = Emitter.tryEmitPrivateForMemory(ILE->getInit(i), EltTy);
1053+
if (!Value)
1054+
return {};
1055+
Elts.push_back(std::move(Value));
1056+
}
1057+
// Zero-fill the rest of the vector
1058+
for (unsigned i = NumInits; i < NumElements; ++i) {
1059+
Elts.push_back(CGM.getBuilder().getZeroInitAttr(VecTy.getEltType()));
1060+
}
1061+
return mlir::cir::ConstVectorAttr::get(
1062+
VecTy, mlir::ArrayAttr::get(CGM.getBuilder().getContext(), Elts));
1063+
}
1064+
10421065
mlir::Attribute VisitImplicitValueInitExpr(ImplicitValueInitExpr *E,
10431066
QualType T) {
10441067
return CGM.getBuilder().getZeroInitAttr(CGM.getCIRType(T));
@@ -1054,6 +1077,9 @@ class ConstExprEmitter
10541077
if (ILE->getType()->isRecordType())
10551078
return EmitRecordInitialization(ILE, T);
10561079

1080+
if (ILE->getType()->isVectorType())
1081+
return EmitVectorInitialization(ILE, T);
1082+
10571083
return nullptr;
10581084
}
10591085

@@ -1772,6 +1798,23 @@ mlir::Attribute ConstantEmitter::tryEmitPrivate(const APValue &Value,
17721798
return buildArrayConstant(CGM, Desired, CommonElementType, NumElements,
17731799
Elts, typedFiller);
17741800
}
1801+
case APValue::Vector: {
1802+
const QualType ElementType =
1803+
DestType->castAs<VectorType>()->getElementType();
1804+
unsigned NumElements = Value.getVectorLength();
1805+
SmallVector<mlir::Attribute, 16> Elts;
1806+
Elts.reserve(NumElements);
1807+
for (int i = 0; i < NumElements; ++i) {
1808+
auto C = tryEmitPrivateForMemory(Value.getVectorElt(i), ElementType);
1809+
if (!C)
1810+
return {};
1811+
Elts.push_back(C);
1812+
}
1813+
auto Desired =
1814+
mlir::cast<mlir::cir::VectorType>(CGM.getTypes().ConvertType(DestType));
1815+
return mlir::cir::ConstVectorAttr::get(
1816+
Desired, mlir::ArrayAttr::get(CGM.getBuilder().getContext(), Elts));
1817+
}
17751818
case APValue::MemberPointer: {
17761819
assert(!MissingFeatures::cxxABI());
17771820

@@ -1795,7 +1838,6 @@ mlir::Attribute ConstantEmitter::tryEmitPrivate(const APValue &Value,
17951838
case APValue::FixedPoint:
17961839
case APValue::ComplexInt:
17971840
case APValue::ComplexFloat:
1798-
case APValue::Vector:
17991841
case APValue::AddrLabelDiff:
18001842
assert(0 && "not implemented");
18011843
}

clang/lib/CIR/Dialect/IR/CIRDialect.cpp

+81
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ static LogicalResult checkConstantTypes(mlir::Operation *op, mlir::Type opType,
347347
if (mlir::isa<mlir::cir::GlobalViewAttr>(attrType) ||
348348
mlir::isa<mlir::cir::TypeInfoAttr>(attrType) ||
349349
mlir::isa<mlir::cir::ConstArrayAttr>(attrType) ||
350+
mlir::isa<mlir::cir::ConstVectorAttr>(attrType) ||
350351
mlir::isa<mlir::cir::ConstStructAttr>(attrType) ||
351352
mlir::isa<mlir::cir::VTableAttr>(attrType))
352353
return success();
@@ -2825,6 +2826,86 @@ void ConstArrayAttr::print(::mlir::AsmPrinter &printer) const {
28252826
printer << ">";
28262827
}
28272828

2829+
LogicalResult mlir::cir::ConstVectorAttr::verify(
2830+
::llvm::function_ref<::mlir::InFlightDiagnostic()> emitError,
2831+
::mlir::Type type, mlir::ArrayAttr arrayAttr) {
2832+
2833+
if (!mlir::isa<mlir::cir::VectorType>(type)) {
2834+
return emitError()
2835+
<< "type of cir::ConstVectorAttr is not a cir::VectorType: " << type;
2836+
}
2837+
auto vecType = mlir::cast<mlir::cir::VectorType>(type);
2838+
2839+
// Do the number of elements match?
2840+
if (vecType.getSize() != arrayAttr.size()) {
2841+
return emitError()
2842+
<< "number of constant elements should match vector size";
2843+
}
2844+
// Do the types of the elements match?
2845+
LogicalResult elementTypeCheck = success();
2846+
arrayAttr.walkImmediateSubElements(
2847+
[&](Attribute element) {
2848+
if (elementTypeCheck.failed()) {
2849+
// An earlier element didn't match
2850+
return;
2851+
}
2852+
auto typedElement = mlir::dyn_cast<TypedAttr>(element);
2853+
if (!typedElement || typedElement.getType() != vecType.getEltType()) {
2854+
elementTypeCheck = failure();
2855+
emitError() << "constant type should match vector element type";
2856+
}
2857+
},
2858+
[&](Type) {});
2859+
return elementTypeCheck;
2860+
}
2861+
2862+
::mlir::Attribute ConstVectorAttr::parse(::mlir::AsmParser &parser,
2863+
::mlir::Type type) {
2864+
::mlir::FailureOr<::mlir::Type> resultType;
2865+
::mlir::FailureOr<ArrayAttr> resultValue;
2866+
::llvm::SMLoc loc = parser.getCurrentLocation();
2867+
2868+
// Parse literal '<'
2869+
if (parser.parseLess()) {
2870+
return {};
2871+
}
2872+
2873+
// Parse variable 'value'
2874+
resultValue = ::mlir::FieldParser<ArrayAttr>::parse(parser);
2875+
if (failed(resultValue)) {
2876+
parser.emitError(parser.getCurrentLocation(),
2877+
"failed to parse ConstVectorAttr parameter 'value' as "
2878+
"an attribute");
2879+
return {};
2880+
}
2881+
2882+
if (parser.parseOptionalColon().failed()) {
2883+
resultType = type;
2884+
} else {
2885+
resultType = ::mlir::FieldParser<::mlir::Type>::parse(parser);
2886+
if (failed(resultType)) {
2887+
parser.emitError(parser.getCurrentLocation(),
2888+
"failed to parse ConstVectorAttr parameter 'type' as "
2889+
"an MLIR type");
2890+
return {};
2891+
}
2892+
}
2893+
2894+
// Parse literal '>'
2895+
if (parser.parseGreater()) {
2896+
return {};
2897+
}
2898+
2899+
return parser.getChecked<ConstVectorAttr>(
2900+
loc, parser.getContext(), resultType.value(), resultValue.value());
2901+
}
2902+
2903+
void ConstVectorAttr::print(::mlir::AsmPrinter &printer) const {
2904+
printer << "<";
2905+
printer.printStrippedAttrOrType(getElts());
2906+
printer << ">";
2907+
}
2908+
28282909
::mlir::Attribute SignedOverflowBehaviorAttr::parse(::mlir::AsmParser &parser,
28292910
::mlir::Type type) {
28302911
if (parser.parseLess())

clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp

+36
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,35 @@ mlir::Value lowerCirAttrAsValue(mlir::Operation *parentOp,
315315
return result;
316316
}
317317

318+
// ConstVectorAttr visitor.
319+
mlir::Value lowerCirAttrAsValue(mlir::Operation *parentOp,
320+
mlir::cir::ConstVectorAttr constVec,
321+
mlir::ConversionPatternRewriter &rewriter,
322+
const mlir::TypeConverter *converter) {
323+
auto llvmTy = converter->convertType(constVec.getType());
324+
auto loc = parentOp->getLoc();
325+
SmallVector<mlir::Attribute> mlirValues;
326+
for (auto elementAttr : constVec.getElts()) {
327+
mlir::Attribute mlirAttr;
328+
if (auto intAttr = mlir::dyn_cast<mlir::cir::IntAttr>(elementAttr)) {
329+
mlirAttr = rewriter.getIntegerAttr(
330+
converter->convertType(intAttr.getType()), intAttr.getValue());
331+
} else if (auto floatAttr =
332+
mlir::dyn_cast<mlir::cir::FPAttr>(elementAttr)) {
333+
mlirAttr = rewriter.getFloatAttr(
334+
converter->convertType(floatAttr.getType()), floatAttr.getValue());
335+
} else {
336+
llvm_unreachable(
337+
"vector constant with an element that is neither an int nor a float");
338+
}
339+
mlirValues.push_back(mlirAttr);
340+
}
341+
return rewriter.create<mlir::LLVM::ConstantOp>(
342+
loc, llvmTy,
343+
mlir::DenseElementsAttr::get(mlir::cast<mlir::ShapedType>(llvmTy),
344+
mlirValues));
345+
}
346+
318347
// GlobalViewAttr visitor.
319348
mlir::Value lowerCirAttrAsValue(mlir::Operation *parentOp,
320349
mlir::cir::GlobalViewAttr globalAttr,
@@ -385,6 +414,8 @@ lowerCirAttrAsValue(mlir::Operation *parentOp, mlir::Attribute attr,
385414
return lowerCirAttrAsValue(parentOp, constStruct, rewriter, converter);
386415
if (const auto constArr = mlir::dyn_cast<mlir::cir::ConstArrayAttr>(attr))
387416
return lowerCirAttrAsValue(parentOp, constArr, rewriter, converter);
417+
if (const auto constVec = mlir::dyn_cast<mlir::cir::ConstVectorAttr>(attr))
418+
return lowerCirAttrAsValue(parentOp, constVec, rewriter, converter);
388419
if (const auto boolAttr = mlir::dyn_cast<mlir::cir::BoolAttr>(attr))
389420
return lowerCirAttrAsValue(parentOp, boolAttr, rewriter, converter);
390421
if (const auto zeroAttr = mlir::dyn_cast<mlir::cir::ZeroAttr>(attr))
@@ -1184,6 +1215,11 @@ class CIRConstantLowering
11841215

11851216
return op.emitError() << "unsupported lowering for struct constant type "
11861217
<< op.getType();
1218+
} else if (const auto vecTy =
1219+
mlir::dyn_cast<mlir::cir::VectorType>(op.getType())) {
1220+
rewriter.replaceOp(op, lowerCirAttrAsValue(op, op.getValue(), rewriter,
1221+
getTypeConverter()));
1222+
return mlir::success();
11871223
} else
11881224
return op.emitError() << "unsupported constant type " << op.getType();
11891225

clang/lib/CIR/Lowering/ThroughMLIR/LowerCIRToMLIR.cpp

+41-16
Original file line numberDiff line numberDiff line change
@@ -511,24 +511,49 @@ class CIRConstantOpLowering
511511
public:
512512
using OpConversionPattern<mlir::cir::ConstantOp>::OpConversionPattern;
513513

514+
private:
515+
// This code is in a separate function rather than part of matchAndRewrite
516+
// because it is recursive. There is currently only one level of recursion;
517+
// when lowing a vector attribute the attributes for the elements also need
518+
// to be lowered.
519+
mlir::TypedAttr
520+
lowerCirAttrToMlirAttr(mlir::Attribute cirAttr,
521+
mlir::ConversionPatternRewriter &rewriter) const {
522+
assert(mlir::isa<mlir::TypedAttr>(cirAttr) &&
523+
"Can't lower a non-typed attribute");
524+
auto mlirType = getTypeConverter()->convertType(
525+
mlir::cast<mlir::TypedAttr>(cirAttr).getType());
526+
if (auto vecAttr = mlir::dyn_cast<mlir::cir::ConstVectorAttr>(cirAttr)) {
527+
assert(mlir::isa<mlir::VectorType>(mlirType) &&
528+
"MLIR type for CIR vector attribute is not mlir::VectorType");
529+
assert(mlir::isa<mlir::ShapedType>(mlirType) &&
530+
"mlir::VectorType is not a mlir::ShapedType ??");
531+
SmallVector<mlir::Attribute> mlirValues;
532+
for (auto elementAttr : vecAttr.getElts()) {
533+
mlirValues.push_back(
534+
this->lowerCirAttrToMlirAttr(elementAttr, rewriter));
535+
}
536+
return mlir::DenseElementsAttr::get(
537+
mlir::cast<mlir::ShapedType>(mlirType), mlirValues);
538+
} else if (auto boolAttr = mlir::dyn_cast<mlir::cir::BoolAttr>(cirAttr)) {
539+
return rewriter.getIntegerAttr(mlirType, boolAttr.getValue());
540+
} else if (auto floatAttr = mlir::dyn_cast<mlir::cir::FPAttr>(cirAttr)) {
541+
return rewriter.getFloatAttr(mlirType, floatAttr.getValue());
542+
} else if (auto intAttr = mlir::dyn_cast<mlir::cir::IntAttr>(cirAttr)) {
543+
return rewriter.getIntegerAttr(mlirType, intAttr.getValue());
544+
} else {
545+
llvm_unreachable("NYI: unsupported attribute kind lowering to MLIR");
546+
return {};
547+
}
548+
}
549+
550+
public:
514551
mlir::LogicalResult
515552
matchAndRewrite(mlir::cir::ConstantOp op, OpAdaptor adaptor,
516553
mlir::ConversionPatternRewriter &rewriter) const override {
517-
auto ty = getTypeConverter()->convertType(op.getType());
518-
mlir::TypedAttr value;
519-
if (mlir::isa<mlir::cir::BoolType>(op.getType())) {
520-
auto boolValue = mlir::cast<mlir::cir::BoolAttr>(op.getValue());
521-
value = rewriter.getIntegerAttr(ty, boolValue.getValue());
522-
} else if (mlir::isa<mlir::cir::CIRFPTypeInterface>(op.getType())) {
523-
value = rewriter.getFloatAttr(
524-
ty, mlir::cast<mlir::cir::FPAttr>(op.getValue()).getValue());
525-
} else {
526-
auto cirIntAttr = mlir::dyn_cast<mlir::cir::IntAttr>(op.getValue());
527-
assert(cirIntAttr && "NYI non cir.int attr");
528-
value = rewriter.getIntegerAttr(
529-
ty, cast<mlir::cir::IntAttr>(op.getValue()).getValue());
530-
}
531-
rewriter.replaceOpWithNewOp<mlir::arith::ConstantOp>(op, ty, value);
554+
rewriter.replaceOpWithNewOp<mlir::arith::ConstantOp>(
555+
op, getTypeConverter()->convertType(op.getType()),
556+
this->lowerCirAttrToMlirAttr(op.getValue(), rewriter));
532557
return mlir::LogicalResult::success();
533558
}
534559
};
@@ -1419,4 +1444,4 @@ mlir::ModuleOp lowerFromCIRToMLIR(mlir::ModuleOp theModule,
14191444
return theModule;
14201445
}
14211446

1422-
} // namespace cir
1447+
} // namespace cir

clang/test/CIR/CodeGen/vectype-ext.cpp

+8-11
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ typedef unsigned short vus2 __attribute__((ext_vector_type(2)));
1414
// LLVM: define void {{@.*vector_int_test.*}}
1515
void vector_int_test(int x) {
1616

17-
// Vector constant. Not yet implemented. Expected results will change from
18-
// cir.vec.create to cir.const.
17+
// Vector constant.
1918
vi4 a = { 1, 2, 3, 4 };
20-
// CIR: %{{[0-9]+}} = cir.vec.create(%{{[0-9]+}}, %{{[0-9]+}}, %{{[0-9]+}}, %{{[0-9]+}} : !s32i, !s32i, !s32i, !s32i) : !cir.vector<!s32i x 4>
19+
// CIR: %{{[0-9]+}} = cir.const #cir.const_vector<[#cir.int<1> : !s32i, #cir.int<2> : !s32i, #cir.int<3> : !s32i, #cir.int<4> : !s32i]> : !cir.vector<!s32i x 4>
2120
// LLVM: store <4 x i32> <i32 1, i32 2, i32 3, i32 4>, ptr %{{[0-9]+}}, align 16
2221

2322
// Non-const vector initialization.
@@ -199,10 +198,9 @@ void vector_int_test(int x) {
199198
// CIR: cir.func {{@.*vector_double_test.*}}
200199
// LLVM: define void {{@.*vector_double_test.*}}
201200
void vector_double_test(int x, double y) {
202-
// Vector constant. Not yet implemented. Expected results will change from
203-
// cir.vec.create to cir.const.
201+
// Vector constant.
204202
vd2 a = { 1.5, 2.5 };
205-
// CIR: %{{[0-9]+}} = cir.vec.create(%{{[0-9]+}}, %{{[0-9]+}} : !cir.double, !cir.double) : !cir.vector<!cir.double x 2>
203+
// CIR: %{{[0-9]+}} = cir.const #cir.const_vector<[#cir.fp<1.500000e+00> : !cir.double, #cir.fp<2.500000e+00> : !cir.double]> : !cir.vector<!cir.double x 2>
206204

207205
// LLVM: store <2 x double> <double 1.500000e+00, double 2.500000e+00>, ptr %{{[0-9]+}}, align 16
208206

@@ -491,13 +489,12 @@ void test_build_lvalue() {
491489
// LLVM: define void {{@.*test_vec3.*}}
492490
void test_vec3() {
493491
vi3 v = {};
494-
// CIR-NEXT: %[[#PV:]] = cir.alloca !cir.vector<!s32i x 3>, !cir.ptr<!cir.vector<!s32i x 3>>, ["v", init] {alignment = 16 : i64}
495-
// CIR: %[[#VEC4:]] = cir.vec.shuffle(%{{[0-9]+}}, %{{[0-9]+}} : !cir.vector<!s32i x 3>) [#cir.int<0> : !s32i, #cir.int<1> : !s32i, #cir.int<2> : !s32i, #cir.int<-1> : !s32i] : !cir.vector<!s32i x 4>
496-
// CIR-NEXT: %[[#PV4:]] = cir.cast(bitcast, %[[#PV]] : !cir.ptr<!cir.vector<!s32i x 3>>), !cir.ptr<!cir.vector<!s32i x 4>>
497-
// CIR-NEXT: cir.store %[[#VEC4]], %[[#PV4]] : !cir.vector<!s32i x 4>, !cir.ptr<!cir.vector<!s32i x 4>>
492+
// CIR-NEXT: %[[#PV:]] = cir.alloca !cir.vector<!s32i x 3>, !cir.ptr<!cir.vector<!s32i x 3>>, ["v"] {alignment = 16 : i64}
493+
// CIR-NEXT: %[[#VVAL:]] = cir.const #cir.const_vector<[#cir.int<0> : !s32i, #cir.int<0> : !s32i, #cir.int<0> : !s32i]> : !cir.vector<!s32i x 3>
494+
// CIR-NEXT: cir.store %[[#VVAL]], %[[#PV]] : !cir.vector<!s32i x 3>, !cir.ptr<!cir.vector<!s32i x 3>>
498495

499496
// LLVM-NEXT: %[[#PV:]] = alloca <3 x i32>, i64 1, align 16
500-
// LLVM-NEXT: store <4 x i32> <i32 0, i32 0, i32 0, i32 undef>, ptr %[[#PV]], align 16
497+
// LLVM-NEXT: store <3 x i32> zeroinitializer, ptr %[[#PV]], align 16
501498

502499
v + 1;
503500
// CIR-NEXT: %[[#PV4:]] = cir.cast(bitcast, %[[#PV]] : !cir.ptr<!cir.vector<!s32i x 3>>), !cir.ptr<!cir.vector<!s32i x 4>>

0 commit comments

Comments
 (0)