Skip to content

Commit f3b20cb

Browse files
[IPSCCP] Variable not visible at Og. (llvm#66745)
https://bugs.llvm.org/show_bug.cgi?id=51559 llvm#50901 IPSCCP pass removes the global variable and does not create a constant expression for the initializer value.
1 parent 099ce25 commit f3b20cb

File tree

5 files changed

+318
-0
lines changed

5 files changed

+318
-0
lines changed

llvm/include/llvm/Transforms/Utils/Local.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,10 @@ void dropDebugUsers(Instruction &I);
461461
void hoistAllInstructionsInto(BasicBlock *DomBlock, Instruction *InsertPt,
462462
BasicBlock *BB);
463463

464+
/// Given a constant, create a debug information expression.
465+
DIExpression *getExpressionForConstant(DIBuilder &DIB, const Constant &C,
466+
Type &Ty);
467+
464468
//===----------------------------------------------------------------------===//
465469
// Intrinsic pattern matching
466470
//

llvm/lib/Transforms/IPO/SCCP.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "llvm/Analysis/ValueTracking.h"
2323
#include "llvm/IR/AttributeMask.h"
2424
#include "llvm/IR/Constants.h"
25+
#include "llvm/IR/DIBuilder.h"
2526
#include "llvm/IR/IntrinsicInst.h"
2627
#include "llvm/Support/CommandLine.h"
2728
#include "llvm/Support/ModRef.h"
@@ -371,6 +372,18 @@ static bool runIPSCCP(
371372
StoreInst *SI = cast<StoreInst>(GV->user_back());
372373
SI->eraseFromParent();
373374
}
375+
376+
// Try to create a debug constant expression for the global variable
377+
// initializer value.
378+
SmallVector<DIGlobalVariableExpression *, 1> GVEs;
379+
GV->getDebugInfo(GVEs);
380+
if (GVEs.size() == 1) {
381+
DIBuilder DIB(M);
382+
if (DIExpression *InitExpr = getExpressionForConstant(
383+
DIB, *GV->getInitializer(), *GV->getValueType()))
384+
GVEs[0]->replaceOperandWith(1, InitExpr);
385+
}
386+
374387
MadeChanges = true;
375388
M.eraseGlobalVariable(GV);
376389
++NumGlobalConst;

llvm/lib/Transforms/Utils/Local.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3077,6 +3077,42 @@ void llvm::hoistAllInstructionsInto(BasicBlock *DomBlock, Instruction *InsertPt,
30773077
BB->getTerminator()->getIterator());
30783078
}
30793079

