1 /*
  2  * Copyright (c) 2024, 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.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package jdk.incubator.code.extern;
 27 
 28 import java.io.*;
 29 
 30 import jdk.incubator.code.*;
 31 import jdk.incubator.code.dialect.java.JavaType;
 32 import jdk.incubator.code.dialect.java.impl.JavaTypeUtils;
 33 
 34 import java.lang.reflect.Array;
 35 import java.nio.charset.StandardCharsets;
 36 import java.util.HashMap;
 37 import java.util.Map;
 38 import java.util.function.Consumer;
 39 import java.util.function.Function;
 40 
 41 /**
 42  * A writer of code models to the textual form.
 43  * <p>
 44  * A code model in textual form may be parsed back into the runtime form by parsing it.
 45  */
 46 public final class OpWriter {
 47 
 48     /**
 49      * The attribute name associated with the location attribute.
 50      */
 51     static final String ATTRIBUTE_LOCATION = "loc";
 52 
 53     static final class GlobalValueBlockNaming implements Function<CodeItem, String> {
 54         final Map<CodeItem, String> gn;
 55         int valueOrdinal = 0;
 56 
 57         GlobalValueBlockNaming() {
 58             this.gn = new HashMap<>();
 59         }
 60 
 61         private String name(Block b) {
 62             Block p = b.ancestorBlock();
 63             return (p == null ? "block_" : name(p) + "_") + b.index();
 64         }
 65 
 66         @Override
 67         public String apply(CodeItem codeItem) {
 68             return switch (codeItem) {
 69                 case Block block -> gn.computeIfAbsent(block, _b -> name(block));
 70                 case Value value -> gn.computeIfAbsent(value, _v -> String.valueOf(valueOrdinal++));
 71                 default -> throw new IllegalStateException("Unexpected code item: " + codeItem);
 72             };
 73         }
 74     }
 75 
 76     static final class AttributeMapper {
 77         static String toString(Object value) {
 78             if (value == ExternalizedOp.NULL_ATTRIBUTE_VALUE) {
 79                 return "null";
 80             }
 81 
 82             StringBuilder sb = new StringBuilder();
 83             toString(value, sb);
 84             return sb.toString();
 85         }
 86 
 87         static void toString(Object o, StringBuilder sb) {
 88             if (o.getClass().isArray()) {
 89                 // note, while we can't parse back the array representation, this might be useful
 90                 // for non-externalizable ops that want better string representation of array attribute values (e.g. ONNX)
 91                 arrayToString(o, sb);
 92             } else {
 93                 switch (o) {
 94                     case Integer i -> sb.append(i);
 95                     case Long l -> sb.append(l).append('L');
 96                     case Float f -> sb.append(f).append('f');
 97                     case Double d -> sb.append(d).append('d');
 98                     case Character c -> sb.append('\'').append(c).append('\'');
 99                     case Boolean b -> sb.append(b);
100                     case TypeElement te -> sb.append(JavaTypeUtils.flatten(te.externalize()));
101                     default -> {  // fallback to a string
102                         sb.append('"');
103                         quote(o.toString(), sb);
104                         sb.append('"');
105                     }
106                 }
107             }
108         }
109 
110         static void arrayToString(Object a, StringBuilder sb) {
111             boolean first = true;
112             sb.append("[");
113             for (int i = 0; i < Array.getLength(a); i++) {
114                 if (!first) {
115                     sb.append(", ");
116                 }
117 
118                 toString(Array.get(a, i), sb);
119                 first = false;
120             }
121             sb.append("]");
122         }
123     }
124 
125     static void quote(String s, StringBuilder sb) {
126         for (int i = 0; i < s.length(); i++) {
127             sb.append(quote(s.charAt(i)));
128         }
129     }
130 
131     /**
132      * Escapes a character if it has an escape sequence or is
133      * non-printable ASCII.  Leaves non-ASCII characters alone.
134      */
135     // Copied from com.sun.tools.javac.util.Convert
136     static String quote(char ch) {
137         return switch (ch) {
138             case '\b' -> "\\b";
139             case '\f' -> "\\f";
140             case '\n' -> "\\n";
141             case '\r' -> "\\r";
142             case '\t' -> "\\t";
143             case '\'' -> "\\'";
144             case '\"' -> "\\\"";
145             case '\\' -> "\\\\";
146             default -> (isPrintableAscii(ch))
147                     ? String.valueOf(ch)
148                     : String.format("\\u%04x", (int) ch);
149         };
150     }
151 
152     /**
153      * Is a character printable ASCII?
154      */
155     static boolean isPrintableAscii(char ch) {
156         return ch >= ' ' && ch <= '~';
157     }
158 
159     static final class IndentWriter extends Writer {
160         static final int INDENT = 2;
161 
162         final Writer w;
163         int indent;
164         boolean writeIndent = true;
165 
166         IndentWriter(Writer w) {
167             this(w, 0);
168         }
169 
170         IndentWriter(Writer w, int indent) {
171             this.w = w;
172             this.indent = indent;
173         }
174 
175         @Override
176         public void write(char[] cbuf, int off, int len) throws IOException {
177             if (writeIndent) {
178                 w.write(" ".repeat(indent));
179                 writeIndent = false;
180             }
181             w.write(cbuf, off, len);
182             if (len > 0 && cbuf[off + len - 1] == '\n') {
183                 writeIndent = true;
184             }
185         }
186 
187         @Override
188         public void flush() throws IOException {
189             w.flush();
190         }
191 
192         @Override
193         public void close() throws IOException {
194             w.close();
195         }
196 
197         void in() {
198             in(INDENT);
199         }
200 
201         void in(int i) {
202             indent += i;
203         }
204 
205         void out() {
206             out(INDENT);
207         }
208 
209         void out(int i) {
210             indent -= i;
211         }
212     }
213 
214     /**
215      * Computes global names for blocks and values in a code model.
216      * <p>
217      * The code model is traversed in the same order as if the model
218      * was written. Therefore, the names in the returned map will the
219      * same as the names that are written. This can be useful for debugging
220      * and testing.
221      *
222      * @param root the code model
223      * @return the map of computed names, modifiable
224      */
225     public static Function<CodeItem, String> computeGlobalNames(Op root) {
226         OpWriter w = new OpWriter(Writer.nullWriter());
227         w.writeOp(root);
228         return w.namer();
229     }
230 
231     /**
232      * Writes a code model (an operation) to the output stream, using the UTF-8 character set.
233      *
234      * @param out the output stream
235      * @param op the code model
236      * @param options writer options
237      */
238     public static void writeTo(OutputStream out, Op op, Option... options) {
239         writeTo(new OutputStreamWriter(out, StandardCharsets.UTF_8), op, options);
240     }
241 
242     /**
243      * Writes a code model (an operation) to the character stream.
244      * <p>
245      * The character stream will be flushed after the model is writen.
246      *
247      * @param w the character stream
248      * @param op the code model
249      * @param options the writer options
250      */
251     public static void writeTo(Writer w, Op op, Option... options) {
252         OpWriter ow = new OpWriter(w, options);
253         ow.writeOp(op);
254         try {
255             // @@@ Is this needed?
256             w.flush();
257         } catch (IOException e) {
258             throw new UncheckedIOException(e);
259         }
260     }
261 
262     /**
263      * {@return the textual representation of the code model operation}
264      *
265      * @param op the code model operation
266      * @param options the writer options
267      */
268     public static String toText(Op op, OpWriter.Option... options) {
269         StringWriter w = new StringWriter();
270         writeTo(w, op, options);
271         return w.toString();
272     }
273 
274     /**
275      * An option that affects the writing operations.
276      */
277     public sealed interface Option {
278     }
279 
280     /**
281      * An option describing the function to use for naming code items.
282      */
283     public sealed interface CodeItemNamerOption extends Option
284             permits NamerOptionImpl {
285 
286         /**
287          * {@return an code item naming option with the provided function}
288          *
289          * @param named the function used to name code items
290          */
291         static CodeItemNamerOption of(Function<CodeItem, String> named) {
292             return new NamerOptionImpl(named);
293         }
294 
295         /**
296          * {@return the default code item naming option}
297          */
298         static CodeItemNamerOption defaultValue() {
299             return of(new GlobalValueBlockNaming());
300         }
301 
302         /**
303          * {@return the associated naming function for code items}
304          */
305         Function<CodeItem, String> namer();
306     }
307     private record NamerOptionImpl(Function<CodeItem, String> namer) implements CodeItemNamerOption {
308     }
309 
310     /**
311      * An option describing whether location information should be written or dropped.
312      */
313     public enum LocationOption implements Option {
314         /** Writes location */
315         WRITE_LOCATION,
316         /** Drops location */
317         DROP_LOCATION;
318 
319         /**
320          * {@return the default location option}
321          */
322         public static LocationOption defaultValue() {
323             return WRITE_LOCATION;
324         }
325     }
326 
327     /**
328      * An option describing whether an operation's descendant code elements should be written or dropped.
329      */
330     public enum OpDescendantsOption implements Option {
331         /** Writes descendants of an operation, if any */
332         WRITE_DESCENDANTS,
333         /** Drops descendants of an operation, if any */
334         DROP_DESCENDANTS;
335 
336         /**
337          * {@return the default writing option for descendant operations}
338          */
339         public static OpDescendantsOption defaultValue() {
340             return WRITE_DESCENDANTS;
341         }
342     }
343 
344     /**
345      * An option describing whether an operation's result be written or dropped if its type is void.
346      */
347     public enum VoidOpResultOption implements Option {
348         /** Writes void operation result */
349         WRITE_VOID,
350         /** Drops void operation result */
351         DROP_VOID;
352 
353         /**
354          * {@return the default option for writing operation results}
355          */
356         public static VoidOpResultOption defaultValue() {
357             return DROP_VOID;
358         }
359     }
360 
361     final Function<CodeItem, String> namer;
362     final IndentWriter w;
363     final boolean dropLocation;
364     final boolean dropOpDescendants;
365     final boolean writeVoidOpResult;
366 
367     /**
368      * Creates a writer of code models (operations) to their textual form.
369      *
370      * @param w the character stream writer to write the textual form.
371      */
372     public OpWriter(Writer w) {
373         this.w = new IndentWriter(w);
374         this.namer = new GlobalValueBlockNaming();
375         this.dropLocation = false;
376         this.dropOpDescendants = false;
377         this.writeVoidOpResult = false;
378     }
379 
380     /**
381      * Creates a writer of code models (operations) to their textual form.
382      *
383      * @param w the character stream writer to write the textual form.
384      * @param options the writer options
385      */
386     public OpWriter(Writer w, Option... options) {
387         Function<CodeItem, String> namer = null;
388         boolean dropLocation = false;
389         boolean dropOpDescendants = false;
390         boolean writeVoidOpResult = false;
391         for (Option option : options) {
392             switch (option) {
393                 case CodeItemNamerOption namerOption -> {
394                     namer = namerOption.namer();
395                 }
396                 case LocationOption locationOption -> {
397                     dropLocation = locationOption ==
398                             LocationOption.DROP_LOCATION;
399                 }
400                 case OpDescendantsOption opDescendantsOption -> {
401                     dropOpDescendants = opDescendantsOption ==
402                             OpDescendantsOption.DROP_DESCENDANTS;
403                 }
404                 case VoidOpResultOption voidOpResultOption -> {
405                     writeVoidOpResult = voidOpResultOption == VoidOpResultOption.WRITE_VOID;
406                 }
407             }
408         }
409 
410         this.w = new IndentWriter(w);
411         this.namer = (namer == null) ? new GlobalValueBlockNaming() : namer;
412         this.dropLocation = dropLocation;
413         this.dropOpDescendants = dropOpDescendants;
414         this.writeVoidOpResult = writeVoidOpResult;
415     }
416 
417     /**
418      * {@return the function that names blocks and values.}
419      */
420     public Function<CodeItem, String> namer() {
421         return namer;
422     }
423 
424     /**
425      * Writes a code model, an operation, to the character stream.
426      *
427      * @param op the code model
428      */
429     public void writeOp(Op op) {
430         if (op.parent() != null) {
431             Op.Result opr = op.result();
432             if (writeVoidOpResult || !opr.type().equals(JavaType.VOID)) {
433                 writeValueDeclaration(opr);
434                 write(" = ");
435             }
436         }
437         write(op.externalizeOpName());
438 
439         if (!op.operands().isEmpty()) {
440             write(" ");
441             writeSpaceSeparatedList(op.operands(), this::writeValueUse);
442         }
443 
444         if (!op.successors().isEmpty()) {
445             write(" ");
446             writeSpaceSeparatedList(op.successors(), this::writeSuccessor);
447         }
448 
449         if (!dropLocation) {
450             Op.Location location = op.location();
451             if (location != null) {
452                 write(" ");
453                 writeLocation(location);
454             }
455         }
456         Map<String, Object> attributes = op.externalize();
457         if (!attributes.isEmpty()) {
458             write(" ");
459             writeSpaceSeparatedList(attributes.entrySet(), e -> writeAttribute(e.getKey(), e.getValue()));
460         }
461 
462         if (!dropOpDescendants && !op.bodies().isEmpty()) {
463             int nBodies = op.bodies().size();
464             if (nBodies == 1) {
465                 write(" ");
466             } else {
467                 write("\n");
468                 w.in();
469                 w.in();
470             }
471             boolean first = true;
472             for (Body body : op.bodies()) {
473                 if (!first) {
474                     write("\n");
475                 }
476                 writeBody(body);
477                 first = false;
478             }
479             if (nBodies > 1) {
480                 w.out();
481                 w.out();
482             }
483         }
484 
485         write(";");
486     }
487 
488     void writeLocation(Op.Location location) {
489         StringBuilder s = new StringBuilder();
490         s.append(location.line()).append(":").append(location.column());
491         if (location.sourceRef() != null) {
492             s.append(":").append(location.sourceRef());
493         }
494         writeAttribute(ATTRIBUTE_LOCATION, s);
495     }
496 
497     void writeSuccessor(Block.Reference successor) {
498         writeBlockName(successor.targetBlock());
499         if (!successor.arguments().isEmpty()) {
500             write("(");
501             writeCommaSeparatedList(successor.arguments(), this::writeValueUse);
502             write(")");
503         }
504     }
505 
506     void writeAttribute(String name, Object value) {
507         write("@");
508         if (!name.isEmpty()) {
509             write(name);
510             write("=");
511         }
512         write(AttributeMapper.toString(value));
513     }
514 
515     void writeBody(Body body) {
516         Block eb = body.entryBlock();
517         write("(");
518         writeCommaSeparatedList(eb.parameters(), this::writeValueDeclaration);
519         write(")");
520         writeType(body.bodyType().returnType());
521         write(" -> {\n");
522         w.in();
523         for (Block b : body.blocks()) {
524             if (!b.isEntryBlock()) {
525                 write("\n");
526             }
527             writeBlock(b);
528         }
529         w.out();
530         write("}");
531     }
532 
533     void writeBlock(Block block) {
534         if (!block.isEntryBlock()) {
535             writeBlockName(block);
536             if (!block.parameters().isEmpty()) {
537                 write("(");
538                 writeCommaSeparatedList(block.parameters(), this::writeValueDeclaration);
539                 write(")");
540             }
541             write(":\n");
542         }
543         w.in();
544         for (Op op : block.ops()) {
545             writeOp(op);
546             write("\n");
547         }
548         w.out();
549     }
550 
551     void writeBlockName(Block b) {
552         write("^");
553         write(namer.apply(b));
554     }
555 
556     void writeValueUse(Value v) {
557         write("%");
558         write(namer.apply(v));
559     }
560 
561     void writeValueDeclaration(Value v) {
562         write("%");
563         write(namer.apply(v));
564         write(" : ");
565         writeType(v.type());
566     }
567 
568     <T> void writeSpaceSeparatedList(Iterable<T> l, Consumer<T> c) {
569         writeSeparatedList(" ", l, c);
570     }
571 
572     <T> void writeCommaSeparatedList(Iterable<T> l, Consumer<T> c) {
573         writeSeparatedList(", ", l, c);
574     }
575 
576     <T> void writeSeparatedList(String separator, Iterable<T> l, Consumer<T> c) {
577         boolean first = true;
578         for (T t : l) {
579             if (!first) {
580                 write(separator);
581             }
582             c.accept(t);
583             first = false;
584         }
585     }
586 
587     void writeType(TypeElement te) {
588         write(JavaTypeUtils.flatten(te.externalize()).toString());
589     }
590 
591     void write(String s) {
592         try {
593             w.write(s);
594         } catch (IOException e) {
595             throw new UncheckedIOException(e);
596         }
597     }
598 }