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 }