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 }