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

il.ac.bgu.cs.bp.bpjs.model.BProgram Maven / Gradle / Ivy

package il.ac.bgu.cs.bp.bpjs.model;

import il.ac.bgu.cs.bp.bpjs.execution.jsproxy.BProgramJsProxy;
import il.ac.bgu.cs.bp.bpjs.model.eventselection.EventSelectionStrategy;
import il.ac.bgu.cs.bp.bpjs.model.eventselection.SimpleEventSelectionStrategy;

import java.util.*;
import java.util.concurrent.*;

import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsCodeEvaluationException;
import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsException;
import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsRuntimeException;
import il.ac.bgu.cs.bp.bpjs.execution.jsproxy.BpLog;
import il.ac.bgu.cs.bp.bpjs.execution.tasks.FailedAssertionException;
import il.ac.bgu.cs.bp.bpjs.internal.ScriptableUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Scriptable;
import java.util.concurrent.atomic.AtomicInteger;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.WrappedException;

/**
 * Base class for BPrograms. Provides the context (JavaScript scope, external
 * event queue, etc.) b-threads interact with while running. Concrete BProgram
 * extend this class by implementing the
 * {@link #setupProgramScope(org.mozilla.javascript.Scriptable)} method.
 *
 * For creating a BProgram that uses a single JavaScript file available in the
 * classpath, see {@link ResourceBProgram}. For creating them from a
 * hard-coded string, see {@link StringBProgram}.
 *
 * @author michael
 */
public abstract class BProgram {

    // ------------- Static Members ---------------
    /**
     * "Poison pill" to insert to the external event queue. Used only to turn
     * the wait-for-external-events mode off.
     */
    static final BEvent NO_MORE_WAIT_EXTERNAL = new BEvent("___bpjs-internal____NO_MORE_WAIT_EXTERNAL");

    /**
     * Counter for giving anonymous instances some semantic name.
     */
    private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();

    /**
     * A callback interface invoked when a b-thread is added to {@code this}.
     */
    public interface BProgramCallback {

        void bthreadAdded(BProgram bp, BThreadSyncSnapshot theBThread);
    }

    // ------------- Instance Members ---------------
    private String name;

    /**
     * When {@code true}, the BProgram waits for an external event when no
     * internal ones are available. Corollary: the program does not terminate at
     * the end of a super-step.
     */
    private boolean waitForExternalEvents;

    /**
     * Events are enqueued here by external threads
     */
    private final BlockingQueue recentlyEnqueuedExternalEvents = new LinkedBlockingQueue<>();

    /**
     * BThreads added between syncs are added here.
     */
    private final BlockingQueue recentlyRegisteredBthreads = new LinkedBlockingDeque<>();
    
    private final BlockingQueue recentlyAddedForks = new LinkedBlockingDeque<>();

    private volatile boolean started = false;

    protected Scriptable programScope;

    private EventSelectionStrategy eventSelectionStrategy;

    private BProgramJsProxy jsProxy;
    
    private BpLog.LogLevel preSetLogLevel = null;
    
    /**
     * Objects that client code wishes to put in scope before the scope is
     * initialized are collected here.
     */
    protected Map initialScopeValues = new HashMap<>();

    private Optional addBThreadCallback = Optional.empty();

    private List appendedCode;
    private List prependedCode;

    /**
     * Constructs a BProgram with a default name, guaranteed to be unique within
     * a given run.
     */
    public BProgram() {
        this("BProgram-" + INSTANCE_COUNTER.incrementAndGet());
    }

    /**
     * Constructs a BProgram with a specific name.
     *
     * @param aName name for the new BProgram.
     */
    public BProgram(String aName) {
        name = aName;
    }

    /**
     * Creates a BProgram with a specific name and an event selection strategy.
     *
     * @param aName Name for the program.
     * @param anEss Event selection strategy.
     */
    public BProgram(String aName, EventSelectionStrategy anEss) {
        name = aName;
        eventSelectionStrategy = anEss;
    }

