| // 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 |