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