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 }