All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.protelis.lang.ProtelisLoader Maven / Gradle / Ivy

There is a newer version: 17.6.0
Show newest version
/*
 * 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("Program " + moduleName
                            + " from resource " + resource.getURI()
                            + " 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 ");
                                sb.append(place);
                            }
                            try {
                                final int line = d.getLine();
                                sb.append(", line ");
                                sb.append(line);
                            } catch (final UnsupportedOperationException e) { // NOPMD
                                // The line information is not available
                            }
                            try {
                                final int column = d.getColumn();
                                sb.append(", column ");
                                sb.append(column);
                            } catch (final UnsupportedOperationException e) { // NOPMD
                                // The column information is not available
                            }
                            sb.append(": ");
                            sb.append(d.getMessage());
                            sb.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 nullResult = expression(hood.getDefault());
                final ProtelisAST> field = expression(hood.getArg());
                final VarUse ref = hood.getReference();
                if (ref == null) {
                    return new GenericHoodCall(meta, inclusive, lambda(hood.getOp()), nullResult, field);
                }
                if (ref.getReference() instanceof JvmOperation) {
                    return new GenericHoodCall(meta, inclusive, (JvmOperation) ref.getReference(), nullResult, field);
                }
                return new GenericHoodCall(meta, inclusive, variableUnsafe(ref), nullResult, field);
            }
            if (expression instanceof It) {
                return new Variable(meta, IT);
            }
            if (expression instanceof Mux) {
                final Mux mux = (Mux) expression;
                return new TernaryOp(meta, mux.getName(), expression(mux.getCond()), block(mux.getThen()), block(mux.getElse()));
            }
            if (expression instanceof org.protelis.parser.protelis.Self) {
                return new Self(meta);
            }
            throw new IllegalStateException("Unknown builtin of type " + expression.getClass().getSimpleName());
        }

        private static AssignmentOp declaration(final Declaration declaration) {
            final VarDef name = declaration.getName();
            return new AssignmentOp(metadataFor(declaration), referenceFor(name), expression(declaration.getRight()));
        }

        @SuppressWarnings("unchecked")
        private static  ProtelisAST expression(final Expression expression) {
            return (ProtelisAST) expressionRaw(expression);
        }

        private static List> expressionList(@Nullable final ExpressionList list) {
            return Optional.ofNullable(list)
                .>map(ExpressionList::getArgs)
                .orElseGet(Collections::emptyList)
                .stream()
                .map(Dispatch::expression)
                .collect(ImmutableList.toImmutableList());
        }

        private static ProtelisAST expressionRaw(final Expression expression) {
            if (expression instanceof Builtin) {
                return builtin((Builtin) expression);
            }
            if (expression instanceof org.protelis.parser.protelis.If) {
                return ifOp((org.protelis.parser.protelis.If) expression);
            }
            if (expression instanceof Lambda) {
                return lambda((Lambda) expression);
            }
            if (expression instanceof NBR) {
                return nbr((NBR) expression);
            }
            if (expression instanceof Rep) {
                return rep((Rep) expression);
            }
            if (expression instanceof Scalar) {
                return scalar((Scalar) expression);
            }
            if (expression instanceof Share) {
                return share((Share) expression);
            }
            if (expression instanceof VarUse) {
                return variable((VarUse) expression);
            }
            /*
             * Pure expression
             */
            final List elements = expression.getElements();
            final Metadata meta = metadataFor(expression);
            switch (elements.size()) {
                case 1: return new UnaryOp(meta, expression.getName(), expression((Expression) elements.get(0)));
                case 2: 
                    final ProtelisAST first = expression((Expression) expression.getElements().get(0));
                    final EObject second = expression.getElements().get(1);
                    if (expression.getName() == null && second instanceof InvocationArguments) {
                        // Invoke
                        final InvocationArguments invokeArgs = (InvocationArguments) second;
                        if (first instanceof Constant) {
                           final Object constant = ((Constant) first).getConstantValue();
                           if (constant instanceof FunctionDefinition) {
                               // It's a plain function call, possibly on a lambda, don't go through Invoke
                               return new FunctionCall(meta, (FunctionDefinition) constant, invocationArguments(invokeArgs));
                           }
                        }
                        // TODO: Drop "apply", and allow only standard invocations with better system
                        return new Invoke(meta, "apply", first, invocationArguments(invokeArgs));
                    }
                    if (".".equals(expression.getName()) && second instanceof MethodCall) {
                        // Method call
                        final MethodCall method = (MethodCall) second;
                        return new Invoke(meta, method.getName(), first, invocationArguments(method.getArguments()));
                    }
                    if (expression.getName() != null) {
                        return new BinaryOp(meta, expression.getName(), first, expression((Expression) second));
                    }
                default: throw new IllegalStateException("Unknown AST node " + expression);
            }
        }

        private static ProtelisAST ifOp(final org.protelis.parser.protelis.If ifOp) {
            return new If<>(metadataFor(ifOp), expression(ifOp.getCond()),
                    blockUnsafe(ifOp.getThen()),
                    block(ifOp.getElse()));
        }

        private static ConditionalSideEffect ifWithoutElse(final IfWithoutElse ifOp) {
            final Metadata meta = metadataFor(ifOp);
            final List> then = ifOp.getThen().stream()
                    .map(Dispatch::statement)
                    .collect(Collectors.toList());
            final ProtelisAST thenBranch = then.size() == 1
                    ? then.get(0)
                    : new All(meta, then);
            return new ConditionalSideEffect(meta, expression(ifOp.getCond()), thenBranch);
        }

        private static List> invocationArguments(@Nonnull final InvocationArguments args) {
            return argumentsToExpressionStream(args)
                .map(Dispatch::expression)
                .collect(ImmutableList.toImmutableList());
        }

        private static Constant lambda(@Nonnull final Lambda expression) {
            final List arguments = expression instanceof LongLambda
                ? ((LongLambda) expression).getArgs().getArgs()
                : expression instanceof OldLongLambda
                    ? Optional.ofNullable(((OldLongLambda) expression).getArgs())
                        .>map(VarDefList::getArgs)
                        .orElseGet(Collections::emptyList)
                    : expression instanceof OldShortLambda
                        ? Collections.singletonList(((OldShortLambda) expression).getSingleArg())
                        : Collections.emptyList();
            final FunctionDefinition lambda =
                new FunctionDefinition(expression, referenceListFor(arguments), block(expression.getBody()));
            return new Constant<>(metadataFor(expression), lambda);
        }

        private static  NBRCall nbr(final NBR nbr) {
            return new NBRCall<>(metadataFor(nbr), expression(nbr.getArg()));
        }

        private static ShareCall rep(final Rep rep) {
            final Metadata meta = metadataFor(rep);
            final RepInitialize init = rep.getInit();
            final Optional local = Optional.of(referenceFor(init.getX()));
            final Optional> yield = Optional.ofNullable(rep.getYields())
                    .map(Yield::getBody)
                    .map(Dispatch::blockUnsafe);
            return new ShareCall<>(meta, local, Optional.empty(), expression(init.getW()), block(rep.getBody()), yield);
        }

        private static ProtelisAST scalar(@Nonnull final Scalar expression) {
            final Metadata meta = metadataFor(expression);
            if (expression instanceof BooleanVal) {
                return new Constant<>(meta, ((BooleanVal) expression).isVal());
            }
            if (expression instanceof DoubleVal) {
                return new Constant<>(meta, ((DoubleVal) expression).getVal());
            }
            if (expression instanceof StringVal) {
                return new Constant<>(meta, ((StringVal) expression).getVal());
            }
            if (expression instanceof TupleVal) {
                return new CreateTuple(meta, expressionList(((TupleVal) expression).getArgs()));
            }
            throw new IllegalStateException("Unknown scalar of type " + expression.getClass().getSimpleName());
        }

        private static ShareCall share(final Share share) {
            final ShareInitialize init = share.getInit();
            final Optional local = Optional.ofNullable(init.getLocal()).map(ProtelisLoadingUtilities::referenceFor);
            final Optional field = Optional.ofNullable(init.getField()).map(ProtelisLoadingUtilities::referenceFor);
            final Optional> yield = Optional.ofNullable(share.getYields())
                    .map(Yield::getBody)
                    .map(Dispatch::blockUnsafe);
            return new ShareCall<>(metadataFor(share), local, field, expression(init.getW()), block(share.getBody()), yield);
        }

        private static ProtelisAST statement(@Nonnull final Statement statement) {
            if (statement instanceof Expression) {
                return expression((Expression) statement);
            }
            if (statement instanceof Declaration) {
                return declaration((Declaration) statement);
            }
            if (statement instanceof Assignment) {
                return assignment((Assignment) statement);
            }
            if (statement instanceof IfWithoutElse) {
                return ifWithoutElse((IfWithoutElse) statement);
            }
            throw new IllegalStateException("Unknown statement of type " + statement.getClass().getSimpleName());
        }

        private static ProtelisAST variable(@Nonnull final VarUse expression) {
            /*
             * VarUse can reference:
             * 
             * - JvmFeature (imported methods or fields)
             * 
             * - FunctionDef (imported or locally defined Protelis functions)
             * 
             * - VarDef (variables defined in scope, parameters in scope)
             * 
             * The former can be treated as constants. They are immutable and do not require restriction
             * (as they cannot bind a Field).
             */
            final Metadata meta = metadataFor(expression);
            final EObject ref = expression.getReference();
            if (ref instanceof JvmFeature) {
                /*
                 * JVMFeature is not serializable
                 */
                return new JvmConstant(meta, new JVMEntity((JvmFeature) ref));
            }
            if (ref instanceof FunctionDef) {
                final FunctionDef functionDefinition = (FunctionDef) ref;
                FunctionDefinition target;
                try {
                    target = VIRTUAL_METHOD_TABLE.get(functionDefinition,
                            () -> new FunctionDefinition(functionDefinition, () -> functionBody(functionDefinition)));
                    return new Constant<>(meta, target);
                } catch (ExecutionException e) {
                    throw new IllegalStateException(e);
                }
            }
            return new Variable(meta, referenceFor(ref));
        }

        @SuppressWarnings("unchecked")
        private static  ProtelisAST variableUnsafe(final VarUse expression) {
            return (ProtelisAST) variable(expression);
        }

        private static ProtelisAST functionBody(final FunctionDef fun) {
            if (fun.getSingleExpression() == null) {
                return block(fun.getBody());
            } else {
                return expression(fun.getSingleExpression());
            }
        }

    }

    private static final class ResolvedResource {
        private static final String CLASSPATH_PROTOCOL = "classpath:";
        private final String classpathURL;
        private final String realURI;
        private ResolvedResource(final String programURI) {
            realURI = (programURI.startsWith("/") ? CLASSPATH_PROTOCOL : "") + programURI;
            classpathURL = realURI.startsWith(CLASSPATH_PROTOCOL) ? realURI.substring(CLASSPATH_PROTOCOL.length() + 1) : realURI;
        }
        private boolean exists() {
            return Thread.currentThread().getContextClassLoader().getResource(classpathURL) != null;
        }
        private InputStream openStream() {
            return Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathURL);
        }
        @Override
        public String toString() {
            return "From classpath: " + classpathURL + ", complete URI: " + realURI;
        }
    }

}