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