001/* 002 * Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others 003 * 004 * This program and the accompanying materials are made available under the 005 * terms of the Eclipse Public License 2.0 which is available at 006 * http://www.eclipse.org/legal/epl-2.0. 007 * 008 * SPDX-License-Identifier: EPL-2.0 009 */ 010 011package org.eclipse.golo.compiler; 012 013import gololang.ir.GoloModule; 014import org.eclipse.golo.compiler.parser.ASTCompilationUnit; 015import org.eclipse.golo.compiler.parser.GoloOffsetParser; 016import org.eclipse.golo.compiler.parser.GoloParser; 017import org.eclipse.golo.compiler.parser.ParseException; 018 019import java.io.*; 020import java.nio.charset.Charset; 021import java.nio.charset.UnsupportedCharsetException; 022import java.util.Collections; 023import java.util.List; 024import java.util.jar.JarOutputStream; 025import java.util.zip.ZipEntry; 026 027import static gololang.Messages.message; 028 029/** 030 * The Golo compiler. 031 * <p> 032 * Instances of this class may be reused to compile several sources. 033 * <p> 034 * Several methods are made public while they do not necessarily need so for the needs of the Golo compiler. 035 * Such deviations from a "good and clean" design are on-purpose, as this facilitates the implementation of 036 * Golo support in IDEs. 037 */ 038public class GoloCompiler { 039 040 private GoloParser parser; 041 private GoloCompilationException.Builder exceptionBuilder = null; 042 043 044 /** 045 * Initializes an ExceptionBuilder to collect errors instead of throwing immediately. 046 * This method is made public for the requirements of IDEs support. 047 * 048 * @param builder the exception builder to add problems into. 049 */ 050 public final void setExceptionBuilder(GoloCompilationException.Builder builder) { 051 exceptionBuilder = builder; 052 } 053 054 private GoloCompilationException.Builder getOrCreateExceptionBuilder(String goloSourceFile) { 055 if (exceptionBuilder == null) { 056 exceptionBuilder = new GoloCompilationException.Builder(goloSourceFile); 057 } 058 return exceptionBuilder; 059 } 060 061 public void resetExceptionBuilder() { 062 exceptionBuilder = null; 063 } 064 065 /** 066 * Initializes a parser from an input stream. This method is made public for the requirements of IDEs support. 067 * 068 * @param sourceCodeInputStream the source code input stream. 069 * @return the parser. 070 */ 071 public final GoloParser initParser(String goloSourceFilename, InputStream sourceCodeInputStream) throws GoloCompilationException { 072 try { 073 return initParser(new InputStreamReader(sourceCodeInputStream, Charset.forName("UTF-8"))); 074 } catch (UnsupportedCharsetException e) { 075 getOrCreateExceptionBuilder(goloSourceFilename).report(e).doThrow(); 076 return null; 077 } 078 } 079 080 /** 081 * Initializes a parser from a reader. This method is made public for the requirements of IDEs support. 082 * 083 * @param sourceReader the source code reader. 084 * @return the parser. 085 */ 086 public final GoloParser initParser(Reader sourceReader) { 087 if (parser == null) { 088 parser = createGoloParser(sourceReader); 089 } else { 090 parser.ReInit(sourceReader); 091 } 092 return parser; 093 } 094 095 /** 096 * Compiles a Golo source file from an input stream, and returns a collection of results. 097 * 098 * @param goloSourceFilename the source file name. 099 * @param sourceCodeInputStream the source code input stream. 100 * @return a list of compilation results. 101 * @throws GoloCompilationException if a problem occurs during any phase of the compilation work. 102 */ 103 public final List<CodeGenerationResult> compile(String goloSourceFilename, InputStream sourceCodeInputStream) throws GoloCompilationException { 104 resetExceptionBuilder(); 105 ASTCompilationUnit compilationUnit = parse(goloSourceFilename, 106 initParser(goloSourceFilename, sourceCodeInputStream)); 107 GoloModule goloModule = check(compilationUnit); 108 if (goloModule.isEmpty()) { 109 return Collections.emptyList(); 110 } 111 return generate(goloModule, goloSourceFilename); 112 } 113 114 private void throwIfErrorEncountered() { 115 if (!getProblems().isEmpty()) { 116 exceptionBuilder.doThrow(); 117 } 118 } 119 120 /** 121 * Returns the list of problems encountered during the last compilation 122 * 123 * @return a list of compilation problems. 124 */ 125 public List<GoloCompilationException.Problem> getProblems() { 126 if (exceptionBuilder == null) { 127 return Collections.emptyList(); 128 } 129 return exceptionBuilder.getProblems(); 130 } 131 132 /** 133 * Compiles a Golo source file and writes the resulting JVM bytecode {@code .class} files to a target 134 * folder. The class files are written in a directory structure that respects package names. 135 * 136 * @param goloSourceFilename the source file name. 137 * @param sourceCodeInputStream the source code input stream. 138 * @param targetFolder the output target folder. 139 * @throws GoloCompilationException if a problem occurs during any phase of the compilation work. 140 * @throws IOException if writing the {@code .class} files fails for some reason. 141 */ 142 public final void compileTo(String goloSourceFilename, InputStream sourceCodeInputStream, File targetFolder) throws GoloCompilationException, IOException { 143 if (targetFolder.isFile()) { 144 throw new IllegalArgumentException(message("file_exists", targetFolder)); 145 } 146 List<CodeGenerationResult> results = compile(goloSourceFilename, sourceCodeInputStream); 147 for (CodeGenerationResult result : results) { 148 File outputFolder = new File(targetFolder, result.getPackageAndClass().packageName().replaceAll("\\.", "/")); 149 if (!outputFolder.exists() && !outputFolder.mkdirs()) { 150 throw new IOException(message("directory_not_created", outputFolder)); 151 } 152 File outputFile = new File(outputFolder, result.getPackageAndClass().className() + ".class"); 153 try (FileOutputStream out = new FileOutputStream(outputFile)) { 154 out.write(result.getBytecode()); 155 } 156 } 157 } 158 159 /** 160 * Compiles a Golo source fila and writes the resulting JVM bytecode {@code .class} files to a Jar file stream. 161 * The class files are written in a directory structure that respects package names. 162 * 163 * @param goloSourceFilename the source file name. 164 * @param sourceCodeInputStream the source code input stream. 165 * @param jarOutputStream the output Jar stream 166 * @throws IOException if writing the {@code .class} files fails for some reason. 167 */ 168 public final void compileToJar(String goloSourceFilename, InputStream sourceCodeInputStream, JarOutputStream jarOutputStream) throws IOException { 169 List<CodeGenerationResult> results = compile(goloSourceFilename, sourceCodeInputStream); 170 for (CodeGenerationResult result : results) { 171 String entryName = result.getPackageAndClass().packageName().replaceAll("\\.", "/"); 172 if (!entryName.isEmpty()) { 173 entryName += "/"; 174 } 175 entryName = entryName + result.getPackageAndClass().className() + ".class"; 176 jarOutputStream.putNextEntry(new ZipEntry(entryName)); 177 jarOutputStream.write(result.getBytecode()); 178 jarOutputStream.closeEntry(); 179 } 180 } 181 182 /** 183 * Produces a parse tree for a Golo source file. This is mostly useful to IDEs. 184 * 185 * @param goloSourceFilename the source file name. 186 * @param parser the parser to use. 187 * @return the resulting parse tree. 188 * @throws GoloCompilationException if the parser encounters an error. 189 */ 190 public final ASTCompilationUnit parse(String goloSourceFilename, GoloParser parser) throws GoloCompilationException { 191 ASTCompilationUnit compilationUnit = null; 192 parser.exceptionBuilder = getOrCreateExceptionBuilder(goloSourceFilename); 193 try { 194 compilationUnit = parser.CompilationUnit(); 195 compilationUnit.setFilename(goloSourceFilename); 196 } catch (ParseException pe) { 197 exceptionBuilder.report(pe, compilationUnit); 198 } 199 throwIfErrorEncountered(); 200 return compilationUnit; 201 } 202 203 public final ASTCompilationUnit parse(String goloSourceFilename) throws GoloCompilationException, IOException { 204 try (FileInputStream in = new FileInputStream(goloSourceFilename)) { 205 return parse(goloSourceFilename, initParser(goloSourceFilename, in)); 206 } 207 } 208 209 /** 210 * Checks that the source code is minimally sound by converting a parse tree to an intermediate representation, and 211 * running a few classic visitors over it. This is mostly useful to IDEs. 212 * 213 * @param compilationUnit the source parse tree. 214 * @return the intermediate representation of the source. 215 * @throws GoloCompilationException if an error exists in the source represented by the input parse tree. 216 */ 217 public final GoloModule check(ASTCompilationUnit compilationUnit) { 218 return refine(transform(compilationUnit)); 219 } 220 221 public final List<CodeGenerationResult> generate(GoloModule goloModule, String goloSourceFilename) { 222 JavaBytecodeGenerationGoloIrVisitor bytecodeGenerator = new JavaBytecodeGenerationGoloIrVisitor(); 223 return bytecodeGenerator.generateBytecode(goloModule, goloSourceFilename); 224 } 225 226 public final GoloModule transform(ASTCompilationUnit compilationUnit) { 227 return new ParseTreeToGoloIrVisitor().transform(compilationUnit, exceptionBuilder); 228 } 229 230 public final GoloModule refine(GoloModule goloModule) { 231 if (goloModule != null) { 232 goloModule.accept(new SugarExpansionVisitor()); 233 goloModule.accept(new ClosureCaptureGoloIrVisitor()); 234 goloModule.accept(new LocalReferenceAssignmentAndVerificationVisitor(exceptionBuilder)); 235 } 236 throwIfErrorEncountered(); 237 return goloModule; 238 } 239 240 /** 241 * Makes a Golo parser from a reader. 242 * 243 * @param sourceReader the reader. 244 * @return the parser for <code>sourceReader</code>. 245 */ 246 protected GoloParser createGoloParser(Reader sourceReader) { 247 return new GoloOffsetParser(sourceReader); 248 } 249}