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