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