From a7818ec324de3031462e5c4d4cb055bf7fde4ebb Mon Sep 17 00:00:00 2001 From: Dibyendu Majumdar Date: Sat, 19 Aug 2023 08:52:57 -0700 Subject: Chapter 1: Introduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cliff Click Co-authored-by: Thalia Archibald Co-authored-by: hadha Co-authored-by: XmiliaH <45106915+XmiliaH@users.noreply.github.com> Co-authored-by: Tamás Hadházy <85063808+Hels15@users.noreply.github.com> --- transpile-tests/Makefile | 93 +++++++++++ transpile-tests/README.md | 15 ++ transpile-tests/pom.xml | 14 ++ .../seaofnodes/simple/transpile/AstVisitor.java | 173 +++++++++++++++++++++ .../seaofnodes/simple/transpile/JUnitParser.java | 71 +++++++++ .../com/seaofnodes/simple/transpile/TestClass.java | 6 + .../seaofnodes/simple/transpile/TestMethod.java | 39 +++++ .../simple/transpile/printers/RustPrinter.java | 171 ++++++++++++++++++++ .../simple/transpile/TranspilerTest.java | 26 ++++ 9 files changed, 608 insertions(+) create mode 100644 transpile-tests/Makefile create mode 100644 transpile-tests/README.md create mode 100644 transpile-tests/pom.xml create mode 100644 transpile-tests/src/main/java/com/seaofnodes/simple/transpile/AstVisitor.java create mode 100644 transpile-tests/src/main/java/com/seaofnodes/simple/transpile/JUnitParser.java create mode 100644 transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestClass.java create mode 100644 transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestMethod.java create mode 100644 transpile-tests/src/main/java/com/seaofnodes/simple/transpile/printers/RustPrinter.java create mode 100644 transpile-tests/src/test/java/com/seaofnodes/simple/transpile/TranspilerTest.java (limited to 'transpile-tests') diff --git a/transpile-tests/Makefile b/transpile-tests/Makefile new file mode 100644 index 00000000..b2a6e74b --- /dev/null +++ b/transpile-tests/Makefile @@ -0,0 +1,93 @@ +SHELL := /bin/bash +.DELETE_ON_ERROR: + +# for printing variable values +# usage: make print-VARIABLE +# > VARIABLE = value_of_variable +print-% : ; @echo $* = $($*) + +# literal space +space := $() $() + +# Decide OS-specific questions +# jar-file seperator +ifeq ($(OS),Windows_NT) + SEP = ; +else + SEP = : +endif +# Find a reasonable ctags. +CTAGS = $(shell which ctags) +# Hack for MacOS: /usr/bin/ctags is unfriendly, so look for ctags from brew +ifeq ($(UNAME),Darwin) + CTAGS = $(shell brew list ctags 2> /dev/null | grep bin/ctags) +endif + +# Fun Args to javac. Mostly limit to java8 source definitions, and fairly +# aggressive lint warnings. +JAVAC_ARGS = -g + +# Source code +SIMPLE := com/seaofnodes/simple/transpile +SRC := src/main/java +TST := src/test/java +CLZDIR:= build/classes +main_javas := $(wildcard $(SRC)/$(SIMPLE)/*java $(SRC)/$(SIMPLE)/*/*java) +test_javas := $(wildcard $(TST)/$(SIMPLE)/*java $(TST)/$(SIMPLE)/*/*java) +main_classes := $(patsubst $(SRC)/%java,$(CLZDIR)/main/%class,$(main_javas)) +test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas)) +test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.transpile.%,$(wildcard $(TST)/$(SIMPLE)/*Test.java)) +classes = $(main_classes) $(test_classes) +# All the libraries +libs = $(wildcard lib/*jar) +jars = $(subst $(space),$(SEP),$(libs)) + + +default_targets := $(main_classes) $(test_classes) +# Optionally add ctags to the default target if a reasonable one was found. +ifneq ($(CTAGS),) +default_targets += tags +endif + +default: $(default_targets) + +# Compile just the out-of-date files +$(main_classes): build/classes/main/%class: $(SRC)/%java + @echo "compiling " $@ " because " $? + @[ -d $(CLZDIR)/main ] || mkdir -p $(CLZDIR)/main + @javac $(JAVAC_ARGS) -cp "$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(SRC) -d $(CLZDIR)/main $(main_javas) + +$(test_classes): $(CLZDIR)/test/%class: $(TST)/%java $(main_classes) + @echo "compiling " $@ " because " $? + @[ -d $(CLZDIR)/test ] || mkdir -p $(CLZDIR)/test + @javac $(JAVAC_ARGS) -cp "$(CLZDIR)/test$(SEP)$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(TST) -d $(CLZDIR)/test $(test_javas) + +# Base launch line for JVM tests +JVM=nice java -ea -cp "build/classes/main${SEP}${jars}${SEP}$(CLZDIR)/test" + +tests: $(default_targets) + @echo "testing " $(test_cp) + @$(JVM) org.junit.runner.JUnitCore $(test_cp) + +.PHONY: clean +clean: + rm -rf build + rm -f TAGS + (find . -name "*~" -exec rm {} \; 2>/dev/null; exit 0) + +# Download libs from maven +lib: lib/junit-4.12.jar lib/hamcrest-core-1.3.jar + +# Jars +lib/junit-4.12.jar: + @[ -d lib ] || mkdir -p lib + @(cd lib; wget https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar) + +lib/hamcrest-core-1.3.jar: + @[ -d lib ] || mkdir -p lib + @(cd lib; wget https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar) + +# Build emacs tags (part of a tasty emacs ide experience) +tags: $(main_javas) $(test_javas) + @rm -f TAGS + @$(CTAGS) -e --recurse=yes --extra=+q --fields=+fksaiS $(SRC) $(TST) diff --git a/transpile-tests/README.md b/transpile-tests/README.md new file mode 100644 index 00000000..aafd3161 --- /dev/null +++ b/transpile-tests/README.md @@ -0,0 +1,15 @@ +# Transpile Tests + +This module may be helpful if you want to create an implementation of Simple in another language. +It can parse the JUnit tests to automatically translate most of the boilerplate. +Some manual tweaks will still be required though, because only common patterns are detected +(e.g. 01: testSimpleProgram, testZero; 04: testVarArg; 08: testRegress5; 09: testMeet, ...) + +## Usage + +Just call `JUnitParser.parseRepository` to get a list of parsed test classes for each chapter, +and implement a pretty printer for your favourite language. + +For an example you can refer to `printers.RustPrinter`, +which produces tests similar to the ones used by +[Simple-Rust](https://github.com/SeaOfNodes/Simple-Rust). diff --git a/transpile-tests/pom.xml b/transpile-tests/pom.xml new file mode 100644 index 00000000..092e26ee --- /dev/null +++ b/transpile-tests/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + com.seaofnodes + simple + 1.0 + + transpile-tests + jar + Transpile Tests + diff --git a/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/AstVisitor.java b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/AstVisitor.java new file mode 100644 index 00000000..19709b9e --- /dev/null +++ b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/AstVisitor.java @@ -0,0 +1,173 @@ +package com.seaofnodes.simple.transpile; + +import com.sun.source.tree.*; +import com.sun.source.util.TreeScanner; + +import java.util.ArrayList; +import java.util.HashMap; + +class AstVisitor extends TreeScanner { + final HashMap results = new HashMap<>(); + + private String className; + private TestMethod current; + private boolean inCatch; + + @Override + public Void visitClass(ClassTree node, Void unused) { + var name = node.getSimpleName().toString(); + if (name.matches("Chapter\\d+Test")) { + var backup = className; + className = name; + super.visitClass(node, unused); + className = backup; + } + return null; + } + + @Override + public Void visitMethod(MethodTree node, Void unused) { + if (className != null) { + current = new TestMethod(); + current.name = node.getName().toString(); + super.visitMethod(node, unused); + results.compute(className, (k, v) -> { + if (v == null) + v = new TestClass(className, new ArrayList<>()); + v.methods().add(current); + return v; + }); + current = null; + } + return null; + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { + if (current != null) { + var methodSelect = node.getMethodSelect().toString(); + var args = node.getArguments(); + + if (methodSelect.endsWith(".parse")) { + current.showAfterParse = switch (args.size()) { + case 0 -> false; + case 1 -> (Boolean) literal(args.getFirst()); + default -> throw new RuntimeException("unexpected parse arguments" + node); + }; + } + if (methodSelect.endsWith(".iterate")) { + current.iterate = true; + current.showAfterIterate = switch (args.size()) { + case 0 -> false; + case 1 -> (Boolean) literal(args.getFirst()); + default -> throw new RuntimeException("unexpected parse arguments" + node); + }; + } + if (methodSelect.endsWith("assertEquals")) { + if (inCatch) { + if (args.size() != 2) + throw new RuntimeException("Unexpected number of arguments " + node); + if (args.get(1).toString().contains("e.getMessage()")) { + current.parseErrorMessage = (String) literal(args.get(0)); + } + } else { + if (args.size() != 2) + throw new RuntimeException("Unexpected number of arguments " + node); + if (args.get(1).toString().contains("evaluate(")) { + // visit evaluate(..) or evaluate(..).toString() + var eval = args.get(1).accept(new TreeScanner() { + @Override + public MethodInvocationTree reduce(MethodInvocationTree r1, MethodInvocationTree r2) { + return r1 != null ? r1 : r2; + } + + @Override + public MethodInvocationTree visitMethodInvocation(MethodInvocationTree n, Void unused1) { + if (n.getMethodSelect().toString().endsWith(".evaluate")) + return n; + return super.visitMethodInvocation(n, unused1); + } + }, null); + + Object result = literal(args.get(0)); + var evalArgs = eval.getArguments(); + if (evalArgs.size() != 2 || !evalArgs.get(0).toString().equals("stop")) + throw new IllegalArgumentException("Unexpected eval arguments " + node); + long parameter = Long.parseLong(literal(evalArgs.get(1)).toString()); + current.evaluations.add(new TestMethod.Evaluation(result, parameter)); + } else if (args.get(1).toString().endsWith(".toString()") || args.get(1).toString().endsWith(".print()")) { + if (current.assertStopEquals == null) { + // Assume the first one prints the stop node. Ignore assertions from evaluator: + // assertEquals("int[] {\n # :int;\n [] :int;\n}",obj.struct().toString()); + current.assertStopEquals = (String) literal(args.get(0)); + } + } + } + } + if (methodSelect.endsWith("assertTrue")) { + var arg = args.getFirst().toString(); + current.assertStopRetCtrlIsProj = arg.contains("stop.ret().ctrl() instanceof ProjNode"); + current.assertStopRetCtrlIsCProj = arg.contains("stop.ret().ctrl() instanceof CProjNode"); + current.assertStopRetCtrlIsRegion = arg.contains("stop.ret().ctrl() instanceof RegionNode"); + } + if (methodSelect.endsWith("prettyPrint")) { + if (args.size() < 2 || args.size() > 3 || !args.get(0).toString().equals("stop") || !args.get(1).toString().equals("99")) + throw new RuntimeException("unexpected prettyPrint args " + node); + current.irPrinter = true; + current.irPrinterLLVM = args.size() == 3 && (Boolean) literal(args.get(2)); + } + } + return super.visitMethodInvocation(node, unused); // otherwise we would miss the first part of new Parser("").parse() + } + + @Override + public Void visitNewClass(NewClassTree node, Void unused) { + if (current != null) { + if (node.getIdentifier().toString().equals("Parser")) { + var args = node.getArguments(); + switch (args.size()) { + case 1 -> { + } + case 2 -> current.parserArg = switch (args.get(1).toString()) { + case "TypeInteger.BOT" -> new TestMethod.Arg.IntBot(); + case "TypeInteger.TOP" -> new TestMethod.Arg.IntTop(); + case String s -> { + var parts = s.split("[()]"); + if (parts.length != 2 | !parts[0].equals("TypeInteger.constant")) + throw new RuntimeException("Unexpected Argument " + s); + yield new TestMethod.Arg.IntConstant(Long.parseLong(parts[1])); + } + }; + default -> throw new RuntimeException("Parser constructor with unexpected arguments: " + node); + } + current.parserInput = (String) literal(args.get(0)); + } else if (node.getIdentifier().toString().equals("GraphVisualizer")) { + // only happens in Chapter02Tests + current.showAfterParse = true; + } + } + return super.visitNewClass(node, unused); + } + + @Override + public Void visitCatch(CatchTree node, Void unused) { + inCatch = true; + super.visitCatch(node, unused); + inCatch = false; + return null; + } + + @Override + public Void visitAssignment(AssignmentTree node, Void unused) { + if (current != null && node.getVariable().toString().equals("Node._disablePeephole")) { + if ((Boolean) literal(node.getExpression())) { + current.disablePeephole = true; + } + } + return super.visitAssignment(node, unused); + } + + private Object literal(ExpressionTree t) { + return ((LiteralTree) t).getValue(); + } +} diff --git a/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/JUnitParser.java b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/JUnitParser.java new file mode 100644 index 00000000..2db2a08c --- /dev/null +++ b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/JUnitParser.java @@ -0,0 +1,71 @@ +package com.seaofnodes.simple.transpile; + +import com.sun.source.util.JavacTask; + +import javax.tools.ToolProvider; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; + +import static java.util.Comparator.comparing; + +public class JUnitParser { + + /** + * @param simpleRepo The path to the project root + * @return A sorted map from chapter name to a sorted list of parsed test classes. + */ + public static TreeMap> parseRepository(Path simpleRepo) throws IOException { + var chapters = new TreeMap>(); + var inputs = simpleRepo.toFile().listFiles(File::isDirectory); + if (inputs == null) + throw new IOException("failed to read Simple project root"); + Arrays.sort(inputs); + for (File file : inputs) { + var chapter = file.getName(); + if (chapter.matches("chapter\\d+")) { + chapters.put(chapter, parseChapter(file.toPath())); + } + } + return chapters; + } + + /** + * @param input The path to a chapter in the repository. + * @return a sorted list of parsed test classes + */ + public static List parseChapter(Path input) throws IOException { + var tests = new ArrayList(); + Files.walkFileTree(input, Set.of(), 20, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (file.getFileName().toString().matches("Chapter\\d+Test\\.java")) { + tests.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + var compiler = ToolProvider.getSystemJavaCompiler(); + var compilationUnits = compiler + .getStandardFileManager(null, null, null) + .getJavaFileObjectsFromPaths(tests); + + var task = (JavacTask) compiler.getTask(null, null, null, null, null, compilationUnits); + + var visitor = new AstVisitor(); + for (var tree : task.parse()) { + try { tree.accept(visitor, null); } + catch( IllegalArgumentException iae ) {} + } + + var testClasses = new ArrayList<>(visitor.results.values()); + testClasses.sort(comparing(TestClass::name)); + return testClasses; + } +} diff --git a/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestClass.java b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestClass.java new file mode 100644 index 00000000..5bb5609c --- /dev/null +++ b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestClass.java @@ -0,0 +1,6 @@ +package com.seaofnodes.simple.transpile; + +import java.util.ArrayList; + +public record TestClass(String name, ArrayList methods) { +} diff --git a/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestMethod.java b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestMethod.java new file mode 100644 index 00000000..d0c6cfa8 --- /dev/null +++ b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/TestMethod.java @@ -0,0 +1,39 @@ +package com.seaofnodes.simple.transpile; + +import java.util.ArrayList; + +public class TestMethod { + public String name; + public String parserInput; + public Arg parserArg; + // if this is set you can ignore the rest + public String parseErrorMessage; + public boolean disablePeephole; + public boolean showAfterParse; + public boolean iterate; + public boolean showAfterIterate; + public boolean irPrinter; + public boolean irPrinterLLVM; + public String assertStopEquals; + public boolean assertStopRetCtrlIsProj; + public boolean assertStopRetCtrlIsCProj; + public boolean assertStopRetCtrlIsRegion; + public final ArrayList evaluations = new ArrayList<>(); + + public sealed interface Arg { + record IntBot() implements Arg { + } + + record IntTop() implements Arg { + } + + record IntConstant(long value) implements Arg { + } + } + + /** + * result is a literal value + **/ + public record Evaluation(Object result, long parameter) { + } +} diff --git a/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/printers/RustPrinter.java b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/printers/RustPrinter.java new file mode 100644 index 00000000..115a075a --- /dev/null +++ b/transpile-tests/src/main/java/com/seaofnodes/simple/transpile/printers/RustPrinter.java @@ -0,0 +1,171 @@ +package com.seaofnodes.simple.transpile.printers; + +import com.seaofnodes.simple.transpile.JUnitParser; +import com.seaofnodes.simple.transpile.TestClass; +import com.seaofnodes.simple.transpile.TestMethod; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class RustPrinter { + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.out.println("Usage: "); + System.exit(1); + } + var printer = new RustPrinter(); + var output = Path.of(args[1]); + + for (var e : JUnitParser.parseRepository(Path.of(args[0])).entrySet()) { + printer.printToFile(e.getKey(), e.getValue(), output); + } + } + + private void printToFile(String chapter, List testClasses, Path outputDirectory) throws IOException, InterruptedException { + var output = outputDirectory.resolve(chapter); + Files.createDirectories(output); + for (var testClass : testClasses) { + var outputFile = output.resolve(testClass.name().replace("Test", "").toLowerCase() + ".rs"); + System.out.println(outputFile); + Files.write(outputFile, printAndRustfmt(testClass.methods())); + } + } + + private byte[] printAndRustfmt(ArrayList tests) throws IOException, InterruptedException { + var p = Runtime.getRuntime().exec(new String[]{"rustfmt"}); + try (var print = new PrintStream(new BufferedOutputStream(p.getOutputStream()))) { + printFile(tests, print); + } + p.waitFor(); + + var errors = new String(p.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); + if (!errors.isEmpty() || p.exitValue() != 0) { + throw new RuntimeException("rustfmt exit=" + p.exitValue() + " stderr=" + errors); + } + + return p.getInputStream().readAllBytes(); + } + + private void printFile(ArrayList tests, PrintStream out) { + out.println(""" + use crate::datastructures::arena::DroplessArena; + use crate::sea_of_nodes::parser::Parser; + use crate::sea_of_nodes::types::Types;"""); + + if (tests.stream().anyMatch(t -> t.assertStopRetCtrlIsProj || t.assertStopRetCtrlIsCProj || t.assertStopRetCtrlIsRegion)) { + out.println("use crate::sea_of_nodes::nodes::Op;"); + } + if (tests.stream().anyMatch(t -> t.parseErrorMessage != null && !t.iterate)) { + out.println("use crate::sea_of_nodes::tests::test_error;"); + } + if (tests.stream().anyMatch(t -> t.parseErrorMessage != null && t.iterate)) { + out.println("use crate::sea_of_nodes::tests::test_error_iterate;"); + } + if (tests.stream().anyMatch(t -> t.irPrinter && !t.irPrinterLLVM)) { + out.println("use crate::sea_of_nodes::ir_printer::pretty_print;"); + } + if (tests.stream().anyMatch(t -> t.irPrinterLLVM)) { + out.println("use crate::sea_of_nodes::ir_printer::pretty_print_llvm;"); + } + if (tests.stream().anyMatch(t -> !t.evaluations.isEmpty())) { + out.println("use crate::sea_of_nodes::tests::evaluator::{evaluate, Object};"); + } + + for (TestMethod test : tests) { + printTest(test, out); + } + } + + private void printTest(TestMethod test, PrintStream out) { + out.println(); + out.println("#[test]"); + out.println("fn " + snakeCase(test.name) + "() {"); + if (test.parseErrorMessage != null) { + if (test.iterate) { + out.println("test_error_iterate(" + string(test.parserInput) + ", " + string(test.parseErrorMessage) + ");"); + } else { + out.println("test_error(" + string(test.parserInput) + ", " + string(test.parseErrorMessage) + ");"); + } + } else if (test.parserInput != null) { + out.println("let arena = DroplessArena::new();"); + out.println("let types = Types::new(&arena);"); + if (test.parserArg == null) { + out.println("let mut parser = Parser::new(" + string(test.parserInput) + ", &types);"); + } else { + out.printf("let mut parser = Parser::new_with_arg(%s, &types, %s);\n", string(test.parserInput), switch (test.parserArg) { + case TestMethod.Arg.IntBot ignored -> "types.ty_int_bot"; + case TestMethod.Arg.IntTop ignored -> "types.ty_int_top"; + case TestMethod.Arg.IntConstant c -> "types.get_int(" + c.value() + ")"; + }); + } + if (test.disablePeephole) { + out.println("parser.nodes.disable_peephole = true;"); + } + out.println("let stop = parser.parse().unwrap();"); + if (test.showAfterParse) { + out.println("parser.show_graph();"); + } + if (test.iterate) { + out.println("parser.iterate(stop);"); + out.println("parser.type_check(stop).unwrap();\n"); + } + if (test.showAfterIterate) { + out.println("parser.show_graph();"); + } + if (test.irPrinter) { + out.println("println!(\"{}\", pretty_print" + (test.irPrinterLLVM ? "_llvm" : "") + "(&parser.nodes, stop, 99));"); + } + if (test.assertStopEquals != null) { + out.println("assert_eq!(parser.print(stop), " + string(test.assertStopEquals) + ");"); + } + if (test.assertStopRetCtrlIsProj) { + out.println("assert!(matches!(parser.nodes.ret_ctrl(stop), Op::Proj(_)));"); + } + if (test.assertStopRetCtrlIsCProj) { + out.println("assert!(matches!(parser.nodes.ret_ctrl(stop), Op::CProj(_)));"); + } + if (test.assertStopRetCtrlIsRegion) { + out.println("assert!(matches!(parser.nodes.ret_ctrl(stop), Op::Region{..}));"); + } + for (var evaluation : test.evaluations) { + var result = switch (evaluation.result()) { + case null -> "Object::Null"; + case Integer l -> "Object::Long(" + l + ")"; + case Long l -> "Object::Long(" + l + ")"; + case Float d -> "Object::Double(" + d + ")"; + case Double d -> "Object::Double(" + d + ")"; + case String s -> string(s); + default -> + throw new RuntimeException("unexpected evaluation result: " + evaluation.result().getClass()); + }; + out.println("assert_eq!(" + result + ", evaluate(&parser.nodes, stop, Some(" + evaluation.parameter() + "), None).1);"); + } + } else { + out.println("todo!();"); + } + out.println("}"); + } + + private String string(String value) { + var escaped = value.replace("\\", "\\\\") + .replace("\r", "\\r") + .replace("\t", "\\t") + .replace("\"", "\\\""); + if (escaped.contains("\n")) { + return "\"\\\n" + escaped + "\""; + } else { + return "\"" + escaped + "\""; + } + } + + private String snakeCase(String identifier) { + return identifier.replaceAll("([A-Z]+)|([0-9]+)", "_$0").toLowerCase(); + } +} diff --git a/transpile-tests/src/test/java/com/seaofnodes/simple/transpile/TranspilerTest.java b/transpile-tests/src/test/java/com/seaofnodes/simple/transpile/TranspilerTest.java new file mode 100644 index 00000000..a2c3a782 --- /dev/null +++ b/transpile-tests/src/test/java/com/seaofnodes/simple/transpile/TranspilerTest.java @@ -0,0 +1,26 @@ +package com.seaofnodes.simple.transpile; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TranspilerTest { + @Test + public void testParser() throws IOException { + var cwd = Path.of(".").toAbsolutePath().normalize(); + assertEquals("transpile-tests", cwd.getFileName().toString()); + var all = JUnitParser.parseRepository(cwd.getParent()); + assertTrue(all.size() >= 14); + + var chapter09 = all.get("chapter09"); + assertEquals(9, chapter09.size()); + + var class6 = chapter09.get(5); + assertEquals("Chapter06Test", class6.name()); + assertEquals(1, class6.methods().stream().filter(m -> m.name.equals("testPeepholeReturn")).count()); + } +} -- cgit v1.2.3