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 gololang;
011
012import org.eclipse.golo.compiler.GoloClassLoader;
013import org.eclipse.golo.compiler.GoloCompilationException;
014
015import java.io.ByteArrayInputStream;
016import java.io.IOException;
017import java.io.InputStream;
018import java.nio.charset.StandardCharsets;
019import java.lang.reflect.InvocationTargetException;
020import java.util.*;
021
022/**
023 * An evaluation environment offers facilities for dynamic code compilation, loading and execution from Golo code as
024 * strings.
025 * <p>
026 * An evaluation environment is reusable across several executions. The only exception is when using {@code asModule()},
027 * as attempts to load a module with the same name as an already loaded one fails.
028 * <p>
029 * Each instance of this class uses a dedicated {@link GoloClassLoader}, hence usual rules about classloader delegation
030 * and isolation apply to evaluation environments.
031 * <p>
032 * While dynamic code evaluation is useful, it shall still be used with care and parsimony. It is especially important
033 * not to abuse {@code run()}, as each invocation triggers the generation of a one-shot class.
034 * <p>
035 * Here is an example usage of this API:
036 * <pre class="listing"><code class="lang-golo" data-lang="golo">
037 * let env = EvaluationEnvironment()
038 * let code =
039 * """
040 * function a = -> "a."
041 * function b = -> "b."
042 * """
043 * let mod = env: anonymousModule(code)
044 * let a = fun("a", mod)
045 * let b = fun("b", mod)
046 * println(a())
047 * println(b())
048 * </code></pre>
049 * <p>
050 * While this class is expected to be used from Golo code, it can also be used as a convenient way to embed Golo into
051 * polyglot JVM applications.
052 */
053public class EvaluationEnvironment {
054
055  private final GoloClassLoader goloClassLoader;
056  private final List<String> imports = new LinkedList<>();
057
058  private static String anonymousFilename() {
059    return "$Anonymous$_" + System.nanoTime() + ".golo";
060  }
061
062  private static String anonymousModuleName() {
063    return "module anonymous" + System.nanoTime();
064  }
065
066  /**
067   * Creates an evaluation environment using the current thread context classloader.
068   */
069  public EvaluationEnvironment() {
070    this(Thread.currentThread().getContextClassLoader());
071  }
072
073  /**
074   * Creates an evaluation environment using a parent classloader.
075   *
076   * @param parentClassLoader the parent classloader.
077   */
078  public EvaluationEnvironment(ClassLoader parentClassLoader) {
079    goloClassLoader = new GoloClassLoader(parentClassLoader);
080  }
081
082  /**
083   * Imports symbols.
084   * <p>
085   * Each symbol generates an equivalent {@code import} statement in the corresponding Golo code. Calling
086   * {@code imports("foo.Bar", "bar.Baz")} means that the subsequent code evaluations have {@code import foo.Bar} and
087   * {@code import bar.Baz} statements.
088   * <p>
089   * Note that this has no effect for {@link #asModule(String)}. Also, calling this method several times accumulates
090   * the imports, in order.
091   *
092   * @param head the first imported symbol.
093   * @param tail the next imported symbols.
094   * @return this evaluation environment.
095   */
096  public EvaluationEnvironment imports(String head, String... tail) {
097    imports.add(head);
098    Collections.addAll(imports, tail);
099    return this;
100  }
101
102  /**
103   * Clears all import symbols for the next code evaluation requests.
104   *
105   * @return this evaluation environment.
106   */
107  public EvaluationEnvironment clearImports() {
108    imports.clear();
109    return this;
110  }
111
112  /**
113   * Evaluates a complete module string.
114   *<p>
115   * For instance:
116   * <pre class="listing"><code class="lang-golo" data-lang="golo">
117   * let code =
118   * """
119   * module foo
120   *
121   * function a = -> "a!"
122   * function b = -> "b!"
123   * """
124   * let mod = env: asModule(code)
125   * let a = fun("a", mod)
126   * let b = fun("b", mod)
127   * println(a())
128   * println(b())
129   * </code></pre>
130   *
131   * @param source the module Golo source code as a string.
132   * @return the corresponding module, as a {@link Class}.
133   * @see Predefined#fun(Object, Object)
134   */
135  public Object asModule(String source) {
136    try (InputStream in = new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))) {
137      return goloClassLoader.load(anonymousFilename(), in);
138    } catch (IOException e) {
139      throw new RuntimeException(e);
140    } catch (GoloCompilationException e) {
141      e.setSourceCode(source);
142      throw e;
143    }
144  }
145
146  /**
147   * Loads an anonymous module. This is the same as {@link #asModule(String)}, except that the code does not contain
148   * a {@code module} declaration.
149   *
150   * <pre class="listing"><code class="lang-golo" data-lang="golo">
151   * let code =
152   * """
153   * function a = -> "a!"
154   * function b = -> "b!"
155   * """
156   * let mod = env: anonymousModule(code)
157   * let a = fun("a", mod)
158   * let b = fun("b", mod)
159   * println(a())
160   * println(b())
161   * </code></pre>
162   *
163   * @param source the module Golo source code as a string.
164   * @return the corresponding module, as a {@link Class}.
165   * @see Predefined#fun(Object, Object)
166   */
167  public Object anonymousModule(String source) {
168    return asModule(anonymousModuleName() + "\n\n" + source);
169  }
170
171  /**
172   * Defines a function, and returns it.
173   *
174   * <pre class="listing"><code class="lang-golo" data-lang="golo">
175   * let code = "|a, b| -> (a + b) * 2"
176   * let f = env: def(code)
177   * println(f(10, 20))
178   * </code></pre>
179   *
180   * @param source the function code.
181   * @return the function as a {@link gololang.FunctionReference} instance.
182   */
183  public Object def(String source) {
184    return loadAndRun("return " + source, "$_code");
185  }
186
187  /**
188   * Evaluates some code as the body of a function and returns it.
189   *
190   * <pre class="listing"><code class="lang-golo" data-lang="golo">
191   * let code = "return (a + b) * 2"
192   * let f = env: asFunction(code, "a", "b")
193   * println(f(10, 20))
194   * </code></pre>
195   *
196   * @param source        the function body source code.
197   * @param argumentNames the argument names.
198   * @return the function as a {@link gololang.FunctionReference} instance.
199   */
200  public Object asFunction(String source, String... argumentNames) {
201    return loadAndRun(source, "$_code_ref", argumentNames);
202  }
203
204  /**
205   * Runs some code as the body of a function and returns the value. The code shall use {@code return} statements
206   * to provide return values, if any.
207   *
208   * <pre class="listing"><code class="lang-golo" data-lang="golo">
209   * let code = """println(">>> run")
210   * foreach (i in range(0, 3)) {
211   *   println("w00t")
212   * }
213   * return 666"""
214   * env: run(code)
215   *
216   * </code></pre>
217   *
218   * @param source the source to run.
219   * @return the return value, or {@code null} if no {@code return} statement is used.
220   */
221  public Object run(String source) {
222    return loadAndRun(source, "$_code");
223  }
224
225  /**
226   * Runs some code as the body of a function and returns the value. This is the same as {@link #run(String)}, but it
227   * takes a set of reference bindings in a map. Each reference is equivalent to a {@code let} statement.
228   *
229   * <pre class="listing"><code class="lang-golo" data-lang="golo">
230   * let code = """println(">>> run_map")
231   * println(a)
232   * println(b)
233   * """
234   * let values = java.util.TreeMap(): add("a", 1): add("b", 2)
235   * env: run(code, values)
236   * </code></pre>
237   *
238   * @param source  the source to run.
239   * @param context a map of bindings from name to values.
240   * @return the return value, or {@code null} if no {@code return} statement is used.
241   */
242  public Object run(String source, Map<String, Object> context) {
243    StringBuilder builder = new StringBuilder();
244    for (String param : context.keySet()) {
245      builder
246          .append("let ")
247          .append(param)
248          .append(" = $_env: get(\"")
249          .append(param)
250          .append("\")\n");
251    }
252    builder.append(source);
253    return loadAndRun(builder.toString(), "$_code", new String[]{"$_env"}, new Object[]{context});
254  }
255
256  private Class<?> wrapAndLoad(String source, String... argumentNames) {
257    StringBuilder builder = new StringBuilder()
258        .append(anonymousModuleName())
259        .append("\n");
260    for (String importSymbol : imports) {
261      builder.append("import ").append(importSymbol).append("\n");
262    }
263    builder.append("\nfunction $_code = ");
264    if (argumentNames.length > 0) {
265      builder.append("| ");
266      final int lastIndex = argumentNames.length - 1;
267      for (int i = 0; i < argumentNames.length; i++) {
268        builder.append(argumentNames[i]);
269        if (i < lastIndex) {
270          builder.append(", ");
271        }
272      }
273      builder.append(" |");
274    }
275    builder
276        .append(" {\n")
277        .append(source)
278        .append("\n}\n\n")
279        .append("function $_code_ref = -> ^$_code\n\n");
280    return (Class<?>) asModule(builder.toString());
281  }
282
283  private Object loadAndRun(String source, String target, String... argumentNames) {
284    try {
285      Class<?> module = wrapAndLoad(source, argumentNames);
286      return module.getMethod(target).invoke(null);
287    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
288      throw new RuntimeException(e);
289    }
290  }
291
292  private Object loadAndRun(String source, String target, String[] argumentNames, Object[] arguments) {
293    try {
294      Class<?> module = wrapAndLoad(source, argumentNames);
295      Class<?>[] type = new Class<?>[argumentNames.length];
296      Arrays.fill(type, Object.class);
297      return module.getMethod(target, type).invoke(null, arguments);
298    } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
299      throw new RuntimeException(e);
300    }
301  }
302}