blob: 75c329925bd3eddd0e6ea53fcf83ff2271cceb24 [file] [log] [blame]
// Copyright 2017-2020 The Verible Authors.
//
// 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.
#include "common/formatting/line_wrap_searcher.h"
#include <memory>
#include <queue>
#include <vector>
#include "absl/strings/string_view.h"
#include "common/formatting/basic_format_style.h"
#include "common/formatting/format_token.h"
#include "common/formatting/state_node.h"
#include "common/formatting/unwrapped_line.h"
#include "common/text/token_info.h"
#include "common/util/logging.h"
#include "common/util/spacer.h"
namespace verible {
namespace {
// Wrapped class around StateNode for the sake of adapting to a
// std::priority_queue interface.
// TODO(fangism): if performance of memory allocations is an issue,
// use some sort of pool-allocator.
struct SearchState {
std::shared_ptr<const StateNode> state;
explicit SearchState(const std::shared_ptr<const StateNode>& s) : state(s) {}
// Inverted to min-heap: *lowest* penalty has the highest search priority.
bool operator<(const SearchState& r) const { return *r.state < *state; }
};
} // namespace
std::vector<FormattedExcerpt> SearchLineWraps(const UnwrappedLine& uwline,
const BasicFormatStyle& style,
int max_search_states) {
// Dijkstra's algorithm for now: prioritize searching minimum penalty path
// until destination is reached.
VLOG(2) << "SearchLineWraps on: " << uwline;
if (uwline.TokensRange().empty()) {
std::vector<FormattedExcerpt> result(1);
return result;
}
// Worklist for decision searching, ordered by cumulative penalty.
// Note: a heap-based priority-queue will not guarantee stable ordering
// among equal-valued keys. If first-come-first-serve tie-breaking is
// important, consider switching to a std::map.
std::priority_queue<SearchState> worklist;
// Seed worklist with a NodeState that should have 0 penalty.
SearchState seed(std::make_shared<StateNode>(uwline, style));
worklist.push(seed);
bool aborted_search = false;
std::vector<std::shared_ptr<const StateNode>> winning_paths;
int state_count = 0;
while (!worklist.empty()) {
++state_count;
SearchState next(worklist.top());
worklist.pop();
VLOG(4) << "\n---- line wrapping search state " << state_count << " ----"
<< "\ncurrent cost: " << next.state->cumulative_cost
<< "\ncurrent column: " << next.state->current_column;
if (!winning_paths.empty()) {
// We already found at least one winning solution.
// As soon as the current cost exceeds the optimal (by 1 or tie-breaker),
// then stop.
// This guarantees that we've collected all equally optimal solutions.
if (*winning_paths.front() < *next.state) {
break;
}
}
// Check for done condition: reached the end of the UnwrappedLine's
// FormatTokens.
// First to reach the end has the lowest penalty and wins.
// TODO(fangism): if we compare against uwline.end() iterator, we could save
// some space from each StateNode object.
if (next.state->Done()) {
winning_paths.push_back(next.state);
VLOG(3) << "winning path cost: " << next.state->cumulative_cost;
// Continue until all equally good solutions have been found.
continue;
}
if (state_count >= max_search_states) {
// Search limit exceeded, abandon search.
// Greedily finish formatting this partition, and return it.
winning_paths.push_back(StateNode::QuickFinish(next.state, style));
aborted_search = true;
break;
}
// Consider the new penalties incurred for the next decision:
// break, or no break. Calculate new penalties.
// Push one or both branches into the worklist.
const auto& token = next.state->GetNextToken();
if (token.before.break_decision == SpacingOptions::Preserve) {
VLOG(4) << "preserving spaces before \'" << token.token->text() << '\'';
SearchState preserved(std::make_shared<StateNode>(
next.state, style, SpacingDecision::Preserve));
worklist.push(preserved);
} else {
// Remaining options are: Undecided, MustWrap, MustAppend
// Explore one or both: SpacingDecision::Wrap/Append
if (token.before.break_decision != SpacingOptions::MustWrap) {
VLOG(4) << "considering appending \'" << token.token->text() << '\'';
// Consider cost of appending token to current line.
SearchState appended(std::make_shared<StateNode>(
next.state, style, SpacingDecision::Append));
worklist.push(appended);
VLOG(4) << " cost: " << appended.state->cumulative_cost;
VLOG(4) << " column: " << appended.state->current_column;
}
if (token.before.break_decision != SpacingOptions::MustAppend) {
VLOG(4) << "considering wrapping \'" << token.token->text() << '\'';
// Consider cost of line wrapping here.
SearchState wrapped(std::make_shared<StateNode>(next.state, style,
SpacingDecision::Wrap));
worklist.push(wrapped);
VLOG(4) << " cost: " << wrapped.state->cumulative_cost;
VLOG(4) << " column: " << wrapped.state->current_column;
}
}
// TODO(fangism): Use an admissibility heuristic to prune search space from
// paths whose best-case outcome is worse than a conservatively achievable
// goal. Without a heuristic, this is just Dijkstra.
// With heuristic pruning, this is A* (A-star).
} // while (!worklist.empty())
CHECK_GE(winning_paths.size(), 1);
// Reconstruct the unwrapped_line to reflect the decisions made to reach the
// winning_paths. Return a modified copy of the original UnwrappedLine.
std::vector<FormattedExcerpt> results;
results.reserve(winning_paths.size());
for (const auto& path : winning_paths) {
results.emplace_back(uwline);
auto& result = results.back();
CHECK_EQ(path->Depth(), result.Tokens().size());
path->ReconstructFormatDecisions(&result);
if (aborted_search) {
result.MarkIncomplete();
}
}
return results;
}
void DisplayEquallyOptimalWrappings(
std::ostream& stream, const UnwrappedLine& uwline,
const std::vector<FormattedExcerpt>& solutions) {
stream << "Found " << solutions.size()
<< " equally good solutions for the partition: " << uwline
<< std::endl;
for (const auto& solution : solutions) {
stream << Spacer(40, '-') << std::endl << solution.Render() << std::endl;
}
stream << Spacer(40, '=') << std::endl;
}
FitResult FitsOnLine(const UnwrappedLine& uwline,
const BasicFormatStyle& style) {
VLOG(3) << __FUNCTION__;
// Leverage search functionality to compute effective line length of a slice
// of tokens, taking into account minimum spacing requirements.
// Similar to SearchLineWraps, but only calculates by appending tokens until
// a line break is required.
if (uwline.TokensRange().empty()) return {true, 0};
// Initialize on first token.
// This accounts for space consumed by left-indentation.
auto state = std::make_shared<const StateNode>(uwline, style);
while (!state->Done()) {
const auto& token = state->GetNextToken();
// If a line break is required before this token, return false.
if (token.before.break_decision == SpacingOptions::MustWrap) {
return {false, state->current_column};
}
// Append token onto same line while it fits.
state = std::make_shared<StateNode>(state, style, SpacingDecision::Append);
if (state->current_column > style.column_limit) {
return {false, state->current_column};
}
} // while (!state->Done())
// Reached the end of token-range, thus, it fits.
return {true, state->current_column};
}
} // namespace verible