3080+
DIExpression *llvm::getExpressionForConstant(DIBuilder &DIB, const Constant &C,
3081+
Type &Ty) {
3082+
3083+
// Create integer constant expression.
3084+
auto createIntegerExpression = [&DIB](const Constant &CV) -> DIExpression * {
3085+
const APInt &API = cast<ConstantInt>(&CV)->getValue();
3086+
std::optional<int64_t> InitIntOpt = API.trySExtValue();
3087+
return InitIntOpt ? DIB.createConstantValueExpression(
3088+
static_cast<uint64_t>(*InitIntOpt))
3089+
: nullptr;
3090+
};
3091+
3092+
if (Ty.isIntegerTy())
3093+
return createIntegerExpression(C);
3094+
3095+
if (Ty.isFloatTy() || Ty.isDoubleTy()) {
3096+
const APFloat &APF = cast<ConstantFP>(&C)->getValueAPF();
3097+
return DIB.createConstantValueExpression(
3098+
APF.bitcastToAPInt().getZExtValue());
3099+
}
3100+
3101+
if (!Ty.isPointerTy())
3102+
return nullptr;
3103+
3104+
if (isa<ConstantPointerNull>(C))
3105+
return DIB.createConstantValueExpression(0);
3106+
3107+
if (const ConstantExpr *CE = dyn_cast<ConstantExpr>(&C))
3108+
if (CE->getOpcode() == Instruction::IntToPtr) {
3109+
const Value *V = CE->getOperand(0);
3110+
if (auto CI = dyn_cast_or_null<ConstantInt>(V))
3111+
return createIntegerExpression(*CI);
3112+
}
3113+
return nullptr;
3114+
}
3115+
30803116
namespace {
30813117

30823118
/// A potential constituent of a bitreverse or bswap expression. See

llvm/test/Transforms/SCCP/pr50901.ll

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
; RUN: opt -passes=ipsccp -S -o - < %s | FileCheck %s
2+
3+
; Global variables g_11, g_22, g_33, g_44, g_55, g_66 and g_77
4+
; are not visible in the debugger.
5+
6+
; 1 int g_1 = -4;
7+
; 2 float g_2 = 4.44;
8+
; 3 char g_3 = 'a';
9+
; 4 unsigned g_4 = 4;
10+
; 5 bool g_5 = true;
11+
; 6 int *g_6 = nullptr;
12+
; 7 float *g_7 = nullptr;
13+
; 8
14+
; 9 static int g_11 = -5;
15+
; 10 static float g_22 = 5.55;
16+
; 11 static char g_33 = 'b';
17+
; 12 static unsigned g_44 = 5;
18+
; 13 static bool g_55 = true;
19+
; 14 static int *g_66 = nullptr;
20+
; 15 static float *g_77 = (float *)(55 + 15);
21+
; 16
22+
; 17 void bar() {
23+
; 18 g_1 = g_11;
24+
; 19 g_2 = g_22;
25+
; 20 g_3 = g_33;
26+
; 21 g_4 = g_44;
27+
; 22 g_5 = g_55;
28+
; 23 g_6 = g_66;
29+
; 24 g_7 = g_77;
30+
; 25 }
31+
; 26
32+
; 27 int main() {
33+
; 28 {
34+
; 29 bar();
35+
; 30 }
36+
; 31 }
37+
38+
; CHECK: ![[G1:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG1:[0-9]+]], expr: !DIExpression(DW_OP_constu, 18446744073709551611, DW_OP_stack_value))
39+
; CHECK-DAG: ![[DBG1]] = distinct !DIGlobalVariable(name: "g_11", {{.*}}
40+
; CHECK: ![[G2:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG2:[0-9]+]], expr: !DIExpression(DW_OP_constu, 1085381018, DW_OP_stack_value))
41+
; CHECK-DAG: ![[DBG2]] = distinct !DIGlobalVariable(name: "g_22", {{.*}}
42+
; CHECK: ![[G3:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG3:[0-9]+]], expr: !DIExpression(DW_OP_constu, 98, DW_OP_stack_value))
43+
; CHECK-DAG: ![[DBG3]] = distinct !DIGlobalVariable(name: "g_33", {{.*}}
44+
; CHECK: ![[G4:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG4:[0-9]+]], expr: !DIExpression(DW_OP_constu, 5, DW_OP_stack_value))
45+
; CHECK-DAG: ![[DBG4]] = distinct !DIGlobalVariable(name: "g_44", {{.*}}
46+
; CHECK: ![[G5:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG5:[0-9]+]], expr: !DIExpression(DW_OP_constu, 1, DW_OP_stack_value))
47+
; CHECK-DAG: ![[DBG5]] = distinct !DIGlobalVariable(name: "g_55", {{.*}}
48+
; CHECK: ![[G6:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG6:[0-9]+]], expr: !DIExpression(DW_OP_constu, 0, DW_OP_stack_value))
49+
; CHECK-DAG: ![[DBG6]] = distinct !DIGlobalVariable(name: "g_66", {{.*}}
50+
; CHECK: ![[G7:[0-9]+]] = !DIGlobalVariableExpression(var: ![[DBG7:[0-9]+]], expr: !DIExpression(DW_OP_constu, 70, DW_OP_stack_value))
51+
; CHECK-DAG: ![[DBG7]] = distinct !DIGlobalVariable(name: "g_77", {{.*}}
52+
53+
@g_1 = dso_local global i32 -4, align 4, !dbg !0
54+
@g_2 = dso_local global float 0x4011C28F60000000, align 4, !dbg !8
55+
@g_3 = dso_local global i8 97, align 1, !dbg !10
56+
@g_4 = dso_local global i32 4, align 4, !dbg !13
57+
@g_5 = dso_local global i8 1, align 1, !dbg !16
58+
@g_6 = dso_local global ptr null, align 8, !dbg !19
59+
@g_7 = dso_local global ptr null, align 8, !dbg !23
60+
@_ZL4g_11 = internal global i32 -5, align 4, !dbg !25
61+
@_ZL4g_22 = internal global float 0x4016333340000000, align 4, !dbg !27
62+
@_ZL4g_33 = internal global i8 98, align 1, !dbg !29
63+
@_ZL4g_44 = internal global i32 5, align 4, !dbg !31
64+
@_ZL4g_55 = internal global i8 1, align 1, !dbg !33
65+
@_ZL4g_66 = internal global ptr null, align 8, !dbg !35
66+
@_ZL4g_77 = internal global ptr inttoptr (i64 70 to ptr), align 8, !dbg !37
67+
68+
define dso_local void @_Z3barv() !dbg !46 {
69+
entry:
70+
%0 = load i32, ptr @_ZL4g_11, align 4, !dbg !59
71+
store i32 %0, ptr @g_1, align 4, !dbg !59
72+
%1 = load float, ptr @_ZL4g_22, align 4, !dbg !59
73+
store float %1, ptr @g_2, align 4, !dbg !59
74+
%2 = load i8, ptr @_ZL4g_33, align 1, !dbg !59
75+
store i8 %2, ptr @g_3, align 1, !dbg !59
76+
%3 = load i32, ptr @_ZL4g_44, align 4, !dbg !59
77+
store i32 %3, ptr @g_4, align 4, !dbg !59
78+
%4 = load i8, ptr @_ZL4g_55, align 1, !dbg !59
79+
%tobool = trunc i8 %4 to i1, !dbg !59
80+
%frombool = zext i1 %tobool to i8, !dbg !59
81+
store i8 %frombool, ptr @g_5, align 1, !dbg !59
82+
%5 = load ptr, ptr @_ZL4g_66, align 8, !dbg !59
83+
store ptr %5, ptr @g_6, align 8, !dbg !59
84+
%6 = load ptr, ptr @_ZL4g_77, align 8, !dbg !59
85+
store ptr %6, ptr @g_7, align 8, !dbg !59
86+
ret void, !dbg !59
87+
}
88+
89+
define dso_local noundef i32 @main() !dbg !77 {
90+
entry:
91+
call void @_Z3barv(), !dbg !80
92+
ret i32 0, !dbg !82
93+
}
94+
95+
!llvm.dbg.cu = !{!2}
96+
!llvm.module.flags = !{!39, !40, !41, !42, !43, !44}
97+
!llvm.ident = !{!45}
98+
99+
!0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression())
100+
!1 = distinct !DIGlobalVariable(name: "g_1", scope: !2, file: !3, line: 1, type: !22, isLocal: false, isDefinition: true)
101+
!2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !3, producer: "clang version 18.0.0", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, retainedTypes: !4, globals: !7, splitDebugInlining: false, nameTableKind: None)
102+
!3 = !DIFile(filename: "test.cpp", directory: "")
103+
!4 = !{!5}
104+
!5 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
105+
!6 = !DIBasicType(name: "float", size: 32, encoding: DW_ATE_float)
106+
!7 = !{!0, !8, !10, !13, !16, !19, !23, !25, !27, !29, !31, !33, !35, !37}
107+
!8 = !DIGlobalVariableExpression(var: !9, expr: !DIExpression())
108+
!9 = distinct !DIGlobalVariable(name: "g_2", scope: !2, file: !3, line: 2, type: !6, isLocal: false, isDefinition: true)
109+
!10 = !DIGlobalVariableExpression(var: !11, expr: !DIExpression())
110+
!11 = distinct !DIGlobalVariable(name: "g_3", scope: !2, file: !3, line: 3, type: !12, isLocal: false, isDefinition: true)
111+
!12 = !DIBasicType(name: "char", size: 8, encoding: DW_ATE_signed_char)
112+
!13 = !DIGlobalVariableExpression(var: !14, expr: !DIExpression())
113+
!14 = distinct !DIGlobalVariable(name: "g_4", scope: !2, file: !3, line: 4, type: !15, isLocal: false, isDefinition: true)
114+
!15 = !DIBasicType(name: "unsigned int", size: 32, encoding: DW_ATE_unsigned)
115+
!16 = !DIGlobalVariableExpression(var: !17, expr: !DIExpression())
116+
!17 = distinct !DIGlobalVariable(name: "g_5", scope: !2, file: !3, line: 5, type: !18, isLocal: false, isDefinition: true)
117+
!18 = !DIBasicType(name: "bool", size: 8, encoding: DW_ATE_boolean)
118+
!19 = !DIGlobalVariableExpression(var: !20, expr: !DIExpression())
119+
!20 = distinct !DIGlobalVariable(name: "g_6", scope: !2, file: !3, line: 6, type: !21, isLocal: false, isDefinition: true)
120+
!21 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !22, size: 64)
121+
!22 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
122+
!23 = !DIGlobalVariableExpression(var: !24, expr: !DIExpression())
123+
!24 = distinct !DIGlobalVariable(name: "g_7", scope: !2, file: !3, line: 7, type: !5, isLocal: false, isDefinition: true)
124+
!25 = !DIGlobalVariableExpression(var: !26, expr: !DIExpression())
125+
!26 = distinct !DIGlobalVariable(name: "g_11", linkageName: "_ZL4g_11", scope: !2, file: !3, line: 9, type: !22, isLocal: true, isDefinition: true)
126+
!27 = !DIGlobalVariableExpression(var: !28, expr: !DIExpression())
127+
!28 = distinct !DIGlobalVariable(name: "g_22", linkageName: "_ZL4g_22", scope: !2, file: !3, line: 10, type: !6, isLocal: true, isDefinition: true)
128+
!29 = !DIGlobalVariableExpression(var: !30, expr: !DIExpression())
129+
!30 = distinct !DIGlobalVariable(name: "g_33", linkageName: "_ZL4g_33", scope: !2, file: !3, line: 11, type: !12, isLocal: true, isDefinition: true)
130+
!31 = !DIGlobalVariableExpression(var: !32, expr: !DIExpression())
131+
!32 = distinct !DIGlobalVariable(name: "g_44", linkageName: "_ZL4g_44", scope: !2, file: !3, line: 12, type: !15, isLocal: true, isDefinition: true)
132+
!33 = !DIGlobalVariableExpression(var: !34, expr: !DIExpression())
133+
!34 = distinct !DIGlobalVariable(name: "g_55", linkageName: "_ZL4g_55", scope: !2, file: !3, line: 13, type: !18, isLocal: true, isDefinition: true)
134+
!35 = !DIGlobalVariableExpression(var: !36, expr: !DIExpression())
135+
!36 = distinct !DIGlobalVariable(name: "g_66", linkageName: "_ZL4g_66", scope: !2, file: !3, line: 14, type: !21, isLocal: true, isDefinition: true)
136+
!37 = !DIGlobalVariableExpression(var: !38, expr: !DIExpression())
137+
!38 = distinct !DIGlobalVariable(name: "g_77", linkageName: "_ZL4g_77", scope: !2, file: !3, line: 15, type: !5, isLocal: true, isDefinition: true)
138+
!39 = !{i32 7, !"Dwarf Version", i32 5}
139+
!40 = !{i32 2, !"Debug Info Version", i32 3}
140+
!41 = !{i32 1, !"wchar_size", i32 4}
141+
!42 = !{i32 8, !"PIC Level", i32 2}
142+
!43 = !{i32 7, !"PIE Level", i32 2}
143+
!44 = !{i32 7, !"uwtable", i32 2}
144+
!45 = !{!"clang version 18.0.0"}
145+
!46 = distinct !DISubprogram(name: "bar", linkageName: "_Z3barv", scope: !3, file: !3, line: 17, type: !47, scopeLine: 17, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !2)
146+
!47 = !DISubroutineType(types: !48)
147+
!48 = !{null}
148+
!59 = !DILocation(line: 20, column: 9, scope: !46)
149+
!77 = distinct !DISubprogram(name: "main", scope: !3, file: !3, line: 27, type: !78, scopeLine: 27, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !2)
150+
!78 = !DISubroutineType(types: !79)
151+
!79 = !{!22}
152+
!80 = !DILocation(line: 29, column: 5, scope: !81)
153+
!81 = distinct !DILexicalBlock(scope: !77, file: !3, line: 28, column: 3)
154+
!82 = !DILocation(line: 31, column: 1, scope: !77)

