/*
* Copyright (c) 2010-2011, IETR/INSA of Rennes
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of the IETR/INSA of Rennes nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package net.sf.orcc.cal.validation;
import static net.sf.orcc.cal.cal.CalPackage.eINSTANCE;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sf.orcc.cal.CalDiagnostic;
import net.sf.orcc.cal.cal.AstAction;
import net.sf.orcc.cal.cal.AstActor;
import net.sf.orcc.cal.cal.AstEntity;
import net.sf.orcc.cal.cal.AstExpression;
import net.sf.orcc.cal.cal.AstPort;
import net.sf.orcc.cal.cal.AstProcedure;
import net.sf.orcc.cal.cal.AstState;
import net.sf.orcc.cal.cal.AstTag;
import net.sf.orcc.cal.cal.AstTransition;
import net.sf.orcc.cal.cal.AstUnit;
import net.sf.orcc.cal.cal.CalPackage;
import net.sf.orcc.cal.cal.ExpressionCall;
import net.sf.orcc.cal.cal.Function;
import net.sf.orcc.cal.cal.Generator;
import net.sf.orcc.cal.cal.Inequality;
import net.sf.orcc.cal.cal.InputPattern;
import net.sf.orcc.cal.cal.OutputPattern;
import net.sf.orcc.cal.cal.Priority;
import net.sf.orcc.cal.cal.RegExp;
import net.sf.orcc.cal.cal.ScheduleFsm;
import net.sf.orcc.cal.cal.StatementCall;
import net.sf.orcc.cal.cal.Variable;
import net.sf.orcc.cal.cal.VariableReference;
import net.sf.orcc.cal.services.Evaluator;
import net.sf.orcc.cal.util.CalActionList;
import net.sf.orcc.util.OrccUtil;
import net.sf.orcc.util.util.EcoreHelper;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.CheckType;
import org.jgrapht.DirectedGraph;
import org.jgrapht.alg.CycleDetector;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
/**
* This class describes a validator that performs structural validation of an
* RVC-CAL actor/unit. The checks tagged as "normal" are only performed when the
* file is saved and before code generation.
*
* @author Matthieu Wipliez
*
*/
public class StructuralValidator extends AbstractCalValidator {
private static final String DEFAULT = "(default)";
/**
* Checks the inputs patterns.
*
* @param inputs
* the input patterns of an action
*/
private void checkActionInputs(List<InputPattern> inputs) {
List<AstPort> ports = new ArrayList<AstPort>();
for (InputPattern pattern : inputs) {
AstPort port = pattern.getPort();
if (ports.contains(port)) {
error("duplicate reference to port " + port.getName(), pattern,
eINSTANCE.getInputPattern_Port(),
CalDiagnostic.ERROR_DUPLICATE_PORT_REFERENCE);
} else {
ports.add(port);
}
AstExpression astRepeat = pattern.getRepeat();
if (astRepeat != null) {
int repeat = Evaluator.getIntValue(astRepeat);
if (repeat <= 0) {
error("This repeat clause must evaluate to a compile-time "
+ "constant greater than zero", pattern,
eINSTANCE.getInputPattern_Repeat(), -1);
}
}
}
}
/**
* Checks the token expressions are correctly typed.
*
* @param outputs
* the output patterns of an action
*/
private void checkActionOutputs(List<OutputPattern> outputs) {
List<AstPort> ports = new ArrayList<AstPort>();
for (OutputPattern pattern : outputs) {
AstPort port = pattern.getPort();
if (ports.contains(port)) {
error("duplicate reference to port " + port.getName(), pattern,
eINSTANCE.getOutputPattern_Port(),
CalDiagnostic.ERROR_DUPLICATE_PORT_REFERENCE);
} else {
ports.add(port);
}
AstExpression astRepeat = pattern.getRepeat();
if (astRepeat != null) {
int repeat = Evaluator.getIntValue(astRepeat);
if (repeat <= 0) {
error("This repeat clause must evaluate to a compile-time "
+ "constant greater than zero", pattern,
eINSTANCE.getOutputPattern_Repeat(), -1);
}
}
}
}
/**
* Check that the action tag is different from port and state variable
* names.
*
* @param action
* the action
*/
private void checkActionTag(AstAction action) {
AstActor actor = EcoreUtil2.getContainerOfType(action, AstActor.class);
String name = getName(action);
// Check if tag name is not already used in a state variable
List<Variable> variables = actor.getStateVariables();
for (Variable variable : variables) {
if (name.equals(variable.getName())) {
error("Action " + name
+ " has the same name as a state variable",
eINSTANCE.getAstAction_Tag());
}
}
// Check if tag name is not already used in an input port
List<AstPort> inputs = actor.getInputs();
for (AstPort input : inputs) {
if (name.equals(input.getName())) {
error("Action " + name + " has the same name as an input port",
eINSTANCE.getAstAction_Tag());
}
}
// Check if tag name is not already used in an output port
List<AstPort> outputs = actor.getOutputs();
for (AstPort output : outputs) {
if (name.equals(output.getName())) {
error("Action " + name + " has the same name as an output port",
eINSTANCE.getAstAction_Tag());
}
}
}
@Check(CheckType.NORMAL)
public void checkAstAction(AstAction action) {
checkActionTag(action);
checkActionInputs(action.getInputs());
checkActionOutputs(action.getOutputs());
checkInnerVarDecls(action, eINSTANCE.getGuard_Expressions());
checkInnerVarDecls(action, eINSTANCE.getAstAction_Statements());
}
@Check(CheckType.NORMAL)
public void checkAstActor(AstActor actor) {
// build action list
CalActionList actionList = new CalActionList();
actionList.addActions(actor.getActions());
// check FSM and priorities
ScheduleFsm schedule = actor.getScheduleFsm();
if (schedule != null) {
Set<AstAction> actionSet = new HashSet<AstAction>(
actor.getActions());
checkFsm(actionList, schedule, actionSet);
// shows warnings for tagged actions not referenced in the FSM
// not in the warning validator because we need checkFsm
for (AstAction action : actionSet) {
AstTag tag = action.getTag();
if (tag != null) {
warning("Action " + getName(tag) + " is not referenced "
+ "in the FSM", action,
CalPackage.eINSTANCE.getAstAction_Tag(), -1);
}
}
}
RegExp scheduleRegExp = actor.getScheduleRegExp();
if (scheduleRegExp != null && !actor.getPriorities().isEmpty()) {
error("Regexp scheduler with priorities.", actor,
eINSTANCE.getAstActor_ScheduleRegExp(), -1);
}
// check priorities
checkPriorities(actor, actionList);
}
@Check(CheckType.FAST)
public void checkAstEntity(AstEntity entity) {
checkEntityPackage(entity);
checkEntityName(entity);
}
/**
* Checks the name of the given entity.
*
* @param entity
* the entity
*/
private void checkEntityName(AstEntity entity) {
// check entity name matches file name
String path = entity.eResource().getURI().path();
String expectedName = new Path(path).removeFileExtension()
.lastSegment();
String entityName = entity.getName();
if (!expectedName.equals(entityName)) {
error("The qualified name " + entityName
+ " does not match the expected name " + expectedName,
entity, eINSTANCE.getAstEntity_Name(),
CalDiagnostic.ERROR_NAME, entityName, expectedName);
}
}
/**
* Checks the package of the given entity.
*
* @param entity
* the entity
*/
private void checkEntityPackage(AstEntity entity) {
// get platform path /project/folder/.../file
String platformPath = entity.eResource().getURI()
.toPlatformString(true);
if (platformPath == null) {
return;
}
IWorkspace workspace;
try {
// get the file (we know it's a file)
workspace = ResourcesPlugin.getWorkspace();
} catch (IllegalStateException e) {
// This validation step is executed without a workspace open. This
// is normal if the validation is performed from JUnit tests in full
// headless environment (i.e. Without opening the second eclipse and
// without any GUI). In that case, we catch the exception and stop
// this method. Doing this, all others validations will be performed
// as expected.
return;
}
IFile file = (IFile) workspace.getRoot().findMember(platformPath);
if (file == null) {
return;
}
// get the associated Java project (if any)
IProject project = file.getProject();
IJavaProject javaProject = JavaCore.create(project);
if (!javaProject.exists()) {
return;
}
// get segments
String[] segments;
String packageName = entity.getPackage();
if (packageName == null) {
packageName = DEFAULT;
segments = new String[0];
} else {
segments = packageName.split("\\.");
}
try {
IPackageFragmentRoot[] roots = javaProject
.getAllPackageFragmentRoots();
for (IPackageFragmentRoot root : roots) {
IPath rootPath = root.getPath();
if (!rootPath.isPrefixOf(file.getFullPath())) {
continue;
}
IPath path = rootPath;
for (String segment : segments) {
path = path.append(segment);
}
IResource res = workspace.getRoot().findMember(path);
if (res == null || !file.getParent().equals(res)) {
String expectedName = getExpectedName(rootPath,
file.getFullPath());
String code;
if (packageName == DEFAULT) {
code = CalDiagnostic.ERROR_MISSING_PACKAGE;
} else if (expectedName == DEFAULT) {
code = CalDiagnostic.ERROR_EXTRANEOUS_PACKAGE;
} else {
code = CalDiagnostic.ERROR_PACKAGE;
}
error("The package " + packageName
+ " does not match the expected package "
+ expectedName, entity,
eINSTANCE.getAstEntity_Package(), code,
packageName, expectedName);
}
}
} catch (JavaModelException e) {
e.printStackTrace();
}
}
@Check(CheckType.NORMAL)
public void checkExpressionCall(ExpressionCall call) {
Function function = call.getFunction();
String name = function.getName();
EObject rootCter = EcoreUtil.getRootContainer(call);
EObject rootCterFunction = EcoreUtil.getRootContainer(function);
if (function.eContainer() instanceof AstActor
&& rootCter != rootCterFunction) {
// calling an actor's function from another actor/unit
error("function " + name
+ " cannot be called from another actor/unit", call,
eINSTANCE.getExpressionCall_Function(), -1);
}
}
/**
* Display an error if the given name is already used as variable name in
* the given actor. The given source and feature are used to provide an
* error on the right element in the editor
*
* @param entity
* @param name
* @param source
* @param feature
*/
private void checkDuplicatesLitterals(final AstActor actor,
final String name, final EObject source,
final EStructuralFeature feature) {
for (final Variable var : actor.getStateVariables()) {
if (var.getName().equals(name) && var != source) {
error("A state variable is already declared with the name "
+ name, source, feature);
return;
}
}
for (final Variable var : actor.getParameters()) {
if (var.getName().equals(name) && var != source) {
error("A parameter is already declared with the name " + name,
source, feature);
return;
}
}
for (final AstProcedure proc : actor.getProcedures()) {
if (proc.getName().equals(name) && proc != source) {
error("A procedure is already declared with the name " + name,
source, feature);
return;
}
}
for (final Function func : actor.getFunctions()) {
if (func.getName().equals(name) && func != source) {
error("A function is already declared with the name " + name,
source, feature);
return;
}
}
return;
}
/**
* Display an error if the given name is already used as variable name in
* the given unit. The given source and feature are used to provide an error
* on the right element in the editor
*
* @param entity
* @param name
* @param source
* @param feature
*/
private void checkDuplicatesLitterals(final AstUnit unit, final String name,
final EObject source, final EStructuralFeature feature) {
for (final Variable var : unit.getVariables()) {
if (var.getName().equals(name) && var != source) {
error("A variable is already declared with the name " + name,
source, feature);
return;
}
}
for (final AstProcedure proc : unit.getProcedures()) {
if (proc.getName().equals(name) && proc != source) {
error("A procedure is already declared with the name " + name,
source, feature);
return;
}
}
for (final Function func : unit.getFunctions()) {
if (func.getName().equals(name) && func != source) {
error("A function is already declared with the name " + name,
source, feature);
return;
}
}
return;
}
/**
* Checks the given FSM using the given action list. This check is not
* annotated because we need to build the action list, which is also useful
* for checking the priorities, and we do not want to build that twice.
*
* @param actionList
* the action list of the actor
* @param schedule
* the FSM of the actor
* @param actionsSet
* on input the set of all actions; on output the set of actions
* that are not referenced by the FSM
*/
private void checkFsm(CalActionList actionList, ScheduleFsm schedule,
Set<AstAction> actionsSet) {
// we use a map because the transitions departing from a given state can
// be scattered throughout the schedule
Map<AstState, List<AstAction>> stateActionMap = new HashMap<AstState, List<AstAction>>();
for (AstTransition transition : schedule.getContents().getTransitions()) {
AstTag tag = transition.getTag();
if (tag != null) {
List<AstAction> actions = actionList.getTaggedActions(tag
.getIdentifiers());
if (actions == null || actions.isEmpty()) {
error("tag " + getName(tag)
+ " does not refer to any action", transition,
eINSTANCE.getAstTransition_Tag(), -1);
} else {
AstState source = transition.getSource();
List<AstAction> stateActions = stateActionMap.get(source);
if (stateActions == null) {
stateActions = new ArrayList<AstAction>(1);
stateActionMap.put(source, stateActions);
}
for (AstAction action : actions) {
if (stateActions.contains(action)) {
error(source.getName()
+ " has more than one transition associated with "
+ getName(action), transition,
eINSTANCE.getAstTransition_Tag(), -1);
} else {
stateActions.add(action);
actionsSet.remove(action);
}
}
}
}
}
}
@Check(CheckType.NORMAL)
public void checkFunction(Function function) {
checkInnerVarDecls(function, eINSTANCE.getFunction_Expression());
final EObject parent = function.eContainer();
if (parent instanceof AstActor) {
checkDuplicatesLitterals((AstActor) parent, function.getName(),
function, eINSTANCE.getFunction_Name());
} else if (parent instanceof AstUnit) {
checkDuplicatesLitterals((AstUnit) parent, function.getName(),
function, eINSTANCE.getFunction_Name());
}
}
@Check(CheckType.NORMAL)
public void checkGenerator(Generator generator) {
int lower = Evaluator.getIntValue(generator.getLower());
int higher = Evaluator.getIntValue(generator.getHigher());
if (higher < lower) {
error("higher bound must be greater than lower bound", generator,
eINSTANCE.getGenerator_Higher(), -1);
return;
}
Variable variable = EcoreUtil2.getContainerOfType(generator,
Variable.class);
if (variable != null) {
EStructuralFeature feature = variable.eContainingFeature();
if (feature == CalPackage.eINSTANCE.getAstActor_StateVariables()
|| feature == CalPackage.eINSTANCE.getAstUnit_Variables()) {
if (higher - lower > 65536) {
error("List generated is too large, please use an initialize action",
generator, eINSTANCE.getGenerator_Variable(), -1);
}
}
}
}
private void checkInnerVarDecl(Set<String> names, EObject eObject) {
for (Variable variable : EcoreHelper
.getObjects(eObject, Variable.class)) {
String name = variable.getName();
if (names.contains(name)) {
error("Variable " + name
+ " shadows an existing variable with the same name",
variable, CalPackage.eINSTANCE.getVariable_Name(), -1);
}
}
}
private void checkInnerVarDecls(EObject eObject, EStructuralFeature feature) {
Set<String> names = new HashSet<String>();
Set<Variable> variables = new HashSet<Variable>();
for (EObject obj : eObject.eContents()) {
if (obj instanceof Variable) {
Variable variable = (Variable) obj;
variables.add(variable);
names.add(variable.getName());
}
}
for (Variable variable : variables) {
AstExpression value = variable.getValue();
if (value != null) {
checkInnerVarDecl(names, value);
}
}
Object object = eObject.eGet(feature);
if (feature.isMany()) {
for (Object obj : (Iterable<?>) object) {
if (obj instanceof EObject) {
checkInnerVarDecl(names, (EObject) obj);
}
}
} else {
if (object instanceof EObject) {
checkInnerVarDecl(names, (EObject) object);
}
}
}
/**
* Checks the priorities of the given actor using the given action list.
* This check is not annotated because we need to build the action list,
* which is also useful for checking the FSM, and we do not want to build
* that twice.
*
* @param actor
* the actor
* @param actionList
* the action list of the actor
*/
private void checkPriorities(AstActor actor, CalActionList actionList) {
List<Priority> priorities = actor.getPriorities();
DirectedGraph<AstAction, DefaultEdge> graph = new DefaultDirectedGraph<AstAction, DefaultEdge>(
DefaultEdge.class);
// add one vertex per tagged action
for (AstAction action : actionList) {
AstTag tag = action.getTag();
if (tag != null) {
graph.addVertex(action);
}
}
for (Priority priority : priorities) {
for (Inequality inequality : priority.getInequalities()) {
// the grammar requires there be at least two tags
Iterator<AstTag> it = inequality.getTags().iterator();
AstTag previousTag = it.next();
List<AstAction> sources = actionList
.getTaggedActions(previousTag.getIdentifiers());
int index = 0;
if (sources == null || sources.isEmpty()) {
error("tag " + getName(previousTag)
+ " does not refer to any action", inequality,
eINSTANCE.getInequality_Tags(), index);
}
while (it.hasNext()) {
AstTag tag = it.next();
index++;
sources = actionList.getTaggedActions(previousTag
.getIdentifiers());
List<AstAction> targets = actionList.getTaggedActions(tag
.getIdentifiers());
if (targets == null || targets.isEmpty()) {
error("tag " + getName(tag)
+ " does not refer to any action", inequality,
eINSTANCE.getInequality_Tags(), index);
}
if (sources != null && targets != null) {
for (AstAction source : sources) {
for (AstAction target : targets) {
graph.addEdge(source, target);
}
}
}
previousTag = tag;
}
}
}
CycleDetector<AstAction, DefaultEdge> cycleDetector = new CycleDetector<AstAction, DefaultEdge>(
graph);
Set<AstAction> cycle = cycleDetector.findCycles();
if (!cycle.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (AstAction action : cycle) {
builder.append(getName(action.getTag()));
builder.append(", ");
}
Iterator<AstAction> it = cycle.iterator();
builder.append(getName(it.next().getTag()));
error("priorities of actor "
+ ((AstEntity) actor.eContainer()).getName()
+ " contain a cycle: " + builder.toString(), actor,
eINSTANCE.getAstActor_Priorities(), -1);
}
}
@Check(CheckType.NORMAL)
public void checkProcedure(AstProcedure procedure) {
checkInnerVarDecls(procedure, eINSTANCE.getAstProcedure_Statements());
final EObject parent = procedure.eContainer();
if (parent instanceof AstActor) {
checkDuplicatesLitterals((AstActor) parent, procedure.getName(),
procedure, eINSTANCE.getAstProcedure_Name());
} else if (parent instanceof AstUnit) {
checkDuplicatesLitterals((AstUnit) parent, procedure.getName(),
procedure, eINSTANCE.getAstProcedure_Name());
}
}
@Check(CheckType.NORMAL)
public void checkStatementCall(StatementCall call) {
AstProcedure procedure = call.getProcedure();
String name = procedure.getName();
EObject rootCter = EcoreUtil.getRootContainer(call);
EObject rootCterProcedure = EcoreUtil.getRootContainer(procedure);
if (procedure.eContainer() instanceof AstActor
&& rootCter != rootCterProcedure) {
// calling an actor's procedure from another actor/unit
error("procedure " + name
+ " cannot be called from another actor/unit", call,
eINSTANCE.getStatementCall_Procedure(), -1);
}
}
@Check(CheckType.NORMAL)
public void checkVariable(Variable variable) {
if (variable.isConstant() && variable.getValue() == null) {
String name = variable.getName();
error("The constant " + name + " must have a value", variable,
eINSTANCE.getVariable_Name(), -1);
}
}
@Check(CheckType.NORMAL)
public void checkVariableReference(VariableReference ref) {
Variable variable = ref.getVariable();
String name = variable.getName();
EObject rootCter = EcoreUtil.getRootContainer(ref);
EObject rootCter2 = EcoreUtil.getRootContainer(variable);
if (variable.eContainer() instanceof AstActor && rootCter != rootCter2) {
// referencing an actor's variable from another actor/unit
error("variable " + name + " can only be referenced "
+ "within the actor in which it is declared", ref,
eINSTANCE.getVariableReference_Variable(), -1);
}
Variable target = EcoreUtil2.getContainerOfType(ref, Variable.class);
if (target != null) {
EStructuralFeature feature = target.eContainingFeature();
if (feature == CalPackage.eINSTANCE.getAstActor_StateVariables()
|| feature == CalPackage.eINSTANCE.getAstUnit_Variables()) {
if (variable.getValue() == null
&& !(variable.eContainer() instanceof Generator)) {
error("Cannot use the variable " + name + " in this "
+ "context because it has no initial value", ref,
eINSTANCE.getVariableReference_Variable(), -1);
}
}
}
}
private String getExpectedName(IPath rootPath, IPath filePath) {
int count = rootPath.matchingFirstSegments(filePath);
String[] segments = filePath.removeFirstSegments(count)
.removeLastSegments(1).segments();
if (segments.length == 0) {
return DEFAULT;
} else {
StringBuilder builder = new StringBuilder();
builder.append(segments[0]);
for (int i = 1; i < segments.length; i++) {
builder.append('.');
builder.append(segments[i]);
}
return builder.toString();
}
}
private String getName(AstAction action) {
AstTag tag = action.getTag();
if (tag == null) {
return "(untagged)";
} else {
return getName(tag);
}
}
private String getName(AstTag tag) {
return OrccUtil.toString(tag.getIdentifiers(), ".");
}
}