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}