Storm
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
NondeterministicModelBisimulationDecomposition.cpp
Go to the documentation of this file.
2
5
7
10
12
13namespace storm {
14namespace storage {
15
16using namespace bisimulation;
17
18template<typename ModelType>
20 ModelType const& model,
22 : BisimulationDecomposition<ModelType, NondeterministicModelBisimulationDecomposition::BlockDataType>(model, model.getTransitionMatrix().transpose(false),
23 options),
24 choiceToStateMapping(model.getNumberOfChoices()),
25 quotientDistributions(model.getNumberOfChoices()),
26 orderedQuotientDistributions(model.getNumberOfChoices()) {
27 STORM_LOG_THROW(options.getType() == BisimulationType::Strong, storm::exceptions::IllegalFunctionCallException,
28 "Weak bisimulation is currently not supported for nondeterministic models.");
29}
30
31template<typename ModelType>
32std::pair<storm::storage::BitVector, storm::storage::BitVector> NondeterministicModelBisimulationDecomposition<ModelType>::getStatesWithProbability01() {
33 STORM_LOG_THROW(this->options.isOptimizationDirectionSet(), storm::exceptions::IllegalFunctionCallException,
34 "Can only compute states with probability 0/1 with an optimization direction (min/max).");
35 if (this->options.getOptimizationDirection() == OptimizationDirection::Minimize) {
36 return storm::utility::graph::performProb01Min(this->model.getTransitionMatrix(), this->model.getTransitionMatrix().getRowGroupIndices(),
37 this->model.getBackwardTransitions(), this->options.phiStates.get(), this->options.psiStates.get());
38 } else {
39 return storm::utility::graph::performProb01Max(this->model.getTransitionMatrix(), this->model.getTransitionMatrix().getRowGroupIndices(),
40 this->model.getBackwardTransitions(), this->options.phiStates.get(), this->options.psiStates.get());
41 }
42}
43
44template<typename ModelType>
46 this->createChoiceToStateMapping();
47 this->initializeQuotientDistributions();
48}
49
50template<typename ModelType>
52 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
53 for (storm::storage::sparse::state_type state = 0; state < this->model.getNumberOfStates(); ++state) {
54 for (uint_fast64_t choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
55 choiceToStateMapping[choice] = state;
56 }
57 }
58}
59
60template<typename ModelType>
61void NondeterministicModelBisimulationDecomposition<ModelType>::initializeQuotientDistributions() {
62 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
63
64 for (auto const& block : this->partition.getBlocks()) {
65 if (block->data().absorbing()) {
66 // If the block is marked as absorbing, we need to create the corresponding distributions.
67 for (auto stateIt = this->partition.begin(*block), stateIte = this->partition.end(*block); stateIt != stateIte; ++stateIt) {
68 for (uint_fast64_t choice = nondeterministicChoiceIndices[*stateIt]; choice < nondeterministicChoiceIndices[*stateIt + 1]; ++choice) {
69 this->quotientDistributions[choice].addProbability(block->getId(), storm::utility::one<ValueType>());
70 orderedQuotientDistributions[choice] = &this->quotientDistributions[choice];
71 }
72 }
73 } else {
74 // Otherwise, we compute the probabilities from the transition matrix.
75 for (auto stateIt = this->partition.begin(*block), stateIte = this->partition.end(*block); stateIt != stateIte; ++stateIt) {
76 for (uint_fast64_t choice = nondeterministicChoiceIndices[*stateIt]; choice < nondeterministicChoiceIndices[*stateIt + 1]; ++choice) {
77 if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
78 auto const& rewardModel = this->model.getUniqueRewardModel();
79 if (rewardModel.hasStateActionRewards()) {
80 this->quotientDistributions[choice].setReward(rewardModel.getStateActionReward(choice));
81 }
82 }
83 for (auto entry : this->model.getTransitionMatrix().getRow(choice)) {
84 if (!this->comparator.isZero(entry.getValue())) {
85 this->quotientDistributions[choice].addProbability(this->partition.getBlock(entry.getColumn()).getId(), entry.getValue());
86 }
87 }
88 orderedQuotientDistributions[choice] = &this->quotientDistributions[choice];
89 }
90 }
91 }
92 }
93
94 for (decltype(this->model.getNumberOfStates()) state = 0; state < this->model.getNumberOfStates(); ++state) {
95 updateOrderedQuotientDistributions(state);
96 }
97}
98
99template<typename ModelType>
100void NondeterministicModelBisimulationDecomposition<ModelType>::updateOrderedQuotientDistributions(storm::storage::sparse::state_type state) {
101 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
102 std::sort(this->orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state],
103 this->orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state + 1],
105 return dist1->less(*dist2, this->comparator);
106 });
107}
108
109template<typename ModelType>
111 // In order to create the quotient model, we need to construct
112 // (a) the new transition matrix,
113 // (b) the new labeling,
114 // (c) the new reward structures.
115
116 // Prepare a matrix builder for (a).
117 storm::storage::SparseMatrixBuilder<ValueType> builder(0, this->size(), 0, false, true, this->size());
118
119 // Prepare the new state labeling for (b).
120 storm::models::sparse::StateLabeling newLabeling(this->size());
121 std::set<std::string> atomicPropositionsSet = this->options.respectedAtomicPropositions.get();
122 atomicPropositionsSet.insert("init");
123 std::vector<std::string> atomicPropositions = std::vector<std::string>(atomicPropositionsSet.begin(), atomicPropositionsSet.end());
124 for (auto const& ap : atomicPropositions) {
125 newLabeling.addLabel(ap);
126 }
127
128 // If the model had state (action) rewards, we need to build the state rewards for the quotient as well.
129 std::optional<std::vector<ValueType>> stateRewards;
130 std::optional<std::vector<ValueType>> stateActionRewards;
131 if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
132 if (this->model.getUniqueRewardModel().hasStateRewards()) {
133 stateRewards = std::vector<ValueType>(this->blocks.size());
134 }
135 if (this->model.getUniqueRewardModel().hasStateActionRewards()) {
136 stateActionRewards = std::vector<ValueType>();
137 }
138 }
139
140 // Now build (a) and (b) by traversing all blocks.
141 uint_fast64_t currentRow = 0;
142 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
143 for (uint_fast64_t blockIndex = 0; blockIndex < this->blocks.size(); ++blockIndex) {
144 auto const& block = this->blocks[blockIndex];
145
146 // Open new row group for the new meta state.
147 builder.newRowGroup(currentRow);
148
149 // Pick one representative state. For strong bisimulation it doesn't matter which state it is, because
150 // they all behave equally.
151 storm::storage::sparse::state_type representativeState = *block.begin();
152 Block<BlockDataType> const& oldBlock = this->partition.getBlock(representativeState);
153
154 // If the block is absorbing, we simply add a self-loop.
155 if (oldBlock.data().absorbing()) {
156 builder.addNextValue(currentRow, blockIndex, storm::utility::one<ValueType>());
157 ++currentRow;
158
159 // If the block has a special representative state, we retrieve it now.
160 if (oldBlock.data().hasRepresentativeState()) {
161 representativeState = oldBlock.data().representativeState();
162 }
163
164 // Give the choice a reward of zero as we artificially introduced that the block is absorbing.
165 if (this->options.getKeepRewards() && this->model.hasRewardModel() && this->model.getUniqueRewardModel().hasStateActionRewards()) {
166 stateActionRewards.value().push_back(storm::utility::zero<ValueType>());
167 }
168
169 // Add all of the selected atomic propositions that hold in the representative state to the state
170 // representing the block.
171 for (auto const& ap : atomicPropositions) {
172 if (this->model.getStateLabeling().getStateHasLabel(ap, representativeState)) {
173 newLabeling.addLabelToState(ap, blockIndex);
174 }
175 }
176 } else {
177 // Add the outgoing choices of the block.
178 for (uint_fast64_t choice = nondeterministicChoiceIndices[representativeState]; choice < nondeterministicChoiceIndices[representativeState + 1];
179 ++choice) {
180 // If the choice is the same as the last one, we do not need to add it.
181 if (choice > nondeterministicChoiceIndices[representativeState] &&
182 quotientDistributions[choice - 1].equals(quotientDistributions[choice], this->comparator)) {
183 continue;
184 }
185
186 for (auto entry : quotientDistributions[choice]) {
187 builder.addNextValue(currentRow, entry.first, entry.second);
188 }
189 if (this->options.getKeepRewards() && this->model.hasRewardModel() && this->model.getUniqueRewardModel().hasStateActionRewards()) {
190 stateActionRewards.value().push_back(quotientDistributions[choice].getReward());
191 }
192 ++currentRow;
193 }
194
195 // Otherwise add all atomic propositions to the equivalence class that the representative state
196 // satisfies.
197 for (auto const& ap : atomicPropositions) {
198 if (this->model.getStateLabeling().getStateHasLabel(ap, representativeState)) {
199 newLabeling.addLabelToState(ap, blockIndex);
200 }
201 }
202 }
203
204 // If the model has state rewards, we simply copy the state reward of the representative state, because
205 // all states in a block are guaranteed to have the same state reward.
206 if (this->options.getKeepRewards() && this->model.hasRewardModel() && this->model.getUniqueRewardModel().hasStateRewards()) {
207 stateRewards.value()[blockIndex] = this->model.getUniqueRewardModel().getStateRewardVector()[representativeState];
208 }
209 }
210
211 // Now check which of the blocks of the partition contain at least one initial state.
212 for (auto initialState : this->model.getInitialStates()) {
213 Block<BlockDataType> const& initialBlock = this->partition.getBlock(initialState);
214 newLabeling.addLabelToState("init", initialBlock.getId());
215 }
216
217 // Construct the reward model mapping.
218 std::unordered_map<std::string, typename ModelType::RewardModelType> rewardModels;
219 if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
220 STORM_LOG_THROW(this->model.hasUniqueRewardModel(), storm::exceptions::IllegalFunctionCallException, "Cannot preserve more than one reward model.");
221 typename std::unordered_map<std::string, typename ModelType::RewardModelType>::const_iterator nameRewardModelPair =
222 this->model.getRewardModels().begin();
223 rewardModels.insert(std::make_pair(nameRewardModelPair->first, typename ModelType::RewardModelType(stateRewards, stateActionRewards)));
224 }
225
226 // Finally construct the quotient model.
227 this->quotient = std::make_shared<ModelType>(builder.build(0, this->size(), this->size()), std::move(newLabeling), std::move(rewardModels));
228}
229
230template<typename ModelType>
232 return block.getNumberOfStates() > 1 && !block.data().absorbing();
233}
234
235template<typename ModelType>
236void NondeterministicModelBisimulationDecomposition<ModelType>::updateQuotientDistributionsOfPredecessors(Block<BlockDataType> const& newBlock,
237 Block<BlockDataType> const& oldBlock,
238 std::vector<Block<BlockDataType>*>& splitterQueue) {
239 uint_fast64_t lastState = 0;
240 bool lastStateInitialized = false;
241
242 for (auto stateIt = this->partition.begin(newBlock), stateIte = this->partition.end(newBlock); stateIt != stateIte; ++stateIt) {
243 for (auto predecessorEntry : this->backwardTransitions.getRow(*stateIt)) {
244 if (this->comparator.isZero(predecessorEntry.getValue())) {
245 continue;
246 }
247
248 storm::storage::sparse::state_type predecessorChoice = predecessorEntry.getColumn();
249 storm::storage::sparse::state_type predecessorState = choiceToStateMapping[predecessorChoice];
250 Block<BlockDataType>& predecessorBlock = this->partition.getBlock(predecessorState);
251
252 // If the predecessor block is marked as absorbing, we do not need to update anything.
253 if (predecessorBlock.data().absorbing()) {
254 continue;
255 }
256
257 // If the predecessor block is not marked as to-be-refined, we do so now.
258 if (!predecessorBlock.data().splitter()) {
259 predecessorBlock.data().setSplitter();
260 splitterQueue.push_back(&predecessorBlock);
261 }
262
263 if (lastStateInitialized) {
264 // If we have skipped to the choices of the next state, we need to repair the order of the
265 // distributions for the last state.
266 if (lastState != predecessorState) {
267 updateOrderedQuotientDistributions(lastState);
268 lastState = predecessorState;
269 }
270 } else {
271 lastStateInitialized = true;
272 lastState = choiceToStateMapping[predecessorChoice];
273 }
274
275 // Now shift the probability from this transition from the old block to the new one.
276 this->quotientDistributions[predecessorChoice].shiftProbability(oldBlock.getId(), newBlock.getId(), predecessorEntry.getValue());
277 }
278 }
279
280 if (lastStateInitialized) {
281 updateOrderedQuotientDistributions(lastState);
282 }
283}
284
285template<typename ModelType>
286bool NondeterministicModelBisimulationDecomposition<ModelType>::checkQuotientDistributions() const {
287 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
288 for (decltype(this->model.getNumberOfStates()) state = 0; state < this->model.getNumberOfStates(); ++state) {
289 for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
291 if (this->options.getKeepRewards() && this->model.hasRewardModel()) {
292 auto const& rewardModel = this->model.getUniqueRewardModel();
293 if (rewardModel.hasStateActionRewards()) {
294 distribution.setReward(rewardModel.getStateActionReward(choice));
295 }
296 }
297 for (auto const& element : this->model.getTransitionMatrix().getRow(choice)) {
298 distribution.addProbability(this->partition.getBlock(element.getColumn()).getId(), element.getValue());
299 }
300
301 if (!distribution.equals(quotientDistributions[choice])) {
302 std::cout << "the distributions for choice " << choice << " of state " << state << " do not match.\n";
303 std::cout << "is: " << quotientDistributions[choice] << " but should be " << distribution << '\n';
304 exit(-1);
305 }
306
307 bool less1 = distribution.less(quotientDistributions[choice], this->comparator);
308 bool less2 = quotientDistributions[choice].less(distribution, this->comparator);
309
310 if (distribution.equals(quotientDistributions[choice]) && (less1 || less2)) {
311 std::cout << "mismatch of equality and less for \n";
312 std::cout << quotientDistributions[choice] << " vs " << distribution << '\n';
313 exit(-1);
314 }
315 }
316
317 for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1] - 1; ++choice) {
318 if (orderedQuotientDistributions[choice + 1]->less(*orderedQuotientDistributions[choice], this->comparator)) {
319 std::cout << "choice " << (choice + 1) << " is less than predecessor\n";
320 std::cout << *orderedQuotientDistributions[choice] << " should be less than " << *orderedQuotientDistributions[choice + 1] << '\n';
321 exit(-1);
322 }
323 }
324 }
325 return true;
326}
327
328template<typename ModelType>
329bool NondeterministicModelBisimulationDecomposition<ModelType>::printDistributions(uint_fast64_t state) const {
330 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
331 for (auto choice = nondeterministicChoiceIndices[state]; choice < nondeterministicChoiceIndices[state + 1]; ++choice) {
332 std::cout << quotientDistributions[choice] << '\n';
333 }
334 return true;
335}
336
337template<typename ModelType>
338bool NondeterministicModelBisimulationDecomposition<ModelType>::checkBlockStable(bisimulation::Block<BlockDataType> const& newBlock) const {
339 std::cout << "checking stability of new block " << newBlock.getId() << " of size " << newBlock.getNumberOfStates() << '\n';
340 for (auto stateIt1 = this->partition.begin(newBlock), stateIte1 = this->partition.end(newBlock); stateIt1 != stateIte1; ++stateIt1) {
341 for (auto stateIt2 = this->partition.begin(newBlock), stateIte2 = this->partition.end(newBlock); stateIt2 != stateIte2; ++stateIt2) {
342 bool less1 = quotientDistributionsLess(*stateIt1, *stateIt2);
343 bool less2 = quotientDistributionsLess(*stateIt2, *stateIt1);
344 if (less1 || less2) {
345 std::cout << "the partition is not stable for the states " << *stateIt1 << " and " << *stateIt2 << '\n';
346 std::cout << "less1 " << less1 << " and less2 " << less2 << '\n';
347
348 std::cout << "distributions of state " << *stateIt1 << '\n';
349 this->printDistributions(*stateIt1);
350 std::cout << "distributions of state " << *stateIt2 << '\n';
351 this->printDistributions(*stateIt2);
352 exit(-1);
353 }
354 }
355 }
356 return true;
357}
358
359template<typename ModelType>
360bool NondeterministicModelBisimulationDecomposition<ModelType>::checkDistributionsDifferent(bisimulation::Block<BlockDataType> const& block,
362 for (auto stateIt1 = this->partition.begin(block), stateIte1 = this->partition.end(block); stateIt1 != stateIte1; ++stateIt1) {
363 for (auto stateIt2 = this->partition.begin() + block.getEndIndex(), stateIte2 = this->partition.begin() + end; stateIt2 != stateIte2; ++stateIt2) {
364 if (!quotientDistributionsLess(*stateIt1, *stateIt2)) {
365 std::cout << "distributions are not less, even though they should be!\n";
366 exit(-3);
367 } else {
368 std::cout << "less:\n";
369 this->printDistributions(*stateIt1);
370 std::cout << "and\n";
371 this->printDistributions(*stateIt2);
372 }
373 }
374 }
375 return true;
376}
377
378template<typename ModelType>
379bool NondeterministicModelBisimulationDecomposition<ModelType>::splitBlockAccordingToCurrentQuotientDistributions(
380 Block<BlockDataType>& block, std::vector<Block<BlockDataType>*>& splitterQueue) {
381 std::list<Block<BlockDataType>*> newBlocks;
382 bool split = this->partition.splitBlock(
383 block,
385 bool result = quotientDistributionsLess(state1, state2);
386 return result;
387 },
388 [&newBlocks](Block<BlockDataType>& newBlock) { newBlocks.push_back(&newBlock); });
389
390 // Defer updating the quotient distributions until *after* all splits, because
391 // it otherwise influences the subsequent splits!
392 for (auto el : newBlocks) {
393 this->updateQuotientDistributionsOfPredecessors(*el, block, splitterQueue);
394 }
395
396 return split;
397}
398
399template<typename ModelType>
400bool NondeterministicModelBisimulationDecomposition<ModelType>::quotientDistributionsLess(storm::storage::sparse::state_type state1,
402 STORM_LOG_TRACE("Comparing the quotient distributions of state " << state1 << " and " << state2 << ".");
403 std::vector<uint_fast64_t> nondeterministicChoiceIndices = this->model.getTransitionMatrix().getRowGroupIndices();
404
405 auto firstIt = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state1];
406 auto firstIte = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state1 + 1];
407 auto secondIt = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state2];
408 auto secondIte = orderedQuotientDistributions.begin() + nondeterministicChoiceIndices[state2 + 1];
409
410 for (; firstIt != firstIte && secondIt != secondIte; ++firstIt, ++secondIt) {
411 // If the current distributions are in a less-than relationship, we can return a result.
412 if ((*firstIt)->less(**secondIt, this->comparator)) {
413 return true;
414 } else if ((*secondIt)->less(**firstIt, this->comparator)) {
415 return false;
416 }
417
418 // If the distributions matched, we need to advance both distribution iterators to the next distribution
419 // that is larger.
420 while (firstIt != firstIte && std::next(firstIt) != firstIte && !(*firstIt)->less(**std::next(firstIt), this->comparator)) {
421 ++firstIt;
422 }
423 while (secondIt != secondIte && std::next(secondIt) != secondIte && !(*secondIt)->less(**std::next(secondIt), this->comparator)) {
424 ++secondIt;
425 }
426 }
427
428 if (firstIt == firstIte && secondIt != secondIte) {
429 return true;
430 }
431 return false;
432}
433
434template<typename ModelType>
436 bisimulation::Block<BlockDataType>& splitter, std::vector<bisimulation::Block<BlockDataType>*>& splitterQueue) {
437 if (!possiblyNeedsRefinement(splitter)) {
438 return;
439 }
440
441 STORM_LOG_TRACE("Refining block " << splitter.getId());
442
443 splitBlockAccordingToCurrentQuotientDistributions(splitter, splitterQueue);
444}
445
447
448#ifdef STORM_HAVE_CARL
451#endif
452} // namespace storage
453} // namespace storm
void addLabel(std::string const &label)
Adds a new label to the labelings.
This class manages the labeling of the state space with a number of (atomic) labels.
void addLabelToState(std::string const &label, storm::storage::sparse::state_type state)
Adds a label to a given state.
This class is the superclass of all decompositions of a sparse model into its bisimulation quotient.
void addProbability(StateType const &state, ValueType const &probability)
Assigns the given state the given probability under this distribution.
bool equals(DistributionWithReward< ValueType, StateType > const &other, storm::utility::ConstantsComparator< ValueType > const &comparator=storm::utility::ConstantsComparator< ValueType >()) const
Checks whether the two distributions specify the same probabilities to go to the same states.
bool less(DistributionWithReward< ValueType, StateType > const &other, storm::utility::ConstantsComparator< ValueType > const &comparator) const
void setReward(ValueType const &reward)
Sets the reward of this distribution.
This class represents the decomposition of a nondeterministic model into its bisimulation quotient.
virtual void refinePartitionBasedOnSplitter(bisimulation::Block< BlockDataType > &splitter, std::vector< bisimulation::Block< BlockDataType > * > &splitterQueue) override
virtual void buildQuotient() override
Builds the quotient model based on the previously computed equivalence classes (stored in the blocks ...
virtual std::pair< storm::storage::BitVector, storm::storage::BitVector > getStatesWithProbability01() override
Computes the set of states with probability 0/1 for satisfying phi until psi.
NondeterministicModelBisimulationDecomposition(ModelType const &model, typename BisimulationDecomposition< ModelType, BlockDataType >::Options const &options=typename BisimulationDecomposition< ModelType, BlockDataType >::Options())
Computes the bisimulation relation for the given model.
virtual void initialize() override
A function that can initialize auxiliary data structures.
A class that can be used to build a sparse matrix by adding value by value.
void addNextValue(index_type row, index_type column, value_type const &value)
Sets the matrix entry at the given row and column to the given value.
void newRowGroup(index_type startingRow)
Starts a new row group in the matrix.
SparseMatrix< value_type > build(index_type overriddenRowCount=0, index_type overriddenColumnCount=0, index_type overriddenRowGroupCount=0)
std::size_t getNumberOfStates() const
Definition Block.cpp:127
std::size_t getId() const
Definition Block.cpp:64
#define STORM_LOG_TRACE(message)
Definition logging.h:17
#define STORM_LOG_THROW(cond, exception, message)
Definition macros.h:30
std::pair< storm::storage::BitVector, storm::storage::BitVector > performProb01Max(storm::storage::SparseMatrix< T > const &transitionMatrix, std::vector< uint_fast64_t > const &nondeterministicChoiceIndices, storm::storage::SparseMatrix< T > const &backwardTransitions, storm::storage::BitVector const &phiStates, storm::storage::BitVector const &psiStates)
Definition graph.cpp:835
std::pair< storm::storage::BitVector, storm::storage::BitVector > performProb01Min(storm::storage::SparseMatrix< T > const &transitionMatrix, std::vector< uint_fast64_t > const &nondeterministicChoiceIndices, storm::storage::SparseMatrix< T > const &backwardTransitions, storm::storage::BitVector const &phiStates, storm::storage::BitVector const &psiStates)
Definition graph.cpp:1079
LabParser.cpp.
Definition cli.cpp:18