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}