llvm/unittests/Transforms/Utils/LocalTest.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,3 +1161,114 @@ TEST(Local, CanReplaceOperandWithVariable) {
11611161

11621162
BB0->dropAllReferences();
11631163
}
1164+
1165+
TEST(Local, ExpressionForConstant) {
1166+
LLVMContext Context;
1167+
Module M("test_module", Context);
1168+
DIBuilder DIB(M);
1169+
DIExpression *Expr = nullptr;
1170+
1171+
auto createExpression = [&](Constant *C, Type *Ty) -> DIExpression * {
1172+
EXPECT_NE(C, nullptr);
1173+
EXPECT_NE(Ty, nullptr);
1174+
EXPECT_EQ(C->getType(), Ty);
1175+
std::unique_ptr<GlobalVariable> GV = std::make_unique<GlobalVariable>(
1176+
Ty, false, GlobalValue::ExternalLinkage, C, "GV");
1177+
EXPECT_NE(GV, nullptr);
1178+
1179+
DIExpression *Expr = getExpressionForConstant(DIB, *GV->getInitializer(),
1180+
*GV->getValueType());
1181+
if (Expr) {
1182+
EXPECT_EQ(Expr->getNumElements(), 3u);
1183+
EXPECT_EQ(Expr->getElement(0), dwarf::DW_OP_constu);
1184+
EXPECT_EQ(Expr->getElement(2), dwarf::DW_OP_stack_value);
1185+
}
1186+
return Expr;
1187+
};
1188+
1189+
// Integer.
1190+
IntegerType *Int1Ty = Type::getInt1Ty(Context);
1191+
Expr = createExpression(ConstantInt::getTrue(Context), Int1Ty);
1192+
EXPECT_NE(Expr, nullptr);
1193+
EXPECT_EQ(Expr->getElement(1), 18446744073709551615);
1194+
1195+
Expr = createExpression(ConstantInt::getFalse(Context), Int1Ty);
1196+
EXPECT_NE(Expr, nullptr);
1197+
EXPECT_EQ(Expr->getElement(1), 0);
1198+
1199+
IntegerType *Int8Ty = Type::getInt8Ty(Context);
1200+
Expr = createExpression(ConstantInt::get(Int8Ty, 100), Int8Ty);
1201+
EXPECT_NE(Expr, nullptr);
1202+
EXPECT_EQ(Expr->getElement(1), 100);
1203+
1204+
IntegerType *Int16Ty = Type::getInt16Ty(Context);
1205+
Expr = createExpression(ConstantInt::getSigned(Int16Ty, -50), Int16Ty);
1206+
EXPECT_NE(Expr, nullptr);
1207+
EXPECT_EQ(Expr->getElement(1), -50);
1208+
1209+
IntegerType *Int32Ty = Type::getInt32Ty(Context);
1210+
Expr = createExpression(ConstantInt::get(Int32Ty, 0x7FFFFFFF), Int32Ty);
1211+
EXPECT_NE(Expr, nullptr);
1212+
EXPECT_EQ(Expr->getElement(1), 0x7FFFFFFF);
1213+
1214+
IntegerType *Int64Ty = Type::getInt64Ty(Context);
1215+
Expr =
1216+
createExpression(ConstantInt::get(Int64Ty, 0x7FFFFFFFFFFFFFFF), Int64Ty);
1217+
EXPECT_NE(Expr, nullptr);
1218+
EXPECT_EQ(Expr->getElement(1), 0x7FFFFFFFFFFFFFFF);
1219+
1220+
IntegerType *Int128Ty = Type::getInt128Ty(Context);
1221+
Expr = createExpression(ConstantInt::get(Int128Ty, 0x7FFFFFFFFFFFFFFF),
1222+
Int128Ty);
1223+
EXPECT_NE(Expr, nullptr);
1224+
EXPECT_EQ(Expr->getElement(1), 0x7FFFFFFFFFFFFFFF);
1225+
1226+
// Float.
1227+
Type *FloatTy = Type::getFloatTy(Context);
1228+
Expr = createExpression(ConstantFP::get(FloatTy, 5.55), FloatTy);
1229+
EXPECT_NE(Expr, nullptr);
1230+
EXPECT_EQ(Expr->getElement(1), 1085381018);
1231+
1232+
// Double.
1233+
Type *DoubleTy = Type::getDoubleTy(Context);
1234+
Expr = createExpression(ConstantFP::get(DoubleTy, -5.55), DoubleTy);
1235+
EXPECT_NE(Expr, nullptr);
1236+
EXPECT_EQ(Expr->getElement(1), 13841306799765140275);
1237+
1238+
// Pointer.
1239+
PointerType *PtrTy = PointerType::get(Context, 0);
1240+
Expr = createExpression(ConstantPointerNull::get(PtrTy), PtrTy);
1241+
EXPECT_NE(Expr, nullptr);
1242+
EXPECT_EQ(Expr->getElement(1), 0);
1243+
1244+
ConstantInt *K1 = ConstantInt::get(Type::getInt32Ty(Context), 1234);
1245+
Expr = createExpression(ConstantExpr::getIntToPtr(K1, PtrTy), PtrTy);
1246+
EXPECT_NE(Expr, nullptr);
1247+
EXPECT_EQ(Expr->getElement(1), 1234);
1248+
1249+
ConstantInt *K2 = ConstantInt::get(Type::getInt64Ty(Context), 5678);
1250+
Expr = createExpression(ConstantExpr::getIntToPtr(K2, PtrTy), PtrTy);
1251+
EXPECT_NE(Expr, nullptr);
1252+
EXPECT_EQ(Expr->getElement(1), 5678);
1253+
1254+
// Others.
1255+
Type *HalfTy = Type::getHalfTy(Context);
1256+
Expr = createExpression(ConstantFP::get(HalfTy, 32), HalfTy);
1257+
EXPECT_EQ(Expr, nullptr);
1258+
1259+
Type *BFloatTy = Type::getBFloatTy(Context);
1260+
Expr = createExpression(ConstantFP::get(BFloatTy, 32), BFloatTy);
1261+
EXPECT_EQ(Expr, nullptr);
1262+
1263+
Type *FP128Ty = Type::getFP128Ty(Context);
1264+
Expr = createExpression(ConstantFP::get(FP128Ty, 32), FP128Ty);
1265+
EXPECT_EQ(Expr, nullptr);
1266+
1267+
Type *X86_FP80Ty = Type::getX86_FP80Ty(Context);
1268+
Expr = createExpression(ConstantFP::get(X86_FP80Ty, 32), X86_FP80Ty);
1269+
EXPECT_EQ(Expr, nullptr);
1270+
1271+
Type *PPC_FP128Ty = Type::getPPC_FP128Ty(Context);
1272+
Expr = createExpression(ConstantFP::get(PPC_FP128Ty, 32), PPC_FP128Ty);
1273+
EXPECT_EQ(Expr, nullptr);
1274+
}

0 commit comments

Comments
 (0)