1 /*
2  * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 
25 package org.graalvm.compiler.replacements.test.classfile;
26 
27 import static org.graalvm.compiler.bytecode.Bytecodes.ALOAD;
28 import static org.graalvm.compiler.bytecode.Bytecodes.ANEWARRAY;
29 import static org.graalvm.compiler.bytecode.Bytecodes.ASTORE;
30 import static org.graalvm.compiler.bytecode.Bytecodes.BIPUSH;
31 import static org.graalvm.compiler.bytecode.Bytecodes.CHECKCAST;
32 import static org.graalvm.compiler.bytecode.Bytecodes.DLOAD;
33 import static org.graalvm.compiler.bytecode.Bytecodes.DSTORE;
34 import static org.graalvm.compiler.bytecode.Bytecodes.FLOAD;
35 import static org.graalvm.compiler.bytecode.Bytecodes.FSTORE;
36 import static org.graalvm.compiler.bytecode.Bytecodes.GETFIELD;
37 import static org.graalvm.compiler.bytecode.Bytecodes.GETSTATIC;
38 import static org.graalvm.compiler.bytecode.Bytecodes.GOTO;
39 import static org.graalvm.compiler.bytecode.Bytecodes.GOTO_W;
40 import static org.graalvm.compiler.bytecode.Bytecodes.IFEQ;
41 import static org.graalvm.compiler.bytecode.Bytecodes.IFGE;
42 import static org.graalvm.compiler.bytecode.Bytecodes.IFGT;
43 import static org.graalvm.compiler.bytecode.Bytecodes.IFLE;
44 import static org.graalvm.compiler.bytecode.Bytecodes.IFLT;
45 import static org.graalvm.compiler.bytecode.Bytecodes.IFNE;
46 import static org.graalvm.compiler.bytecode.Bytecodes.IFNONNULL;
47 import static org.graalvm.compiler.bytecode.Bytecodes.IFNULL;
48 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ACMPEQ;
49 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ACMPNE;
50 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPEQ;
51 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPGE;
52 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPGT;
53 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPLE;
54 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPLT;
55 import static org.graalvm.compiler.bytecode.Bytecodes.IF_ICMPNE;
56 import static org.graalvm.compiler.bytecode.Bytecodes.ILOAD;
57 import static org.graalvm.compiler.bytecode.Bytecodes.INSTANCEOF;
58 import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEDYNAMIC;
59 import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEINTERFACE;
60 import static org.graalvm.compiler.bytecode.Bytecodes.INVOKESPECIAL;
61 import static org.graalvm.compiler.bytecode.Bytecodes.INVOKESTATIC;
62 import static org.graalvm.compiler.bytecode.Bytecodes.INVOKEVIRTUAL;
63 import static org.graalvm.compiler.bytecode.Bytecodes.ISTORE;
64 import static org.graalvm.compiler.bytecode.Bytecodes.JSR;
65 import static org.graalvm.compiler.bytecode.Bytecodes.JSR_W;
66 import static org.graalvm.compiler.bytecode.Bytecodes.LDC;
67 import static org.graalvm.compiler.bytecode.Bytecodes.LDC2_W;
68 import static org.graalvm.compiler.bytecode.Bytecodes.LDC_W;
69 import static org.graalvm.compiler.bytecode.Bytecodes.LLOAD;
70 import static org.graalvm.compiler.bytecode.Bytecodes.LOOKUPSWITCH;
71 import static org.graalvm.compiler.bytecode.Bytecodes.LSTORE;
72 import static org.graalvm.compiler.bytecode.Bytecodes.MULTIANEWARRAY;
73 import static org.graalvm.compiler.bytecode.Bytecodes.NEW;
74 import static org.graalvm.compiler.bytecode.Bytecodes.NEWARRAY;
75 import static org.graalvm.compiler.bytecode.Bytecodes.PUTFIELD;
76 import static org.graalvm.compiler.bytecode.Bytecodes.PUTSTATIC;
77 import static org.graalvm.compiler.bytecode.Bytecodes.RET;
78 import static org.graalvm.compiler.bytecode.Bytecodes.SIPUSH;
79 import static org.graalvm.compiler.bytecode.Bytecodes.TABLESWITCH;
80 
81 import java.io.File;
82 import java.io.IOException;
83 import java.lang.reflect.Executable;
84 import java.lang.reflect.Method;
85 import java.util.Enumeration;
86 import java.util.Formatter;
87 import java.util.zip.ZipEntry;
88 import java.util.zip.ZipFile;
89 
90 import org.graalvm.compiler.test.SubprocessUtil;
91 import org.junit.Assert;
92 import org.junit.Assume;
93 import org.junit.Before;
94 import org.junit.Test;
95 
96 import org.graalvm.compiler.api.replacements.SnippetReflectionProvider;
97 import org.graalvm.compiler.api.test.Graal;
98 import org.graalvm.compiler.bytecode.Bytecode;
99 import org.graalvm.compiler.bytecode.BytecodeDisassembler;
100 import org.graalvm.compiler.bytecode.BytecodeLookupSwitch;
101 import org.graalvm.compiler.bytecode.BytecodeStream;
102 import org.graalvm.compiler.bytecode.BytecodeSwitch;
103 import org.graalvm.compiler.bytecode.BytecodeTableSwitch;
104 import org.graalvm.compiler.bytecode.Bytecodes;
105 import org.graalvm.compiler.bytecode.ResolvedJavaMethodBytecode;
106 import org.graalvm.compiler.core.test.GraalCompilerTest;
107 import org.graalvm.compiler.phases.VerifyPhase;
108 import org.graalvm.compiler.phases.util.Providers;
109 import org.graalvm.compiler.replacements.classfile.ClassfileBytecode;
110 import org.graalvm.compiler.replacements.classfile.ClassfileBytecodeProvider;
111 import org.graalvm.compiler.runtime.RuntimeProvider;
112 
113 import jdk.vm.ci.meta.ConstantPool;
114 import jdk.vm.ci.meta.JavaField;
115 import jdk.vm.ci.meta.JavaMethodProfile.ProfiledMethod;
116 import jdk.vm.ci.meta.JavaType;
117 import jdk.vm.ci.meta.MetaAccessProvider;
118 import jdk.vm.ci.meta.ResolvedJavaField;
119 import jdk.vm.ci.meta.ResolvedJavaMethod;
120 import jdk.vm.ci.meta.ResolvedJavaType;
121 
122 /**
123  * Tests that bytecode exposed via {@link ClassfileBytecode} objects is the same as the bytecode
124  * (modulo minor differences in constant pool resolution) obtained directly from
125  * {@link ResolvedJavaMethod} objects.
126  */
127 public class ClassfileBytecodeProviderTest extends GraalCompilerTest {
128 
129     @Before
checkJavaAgent()130     public void checkJavaAgent() {
131         assumeManagementLibraryIsLoadable();
132         Assume.assumeFalse("Java Agent found -> skipping", SubprocessUtil.isJavaAgentAttached());
133     }
134 
shouldProcess(String classpathEntry)135     private static boolean shouldProcess(String classpathEntry) {
136         if (classpathEntry.endsWith(".jar")) {
137             String name = new File(classpathEntry).getName();
138             return name.contains("jvmci") || name.contains("graal");
139         }
140         return false;
141     }
142 
143     /**
144      * Keep test time down by only sampling a limited number of class files per jar.
145      */
146     private static final int CLASSES_PER_JAR = 250;
147 
148     @Test
test()149     public void test() {
150         RuntimeProvider rt = Graal.getRequiredCapability(RuntimeProvider.class);
151         Providers providers = rt.getHostBackend().getProviders();
152         MetaAccessProvider metaAccess = providers.getMetaAccess();
153 
154         Assume.assumeTrue(VerifyPhase.class.desiredAssertionStatus());
155 
156         String propertyName = Java8OrEarlier ? "sun.boot.class.path" : "jdk.module.path";
157         String bootclasspath = System.getProperty(propertyName);
158         Assert.assertNotNull("Cannot find value of " + propertyName, bootclasspath);
159 
160         for (String path : bootclasspath.split(File.pathSeparator)) {
161             if (shouldProcess(path)) {
162                 try {
163                     final ZipFile zipFile = new ZipFile(new File(path));
164                     int index = 0;
165                     int step = zipFile.size() > CLASSES_PER_JAR ? zipFile.size() / CLASSES_PER_JAR : 1;
166                     for (final Enumeration<? extends ZipEntry> entry = zipFile.entries(); entry.hasMoreElements();) {
167                         final ZipEntry zipEntry = entry.nextElement();
168                         if ((index % step) == 0) {
169                             String name = zipEntry.getName();
170                             if (name.endsWith(".class") && !name.equals("module-info.class") && !name.startsWith("META-INF/versions/")) {
171                                 String className = name.substring(0, name.length() - ".class".length()).replace('/', '.');
172                                 if (isInNativeImage(className)) {
173                                     /*
174                                      * Native image requires non-graalsdk classes to be present in
175                                      * the classpath.
176                                      */
177                                     continue;
178                                 }
179                                 if (isGSON(className)) {
180                                     /* uses old class format */
181                                     continue;
182                                 }
183                                 try {
184                                     checkClass(metaAccess, getSnippetReflection(), className);
185                                 } catch (ClassNotFoundException e) {
186                                     throw new AssertionError(e);
187                                 }
188                             }
189                         }
190                         index++;
191                     }
192                 } catch (IOException ex) {
193                     Assert.fail(ex.toString());
194                 }
195             }
196         }
197     }
198 
isInNativeImage(String className)199     private static boolean isInNativeImage(String className) {
200         return className.startsWith("org.graalvm.nativeimage");
201     }
202 
isGSON(String className)203     private static boolean isGSON(String className) {
204         return className.contains("com.google.gson");
205     }
206 
checkClass(MetaAccessProvider metaAccess, SnippetReflectionProvider snippetReflection, String className)207     protected void checkClass(MetaAccessProvider metaAccess, SnippetReflectionProvider snippetReflection, String className) throws ClassNotFoundException {
208         if (className.equals("jdk.vm.ci.services.JVMCIClassLoaderFactory")) {
209             // JVMCIClassLoaderFactory must only be initialized by the VM
210             return;
211         }
212         Class<?> c = Class.forName(className, true, getClass().getClassLoader());
213         ClassfileBytecodeProvider cbp = new ClassfileBytecodeProvider(metaAccess, snippetReflection);
214         for (Method method : c.getDeclaredMethods()) {
215             checkMethod(cbp, metaAccess, method);
216         }
217     }
218 
checkMethod(ClassfileBytecodeProvider cbp, MetaAccessProvider metaAccess, Executable executable)219     private static void checkMethod(ClassfileBytecodeProvider cbp, MetaAccessProvider metaAccess, Executable executable) {
220         ResolvedJavaMethod method = metaAccess.lookupJavaMethod(executable);
221         if (method.hasBytecodes()) {
222             Bytecode actual = getBytecode(cbp, method);
223             if (actual != null) {
224                 ResolvedJavaMethodBytecode expected = new ResolvedJavaMethodBytecode(method);
225                 new BytecodeComparer(expected, actual).compare();
226             }
227         }
228     }
229 
getBytecode(ClassfileBytecodeProvider cbp, ResolvedJavaMethod method)230     protected static Bytecode getBytecode(ClassfileBytecodeProvider cbp, ResolvedJavaMethod method) {
231         try {
232             return cbp.getBytecode(method);
233         } catch (UnsupportedClassVersionError e) {
234             // This can happen when a library containing old class files
235             // is bundled into a Graal jar (GR-12672).
236             return null;
237         } catch (Throwable e) {
238             throw new AssertionError(String.format("Error getting bytecode for %s", method.format("%H.%n(%p)")), e);
239         }
240     }
241 
242     static class BytecodeComparer {
243 
244         private Bytecode expected;
245         private Bytecode actual;
246         private ConstantPool eCp;
247         private ConstantPool aCp;
248         BytecodeStream eStream;
249         BytecodeStream aStream;
250         int bci = -1;
251 
BytecodeComparer(Bytecode expected, Bytecode actual)252         BytecodeComparer(Bytecode expected, Bytecode actual) {
253             this.expected = expected;
254             this.actual = actual;
255             this.eCp = expected.getConstantPool();
256             this.aCp = actual.getConstantPool();
257             Assert.assertEquals(expected.getMethod().toString(), expected.getCodeSize(), actual.getCodeSize());
258             this.eStream = new BytecodeStream(expected.getCode());
259             this.aStream = new BytecodeStream(actual.getCode());
260         }
261 
compare()262         public void compare() {
263             try {
264                 compare0();
265             } catch (Throwable e) {
266                 BytecodeDisassembler dis = new BytecodeDisassembler(true, false);
267                 Formatter msg = new Formatter();
268                 msg.format("Error comparing bytecode for %s", expected.getMethod().format("%H.%n(%p)"));
269                 if (bci >= 0) {
270                     msg.format("%nexpected: %s", dis.disassemble(expected, bci, eStream.nextBCI() - 1));
271                     msg.format("%nactual:   %s", dis.disassemble(actual, bci, aStream.nextBCI() - 1));
272                 }
273                 throw new AssertionError(msg.toString(), e);
274             }
275         }
276 
compare0()277         public void compare0() {
278             int opcode = eStream.currentBC();
279             ResolvedJavaMethod method = expected.getMethod();
280             while (opcode != Bytecodes.END) {
281                 bci = eStream.currentBCI();
282                 int actualOpcode = aStream.currentBC();
283                 if (opcode != actualOpcode) {
284                     Assert.assertEquals(opcode, actualOpcode);
285                 }
286                 if (eStream.nextBCI() > bci + 1) {
287                     switch (opcode) {
288                         case BIPUSH:
289                             Assert.assertEquals(eStream.readByte(), aStream.readByte());
290                             break;
291                         case SIPUSH:
292                             Assert.assertEquals(eStream.readShort(), aStream.readShort());
293                             break;
294                         case NEW:
295                         case CHECKCAST:
296                         case INSTANCEOF:
297                         case ANEWARRAY: {
298                             ResolvedJavaType e = lookupType(eCp, eStream.readCPI(), opcode);
299                             ResolvedJavaType a = lookupType(aCp, aStream.readCPI(), opcode);
300                             assertEqualTypes(e, a);
301                             break;
302                         }
303                         case GETSTATIC:
304                         case PUTSTATIC:
305                         case GETFIELD:
306                         case PUTFIELD: {
307                             ResolvedJavaField e = lookupField(eCp, eStream.readCPI(), method, opcode);
308                             ResolvedJavaField a = lookupField(aCp, aStream.readCPI(), method, opcode);
309                             assertEqualFields(e, a);
310                             break;
311                         }
312                         case INVOKEVIRTUAL:
313                         case INVOKESPECIAL:
314                         case INVOKESTATIC: {
315                             ResolvedJavaMethod e = lookupMethod(eCp, eStream.readCPI(), opcode);
316                             ResolvedJavaMethod a = lookupMethodOrNull(aCp, aStream.readCPI(), opcode);
317                             assertEqualMethods(e, a);
318                             break;
319                         }
320                         case INVOKEINTERFACE: {
321                             ResolvedJavaMethod e = lookupMethod(eCp, eStream.readCPI(), opcode);
322                             ResolvedJavaMethod a = lookupMethod(aCp, aStream.readCPI(), opcode);
323                             assertEqualMethods(e, a);
324                             break;
325                         }
326                         case INVOKEDYNAMIC: {
327                             // INVOKEDYNAMIC is not supported by ClassfileBytecodeProvider
328                             return;
329                         }
330                         case LDC:
331                         case LDC_W:
332                         case LDC2_W: {
333                             Object e = lookupConstant(eCp, eStream.readCPI(), opcode);
334                             Object a = lookupConstant(aCp, aStream.readCPI(), opcode);
335                             assertEqualsConstants(e, a);
336                             break;
337                         }
338                         case RET:
339                         case ILOAD:
340                         case LLOAD:
341                         case FLOAD:
342                         case DLOAD:
343                         case ALOAD:
344                         case ISTORE:
345                         case LSTORE:
346                         case FSTORE:
347                         case DSTORE:
348                         case ASTORE: {
349                             Assert.assertEquals(eStream.readLocalIndex(), aStream.readLocalIndex());
350                             break;
351                         }
352                         case IFEQ:
353                         case IFNE:
354                         case IFLT:
355                         case IFGE:
356                         case IFGT:
357                         case IFLE:
358                         case IF_ICMPEQ:
359                         case IF_ICMPNE:
360                         case IF_ICMPLT:
361                         case IF_ICMPGE:
362                         case IF_ICMPGT:
363                         case IF_ICMPLE:
364                         case IF_ACMPEQ:
365                         case IF_ACMPNE:
366                         case GOTO:
367                         case JSR:
368                         case IFNULL:
369                         case IFNONNULL:
370                         case GOTO_W:
371                         case JSR_W: {
372                             Assert.assertEquals(eStream.readBranchDest(), aStream.readBranchDest());
373                             break;
374                         }
375                         case LOOKUPSWITCH:
376                         case TABLESWITCH: {
377                             BytecodeSwitch e = opcode == LOOKUPSWITCH ? new BytecodeLookupSwitch(eStream, bci) : new BytecodeTableSwitch(eStream, bci);
378                             BytecodeSwitch a = opcode == LOOKUPSWITCH ? new BytecodeLookupSwitch(aStream, bci) : new BytecodeTableSwitch(aStream, bci);
379                             Assert.assertEquals(e.numberOfCases(), a.numberOfCases());
380                             for (int i = 0; i < e.numberOfCases(); i++) {
381                                 Assert.assertEquals(e.keyAt(i), a.keyAt(i));
382                                 Assert.assertEquals(e.targetAt(i), a.targetAt(i));
383                             }
384                             Assert.assertEquals(e.defaultTarget(), a.defaultTarget());
385                             Assert.assertEquals(e.defaultOffset(), a.defaultOffset());
386                             break;
387                         }
388                         case NEWARRAY: {
389                             Assert.assertEquals(eStream.readLocalIndex(), aStream.readLocalIndex());
390                             break;
391                         }
392                         case MULTIANEWARRAY: {
393                             ResolvedJavaType e = lookupType(eCp, eStream.readCPI(), opcode);
394                             ResolvedJavaType a = lookupType(aCp, aStream.readCPI(), opcode);
395                             Assert.assertEquals(e, a);
396                             break;
397                         }
398                     }
399                 }
400                 eStream.next();
401                 aStream.next();
402                 opcode = eStream.currentBC();
403             }
404         }
405 
lookupConstant(ConstantPool cp, int cpi, int opcode)406         static Object lookupConstant(ConstantPool cp, int cpi, int opcode) {
407             cp.loadReferencedType(cpi, opcode);
408             return cp.lookupConstant(cpi);
409         }
410 
lookupField(ConstantPool cp, int cpi, ResolvedJavaMethod method, int opcode)411         static ResolvedJavaField lookupField(ConstantPool cp, int cpi, ResolvedJavaMethod method, int opcode) {
412             cp.loadReferencedType(cpi, opcode);
413             return (ResolvedJavaField) cp.lookupField(cpi, method, opcode);
414         }
415 
lookupMethod(ConstantPool cp, int cpi, int opcode)416         static ResolvedJavaMethod lookupMethod(ConstantPool cp, int cpi, int opcode) {
417             cp.loadReferencedType(cpi, opcode);
418             return (ResolvedJavaMethod) cp.lookupMethod(cpi, opcode);
419         }
420 
lookupMethodOrNull(ConstantPool cp, int cpi, int opcode)421         static ResolvedJavaMethod lookupMethodOrNull(ConstantPool cp, int cpi, int opcode) {
422             try {
423                 return lookupMethod(cp, cpi, opcode);
424             } catch (NoSuchMethodError e) {
425                 // A method hidden to reflection
426                 return null;
427             }
428         }
429 
lookupType(ConstantPool cp, int cpi, int opcode)430         static ResolvedJavaType lookupType(ConstantPool cp, int cpi, int opcode) {
431             cp.loadReferencedType(cpi, opcode);
432             return (ResolvedJavaType) cp.lookupType(cpi, opcode);
433         }
434 
assertEqualsConstants(Object e, Object a)435         static void assertEqualsConstants(Object e, Object a) {
436             if (!e.equals(a)) {
437                 Assert.assertEquals(String.valueOf(e), String.valueOf(a));
438             }
439         }
440 
assertEqualFields(JavaField e, JavaField a)441         static void assertEqualFields(JavaField e, JavaField a) {
442             if (!e.equals(a)) {
443                 Assert.assertEquals(e.format("%H.%n %T"), a.format("%H.%n %T"));
444             }
445         }
446 
assertEqualTypes(JavaType e, JavaType a)447         static void assertEqualTypes(JavaType e, JavaType a) {
448             if (!e.equals(a)) {
449                 Assert.assertEquals(e.toJavaName(), a.toJavaName());
450             }
451         }
452 
assertEqualMethods(ResolvedJavaMethod e, ResolvedJavaMethod a)453         static void assertEqualMethods(ResolvedJavaMethod e, ResolvedJavaMethod a) {
454             if (a != null) {
455                 if (!e.equals(a)) {
456                     if (!e.equals(a)) {
457                         if (!e.getDeclaringClass().equals(a.getDeclaringClass())) {
458 
459                             if (!typesAreRelated(e, a)) {
460                                 throw new AssertionError(String.format("%s and %s are unrelated", a.getDeclaringClass().toJavaName(), e.getDeclaringClass().toJavaName()));
461                             }
462                         }
463                         Assert.assertEquals(e.getName(), a.getName());
464                         Assert.assertEquals(e.getSignature(), a.getSignature());
465                     } else {
466                         Assert.assertEquals(e, a);
467                     }
468                 }
469             }
470         }
471 
472         /**
473          * The VM can resolve references to methods not available via reflection. For example, the
474          * javap output for {@link ProfiledMethod#toString()} includes:
475          *
476          * <pre>
477          *     16: invokeinterface #40, 1 // InterfaceMethod jdk/vm/ci/meta/ResolvedJavaMethod.getName:()Ljava/lang/String;
478          * </pre>
479          *
480          * When resolving via {@code HotSpotConstantPool}, we get:
481          *
482          * <pre>
483          *     16: invokeinterface#4, 1   // jdk.vm.ci.meta.ResolvedJavaMethod.getName:()java.lang.String
484          * </pre>
485          *
486          * However resolving via {@code ClassfileConstantPool}, we get:
487          *
488          * <pre>
489          *     16: invokeinterface#40, 1  // jdk.vm.ci.meta.JavaMethod.getName:()java.lang.String
490          * </pre>
491          *
492          * since the latter relies on {@link ResolvedJavaType#getDeclaredMethods()} which only
493          * returns methods originating from class files.
494          *
495          * We accept such differences for the purpose of this test if the declaring class of two
496          * otherwise similar methods are related (i.e. one is a subclass of the other).
497          */
typesAreRelated(ResolvedJavaMethod e, ResolvedJavaMethod a)498         protected static boolean typesAreRelated(ResolvedJavaMethod e, ResolvedJavaMethod a) {
499             return a.getDeclaringClass().isAssignableFrom(e.getDeclaringClass()) || e.getDeclaringClass().isAssignableFrom(a.getDeclaringClass());
500         }
501     }
502 }
503