    /**
     * Adds more source code to be evaluated after
     * {@link #setupProgramScope(org.mozilla.javascript.Scriptable)} is called.
     * This method allows to programatically add code, e.g. for adding standard
     * environment, mocking non-modeled parts for model-checking.
     *
     * @throws IllegalStateException if the code is appended after the bprogram
     * started.
     * @param source JavaScript source to be added at the end of the current source.
     */
    public void appendSource(String source) {
        if (started) {
            throw new IllegalStateException("Cannot append code after the program had started.");
        } else {
            if (appendedCode == null) {
                appendedCode = new ArrayList<>();
            }
            appendedCode.add(source);
        }
    }

    /**
     * Adds more source code to be evaluated before
     * {@link #setupProgramScope(org.mozilla.javascript.Scriptable)} is called.
     * This method allows to programatically add code, e.g. for adding standard
     * environment, mocking non-modeled parts for model-checking.
     *
     * @throws IllegalStateException if the code is appended after the bprogram
     * started.
     * @param source JavaScript source to be added at the beginning of the current source.
     */
    public void prependSource(String source) {
        if (started) {
            throw new IllegalStateException("Cannot append code after the program had started.");
        } else {
            if (prependedCode == null) {
                prependedCode = new ArrayList<>();
            }
            prependedCode.add(source);
        }
    }

