org.protelis.lang.ProtelisLoader Maven / Gradle / Ivy
Show all versions of protelis-interpreter Show documentation
/*
* Copyright (C) 2022, Danilo Pianini and contributors listed in the project's build.gradle.kts file.
*
* This file is part of Protelis, and is distributed under the terms of the GNU General Public License,
* with a linking exception, as described in the file LICENSE.txt in this project's top directory.
*/
package org.protelis.lang;
import com.google.common.base.Splitter;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.google.inject.Injector;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.Resource.Diagnostic;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.xtext.common.types.JvmFeature;
import org.eclipse.xtext.common.types.JvmOperation;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.resource.XtextResourceSet;
import org.eclipse.xtext.util.StringInputStream;
import org.protelis.lang.datatype.Field;
import org.protelis.lang.datatype.FunctionDefinition;
import org.protelis.lang.datatype.JVMEntity;
import org.protelis.lang.interpreter.ProtelisAST;
import org.protelis.lang.interpreter.impl.AlignedMap;
import org.protelis.lang.interpreter.impl.All;
import org.protelis.lang.interpreter.impl.AssignmentOp;
import org.protelis.lang.interpreter.impl.BinaryOp;
import org.protelis.lang.interpreter.impl.ConditionalSideEffect;
import org.protelis.lang.interpreter.impl.Constant;
import org.protelis.lang.interpreter.impl.CreateTuple;
import org.protelis.lang.interpreter.impl.Env;
import org.protelis.lang.interpreter.impl.Eval;
import org.protelis.lang.interpreter.impl.FunctionCall;
import org.protelis.lang.interpreter.impl.GenericHoodCall;
import org.protelis.lang.interpreter.impl.HoodCall;
import org.protelis.lang.interpreter.impl.If;
import org.protelis.lang.interpreter.impl.Invoke;
import org.protelis.lang.interpreter.impl.JvmConstant;
import org.protelis.lang.interpreter.impl.NBRCall;
import org.protelis.lang.interpreter.impl.Self;
import org.protelis.lang.interpreter.impl.ShareCall;
import org.protelis.lang.interpreter.impl.TernaryOp;
import org.protelis.lang.interpreter.impl.UnaryOp;
import org.protelis.lang.interpreter.impl.Variable;
import org.protelis.lang.interpreter.util.HashingFunnel;
import org.protelis.lang.interpreter.util.HoodOp;
import org.protelis.lang.interpreter.util.Java8CompatibleFunnel;
import org.protelis.lang.interpreter.util.Reference;
import org.protelis.lang.loading.Metadata;
import org.protelis.parser.ProtelisStandaloneSetup;
import org.protelis.parser.protelis.Assignment;
import org.protelis.parser.protelis.Block;
import org.protelis.parser.protelis.BooleanVal;
import org.protelis.parser.protelis.Builtin;
import org.protelis.parser.protelis.BuiltinHoodOp;
import org.protelis.parser.protelis.Declaration;
import org.protelis.parser.protelis.DoubleVal;
import org.protelis.parser.protelis.Expression;
import org.protelis.parser.protelis.ExpressionList;
import org.protelis.parser.protelis.FunctionDef;
import org.protelis.parser.protelis.GenericHood;
import org.protelis.parser.protelis.IfWithoutElse;
import org.protelis.parser.protelis.InvocationArguments;
import org.protelis.parser.protelis.It;
import org.protelis.parser.protelis.Lambda;
import org.protelis.parser.protelis.LongLambda;
import org.protelis.parser.protelis.MethodCall;
import org.protelis.parser.protelis.Mux;
import org.protelis.parser.protelis.NBR;
import org.protelis.parser.protelis.OldLongLambda;
import org.protelis.parser.protelis.OldShortLambda;
import org.protelis.parser.protelis.ProtelisModule;
import org.protelis.parser.protelis.Rep;
import org.protelis.parser.protelis.RepInitialize;
import org.protelis.parser.protelis.Scalar;
import org.protelis.parser.protelis.Share;
import org.protelis.parser.protelis.ShareInitialize;
import org.protelis.parser.protelis.Statement;
import org.protelis.parser.protelis.StringVal;
import org.protelis.parser.protelis.TupleVal;
import org.protelis.parser.protelis.VarDef;
import org.protelis.parser.protelis.VarDefList;
import org.protelis.parser.protelis.VarUse;
import org.protelis.parser.protelis.Yield;
import org.protelis.vm.ProtelisProgram;
import org.protelis.vm.impl.SimpleProgramImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.protelis.lang.ProtelisLoadingUtilities.IT;
import static org.protelis.lang.ProtelisLoadingUtilities.argumentsToExpressionStream;
import static org.protelis.lang.ProtelisLoadingUtilities.referenceFor;
import static org.protelis.lang.ProtelisLoadingUtilities.referenceListFor;
/**
* Main entry-point class for loading/parsing Protelis programs.
*/
public final class ProtelisLoader {
private static final String HOOD_END = "Hood";
private static final ThreadLocal> LOADED_RESOURCES = ThreadLocal.withInitial(() ->
CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.build()
);
private static final Logger LOGGER = LoggerFactory.getLogger(ProtelisLoader.class);
private static final String OPEN_J9_EMF_WORKED_AROUND = "Working around OpenJ9 + Eclipse EMF bug."
+ "See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=549084"
+ "and https://github.com/eclipse/openj9/issues/6370";
private static final String PROTELIS_FILE_EXTENSION = "pt";
private static final Pattern REGEX_PROTELIS_IMPORT = Pattern.compile(
"^\\s*import\\s+((?:\\w+:)*\\w+)\\s+",
Pattern.MULTILINE
);
private static final Pattern REGEX_PROTELIS_MODULE = Pattern.compile("(?:\\w+:)*\\w+");
private static final LoadingCache FLYWEIGHT = CacheBuilder
.newBuilder()
.maximumSize(1000)
.build(
new CacheLoader<>() {
@Override
@Nonnull
public ProtelisProgram load(@Nonnull final Resource resource) {
Objects.requireNonNull(resource);
if (!resource.getErrors().isEmpty()) {
final String moduleName = Optional.ofNullable(resource.getContents())
.map(it -> it.get(0))
.map(it -> (ProtelisModule) it)
.map(ProtelisModule::getName)
.orElse("without declared module");
final StringBuilder sb = new StringBuilder(100)
.append("Program ")
.append(moduleName)
.append(" from resource ")
.append(resource.getURI())
.append(" cannot be created because of the following errors:\n");
boolean first = true;
for (final Diagnostic d : Lists.reverse(recursivelyCollectErrors(resource))) {
if (first) {
sb.append("MOST LIKELY CAUSE ==> ");
first = false;
}
sb.append("Error");
if (d.getLocation() != null) {
final String place = Iterables.get(Splitter.on('#').split(d.getLocation()), 0);
sb.append(" in ").append(place);
}
try {
final int line = d.getLine();
sb.append(", line ").append(line);
} catch (final UnsupportedOperationException e) { // NOPMD
// The line information is not available
}
try {
final int column = d.getColumn();
sb.append(", column ").append(column);
} catch (final UnsupportedOperationException e) { // NOPMD
// The column information is not available
}
sb.append(": ").append(d.getMessage()).append('\n');
}
throw new IllegalArgumentException(sb.toString());
}
final ProtelisModule root = (ProtelisModule) resource.getContents().get(0);
Objects.requireNonNull(Objects.requireNonNull(root).getProgram(),
"The provided resource does not contain any main program, and can not be executed.");
Diagnostician.INSTANCE.validate(root).getChildren()
.forEach(it -> LOGGER.warn("severity {}: {}", it.getSeverity(), it.getMessage()));
return new SimpleProgramImpl(root, Dispatch.block(root.getProgram()));
}
}
);
private static final ThreadLocal XTEXT = ThreadLocal.withInitial(() -> {
final Injector guiceInjector = new ProtelisStandaloneSetup().createInjectorAndDoEMFRegistration();
final XtextResourceSet xtext = guiceInjector.getInstance(XtextResourceSet.class);
xtext.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE);
return xtext;
});
private ProtelisLoader() {
}
private static void loadResourcesRecursively(final XtextResourceSet target, final String programURI)
throws IOException {
loadResourcesRecursively(target, programURI, new LinkedHashSet<>());
}
@SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "False positive")
private static void loadResourcesRecursively(
final XtextResourceSet target,
final String programURI,
final Set alreadyInQueue) throws IOException {
final ResolvedResource resource = new ResolvedResource(programURI);
if (LOADED_RESOURCES.get().getIfPresent(programURI) == null && !alreadyInQueue.contains(programURI)) {
alreadyInQueue.add(programURI);
final URI uri = workAroundOpenJ9EMFBug(() -> URI.createURI(resource.realURI));
if (resource.exists()) {
try (InputStream is = resource.openStream()) {
loadStringResources(target, is, alreadyInQueue);
}
LOADED_RESOURCES.get().put(programURI, workAroundOpenJ9EMFBug(() -> target.getResource(uri, true)));
} else {
throw new IllegalStateException("expected resource " + resource + " was not found");
}
}
}
private static void loadStringResources(
final XtextResourceSet target,
final InputStream is,
final Set alreadyInQueue
) throws IOException {
final String ss = IOUtils.toString(is, StandardCharsets.UTF_8);
final Matcher matcher = REGEX_PROTELIS_IMPORT.matcher(ss);
while (matcher.find()) {
final int start = matcher.start(1);
final int end = matcher.end(1);
final String imp = ss.substring(start, end);
final String classpathResource = "classpath:/" + imp.replace(":", "/") + "." + PROTELIS_FILE_EXTENSION;
loadResourcesRecursively(target, classpathResource, alreadyInQueue);
}
}
private static void loadStringResources(final XtextResourceSet target, final InputStream is) throws IOException {
loadStringResources(target, is, new LinkedHashSet<>());
}
private static Metadata metadataFor(final EObject origin) {
final INode grammarElement = NodeModelUtils.getNode(origin);
final int startLine = grammarElement.getStartLine();
final int endLine = grammarElement.getEndLine();
return new Metadata() {
private static final long serialVersionUID = 1L;
@Override
public int getEndLine() {
return endLine;
}
@Override
public int getStartLine() {
return startLine;
}
};
}
/**
* @param resource
* the {@link Resource} containing the program to execute
* @return a {@link ProtelisProgram}
*/
public static ProtelisProgram parse(@Nonnull final Resource resource) {
try {
return FLYWEIGHT.get(resource);
} catch (Exception e) { // NOPMD: this is intentional.
throw new IllegalArgumentException(e);
}
}
/**
* @param program
* Protelis module, program file or program to be prepared for
* execution. It must be one of:
*
* i) a valid Protelis qualifier name (Java like name, colon
* separated);
*
* ii) a valid {@link URI} string;
*
* iii) a valid Protelis program.
*
* Those possibilities are checked in order.
*
* The URI String can be in the form of a URL like
* "file:///home/user/protelis/myProgram" or a location relative
* to the classpath. In case, for instance,
* "/my/package/myProgram.pt" is passed, it will be automatically
* get converted to "classpath:/my/package/myProgram.pt". All the
* Protelis modules your program relies upon must be included in
* your Java classpath. The Java classpath scanning is done
* automatically by this constructor, linking is performed by
* Xtext transparently. {@link URI}s of type "platform:/" are
* supported, for those who work within an Eclipse environment.
* @return an {@link ProtelisProgram} comprising the constructed program
* @throws IllegalArgumentException
* when the program has errors
*/
public static ProtelisProgram parse(final String program) {
if (Objects.requireNonNull(program, "null is not a valid Protelis program, not a valid Protelis module").isEmpty()) {
throw new IllegalArgumentException("The empty string is not a valid program, nor a valid module name");
}
try {
if (REGEX_PROTELIS_MODULE.matcher(program).matches()) {
final String programURI = "classpath:/" + program.replace(':', '/') + "." + PROTELIS_FILE_EXTENSION;
final Optional programResource = resourceFromURIString(programURI)
.map(ProtelisLoader::parse);
if (programResource.isPresent()) {
return programResource.get();
}
}
return resourceFromURIString(program)
.map(ProtelisLoader::parse)
.orElseGet(() -> parseAnonymousModule(program));
} catch (IOException e) {
throw new IllegalStateException(program + " looks like an URI, but its resolution failed (see cause)", e);
}
}
/**
* @param program
* A valid Protelis program to be prepared for execution.
*
* All the Protelis modules your program relies upon must be
* included in your Java classpath. The Java classpath scanning
* is done automatically by this constructor, linking is
* performed by Xtext transparently. {@link URI}s of type
* "platform:/" are supported, for those who work within an
* Eclipse environment.
* @return a {@link ProtelisProgram}
* @throws IllegalArgumentException
* when the program has errors
*/
public static ProtelisProgram parseAnonymousModule(final String program) {
return parse(resourceFromString(program));
}
/**
* @param programURI
* Protelis program file to be prepared for execution. It must be
* a either a valid {@link URI} string, for instance
* "file:///home/user/protelis/myProgram" or a location relative
* to the classpath. In case, for instance,
* "/my/package/myProgram.pt" is passed, it will be automatically
* get converted to "classpath:/my/package/myProgram.pt". All the
* Protelis modules your program relies upon must be included in
* your Java classpath. The Java classpath scanning is done
* automatically by this constructor, linking is performed by
* Xtext transparently. {@link URI}s of type "platform:/" are
* supported, for those who work within an Eclipse environment.
* @return a new {@link ProtelisProgram}
* @throws IOException
* when the resource cannot be found
* @throws IllegalArgumentException
* when the program has errors
*/
@SuppressWarnings("unused")
public static ProtelisProgram parseURI(final String programURI) throws IOException {
return parse(resourceFromURIString(programURI).orElseThrow(IllegalArgumentException::new));
}
private static List recursivelyCollectErrors(final Resource resource) {
return resource.getResourceSet().getResources().stream()
.map(Resource::getErrors)
.filter(err -> !err.isEmpty())
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
/**
* @param program
* the program in String format
* @return a dummy:/ resource that can be used to interpret the program
*/
public static Resource resourceFromString(final String program) {
final String programId = "dummy:/protelis-generated-program-"
+ Hashing.sha512().hashString(program, StandardCharsets.UTF_8)
+ ".pt";
final URI uri = workAroundOpenJ9EMFBug(() -> URI.createURI(programId));
synchronized (XTEXT) {
Resource r = XTEXT.get().getResource(uri, false);
if (r == null) {
try (InputStream in = new StringInputStream(program)) {
loadStringResources(XTEXT.get(), in);
} catch (IOException e) {
throw new IllegalStateException("Couldn't get resource associated with anonymous program: "
+ e.getMessage(), e);
}
r = XTEXT.get().createResource(uri);
try (InputStream in = new StringInputStream(program)) {
r.load(in, XTEXT.get().getLoadOptions());
} catch (IOException e) {
throw new IllegalStateException("I/O error while reading in RAM: this must be tough.", e);
}
}
return r;
}
}
private static Optional resourceFromURIString(final String programURI) throws IOException {
final ResolvedResource resource = new ResolvedResource(programURI);
if (resource.exists()) {
loadResourcesRecursively(XTEXT.get(), programURI);
final URI uri = workAroundOpenJ9EMFBug(() -> URI.createURI(resource.realURI));
return Optional.of(XTEXT.get().getResource(uri, true));
} else {
return Optional.empty();
}
}
private static R workAroundOpenJ9EMFBug(final Supplier fun) {
try {
return fun.get();
} catch (AssertionError e) {
LOGGER.warn(OPEN_J9_EMF_WORKED_AROUND, e);
return fun.get();
}
}
private static final class Dispatch {
private static final Cache VIRTUAL_METHOD_TABLE =
CacheBuilder.newBuilder().weakKeys().build();
private static final HashingFunnel FUNNEL = new Java8CompatibleFunnel();
private static ProtelisAST> alignedMap(@Nonnull final org.protelis.parser.protelis.AlignedMap alMap) {
return new AlignedMap(
FUNNEL,
metadataFor(alMap),
expression(alMap.getArg()),
expression(alMap.getCond()),
expression(alMap.getOp()),
expression(alMap.getDefault())
);
}
private static AssignmentOp assignment(final Assignment assignment) {
return new AssignmentOp(
metadataFor(assignment),
referenceFor(assignment.getRefVar()),
expression(assignment.getRight())
);
}
private static ProtelisAST> block(@Nonnull final Block block) {
final List statements = block.getStatements();
if (statements.size() == 1) {
return statement(statements.get(0));
}
return new All(metadataFor(block), statements.stream().map(Dispatch::statement).collect(Collectors.toList()));
}
@SuppressWarnings("unchecked")
private static ProtelisAST blockUnsafe(@Nonnull final Block block) {
return (ProtelisAST) block(block);
}
@SuppressWarnings("deprecation")
private static ProtelisAST> builtin(@Nonnull final Builtin expression) {
if (expression instanceof org.protelis.parser.protelis.AlignedMap) {
return alignedMap((org.protelis.parser.protelis.AlignedMap) expression);
}
final Metadata meta = metadataFor(expression);
if (expression instanceof org.protelis.parser.protelis.Env) {
return new Env(meta);
}
if (expression instanceof org.protelis.parser.protelis.Eval) {
return new Eval(meta, expression(((org.protelis.parser.protelis.Eval) expression).getArg()));
}
if (expression instanceof BuiltinHoodOp) {
final BuiltinHoodOp hood = (BuiltinHoodOp) expression;
return new HoodCall(meta, expression(hood.getArg()),
HoodOp.get(hood.getName().replace(HOOD_END, "")),
hood.isInclusive());
}
if (expression instanceof GenericHood) {
final GenericHood hood = (GenericHood) expression;
final boolean inclusive = hood.getName().length() > 4;
final ProtelisAST