/* * Copyright 2003-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.generator.impl; import jetbrains.mps.generator.GenerationCanceledException; import jetbrains.mps.generator.GenerationOptions; import jetbrains.mps.generator.GenerationSessionContext; import jetbrains.mps.generator.GenerationTrace; import jetbrains.mps.generator.GenerationTracerUtil; import jetbrains.mps.generator.IGeneratorLogger; import jetbrains.mps.generator.ModelGenerationPlan.Checkpoint; import jetbrains.mps.generator.impl.CloneUtil.Factory; import jetbrains.mps.generator.impl.CloneUtil.RegularSModelFactory; import jetbrains.mps.generator.impl.FastRuleFinder.BlockedReductionsData; import jetbrains.mps.generator.impl.RoleValidation.RoleValidator; import jetbrains.mps.generator.impl.RoleValidation.Status; import jetbrains.mps.generator.impl.dependencies.DependenciesBuilder; import jetbrains.mps.generator.impl.dependencies.DependenciesReadListener; import jetbrains.mps.generator.impl.dependencies.IncrementalDependenciesBuilder; import jetbrains.mps.generator.impl.dependencies.RootDependenciesBuilder; import jetbrains.mps.generator.impl.plan.CheckpointState; import jetbrains.mps.generator.impl.plan.CrossModelEnvironment; import jetbrains.mps.generator.impl.plan.ModelCheckpoints; import jetbrains.mps.generator.impl.query.GeneratorQueryProvider; import jetbrains.mps.generator.impl.reference.DynamicReferenceUpdate; import jetbrains.mps.generator.impl.reference.PostponedReference; import jetbrains.mps.generator.impl.reference.PostponedReferenceUpdate; import jetbrains.mps.generator.impl.reference.ReferenceInfo_CopiedInputNode; import jetbrains.mps.generator.impl.template.DeltaBuilder; import jetbrains.mps.generator.impl.template.QueryExecutionContextWithDependencyRecording; import jetbrains.mps.generator.impl.template.QueryExecutionContextWithTracing; import jetbrains.mps.generator.runtime.GenerationException; import jetbrains.mps.generator.runtime.NodePostProcessor; import jetbrains.mps.generator.runtime.TemplateContext; import jetbrains.mps.generator.runtime.TemplateCreateRootRule; import jetbrains.mps.generator.runtime.TemplateDropAttributeRule; import jetbrains.mps.generator.runtime.TemplateDropRootRule; import jetbrains.mps.generator.runtime.TemplateExecutionEnvironment; import jetbrains.mps.generator.runtime.TemplateMappingScript; import jetbrains.mps.generator.runtime.TemplateRootMappingRule; import jetbrains.mps.generator.runtime.TemplateRule; import jetbrains.mps.generator.runtime.TemplateSwitchMapping; import jetbrains.mps.generator.template.DefaultQueryExecutionContext; import jetbrains.mps.generator.template.QueryExecutionContext; import jetbrains.mps.smodel.CopyUtil; import jetbrains.mps.smodel.DynamicReference; import jetbrains.mps.smodel.FastNodeFinderManager; import jetbrains.mps.smodel.ModelDependencyUpdate; import jetbrains.mps.smodel.StaticReference; import jetbrains.mps.textgen.trace.TracingUtil; import jetbrains.mps.util.SNodeOperations; import jetbrains.mps.util.performance.IPerformanceTracer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeAccessUtil; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.model.SNodeUtil; import org.jetbrains.mps.openapi.model.SReference; import org.jetbrains.mps.openapi.util.ProgressMonitor; import org.jetbrains.mps.util.DescendantsTreeIterator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Created by: Sergey Dmitriev * Date: Jan 23, 2007 * <p/> * Created once per micro-step. */ public class TemplateGenerator extends AbstractTemplateGenerator { private boolean myChanged = false; private final GenPlanActiveStep myPlanStep; private final DelayedChanges myDelayedChanges; private final Map<SNode, SNode> myNewToOldRoot = new HashMap<SNode, SNode>(); /** * Input nodes coming from a model other than input model (or no model at all), e.g. if * input node query follows a reference from an input model to some outer model. * We track these nodes, including children, to facilitate reference resolution (i.e. if there's * a reference in an input model pointing somewhere to subtree of a foreign node, we redirect * that reference to the copied counterpart). Generally, this approach might not be everyone's * desire, but it's the way it was so far. */ private final Set<SNode> myAdditionalInputNodes = new HashSet<SNode>(); protected final List<SNode> myOutputRoots; private final QueryExecutionContext myExecutionContext; private Map<DependenciesReadListener, QueryExecutionContext> myExecutionContextMap; private final boolean myIsStrict; private boolean myAreMappingsReady = false; /* cached session data */ private BlockedReductionsData myReductionData; private final DependenciesBuilder myDependenciesBuilder; private DeltaBuilder myDeltaBuilder; private boolean myInplaceModelChange = false; // indicates transformation was in-place (even after deltaBuilder was disposed). cries for better approach private WeavingProcessor myWeavingProcessor; private TemplateProcessor myTemplateProcessor; private final PostponedReferenceUpdate myPostponedRefs; private final DynamicReferenceUpdate myDynamicRefs; private final GenerationTrace myNewTrace; private final TransitionTrace myTransitionTrace; static final class StepArguments { public final DependenciesBuilder dependenciesBuilder; public final GenPlanActiveStep planStep; public final GenerationTrace genTrace; public final GeneratorMappings mappingLabels; public final TransitionTrace transitionTrace; public final GeneratorQueryProvider.Source querySource; public StepArguments(DependenciesBuilder dependenciesBuilder, GeneratorQueryProvider.Source gqps) { // FIXME refactor TMC.isApplicable call not to take ITemplateGenerator, or use dedicated ITemplateGenerator implementation // that doesn't need anything we could not provide here anyway. // DependenciesBuilder is in use from ITemplateGenerator#isDirty() // Alternative is to initialize StepArguments once prior to isApplicable check, which we can't do now as isApplicable gives us GenPlanActiveStep // If refactored (e.g. GPAS made TG's argument or use of dedicated fake GPAS for isApplicable), could drop this cons altogether. // I.e. if anyone would like to query e.g. mapping label from isApplicable(), it's a chance not to fail with NPE (and to let the error go unnoticed) this(null, dependenciesBuilder, null, null, null, gqps); } public StepArguments(GenPlanActiveStep planStep, DependenciesBuilder dependenciesBuilder, GenerationTrace genTrace, GeneratorMappings mapLabels, TransitionTrace transitionTrace, GeneratorQueryProvider.Source gqps) { this.dependenciesBuilder = dependenciesBuilder; this.planStep = planStep; this.genTrace = genTrace; this.mappingLabels = mapLabels; this.transitionTrace = transitionTrace; this.querySource = gqps; } } public TemplateGenerator(GenerationSessionContext operationContext, SModel inputModel, SModel outputModel, StepArguments stepArgs) { super(operationContext, inputModel, outputModel, stepArgs.mappingLabels, stepArgs.querySource); myPlanStep = stepArgs.planStep; GenerationOptions options = operationContext.getGenerationOptions(); myIsStrict = options.isStrictMode(); myDelayedChanges = new DelayedChanges(); myDependenciesBuilder = stepArgs.dependenciesBuilder; myOutputRoots = new ArrayList<SNode>(); DefaultQueryExecutionContext ctx = new DefaultQueryExecutionContext(this); myExecutionContext = options.getTracingMode() >= GenerationOptions.TRACE_LANGS ? new QueryExecutionContextWithTracing(ctx, operationContext.getPerformanceTracer()) : ctx; myPostponedRefs = new PostponedReferenceUpdate(); myDynamicRefs = new DynamicReferenceUpdate(this); myNewTrace = stepArgs.genTrace; myTransitionTrace = stepArgs.transitionTrace; } public boolean apply(@NotNull ProgressMonitor progressMonitor, boolean isPrimary) throws GenerationFailureException, GenerationCanceledException { myProgressMonitor = progressMonitor; checkMonitorCanceled(); final IPerformanceTracer ttrace = getGeneratorSessionContext().getPerformanceTracer(); myAreMappingsReady = false; // prepare weaving ttrace.push("weavings", false); myWeavingProcessor = new WeavingProcessor(this); myWeavingProcessor.prepareWeavingRules(getInputModel()); ttrace.pop(); myTemplateProcessor = new TemplateProcessor(this); ttrace.push("reductions", false); applyReductions(isPrimary); ttrace.pop(); myInplaceModelChange = myDeltaBuilder != null; myAreMappingsReady = true; myChanged |= myDependenciesBuilder.isStepRequired(); // TODO optimize: if step is required, it should be the last step if (myDeltaBuilder == null) { // publish roots for (SNode outputRoot : myOutputRoots) { myOutputModel.addRootNode(outputRoot); } // reload "required" roots from cache ttrace.push("reloading roots from cache", false); myDependenciesBuilder.reloadRequired(getMappings()); ttrace.pop(); } // XXX if in-place change, every required root has been reloaded on previous step, imo if (myWeavingProcessor.hasWeavingRulesToApply()) { checkMonitorCanceled(); ttrace.push("weavings", false); myWeavingProcessor.apply(); myWeavingProcessor = null; ttrace.pop(); } if (!myDelayedChanges.isEmpty()) { checkMonitorCanceled(); // execute mapper in all $MAP_SRC$/$MAP_SRCL$ ttrace.push("delayed mappings", false); myDelayedChanges.doAllChanges(this); ttrace.pop(); } ////////////////////////////////////////////////////////////// // replace references with PostponedReference to respect model changes up to this point if (myDeltaBuilder != null && myDeltaBuilder.hasChanges()) { ttrace.push("apply delta changes", false); // myDeltaBuilder.dump(); myDeltaBuilder.prepareReferences(getInputModel(), this); ttrace.pop(); } // resolve PostponedReferences, but do not replace them in the model yet if (!myPostponedRefs.isEmpty()) { // new unresolved references could appear after applying reduction rules (all delayed changes should be done before this, like replacing children) ttrace.push("restoring references", false); myPostponedRefs.prepare(); ttrace.pop(); } // apply structural change delta onto input model if (myDeltaBuilder != null && myDeltaBuilder.hasChanges()) { ttrace.push("apply delta changes", false); myDeltaBuilder.applyInplace(getInputModel()); ttrace.pop(); } if (myChanged) { // Unless we manage to update set of used languages along with changes being generated, // we shall maintain set of used languages prior to any attempt to resolve references, as they might // be using model scopes and TypeSystem, and latter is quite picky about imports. // we don't use any repository as it's ok to reference language's accessory model explicitly for a transient model new ModelDependencyUpdate(getOutputModel()).updateUsedLanguages().updateImportedModels(null); } // replace reference placeholders (PostponedReference) with resolved // replace DynamicReference with StaticReference, if needed if (!myPostponedRefs.isEmpty() || !myDynamicRefs.isEmpty()) { ttrace.push("restoring references", false); myPostponedRefs.replace(); myDynamicRefs.replace(); ttrace.pop(); } myOutputRoots.clear(); myDeltaBuilder = null; /////////////////////////////////////////////////////////////^^^ if (myChanged || isPrimary) { // advance blocked reduction data getBlockedReductionsData().advanceStep(); checkMonitorCanceled(); } return myChanged; } public void executeScript(TemplateMappingScript script) throws GenerationFailureException { getDefaultExecutionContext(null).executeScript(script, myInputModel); } protected void applyReductions(boolean isPrimary) throws GenerationCanceledException, GenerationFailureException { if (isInplaceChangeEnabled()) { if (myWeavingProcessor.hasWeavingRulesToApply()) { getLogger().info("Could have had delta builder here, but can't due to active weavings"); } else { getLogger().info("Active in-place model transformation"); myDeltaBuilder = createDeltaBuilder(); } } final IPerformanceTracer ttrace = getGeneratorSessionContext().getPerformanceTracer(); // create all roots if (isPrimary) { ttrace.push("create roots", false); final QueryExecutionContext executionContext = getExecutionContext(null); if (executionContext != null) { for (TemplateCreateRootRule rule : getRuleManager().getCreateRootRules()) { TemplateExecutionEnvironment environment = new TemplateExecutionEnvironmentImpl(myTemplateProcessor, executionContext, new ReductionTrack(getBlockedReductionsData())); applyCreateRoot(rule, environment); } checkMonitorCanceled(); } ttrace.pop(); } // root mapping rules ttrace.push("root mappings", false); ArrayList<SNode> rootsConsumed = new ArrayList<SNode>(); for (TemplateRootMappingRule rule : getRuleManager().getRoot_MappingRules()) { checkMonitorCanceled(); applyRootRule(rule, rootsConsumed); } ttrace.pop(); // copy roots getGeneratorSessionContext().clearCopiedRootsSet(); for (SNode rootToCopy : myInputModel.getRootNodes()) { if (rootsConsumed.contains(rootToCopy)) { continue; } QueryExecutionContext context = getExecutionContext(rootToCopy); if (context != null) { TemplateExecutionEnvironmentImpl rootenv = new TemplateExecutionEnvironmentImpl(myTemplateProcessor, context, new ReductionTrack(getBlockedReductionsData())); copyRootInputNode(rootToCopy, rootenv); checkMonitorCanceled(); } } } protected void applyCreateRoot(TemplateCreateRootRule rule, TemplateExecutionEnvironment environment) throws GenerationFailureException, GenerationCanceledException { if (!environment.getQueryExecutor().isApplicable(rule, new DefaultTemplateContext(environment, null, null))) { return; } try { Collection<SNode> outputNodes = environment.getQueryExecutor().applyRule(rule, environment); if (outputNodes == null) { return; } environment.getTrace().trace(null, GenerationTracerUtil.translateOutput(outputNodes), rule.getRuleNode()); for (SNode outputNode : outputNodes) { registerRoot(new GeneratedRootDescriptor(outputNode, rule.getRuleNode())); setChanged(); } } catch (DismissTopMappingRuleException ex) { // it's ok, just continue reportDismissRuleException(ex, rule); } catch (TemplateProcessingFailureException ex) { getLogger().error(rule.getRuleNode(), String.format("couldn't create root node: %s", ex.getMessage()), ex.asProblemDescription()); } catch (GenerationCanceledException ex) { throw ex; } catch (GenerationFailureException ex) { throw ex; } catch (GenerationException e) { getLogger().error(rule.getRuleNode(), "internal error: " + e.toString()); } } private void applyRootRule(TemplateRootMappingRule rule, List<SNode> rootsConsumed) throws GenerationFailureException, GenerationCanceledException { Iterable<SNode> inputNodes = FastNodeFinderManager.get(myInputModel).getNodes(rule.getApplicableConcept(), rule.applyToInheritors()); for (SNode inputNode : inputNodes) { // do not apply root mapping if root node has been copied from input model on previous micro-step // because some roots can be already mapped and copied as well (if some rule has 'keep root' = true) if (getGeneratorSessionContext().isCopiedRoot(inputNode)) { continue; } final QueryExecutionContext executionContext = getExecutionContext(inputNode); if (executionContext != null) { TemplateExecutionEnvironmentImpl environment = new TemplateExecutionEnvironmentImpl(myTemplateProcessor, executionContext, new ReductionTrack(getBlockedReductionsData())); final DefaultTemplateContext templateContext = new DefaultTemplateContext(environment, inputNode, null); if (!executionContext.isApplicable(rule, templateContext)) { continue; } boolean copyRootOnFailure = false; if (inputNode.getModel() != null && inputNode.getParent() == null && !rule.keepSourceRoot()) { rootsConsumed.add(inputNode); copyRootOnFailure = true; } createRootNodeByRule(rule, inputNode, environment, copyRootOnFailure); } } } protected void createRootNodeByRule(TemplateRootMappingRule rule, SNode inputNode, TemplateExecutionEnvironmentImpl env, boolean copyRootOnFailure) throws GenerationCanceledException, GenerationFailureException { try { final DefaultTemplateContext templateContext = new DefaultTemplateContext(env, inputNode, null); Collection<SNode> outputNodes = env.getQueryExecutor().applyRule(rule, templateContext); if (outputNodes == null) { return; } copyNodeAttributes(templateContext, outputNodes, env); recordTransformInputTrace(inputNode, outputNodes); env.getTrace().trace(inputNode.getNodeId(), GenerationTracerUtil.translateOutput(outputNodes), rule.getRuleNode()); final boolean inputIsRoot = inputNode.getParent() == null; // if we picked a non-root node to produce a root, it would get processed later as part of copyRootInputNode(), hence inputPersists == true. final boolean inputPersists = !inputIsRoot || rule.keepSourceRoot(); for (SNode outputNode : outputNodes) { registerRoot(new GeneratedRootDescriptor(outputNode, inputNode, inputPersists, rule.getRuleNode())); setChanged(); if (!inputPersists) { // output node should be accessible via 'findCopiedNode' // however, if the node stays in the model, let other rules/copy facility register it as appropriate (see MPS-23159), // it's unlikely anyone referencing original node would need to restore reference to a newly introduced root, when there's // reduction/copy alternative. addCopiedOutputNodeForInputNode(inputNode, outputNode); } // we copy user objects in reduction rules, root mapping rules are no different // in addition, this copies TracingUtil.ORIGINAL_INPUT_NODE, so that outputNodes // are marked as originating at inputNode's origin CopyUtil.copyUserObjects(inputNode, outputNode); } } catch (DismissTopMappingRuleException e) { // it's ok, just continue reportDismissRuleException(e, rule); if (copyRootOnFailure && inputNode.getModel() != null && inputNode.getParent() == null) { final FullCopyFacility copyFacility = new FullCopyFacility(env); copyFacility.copyRootInputNode(inputNode); if (copyFacility.hasChanges()) { setChanged(); } } } catch (TemplateProcessingFailureException ex) { getLogger().error(rule.getRuleNode(), String.format("couldn't create root node: %s", ex.getMessage()), ex.asProblemDescription()); } catch (GenerationException e) { if (e instanceof GenerationCanceledException) throw (GenerationCanceledException) e; if (e instanceof GenerationFailureException) throw (GenerationFailureException) e; getLogger().error(rule.getRuleNode(), "internal error: " + e.toString(), GeneratorUtil.describe(inputNode, "input node")); } } protected void copyRootInputNode(@NotNull SNode inputRootNode, @NotNull TemplateExecutionEnvironmentImpl env) throws GenerationFailureException, GenerationCanceledException { NodeCopyFacility copyProcessor; if (myDeltaBuilder == null) { copyProcessor = new FullCopyFacility(env); } else { copyProcessor = new PartialCopyFacility(env, myDeltaBuilder); } // check if can drop if (copyProcessor.checkDropRules(inputRootNode, getRuleManager().getDropRootRules(inputRootNode))) { setChanged(); return; } copyProcessor.copyRootInputNode(inputRootNode); if(copyProcessor.hasChanges()) { setChanged(); } } @Override public boolean isDirty(SNode node) { RootDependenciesBuilder builder = myDependenciesBuilder.getRootBuilder(node); if (builder != null && builder.isUnchanged()) { return false; } return true; } @Override public SModel getOutputModel() { if (myDeltaBuilder != null || myInplaceModelChange) { return getInputModel(); } return super.getOutputModel(); } /* * Unsynchronized */ @Nullable protected QueryExecutionContext getExecutionContext(SNode inputNode) { RootDependenciesBuilder builder = myDependenciesBuilder.getRootBuilder(inputNode); if (builder != null) { if (builder.isUnchanged()) { if (myDeltaBuilder != null && inputNode != null) { // Unchanged roots shall not participate in further generation steps, hence // we need to tell DeltaBuilder to forget it. SNode inputRoot = inputNode.getContainingRoot(); myDeltaBuilder.deleteInputRoot(inputRoot); } return null; } QueryExecutionContext value; if (myExecutionContextMap == null) { myExecutionContextMap = new HashMap<DependenciesReadListener, QueryExecutionContext>(); value = null; } else { value = myExecutionContextMap.get(builder); } if (value == null) { value = new QueryExecutionContextWithDependencyRecording(myExecutionContext, builder); myExecutionContextMap.put(builder, value); } return value; } return getDefaultExecutionContext(inputNode); } protected QueryExecutionContext getDefaultExecutionContext(SNode inputNode) { return myExecutionContext; } protected DeltaBuilder createDeltaBuilder() { return DeltaBuilder.newSingleThreadDeltaBuilder(); } @Override public SNode findOutputNodeById(SNodeId nodeId) { if (myDeltaBuilder != null) { return myDeltaBuilder.findOutputNodeById(nodeId); } return super.findOutputNodeById(nodeId); } /** * For a cross-mode reference, we expect inputNode to point either at original model (or external non-transient model), or one of checkpoint models. * There's no evidence one could get anything but that for a node referenced from another model during generation (i.e. no chances for inputNode to point * to intermediate transient model). */ @Override public SNode findOutputNodeByInputNodeAndMappingName(SNode inputNode, String mappingName) { SNode existing = super.findOutputNodeByInputNodeAndMappingName(inputNode, mappingName); if (existing != null) { // XXX apparently, there are models that use input nodes from a model other than that being transformed // e.g. in build.workflow, bl.closures, bl.collections. Shall revert the change and try to rebuild // to find out particular uses, as it's potential error (i.e. MLs between different models). // For now, though, just check if there's mapping, and use it. return existing; } if (inputNode == null) { // there are models e.g. bl.plugin, debugger.api.ui.icons, d.java.runtime.ui that pass null as inputNode return null; } SModel inputNodeModel = inputNode.getModel(); if (inputNodeModel == getInputModel()) { // return super.findOutputNodeByInputNodeAndMappingName(inputNode, mappingName); return null; // code down there deals with xModel references only } if (inputNodeModel == null) { return null; } CheckpointState cp = findMatchingStateFor(inputNodeModel); if (cp == null) { return null; } // FIXME we might want to ensure inputNode comes from the myPlanStep.getLastCheckpoint() checkpoint. // However, unless we keep TransitionState along with the // checkpointState, I see no way to confirm inputNode comes from lastPoint (the moment we've built CheckpointState, we dispose // TransitionTrace and could not find out what are origins of the node in checkpoint model. Technically, it's not true now, // as there are user objects in the checkpoint model, however, this might get changed, so I can't rely on that, unless there's // a conscious decision to keep transition trace and to ensure validity of input nodes and their originating CP). Collection<SNode> output = cp.getOutput(mappingName, inputNode); if (output.size() == 1) { return output.iterator().next(); } return null; } private CheckpointState findMatchingStateFor(/*non-null*/SModel model) { CrossModelEnvironment env = getGeneratorSessionContext().getCrossModelEnvironment(); // last and next are not necessarily in immediately adjacent generation steps, i.e. cpLast, transfStep1, transfStep2, activeTransformStep, transfStep3, cpNext Checkpoint lastPoint = myPlanStep.getLastCheckpoint(); // XXX alternatively, we can extract active checkpoint from TransitionTrace. Do we need both ways to get the value I don't care to use? // Isn't it too complicated? Checkpoint targetPoint = myPlanStep.getNextCheckpoint(); if (targetPoint == null) { // XXX can I do anything in this case? return null; } // Note, myPlanStep.getPlanIdentity() points to the plan in action; while we need that of target model // which could be generated against different plan (although with a shared CP). ModelCheckpoints modelHistory = env.getState(model); if (modelHistory == null) { return null; } return modelHistory.find(targetPoint.getIdentity()); } @Nullable @Override public SNode findOutputNode(SModel inputModel, String mappingName) { if (inputModel == null || inputModel == getInputModel()) { // XXX 1. not sure it's proper delegation, perhaps findOutputNodeByInputNodeAndMappingName(null) shall delegate here. Did that // not to bother with refactoring right now. // 2. inputModel might be some intermediate model between original and current input, shall I consult ModelTransitions or similar // facility to find out if this model is from active transformation or truly external. return super.findOutputNodeByInputNodeAndMappingName(null, mappingName); } CheckpointState cps = findMatchingStateFor(inputModel); if (cps == null) { return null; } List<SNode> rv = cps.getOutputWithoutInput(mappingName); if (rv.size() != 1) { return null; } return rv.get(0); } // in fact, it's reasonable to keep this method in TEEI (in ReductionTrack, actually), to reflect narrowing scope of // generator -> TEEI -> TemplateProcessor. This would take another round of refactoring, though // (first of all, shall update TEEI API) @NotNull List<SNode> copyNodes(@NotNull Iterable<SNode> inputNodes, final TemplateContext outerContext, @NotNull String templateId, TemplateExecutionEnvironmentImpl env) throws GenerationCanceledException, GenerationFailureException { final Iterator<SNode> it = inputNodes.iterator(); if (!it.hasNext()) { return Collections.emptyList(); } ArrayList<SNode> outputNodes = new ArrayList<SNode>(); while(it.hasNext()) { SNode newInputNode = it.next(); trackIfForeign(newInputNode); if (myDeltaBuilder != null) { myDeltaBuilder.enterNestedCopySrc(newInputNode); } try { final String mappingName = outerContext.getInputName(); Collection<SNode> _outputNodes = env.tryToReduce(newInputNode); if (_outputNodes != null) { if (mappingName != null && _outputNodes.size() == 1) { registerMappingLabel(newInputNode, mappingName, _outputNodes.iterator().next()); } outputNodes.addAll(_outputNodes); } else { FullCopyFacility copyFacility = new FullCopyFacility(env, new HashSet<SNode>(getForeignNodes())); SNode copiedNode = copyFacility.copyInputNode(newInputNode); addOutputNodeByInputAndTemplateNode(newInputNode, templateId, copiedNode); if (mappingName != null) { registerMappingLabel(newInputNode, mappingName, copiedNode); } outputNodes.add(copiedNode); } } finally { if (myDeltaBuilder != null) { myDeltaBuilder.leaveNestedCopySrc(newInputNode); } } } return outputNodes; } private Set<SNode> getForeignNodes() { synchronized (myAdditionalInputNodes) { return new HashSet<SNode>(myAdditionalInputNodes); } } private void trackIfForeign(@NotNull SNode inputNode) { SModel model = inputNode.getModel(); if (model != getInputModel() || model == null) { synchronized (myAdditionalInputNodes) { if (!myAdditionalInputNodes.contains(inputNode)) { for (SNode n : SNodeUtil.getDescendants(inputNode, null, true)) { myAdditionalInputNodes.add(n); } } } } } BlockedReductionsData getBlockedReductionsData() { if (myReductionData == null) { myReductionData = BlockedReductionsData.getStepData(getGeneratorSessionContext()); } return myReductionData; } @NotNull /*package*/ GenerationTrace getTrace() { return myNewTrace; } /*package*/ void reportDismissRuleException(@NotNull DismissTopMappingRuleException ex, @NotNull TemplateRule rule) { if (!ex.isLoggingNeeded()) { return; } SNodeReference ruleNode = rule.getRuleNode(); String messageText = String.format("-- rule dismissed: %s", ex.getMessage() == null ? "<no message>" : ex.getMessage()); if (ex.isInfo()) { getLogger().info(ruleNode, messageText); } else if (ex.isWarning()) { getLogger().warning(ruleNode, messageText, GeneratorUtil.describeInput(ex.getTemplateContext()), GeneratorUtil.describeTemplateLocation(ex)); } else { getLogger().error(ruleNode, messageText, GeneratorUtil.describeInput(ex.getTemplateContext()), GeneratorUtil.describeTemplateLocation(ex)); } } DelayedChanges getDelayedChanges() { return myDelayedChanges; } final GenPlanActiveStep getGenerationPlan() { return myPlanStep; } final RuleManager getRuleManager() { return myPlanStep.getRuleManager(); } public TemplateSwitchMapping getSwitch(SNodeReference switch_) { return getRuleManager().getSwitch(switch_); } @Override public boolean areMappingsAvailable() { return myIsStrict ? myAreMappingsReady : true; } @Override public boolean isStrict() { return myIsStrict; } public PostponedReference register(@NotNull PostponedReference ref) { myPostponedRefs.add(ref); return ref; } /** * Let generator know about dynamic references produced during generation. * We might handle {@link jetbrains.mps.smodel.DynamicReference} in a special way as long as there's evading reason to * keep references dynamic during generation (as it only slows down model access due to ongoing scope use. * @param dr DynamicReference instance */ public void registerDynamicReference(@NotNull SReference dr) { myDynamicRefs.add(dr); } void setChanged() { myChanged = true; } protected void registerRoot(GeneratedRootDescriptor rd) { myOutputRoots.add(rd.myOutputRoot); myNewToOldRoot.put(rd.myOutputRoot, rd.myInputNode); myDependenciesBuilder.registerRoot(rd.myOutputRoot, rd.myInputNode); if (rd.myIsCopied) { getGeneratorSessionContext().registerCopiedRoot(rd.myOutputRoot); } if (myDeltaBuilder != null) { if (rd.myIsInputPreserved) { // if a new root comes from root mapping rule with keepRoot == true, pretend it's completely new root // if a new root comes from a non-root node, treat it as brand-new root, too. Input node in this case might // be later processed by the rest of the rules (copied or reduced). // FIXME the whole thing with registerRoot shall be refactored - there's little sense to forget about context // root being added at, and to restore this knowledge inside DeltaBuilder.registerRoot based on two node values only. myDeltaBuilder.registerRoot(null, rd.myOutputRoot); } else { myDeltaBuilder.registerRoot(rd.myInputNode, rd.myOutputRoot); } } } void replacePlaceholderNode(@NotNull NodePostProcessor postProcessor, @NotNull SNode substitute) { SNode placeholder = postProcessor.getOutputAnchor(); SNode parent = placeholder.getParent(); if (parent != null) { // check new child SContainmentLink childRole = placeholder.getContainmentLink(); final Status status = getChildRoleValidator(parent, childRole).validate(substitute); if (status != null) { getLogger().warning(postProcessor.getTemplateNode(), status.getMessage("delayed changes"), status.describe( GeneratorUtil.describeInput(postProcessor.getTemplateContext()), GeneratorUtil.describe(parent, "parent") )); } } if (myDeltaBuilder != null) { // placeholders with active inplace may lack both model and parent (top of MAP-SRC-injected subtree) myDeltaBuilder.replacePlaceholderNode(placeholder, substitute); } else { assert placeholder.getModel() != null || parent != null : "Can't replace node that is not part of another structure (hangs in the air)"; SNodeUtil.replaceWithAnother(placeholder, substitute); } if (parent == null && placeholder.getModel() != null) { myDependenciesBuilder.rootReplaced(placeholder, substitute); } } void copyNodeAttributes(@NotNull TemplateContext ctx, @NotNull Collection<SNode> outputNodes, @NotNull TemplateExecutionEnvironmentImpl env) throws GenerationException { final SNode input = ctx.getInput(); if (input == null) { // context in create root rule might have no input return; } for (SNode attr : input.getChildren(jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute)) { if (checkDropNodeAttribute(ctx.subContext(attr))) { continue; } for (SNode output : outputNodes) { // We need FullCopyFacility here as we make a copy of attribute hierarchy and there could be references // outside of attribute, e.g. N1 and N2, with attribute A on the latter that references N1. Mere CopyUtil // won't update reference and its immature node would point to input model that might get disposed at the next step, leaving // broken reference. // NOTE. Use of FCF, however, implies we are going to activate reduction rules on attribute children, which might be quite unexpected. // Perhaps, shall refactor FCF to support two modes, with/without nested reductions. // XXX Likely, current approach does't allow references to attributes to resolve magically (i.e. by matched node id) // Is it important, do we care about references to attributes at all? final SNode attrCopy = new FullCopyFacility(env, getForeignNodes()).copyInputNode(attr); output.addChild(jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute, attrCopy); } } } boolean checkDropNodeAttribute(@NotNull TemplateContext ctx) throws GenerationFailureException { final SNode attr = ctx.getInput(); assert jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute.equals(attr.getContainmentLink()); QueryExecutionContext queryExecutor = ctx.getEnvironment().getQueryExecutor(); for (TemplateDropAttributeRule dropRule : getRuleManager().getDropAttributeRules(attr)) { if (queryExecutor.isApplicable(dropRule, ctx)) { return true; } } return false; } /** * Indicates a node of output model originates from given node of input model by means of mere copy. * * Both parameters are not null. Both may point to the same node (e.g. when it's partial copy * and we just record the 'would-be' copy of a node. Even though the node itself doesn't change (provided * we modify model in-place), we shall record the fact its original input is from last checkpoint state */ /*package*/ void recordCopyInputTrace(SNode inputNode, SNode outputNode) { myTransitionTrace.deriveOrigin(inputNode, outputNode); } /** * Indicates output nodes originate from given input node by means of transformation by a rule. * FIXME Input node is likely not null, though not sure what to do with create root rules. Perhaps, they don't need trace as there's no origin to trace to? */ /*package*/ void recordTransformInputTrace(SNode inputNode, @Nullable Collection<SNode> outputNodes) { if (outputNodes == null) { return; } if (inputNode == null || !myTransitionTrace.hasOrigin(inputNode)) { return; } for (SNode output : outputNodes) { if (output == null) { continue; } myTransitionTrace.deriveOrigin(inputNode, new DescendantsTreeIterator(output)); } } public SNode getOriginalRootByGenerated(SNode root) { SNode node = myNewToOldRoot.get(root); if (node == null) return null; if (node.getModel() == null) return null; if (node.getParent() == null) return node; return node.getContainingRoot(); } public boolean isIncremental() { return myDependenciesBuilder instanceof IncrementalDependenciesBuilder; } private boolean isInplaceChangeEnabled() { return getGeneratorSessionContext().getGenerationOptions().applyTransformationsInplace(); } private abstract static class NodeCopyFacility { protected final TemplateExecutionEnvironmentImpl myEnv; protected boolean myIsChanged = false; protected NodeCopyFacility(@NotNull TemplateExecutionEnvironmentImpl env) { myEnv = env; } public final IGeneratorLogger getLogger() { return myEnv.getLogger(); } public final boolean hasChanges() { return myIsChanged; } /** * @return true if one of drop rules matched */ public final boolean checkDropRules(SNode inputRootNode, List<TemplateDropRootRule> rules) throws GenerationFailureException { final DefaultTemplateContext tc = new DefaultTemplateContext(myEnv, inputRootNode, null); for (TemplateDropRootRule dropRootRule : rules) { if (myEnv.getQueryExecutor().isApplicable(dropRootRule, tc)) { drop(inputRootNode, dropRootRule); myEnv.getTrace().trace(inputRootNode.getNodeId(), Collections.<SNodeId>emptyList(), dropRootRule.getRuleNode()); return true; } } return false; } protected final boolean checkAttributeDropRules(SNode attributeNode) throws GenerationFailureException { final DefaultTemplateContext tc = new DefaultTemplateContext(myEnv, attributeNode, null); return myEnv.getGenerator().checkDropNodeAttribute(tc); } protected abstract void drop(SNode inputRootNode, TemplateDropRootRule rule); public abstract void copyRootInputNode(@NotNull SNode inputRoot) throws GenerationFailureException, GenerationCanceledException; } static class GeneratedRootDescriptor { // newly created root output node final SNode myOutputRoot; // input node, if any; not necessarily root final SNode myInputNode; // rule that produced the root, or null if copy root final SNodeReference myTemplateNode; // true if root is a copy of a root in input model final boolean myIsCopied; /** * true iff outputRoot is created from an inputNode which is either root and is kept in the output model * or from non-root node that might get copied/reduced afterwards. * IOW, indicates if the new myOutputRoot replaced a root or not. */ final boolean myIsInputPreserved; private GeneratedRootDescriptor(@NotNull SNode outputRoot, @Nullable SNode input, boolean isInputPreserved, SNodeReference templateNode, boolean isCopied) { myOutputRoot = outputRoot; myInputNode = input; myTemplateNode = templateNode; myIsInputPreserved = isInputPreserved; myIsCopied = isCopied; } // new root produced public GeneratedRootDescriptor(@NotNull SNode outputRoot, @NotNull SNodeReference templateNode) { this(outputRoot, null, false, templateNode, false); } // new root produced based on existing node, possibly replacing it public GeneratedRootDescriptor(@NotNull SNode outputRoot, @NotNull SNode input, boolean inputPersists, @NotNull SNodeReference templateNode) { this(outputRoot, input, inputPersists, templateNode, false); } // copy of input root in the output model public GeneratedRootDescriptor(@NotNull SNode outputRoot, @NotNull SNode input) { this(outputRoot, input, false, null, true); } } private static class PartialCopyFacility extends NodeCopyFacility { private final DeltaBuilder myDeltaBuilder; public PartialCopyFacility(@NotNull TemplateExecutionEnvironmentImpl env, @NotNull DeltaBuilder deltaBuilder) { super(env); myDeltaBuilder = deltaBuilder; } @Override protected void drop(SNode inputRootNode, TemplateDropRootRule rule) { myDeltaBuilder.deleteInputRoot(inputRootNode); } @Override public void copyRootInputNode(@NotNull SNode inputRootNode) throws GenerationFailureException, GenerationCanceledException { myDeltaBuilder.enterInputRoot(inputRootNode); try { visitInputNode(inputRootNode); } finally { myDeltaBuilder.leaveInputRoot(inputRootNode); } // for now, registerRoot shall go *after* leaveInputRoot, as deltaBuilder expects CopyRoot to be full of replacing nodes // at the moment root is registered (to fill id map of new nodes) // TODO make map building an explicit step in DeltaBuilder so that ordering won't matter that much. // (the question is what if anyone calls findOutputNode while rules are applied (seems !strict model allows that) myEnv.getGenerator().registerRoot(new GeneratedRootDescriptor(inputRootNode, inputRootNode)); // weaving rules need myNewToOldRoot mapping } private void visitInputNode(SNode inputNode) throws GenerationFailureException, GenerationCanceledException { myEnv.getGenerator().recordCopyInputTrace(inputNode, inputNode); myEnv.blockReductionsForCopiedNode(inputNode, inputNode); // prevent infinite applying of the same reduction to the 'same' node. for (SNode inputChildNode : inputNode.getChildren()) { SContainmentLink childRole = inputChildNode.getContainmentLink(); assert childRole != null; Collection<SNode> outputChildNodes = myEnv.tryToReduce(inputChildNode); if (outputChildNodes != null) { myDeltaBuilder.registerSubTree(inputChildNode, childRole, outputChildNodes); myIsChanged = true; } else { if (jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute.equals(childRole) && checkAttributeDropRules(inputChildNode)) { myDeltaBuilder.registerSubTree(inputChildNode, childRole, Collections.<SNode>emptyList()); myIsChanged = true; } else { visitInputNode(inputChildNode); } } } } } private static class FullCopyFacility extends NodeCopyFacility { private final Set<SNode> myAdditionalInputNodes; private final SModel myInputModel; private final SModelReference myOutputModelRef; private final Factory myNodeFactory; public FullCopyFacility(TemplateExecutionEnvironmentImpl env) { this(env, Collections.<SNode>emptySet()); } public FullCopyFacility(TemplateExecutionEnvironmentImpl env, Set<SNode> additionalInputs) { super(env); myAdditionalInputNodes = additionalInputs; myInputModel = env.getGenerator().getInputModel(); myOutputModelRef = env.getOutputModel().getReference(); myNodeFactory = new RegularSModelFactory(); } @Override protected void drop(SNode inputRootNode, TemplateDropRootRule rule) { } @Override public void copyRootInputNode(@NotNull SNode inputRootNode) throws GenerationFailureException, GenerationCanceledException { // copy SNode root = copyInputNode(inputRootNode); myEnv.getGenerator().registerRoot(new GeneratedRootDescriptor(root, inputRootNode)); } private void reportBrokenRef(@NotNull SNode inputNode, @NotNull SReference ref) { getLogger().error(inputNode.getReference(), String.format("broken reference '%s' in %s (target model is null)", ref.getLink().getName(), SNodeOperations.getDebugText(inputNode)), GeneratorUtil.describeIfExists(inputNode, "input node")); } public SNode copyInputNode(@NotNull SNode inputNode) throws GenerationFailureException, GenerationCanceledException { // no reduction found - do node copying SNode outputNode; final SModel inputNodeModel = inputNode.getModel(); if (inputNode.getNodeId() != null && inputNodeModel != null) { // copy preserving id outputNode = myNodeFactory.create(inputNode); } else { outputNode = myEnv.getOutputModel().createNode(inputNode.getConcept()); } myEnv.getGenerator().recordCopyInputTrace(inputNode, outputNode); myEnv.blockReductionsForCopiedNode(inputNode, outputNode); // prevent infinite applying of the same reduction to the 'same' node. // output node should be accessible via 'findCopiedNode' myEnv.getGenerator().addCopiedOutputNodeForInputNode(inputNode, outputNode); CopyUtil.copyProperties(inputNode, outputNode); CopyUtil.copyUserObjects(inputNode, outputNode); for (SReference inputReference : inputNode.getReferences()) { if (inputNodeModel != null) { boolean external = true; if (inputReference instanceof PostponedReference){ external = false; } else if (inputNodeModel.getReference().equals(inputReference.getTargetSModelReference())){ external = false; } if (inputReference instanceof DynamicReference || external) { // dynamic & external references don't need validation => replace input model with output SModelReference targetModelReference = external ? inputReference.getTargetSModelReference() : myOutputModelRef; if (inputReference instanceof StaticReference) { if (targetModelReference == null) { reportBrokenRef(inputNode, inputReference); continue; } SReference reference = new StaticReference( inputReference.getLink(), outputNode, targetModelReference, inputReference.getTargetNodeId(), ((StaticReference) inputReference).getResolveInfo()); outputNode.setReference(reference.getLink(), reference); } else if (inputReference instanceof DynamicReference) { DynamicReference outputReference = new DynamicReference( inputReference.getLink(), outputNode, targetModelReference, ((DynamicReference) inputReference).getResolveInfo()); outputReference.setOrigin(((DynamicReference) inputReference).getOrigin()); outputNode.setReference(outputReference.getLink(), outputReference); } else { String msg = "internal error: can't clone reference '%s' in %s. Reference class: %s"; getLogger().error(inputNode.getReference(), String.format(msg, inputReference.getLink().getName(), SNodeOperations.getDebugText(inputNode), inputReference.getClass().getName())); } continue; } } SNode refTarget = jetbrains.mps.util.SNodeOperations.getTargetNodeSilently(inputReference); if (refTarget == null) { reportBrokenRef(inputNode, inputReference); continue; } if (refTarget.getModel() != null && refTarget.getModel().equals(myInputModel) || myAdditionalInputNodes.contains(refTarget)) { ReferenceInfo_CopiedInputNode refInfo = new ReferenceInfo_CopiedInputNode(inputNode, refTarget); new PostponedReference(inputReference.getLink(), outputNode, refInfo).registerWith(myEnv.getGenerator()); } else if (refTarget.getModel() != null) { SNodeAccessUtil.setReferenceTarget(outputNode, inputReference.getLink(), refTarget); } else { reportBrokenRef(inputNode, inputReference); } } for (SNode inputChildNode : inputNode.getChildren()) { SContainmentLink childRole = inputChildNode.getContainmentLink(); assert childRole != null; Collection<SNode> outputChildNodes = myEnv.tryToReduce(inputChildNode); if (outputChildNodes != null) { myIsChanged = true; RoleValidator rv = myEnv.getGenerator().getChildRoleValidator(outputNode, childRole); for (SNode outputChildNode : outputChildNodes) { // check child Status status = rv.validate(outputChildNode); if (status != null) { getLogger().warning(inputChildNode.getReference(), status.getMessage("copy input node"), status.describe(GeneratorUtil.describeIfExists(TracingUtil.getInput(inputNode), "origin"))); } outputNode.addChild(childRole, outputChildNode); } } else { if (jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute.equals(childRole) && checkAttributeDropRules(inputChildNode)) { // attribute conforms to drop rule, ignore and do not copy. continue; } outputNode.addChild(childRole, copyInputNode(inputChildNode)); } } return outputNode; } } }