    /**
     * Reads and evaluates the code at the passed input stream. The stream is
     * read to its end, but is not closed.
     *
     * @param inStrm Input stream for reading the script to be evaluated.
     * @param scriptName for error reporting purposes.
     * @return Result of evaluating the code at {@code inStrm}.
     */
    protected Object evaluate(InputStream inStrm, String scriptName) {
        InputStreamReader streamReader = new InputStreamReader(inStrm, StandardCharsets.UTF_8);
        BufferedReader br = new BufferedReader(streamReader);
        StringBuilder sb = new StringBuilder();
        String line;
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
        } catch (IOException e) {
            throw new RuntimeException("error while reading javascript from stream", e);
        }
        String script = sb.toString();
        return evaluate(script, scriptName);
    }

    /**
     * Runs the passed code in the passed scope.
     *
     * @param script Code to evaluate
     * @param scriptName For error reporting purposes.
     * @return Result of code evaluation.
     */
    protected Object evaluate(String script, String scriptName) {
        try {
            Context curCtx = Context.getCurrentContext();
            curCtx.setLanguageVersion(Context.VERSION_ES6);
            curCtx.setOptimizationLevel(-1);
            return curCtx.evaluateString(programScope, script, scriptName, 1, null);
        } catch (EcmaError rerr) {
            throw new BPjsCodeEvaluationException(rerr);

        } catch (WrappedException wrapped) {
            try {
                throw wrapped.getCause();
            } catch (BPjsException be ) {
                throw be;
            } catch ( IllegalStateException ise ) {
                String msg = ise.getMessage();
                if ( msg.contains("Cannot capture continuation") && msg.contains("executeScriptWithContinuations or callFunctionWithContinuations") ){
                    throw new BPjsCodeEvaluationException("bp.sync called outside of a b-thread");
                } else {
                    throw ise;
                }
            } catch ( Throwable generalException ) {
                throw new BPjsRuntimeException("(Wrapped) Exception evaluating BProgram code: " + generalException.getMessage(), generalException);
            }

        } catch (EvaluatorException evalExp) {
            throw new BPjsCodeEvaluationException(evalExp);
            
        } catch ( Exception exp ) {
            throw new BPjsRuntimeException("Error evaluating BProgram code: " + exp.getMessage(), exp);
        }
    }

    /**
     * Registers a BThread into the program. If the program started, the BThread
     * will take part in the current bstep.
     *
     * @param bt the BThread to be registered.
     */
    public void registerBThread(BThreadSyncSnapshot bt) {
        recentlyRegisteredBthreads.add(bt);
        addBThreadCallback.ifPresent(cb -> cb.bthreadAdded(this, bt));
    }
    
    void registerForkedChild( BThreadSyncSnapshot btss ) {
        // make the top-level scope be *this* program's programScope
        Scriptable pus = ScriptableUtils.getPenultiamteParent(btss.getScope());
        pus.setParentScope(programScope);
        
        addBThreadCallback.ifPresent(cb -> cb.bthreadAdded(this, btss));
    }
    
    /**
     * Adds an event to {@code this}' external event queue.
     *
     * @param e The event to add.
     */
    public void enqueueExternalEvent(BEvent e) {
        recentlyEnqueuedExternalEvents.add(e);
    }

    /**
     * Sets up the program scope and evaluates the program source.
     *
     * This method can only be called once per instance.
     *
     * @return a snapshot of the program, after source code was executed, and
     * before any registered b-threads have run.
     * @throws IllegalStateException for repeated calls.
     */
    public BProgramSyncSnapshot setup() {
        if (started) {
            throw new IllegalStateException("Program already set up.");
        }
        Set bthreads = drainRecentlyRegisteredBthreads();

        if (eventSelectionStrategy == null) {
            eventSelectionStrategy = new SimpleEventSelectionStrategy();
        }
        FailedAssertion failedAssertion = null;
        try {
            Context cx = ContextFactory.getGlobal().enterContext();
            cx.setOptimizationLevel(-1); // must use interpreter mode
            cx.setLanguageVersion( Context.VERSION_ES6);
            initProgramScope(cx);

            // evaluate code in order
            if (prependedCode != null) {
                prependedCode.forEach(s -> evaluate(s, "prependedCode"));
                prependedCode = null;
            }
            setupProgramScope(programScope);
            if (appendedCode != null) {
                appendedCode.forEach(s -> evaluate(s, "appendedCode"));
                appendedCode = null;
            }

        } catch (FailedAssertionException fae) {
            failedAssertion = new FailedAssertion(fae.getMessage(), "---init_code");

        } finally {
            Context.exit();
        }

        started = true;
        return new BProgramSyncSnapshot(this, bthreads, Collections.emptyList(), failedAssertion);
    }

    private void initProgramScope(Context cx) {
        // load and execute globalScopeInit.js
        ImporterTopLevel importer = new ImporterTopLevel(cx);
        programScope = cx.initStandardObjects(importer);
        jsProxy = new BProgramJsProxy(this);
        if ( preSetLogLevel != null ) {
            jsProxy.log.setLevel(preSetLogLevel.name());
        }
        programScope.put("bp", programScope, Context.javaToJS(jsProxy, programScope));

//        evaluateResource("globalScopeInit.js");// <-- Currently not needed. Leaving in as we might need it soon.
        initialScopeValues.entrySet().forEach(e -> putInGlobalScope(e.getKey(), e.getValue()));
        initialScopeValues = null;
    }
   

    /**
     * The BProgram should set up its scope here. Normally, this amounts to
     * loading the script with the BThreads.
     *
     * @param scope the scope to set up.
     */
    protected abstract void setupProgramScope(Scriptable scope);

    /**
     * Blocks until an external event is added. Then, if that event is not the
     * "stop daemon mode" one, returns the event. Otherwise, returns
     * {@code null}.
     *
     * @return The event, or {@code null} in case the daemon mode is turned off
     * during the wait.
     * @throws InterruptedException (blocking call, we have to do this)
     */
    public BEvent takeExternalEvent() throws InterruptedException {
        BEvent next = recentlyEnqueuedExternalEvents.take();

        if (next == NO_MORE_WAIT_EXTERNAL) {
            waitForExternalEvents = false;
            return null;
        } else {
            return next;
        }
    }

    /**
     * Sets whether this program waits for external events or not.
     *
     * When set to {@code false}, when no events are available for selection,
     * the program terminates.
     *
     * @param shouldWait {@code true} causes the system to wait for external events. {@code false} causes it to not wait.
     */
    public void setWaitForExternalEvents(boolean shouldWait) {
        if (waitForExternalEvents && !shouldWait) {
            waitForExternalEvents = false;
            recentlyEnqueuedExternalEvents.add(NO_MORE_WAIT_EXTERNAL);
        } else {
            waitForExternalEvents = shouldWait;
        }
    }

    /**
     * Returns {@code true} iff the program waits for external events. When
     * {@code true}, the program will not terminate when it has no event
     * available for selection. Rather, it will wait for an external event to be
     * enqueued into its external event queue.
     *
     * @return {@code true} if this BProgram waits for external events,
     * {@code false} otherwise.
     * @see #enqueueExternalEvent(il.ac.bgu.cs.bp.bpjs.model.BEvent) 
     */
    public boolean isWaitForExternalEvents() {
        return waitForExternalEvents;
    }

    /**
     * Returns the program's global scope.
     *
     * @return the global scope of the program.
     */
    public Scriptable getGlobalScope() {
        return programScope;
    }

    /**
     * Adds an object to the program's global scope. JS code can reference the
     * added object by {@code name}.
     *
     * @param name The name under which {@code object} will be available to the
     * JS code.
     * @param obj The object to be added to the program's scope.
     */
    public void putInGlobalScope(String name, Object obj) {
        if (getGlobalScope() == null) {
            initialScopeValues.put(name, obj);
        } else {
            try {
                Context.enter();
                getGlobalScope().put(name, programScope, Context.javaToJS(obj, programScope));
            } finally {
                Context.exit();
            }
        }
    }

    /**
     * Gets the object pointer by the passed name in the global scope.
     *
     * @param  Class of the returned object.
     * @param name Name of the object in the JS heap.
     * @param clazz Class of the returned object
     * @return The object pointer by the passed name in the JS heap, converted
     * to the passed class.
     */
    public  Optional getFromGlobalScope(String name, Class clazz) {
        if (getGlobalScope().has(name, programScope)) {
            return Optional.of((T) Context.jsToJava(getGlobalScope().get(name, getGlobalScope()), clazz));
        } else {
            return Optional.empty();
        }
    }

    /**
     * Sets the name of the program
     *
     * @param name the new program's name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the program's name
     */
    public String getName() {
        return name;
    }
    
    Set drainRecentlyRegisteredBthreads() {
        Set out = new HashSet<>();
        recentlyRegisteredBthreads.drainTo(out);
        return out;
    }

    List drainEnqueuedExternalEvents() {
        List out = new ArrayList<>(recentlyEnqueuedExternalEvents.size());
        recentlyEnqueuedExternalEvents.drainTo(out);
        return out;
    }
    
    void addFork( ForkStatement fkStmt ) {
        recentlyAddedForks.add(fkStmt);
    }
    
    Set drainRecentlyAddedForks() {
        Set out = new HashSet<>();
        recentlyAddedForks.drainTo(out);
        return out;
    }
    
    public void setAddBThreadCallback(BProgramCallback anAddBThreadCallback) {
        addBThreadCallback = Optional.ofNullable(anAddBThreadCallback);
    }

    public EventSelectionStrategy getEventSelectionStrategy() {
        if (eventSelectionStrategy == null) {
            setEventSelectionStrategy(new SimpleEventSelectionStrategy());
        }
        return eventSelectionStrategy;
    }
    
    /**
     * Sets the event selection strategy to be used.
     * @param  Actual type of the strategy
     * @param anEventSelectionStrategy the strategy
     * @return the strategy (so it can be cleanly assigned to a variable).
     */
    public  T setEventSelectionStrategy(T anEventSelectionStrategy) {
        eventSelectionStrategy = anEventSelectionStrategy;
        return anEventSelectionStrategy;
    }
    
    public void setLogLevel( BpLog.LogLevel aLevel ) {
        if ( jsProxy != null ) {
            jsProxy.log.setLevel(aLevel.name());
        } else {
            preSetLogLevel = aLevel;
        }
    }
    
    public BpLog.LogLevel getLogLevel() {
        return (jsProxy != null ) ? BpLog.LogLevel.valueOf(jsProxy.log.getLevel()) : null;
    }
    
    @Override
    public String toString() {
        return "[BProgram " + getName() + "]";
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy