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