mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 20:50:13 +01:00
Replaces several quadratic-time steps in the algorithm with linear or n log n implementations. Worst-case runtime before hitting the complexity bailout drops from 25ms to about 12ms, and typical runtime is single-digit ms on my development machine. Assisted-by: Claude Code
2817 lines
114 KiB
C++
2817 lines
114 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "toolchain/lex/mismatched_brackets.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <optional>
|
|
|
|
#include "common/check.h"
|
|
#include "common/hashing.h"
|
|
#include "common/map.h"
|
|
#include "llvm/ADT/BitVector.h"
|
|
#include "llvm/ADT/BitmaskEnum.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/STLFunctionalExtras.h"
|
|
#include "llvm/ADT/Sequence.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "toolchain/base/index_base.h"
|
|
|
|
namespace Carbon::Lex {
|
|
|
|
LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
|
|
|
|
// Maximum number of collapsed items in a damaged region before falling back to
|
|
// naive greedy recovery. The beam search below is linear in the region size,
|
|
// so this is mostly a defense against pathological inputs.
|
|
constexpr int32_t MaxRegionItemsForSearch = 1500;
|
|
|
|
// Layered beam search width limit.
|
|
constexpr size_t MaxBeamWidth = 16;
|
|
|
|
// Maximum stack depth allowed during search before capping.
|
|
constexpr size_t MaxSearchStackDepth = 12;
|
|
|
|
// Maximum number of distinct optimal repair paths enumerated when checking
|
|
// whether the optimal repairs agree about each correction. Paths beyond this
|
|
// are not examined, so a correction they alone would dispute stays untied.
|
|
constexpr size_t MaxOptimalPaths = 100;
|
|
|
|
// The cost model. All costs are relative; the search finds the cheapest way
|
|
// to make the whole region well-bracketed, and the resulting insertions and
|
|
// replacements become the suggested corrections. When several cheapest ways
|
|
// exist, corrections they disagree about are marked tied and downgraded to
|
|
// error replacements, so it's important that the intended repair for a common
|
|
// mistake is *strictly* cheaper than the alternatives.
|
|
//
|
|
// The individual numbers here and in the rule tables below carry no meaning
|
|
// beyond how they order the repairs they price. They were not derived; they
|
|
// were hill-climbed by coordinate descent against `mismatched_brackets_eval`,
|
|
// which deletes brackets from real Carbon source and scores what recovery puts
|
|
// back, including a per-rule precision breakdown. So a cost is best changed the
|
|
// same way it was chosen: adjust it, rerun the evaluation, and keep the change
|
|
// only if the scores improve. Rebalancing is worth doing when the evaluation
|
|
// reports poor precision for a rule, and is required when adding a rule, whose
|
|
// cost only means something relative to the rules it competes with. See
|
|
// /toolchain/docs/lex/mismatched_brackets.md for how to run it.
|
|
//
|
|
// Costs of replacing a real bracket with an error token. These are the
|
|
// "give up on this bracket" fallbacks; a good targeted repair should beat
|
|
// them, and a dubious one should lose to them.
|
|
constexpr int32_t CostReplaceClosing = 30;
|
|
constexpr int32_t CostReplaceOpening = 50;
|
|
|
|
// Costs of inserting a synthetic closing bracket in front of the current
|
|
// token live in the `CloserRules` table below, which is keyed by how strongly
|
|
// the context suggests the group ends here. These few are shared with the
|
|
// region-end handling, which isn't part of that table.
|
|
// Closing anything at the end of the file or region.
|
|
constexpr int32_t CostCloseAtEnd = 12;
|
|
constexpr int32_t CostCloseParenAtEnd = 22;
|
|
constexpr int32_t CostCloseStructAtEnd = 20;
|
|
// Closing a paren/square bracket before a mid-line `.` that has whitespace
|
|
// before it: member access is normally written without spaces. Priced below the
|
|
// `Adv_SpacedPeriodInParen` rule in `AdvanceRules` below, which penalizes
|
|
// stepping over such a `.` instead, so that closing here beats closing earlier
|
|
// and leaving the spaced `.` unexplained.
|
|
constexpr int32_t CostCloseParenBeforeSpacedPeriod = 6;
|
|
// Closing a group at a wide mid-line whitespace gap, which suggests the
|
|
// closer was deleted in the gap.
|
|
constexpr int32_t CostCloseAtWideGap = 6;
|
|
|
|
// Costs of inserting a synthetic opening bracket in front of the current
|
|
// token live in the `OpenerRules` table below, and penalties for advancing
|
|
// over a token in a context where it doesn't belong in `AdvanceRules`.
|
|
|
|
// Penalty for matching a scope `}` whose indentation doesn't match its
|
|
// opener's header, when they're on different lines.
|
|
constexpr int32_t CostBraceIndentMismatchBase = 30;
|
|
constexpr int32_t CostBraceIndentMismatchPerColumn = 20;
|
|
|
|
namespace {
|
|
|
|
// A contextual cue a rule can test, beyond the bucket's context category and
|
|
// token kind. These are bit indexes into a `CueSet`; no cue's value is ever
|
|
// significant, so cues can be added, removed, and reordered freely.
|
|
enum class Cue : uint8_t {
|
|
// Properties of the current token.
|
|
//
|
|
// The current token is the first on its line.
|
|
FirstOnLine,
|
|
// There is whitespace (or a comment) directly before the current token.
|
|
LeadingSpace,
|
|
// The current token is mid-line with two or more bytes of space before it.
|
|
WideGap,
|
|
// The current token is a `{` with struct-literal cues.
|
|
StructBrace,
|
|
// The current token is `else`.
|
|
ElseKeyword,
|
|
|
|
// Properties of the token directly before the current one. All are false at
|
|
// the start of the input.
|
|
//
|
|
// The previous token is value-ending, an adjacency that is illegal before a
|
|
// leaf.
|
|
PrevValueEnding,
|
|
// As `PrevValueEnding`, but also counting `]` and `}`: the previous token
|
|
// ends a value, whether or not a leaf may follow it.
|
|
PrevValueLike,
|
|
// The previous token itself has whitespace before it.
|
|
PrevHasLeadingSpace,
|
|
// The previous token is a keyword that must be followed by `(` (`if`,
|
|
// `while`, `for`, `match`), or one that must be followed by `[` (`forall`).
|
|
PrevKeywordWantsParen,
|
|
PrevKeywordWantsSquare,
|
|
// The previous token is `as`, `->`, or `where`, so what follows it is a type
|
|
// rather than something callable. (Not `=`, which is followed by a value.)
|
|
PrevIntroducesType,
|
|
// The previous token's kind, for the kinds any rule distinguishes. At most
|
|
// one of these holds at a time.
|
|
PrevIsPeriod,
|
|
PrevIsComma,
|
|
// `PrevIsOpenBracket` covers `(` and `[` together, which no rule separates.
|
|
PrevIsOpenBracket,
|
|
PrevIsOpenCurly,
|
|
PrevIsCloseParen,
|
|
PrevIsCloseSquare,
|
|
PrevIsCloseCurly,
|
|
|
|
// Properties of the item directly before the current one. Unlike the `Prev`
|
|
// cues above, which look at the last token of that item, these look at the
|
|
// item as a whole, so they say nothing about a collapsed `(...)` block's
|
|
// contents.
|
|
//
|
|
// The previous item is a leaf.
|
|
PrevItemIsLeaf,
|
|
// The previous item is a name directly following `as` or `->`, so it is a
|
|
// type rather than something callable.
|
|
PrevItemIsTypeName,
|
|
|
|
// Properties of the item, from the analysis of the surrounding tokens.
|
|
//
|
|
// This item is a collapsed well-bracketed block rather than a single token.
|
|
CollapsedBlock,
|
|
// This collapsed block contains a scope `{`.
|
|
ContainsScopeBrace,
|
|
// The current token starts the body of a statement or declaration whose
|
|
// header is complete, and whose header didn't contain a `{`.
|
|
FollowsStatementHeader,
|
|
HeaderHasOpenCurly,
|
|
|
|
// Properties of the innermost open bracket, and of its relationship to the
|
|
// current token. All are false when no bracket is open.
|
|
//
|
|
// The innermost open bracket is a call or index `(`/`[`, rather than one
|
|
// following a keyword such as `if` or `forall`.
|
|
CallParenTop,
|
|
// The innermost open bracket is the token directly before the current one, so
|
|
// closing here would make an empty group.
|
|
AfterOpenTop,
|
|
// The current token is indented no further than the header of the statement
|
|
// or declaration containing the innermost open bracket.
|
|
DedentToHeader,
|
|
// The current token is indented no further than the line of the innermost
|
|
// open bracket itself, as opposed to that of its statement header.
|
|
DedentToOpenerLine,
|
|
// The current token is on a later line than the innermost open bracket.
|
|
NewLineFromTop,
|
|
// The current token is a closer matching an opener further out on the stack,
|
|
// so the innermost group has to close first.
|
|
Cascade,
|
|
|
|
// Properties of the repair this search path has made so far.
|
|
//
|
|
// This path inserted a closer directly before the current token.
|
|
CloserInserted,
|
|
// The closer this path inserted can be directly followed by a leaf (`]` or
|
|
// `}`), so it repairs an otherwise illegal adjacency.
|
|
CloserFixesAdjacency,
|
|
// This path synthesized an opener directly before the current token.
|
|
OpenerHere,
|
|
// This path inserted some bracket directly before the current token, so
|
|
// suspicious whitespace before it is already explained.
|
|
BracketInsertedHere,
|
|
|
|
// Must stay last: it counts the cues.
|
|
Count,
|
|
};
|
|
|
|
// A set of cues.
|
|
enum class CueSet : uint64_t {
|
|
None = 0,
|
|
LLVM_MARK_AS_BITMASK_ENUM(uint64_t{1}
|
|
<< (static_cast<uint8_t>(Cue::Count) - 1))
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The set containing exactly `cues`.
|
|
template <std::same_as<Cue>... CueT>
|
|
static constexpr auto CueSetOf(CueT... cues) -> CueSet {
|
|
return (CueSet::None | ... |
|
|
static_cast<CueSet>(uint64_t{1} << static_cast<int>(cues)));
|
|
}
|
|
|
|
// Convenience synonym for our token kinds.
|
|
using Kind = BracketTokenKind;
|
|
|
|
namespace {
|
|
|
|
// Internal representation of an item after clean subrange collapsing.
|
|
struct Item {
|
|
int32_t token_start_index;
|
|
int32_t token_end_index;
|
|
MismatchedBracketToken token;
|
|
int32_t effective_header_indent = 0;
|
|
// Every boolean property of this item and its neighbours, computed once by
|
|
// `ComputeItemCues`. This is the single record of them: the rule tables match
|
|
// against these (combined with the few cues that depend on the search state),
|
|
// and the search itself tests them through `Has`.
|
|
CueSet cues = CueSet::None;
|
|
|
|
// Whether every one of `wanted` holds for this item.
|
|
template <typename... CueT>
|
|
constexpr auto HasAll(CueT... wanted) const -> bool {
|
|
CueSet wanted_set = CueSetOf(wanted...);
|
|
return (cues & wanted_set) == wanted_set;
|
|
}
|
|
};
|
|
|
|
// The parts of an unclosed opening bracket that make a search state distinct.
|
|
// Compared and hashed as a whole, so everything here must matter to the search:
|
|
// two stacks that agree on these are interchangeable.
|
|
//
|
|
// Fields are ordered so the type has no padding, which
|
|
// `has_unique_object_representations` requires and `Carbon::Map` relies on to
|
|
// hash and compare every byte.
|
|
struct OpenBracketKey {
|
|
// The real opener token, or None for a synthetic opener.
|
|
TokenIndex token_index = TokenIndex::None;
|
|
// For a synthetic opener, where it would be inserted.
|
|
TokenIndex insertion_token_index = TokenIndex::None;
|
|
// Index of the real opener in the input token array, or -1 if synthetic.
|
|
int32_t token_pos = -1;
|
|
int32_t line = -1;
|
|
// Indentation of the line containing the opener.
|
|
int32_t line_indent = 0;
|
|
// Indentation of the start of the statement or declaration containing the
|
|
// opener.
|
|
int32_t effective_header_indent = 0;
|
|
BracketTokenKind kind;
|
|
bool is_synthetic = false;
|
|
bool is_struct_brace = false;
|
|
// Whether this is a paren/square bracket directly following a value-ending
|
|
// token (a call or index), rather than following a keyword like `if`. This is
|
|
// a function of the opener token, so comparing it is redundant with
|
|
// `token_index` for a real opener, and it is always false for a synthetic
|
|
// one; it's included only because excluding it would cost more than it saves.
|
|
bool is_call_paren = false;
|
|
|
|
friend auto operator==(const OpenBracketKey& lhs, const OpenBracketKey& rhs)
|
|
-> bool = default;
|
|
};
|
|
|
|
static_assert(std::has_unique_object_representations_v<OpenBracketKey>,
|
|
"Padding would leave indeterminate bytes for hashing.");
|
|
|
|
// An unclosed opening bracket on the search stack, plus the bookkeeping that
|
|
// says nothing about which state this is.
|
|
struct OpenBracketInfo : OpenBracketKey {
|
|
// For a synthetic opener, the name of the rule that proposed it, for
|
|
// debugging. Two openers that differ only in which rule proposed them are the
|
|
// same state, so this is deliberately outside the key.
|
|
llvm::StringLiteral rule_name = "";
|
|
};
|
|
|
|
// Names a stack of open brackets held in an `OpenStackStore`.
|
|
struct OpenStackId : public IdBase<OpenStackId> {
|
|
// The empty stack, which every search starts from. Interning gives it no
|
|
// cell, so it needs an id of its own.
|
|
static const OpenStackId Empty;
|
|
|
|
using IdBase::IdBase;
|
|
};
|
|
|
|
inline constexpr OpenStackId OpenStackId::Empty = OpenStackId(NoneIndex);
|
|
|
|
// What distinguishes one stack from another: the bracket on top, and the stack
|
|
// below it. Only the `OpenBracketKey` part of the bracket takes part, since
|
|
// that is all that distinguishes one search state from another.
|
|
struct OpenStackCellKey {
|
|
OpenStackId parent;
|
|
OpenBracketKey entry;
|
|
|
|
friend auto operator==(const OpenStackCellKey& lhs,
|
|
const OpenStackCellKey& rhs) -> bool = default;
|
|
};
|
|
|
|
static_assert(std::has_unique_object_representations_v<OpenStackCellKey>,
|
|
"Padding would leave indeterminate bytes for hashing.");
|
|
|
|
// Holds the open-bracket stacks the search reaches.
|
|
//
|
|
// A stack only ever changes at the top, so it is stored as the bracket on top
|
|
// plus the id of the stack below it, which lets the many states sharing a
|
|
// prefix share its cells instead of each carrying a copy. Equal stacks are
|
|
// interned to one id, so comparing two stacks is comparing two integers, and a
|
|
// `BeamNode` holds no bracket storage of its own — both of which matter, since
|
|
// the search creates far more states than it keeps and never frees them.
|
|
//
|
|
// Each cell also carries the answers to the questions the rules ask about the
|
|
// whole stack, so that asking them doesn't mean walking it.
|
|
class OpenStackStore {
|
|
public:
|
|
// The stack that is `stack` with `entry` pushed onto it.
|
|
auto Push(OpenStackId stack, const OpenBracketInfo& entry) -> OpenStackId {
|
|
auto result =
|
|
intern_.Insert(OpenStackCellKey{.parent = stack, .entry = entry},
|
|
OpenStackId(cells_.size()));
|
|
if (result.is_inserted()) {
|
|
cells_.push_back({
|
|
.parent = stack,
|
|
.entry = entry,
|
|
.depth = Depth(stack) + 1,
|
|
.open_kinds =
|
|
static_cast<uint8_t>(OpenKinds(stack) | KindBit(entry.kind)),
|
|
.has_synthetic = HasSynthetic(stack) || entry.is_synthetic,
|
|
});
|
|
}
|
|
return result.value();
|
|
}
|
|
|
|
// The stack below the top of `stack`, which must not be empty.
|
|
auto Pop(OpenStackId stack) const -> OpenStackId {
|
|
return cells_[stack.index].parent;
|
|
}
|
|
|
|
// The bracket on top of `stack`, which must not be empty.
|
|
auto Top(OpenStackId stack) const -> const OpenBracketInfo& {
|
|
return cells_[stack.index].entry;
|
|
}
|
|
|
|
auto IsEmpty(OpenStackId stack) const -> bool {
|
|
return stack == OpenStackId::Empty;
|
|
}
|
|
|
|
auto Depth(OpenStackId stack) const -> int32_t {
|
|
return IsEmpty(stack) ? 0 : cells_[stack.index].depth;
|
|
}
|
|
|
|
// Whether any bracket in `stack` is synthetic.
|
|
auto HasSynthetic(OpenStackId stack) const -> bool {
|
|
return !IsEmpty(stack) && cells_[stack.index].has_synthetic;
|
|
}
|
|
|
|
// Whether `stack` holds an opener of kind `kind`.
|
|
auto Contains(OpenStackId stack, BracketTokenKind kind) const -> bool {
|
|
return !IsEmpty(stack) &&
|
|
(cells_[stack.index].open_kinds & KindBit(kind)) != 0;
|
|
}
|
|
|
|
private:
|
|
struct Cell {
|
|
OpenStackId parent;
|
|
OpenBracketInfo entry;
|
|
int32_t depth;
|
|
// The kinds of every bracket in this stack, as `KindBit`s.
|
|
uint8_t open_kinds;
|
|
bool has_synthetic;
|
|
};
|
|
|
|
static auto KindBit(BracketTokenKind kind) -> uint8_t {
|
|
return static_cast<uint8_t>(1 << static_cast<int>(kind));
|
|
}
|
|
|
|
auto OpenKinds(OpenStackId stack) const -> uint8_t {
|
|
return IsEmpty(stack) ? 0 : cells_[stack.index].open_kinds;
|
|
}
|
|
|
|
llvm::SmallVector<Cell, 0> cells_;
|
|
Map<OpenStackCellKey, OpenStackId> intern_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The `{` opening the branch that the first-on-line `else` at `else_index`
|
|
// continues. Such an `else` should have been preceded by the `}` closing that
|
|
// branch, so with the `}` missing the `else` is still inside it: the brace is
|
|
// the innermost one that nothing has closed yet. Returns -1 if there is no
|
|
// enclosing brace.
|
|
static auto FindBranchBrace(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
int32_t else_index) -> int32_t {
|
|
int32_t depth = 0;
|
|
for (int32_t j : llvm::reverse(llvm::seq(0, else_index))) {
|
|
if (tokens[j].kind == Kind::CloseCurlyBrace) {
|
|
++depth;
|
|
} else if (tokens[j].kind == Kind::OpenCurlyBrace) {
|
|
if (depth == 0) {
|
|
return j;
|
|
}
|
|
--depth;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Computes the associated line indentation for a token by scanning backwards,
|
|
// skipping matched parens/brackets, looking for a statement introducer.
|
|
static auto GetOuterStatementIntroducerIndent(
|
|
llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> match_partner, int32_t j) -> int32_t {
|
|
int32_t result_indent = tokens[j].line_indent;
|
|
while (j > 0) {
|
|
// An `else` continues the statement its `if` introduced, and the `}`
|
|
// closing its block lines up with that `if`, so keep walking out from the
|
|
// brace of the branch before it. A first-on-line `else`'s own column says
|
|
// nothing about where the statement starts: the `}` that should precede it
|
|
// is missing, so the `else` sits wherever the author left it.
|
|
if (tokens[j].is_else_keyword && tokens[j].line != tokens[j - 1].line) {
|
|
int32_t brace = FindBranchBrace(tokens, j);
|
|
if (brace > 0) {
|
|
j = brace;
|
|
result_indent = tokens[j].line_indent;
|
|
continue;
|
|
}
|
|
}
|
|
int32_t p = j - 1;
|
|
if ((tokens[p].kind == Kind::CloseParen ||
|
|
tokens[p].kind == Kind::CloseSquareBracket) &&
|
|
match_partner[p] != -1 && match_partner[p] < p) {
|
|
p = match_partner[p];
|
|
if (p <= 0) {
|
|
break;
|
|
}
|
|
--p;
|
|
}
|
|
if (tokens[p].kind == Kind::StatementIntroducer) {
|
|
result_indent = tokens[p].line_indent;
|
|
// Keep walking out only while the introducers stack up on one line or
|
|
// dedent; an indented token belongs to this introducer's body.
|
|
if (tokens[j].line == tokens[p].line ||
|
|
tokens[j].line_indent <= tokens[p].line_indent) {
|
|
j = p;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return result_indent;
|
|
}
|
|
|
|
// Computes, for every token, the indentation that its statement or declaration
|
|
// starts at. This is what a `{` opened by that statement should line its `}` up
|
|
// with, which is often not the indentation of the `{` itself.
|
|
//
|
|
// The answer for one token is a walk backwards to the start of the statement it
|
|
// belongs to: it steps over tokens, jumps over matched bracket pairs, stops at
|
|
// a `;` or an unmatched brace, and answers with the indentation of the last
|
|
// token it stepped on, or of the enclosing statement introducer if it reaches
|
|
// one.
|
|
//
|
|
// Performing a separate walk from every token would be quadratic, so dynamic
|
|
// programming is used to compute all of the walks in one pass. Every step of a
|
|
// walk depends only on the token it is standing on, so the walk from token `j`
|
|
// is one step followed by the walk from some earlier token, and its result can
|
|
// be computed from a table of the earlier walks' results: `walk_result[j]`
|
|
// holds the result of the walk starting at token `j`, or `NeedsCaller` when
|
|
// that walk stops before it steps on anything, in which case the result is the
|
|
// caller's own indentation. Each entry depends only on entries at lower
|
|
// indices, so a single forward pass fills the table, and the answer for token
|
|
// `i` is read from the entry for `i - 1`.
|
|
static auto ComputeAssociatedLineIndents(
|
|
llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> match_partner) -> llvm::SmallVector<int32_t> {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
|
|
// A walk that stopped without stepping on a token, so it has no indentation
|
|
// of its own to report.
|
|
constexpr int32_t NeedsCaller = std::numeric_limits<int32_t>::min();
|
|
|
|
llvm::SmallVector<int32_t> walk_result(num_tokens, NeedsCaller);
|
|
// The walk from `j` continues at `next`, so it answers what that walk
|
|
// answers, falling back to `indent` when that walk has nothing to report.
|
|
auto continue_walk = [&](int32_t next, int32_t indent) {
|
|
return next >= 0 && walk_result[next] != NeedsCaller ? walk_result[next]
|
|
: indent;
|
|
};
|
|
|
|
for (int32_t j : llvm::seq(0, num_tokens)) {
|
|
auto kind = tokens[j].kind;
|
|
|
|
// A matched closer jumps to its opener, which the walk passes over without
|
|
// testing it, and continues from the token before it.
|
|
if (IsClosingBracket(kind) && match_partner[j] != -1 &&
|
|
match_partner[j] < j) {
|
|
int32_t open = match_partner[j];
|
|
walk_result[j] = continue_walk(open - 1, tokens[open].line_indent);
|
|
continue;
|
|
}
|
|
|
|
// The statement ends here, so the walk stops without stepping on this
|
|
// token. `walk_result[j]` stays `NeedsCaller`.
|
|
if (kind == Kind::Semi || kind == Kind::OpenCurlyBrace ||
|
|
kind == Kind::CloseCurlyBrace) {
|
|
continue;
|
|
}
|
|
|
|
// The statement introducer this token belongs to answers for it directly.
|
|
if (kind == Kind::StatementIntroducer) {
|
|
walk_result[j] =
|
|
GetOuterStatementIntroducerIndent(tokens, match_partner, j);
|
|
continue;
|
|
}
|
|
|
|
walk_result[j] = continue_walk(j - 1, tokens[j].line_indent);
|
|
}
|
|
|
|
llvm::SmallVector<int32_t> indents(num_tokens, 0);
|
|
for (int32_t i : llvm::seq(0, num_tokens)) {
|
|
if (tokens[i].kind == Kind::FileEnd) {
|
|
continue;
|
|
}
|
|
indents[i] = continue_walk(i - 1, tokens[i].line_indent);
|
|
}
|
|
return indents;
|
|
}
|
|
|
|
// Determines if a token follows a statement/declaration header, so that a
|
|
// scope `{` could naturally be inserted directly before it. Only tokens that
|
|
// could start a body are considered: the first token on a line, a statement
|
|
// introducer (e.g. `return` in `if (c) return;`), or a token directly
|
|
// following a `)`/`]` that ends a header.
|
|
static auto ComputeFollowsStatementHeader(
|
|
llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> match_partner, int32_t token_index) -> bool {
|
|
if (token_index <= 0 || token_index >= static_cast<int32_t>(tokens.size())) {
|
|
return false;
|
|
}
|
|
auto curr_kind = tokens[token_index].kind;
|
|
if (curr_kind == Kind::Semi || curr_kind == Kind::OpenCurlyBrace ||
|
|
IsClosingBracket(curr_kind) || curr_kind == Kind::FileEnd) {
|
|
return false;
|
|
}
|
|
// Only tokens that could start a body are considered: the first token on a
|
|
// line, a statement introducer (`if (c) return;`), or a token directly
|
|
// after a `)` ending a header. (Not after `]`: declaration headers always
|
|
// continue after implicit parameter lists and `forall` clauses.)
|
|
bool is_first_on_line =
|
|
tokens[token_index].line != tokens[token_index - 1].line;
|
|
auto prev_kind = tokens[token_index - 1].kind;
|
|
if (!is_first_on_line && curr_kind != Kind::StatementIntroducer &&
|
|
prev_kind != Kind::CloseParen) {
|
|
return false;
|
|
}
|
|
// A body can't start with an operator like `as` or `==`, or with a `.`
|
|
// designator (a `where`-clause continuation line).
|
|
if (IsStructuralOpKind(curr_kind) || curr_kind == Kind::ComparisonOp ||
|
|
curr_kind == Kind::Period) {
|
|
return false;
|
|
}
|
|
for (int32_t j = token_index - 1; j >= 0; --j) {
|
|
auto kind = tokens[j].kind;
|
|
if ((kind == Kind::CloseParen || kind == Kind::CloseSquareBracket) &&
|
|
match_partner[j] != -1 && match_partner[j] < j) {
|
|
j = match_partner[j];
|
|
continue;
|
|
}
|
|
if (kind == Kind::Semi || kind == Kind::OpenCurlyBrace ||
|
|
kind == Kind::CloseCurlyBrace || kind == Kind::OpenParen ||
|
|
kind == Kind::OpenSquareBracket) {
|
|
return false;
|
|
}
|
|
if (kind == Kind::StatementIntroducer) {
|
|
if (curr_kind == Kind::StatementIntroducer) {
|
|
if (tokens[token_index].line == tokens[j].line) {
|
|
// On the same line, a chain of adjacent introducers (`private fn`)
|
|
// is a single header; but with other tokens in between, this is a
|
|
// body after a single-line header (`if (c) return x;`).
|
|
bool all_introducers = true;
|
|
for (int32_t k = j + 1; k < token_index; ++k) {
|
|
if (tokens[k].kind != Kind::StatementIntroducer) {
|
|
all_introducers = false;
|
|
break;
|
|
}
|
|
}
|
|
if (all_introducers) {
|
|
return false;
|
|
}
|
|
} else if (tokens[token_index].line_indent <= tokens[j].line_indent) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Determines if the statement/declaration header starting at token_index
|
|
// contains an OpenCurlyBrace.
|
|
static auto ComputeHeaderHasOpenCurlyBrace(
|
|
llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> match_partner, int32_t token_index) -> bool {
|
|
if (token_index <= 0 || token_index >= static_cast<int32_t>(tokens.size())) {
|
|
return false;
|
|
}
|
|
for (int32_t j = token_index; j < static_cast<int32_t>(tokens.size()); ++j) {
|
|
auto kind = tokens[j].kind;
|
|
if (kind == Kind::OpenCurlyBrace) {
|
|
return true;
|
|
}
|
|
if ((kind == Kind::OpenParen || kind == Kind::OpenSquareBracket) &&
|
|
match_partner[j] != -1 && match_partner[j] > j) {
|
|
j = match_partner[j];
|
|
continue;
|
|
}
|
|
if (kind == Kind::Semi || kind == Kind::CloseCurlyBrace ||
|
|
kind == Kind::FileEnd || kind == Kind::StatementIntroducer) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct ParentEdge {
|
|
int32_t parent_node_index;
|
|
BracketCorrection correction;
|
|
bool has_correction = false;
|
|
};
|
|
|
|
// Node in the beam search tree.
|
|
//
|
|
// The search creates far more nodes than any one layer keeps, and never frees
|
|
// them, so how big a node is decides how much of the search is spent waiting on
|
|
// memory rather than deciding anything. That is why its stack is an id into a
|
|
// shared `OpenStackStore` rather than a stack of its own, and why
|
|
// `parent_edges` has inline room for the one edge a node usually has rather
|
|
// than for the worst case.
|
|
struct BeamNode {
|
|
int32_t item_index;
|
|
OpenStackId stack;
|
|
int32_t cost;
|
|
// The kind of synthetic closer inserted directly before the current item,
|
|
// or Other if none; an inserted closer repairs illegal adjacency with the
|
|
// preceding token.
|
|
Kind closer_inserted = Kind::Other;
|
|
llvm::SmallVector<ParentEdge, 1> parent_edges;
|
|
};
|
|
|
|
// The parts of a `BeamNode` the search reads while expanding it. A copy — not a
|
|
// reference — is needed because expanding a node appends to the node list,
|
|
// which may reallocate.
|
|
struct SearchState {
|
|
OpenStackId stack;
|
|
int32_t cost;
|
|
Kind closer_inserted;
|
|
};
|
|
|
|
// What makes a search state distinct: the open-bracket stack, plus which
|
|
// closer, if any, was just inserted before the current token. Two nodes in a
|
|
// layer that agree on this are interchangeable and get merged, which is what
|
|
// `RegionSearch::layer_dedup_` is keyed on. Stacks are interned, so this is two
|
|
// integers.
|
|
struct StateKey {
|
|
OpenStackId stack;
|
|
Kind closer_inserted;
|
|
|
|
friend auto operator==(const StateKey& lhs, const StateKey& rhs)
|
|
-> bool = default;
|
|
|
|
friend auto CarbonHashValue(const StateKey& key, uint64_t seed) -> HashCode {
|
|
Hasher hasher(seed);
|
|
hasher.Hash(key.stack, key.closer_inserted);
|
|
return static_cast<HashCode>(hasher);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static auto Snapshot(const BeamNode& node) -> SearchState {
|
|
return {node.stack, node.cost, node.closer_inserted};
|
|
}
|
|
|
|
// A correction that replaces a bracket token with an error token (the "give
|
|
// up on this bracket" repair).
|
|
static auto ReplaceWithError(const MismatchedBracketToken& token,
|
|
BracketDiagnosticKind diagnostic_kind,
|
|
llvm::StringLiteral rule_name)
|
|
-> BracketCorrection {
|
|
return BracketCorrection{
|
|
.diagnostic_kind = diagnostic_kind,
|
|
.diagnostic_token_index = token.token_index,
|
|
.fix_action = BracketFixAction::ReplaceWithError,
|
|
.fix_token_index = token.token_index,
|
|
.fix_token_kind = ToTokenKind(token.kind),
|
|
.rule_name = rule_name,
|
|
};
|
|
}
|
|
|
|
// Solve a damaged region using the simple greedy fallback algorithm.
|
|
static auto SolveNaive(llvm::ArrayRef<Item> items,
|
|
llvm::SmallVectorImpl<BracketCorrection>& corrections)
|
|
-> void {
|
|
llvm::SmallVector<MismatchedBracketToken> open_stack;
|
|
for (const auto& item : items) {
|
|
if (item.HasAll(Cue::CollapsedBlock)) {
|
|
continue;
|
|
}
|
|
auto kind = item.token.kind;
|
|
if (kind == Kind::Semi || kind == Kind::StatementIntroducer ||
|
|
kind == Kind::OpenCurlyBrace || kind == Kind::FileEnd) {
|
|
while (!open_stack.empty() &&
|
|
(open_stack.back().kind == Kind::OpenParen ||
|
|
open_stack.back().kind == Kind::OpenSquareBracket)) {
|
|
corrections.push_back(ReplaceWithError(
|
|
open_stack.pop_back_val(), BracketDiagnosticKind::UnmatchedOpening,
|
|
"Naive_UnclosedParenBracket"));
|
|
}
|
|
}
|
|
|
|
if (IsOpeningBracket(kind)) {
|
|
open_stack.push_back(item.token);
|
|
} else if (IsClosingBracket(kind)) {
|
|
auto search_range = llvm::reverse(open_stack);
|
|
size_t lookback = 0;
|
|
auto match_it = search_range.end();
|
|
for (auto it = search_range.begin();
|
|
it != search_range.end() && lookback < 16; ++it, ++lookback) {
|
|
if (MatchingClosingKind(it->kind) == kind) {
|
|
match_it = it;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (match_it == search_range.end()) {
|
|
corrections.push_back(ReplaceWithError(
|
|
item.token, BracketDiagnosticKind::UnmatchedClosing,
|
|
"Naive_UnmatchedClosing"));
|
|
} else {
|
|
for (auto it = search_range.begin(); it != match_it; ++it) {
|
|
corrections.push_back(
|
|
ReplaceWithError(*it, BracketDiagnosticKind::UnmatchedOpening,
|
|
"Naive_PoppedOpener"));
|
|
}
|
|
open_stack.erase(match_it.base() - 1, open_stack.end());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& open : llvm::reverse(open_stack)) {
|
|
corrections.push_back(ReplaceWithError(
|
|
open, BracketDiagnosticKind::UnmatchedOpening, "Naive_UnclosedAtEnd"));
|
|
}
|
|
}
|
|
|
|
// Whether two parent edges represent the same predecessor and the same repair,
|
|
// so that keeping both would be a redundant duplicate.
|
|
static auto EdgesEqual(const ParentEdge& a, const ParentEdge& b) -> bool {
|
|
if (a.parent_node_index != b.parent_node_index ||
|
|
a.has_correction != b.has_correction) {
|
|
return false;
|
|
}
|
|
return !a.has_correction ||
|
|
(a.correction.diagnostic_token_index ==
|
|
b.correction.diagnostic_token_index &&
|
|
a.correction.fix_action == b.correction.fix_action &&
|
|
a.correction.fix_token_index == b.correction.fix_token_index &&
|
|
a.correction.fix_token_kind == b.correction.fix_token_kind);
|
|
}
|
|
|
|
// Returns true if `kind`'s matching opener appears in `stack` below the top.
|
|
static auto MatchesDeeperOpener(const OpenStackStore& stacks, OpenStackId stack,
|
|
Kind closing_kind) -> bool {
|
|
if (!IsClosingBracket(closing_kind) || stacks.Depth(stack) < 2) {
|
|
return false;
|
|
}
|
|
return stacks.Contains(stacks.Pop(stack), MatchingOpeningKind(closing_kind));
|
|
}
|
|
|
|
// Whether the token before the current one ends a value. Unlike
|
|
// `IsValueEndingKind`, which the leaf-adjacency rules use, this also counts `]`
|
|
// and `}`, which end a value but can be followed by a leaf. `prev_token` is
|
|
// null at the start of the input.
|
|
static auto PrevIsValueLike(const MismatchedBracketToken* prev_token) -> bool {
|
|
return prev_token != nullptr &&
|
|
(IsValueEndingKind(prev_token->kind) ||
|
|
prev_token->kind == Kind::CloseSquareBracket ||
|
|
prev_token->kind == Kind::CloseCurlyBrace);
|
|
}
|
|
|
|
// Whether the top of `stack` is a synthetic opener inserted directly before
|
|
// `item` — i.e. this path just opened a bracket at this position.
|
|
static auto OpenerSynthesizedHere(const OpenStackStore& stacks,
|
|
OpenStackId stack, const Item& item) -> bool {
|
|
return !stacks.IsEmpty(stack) && stacks.Top(stack).is_synthetic &&
|
|
stacks.Top(stack).insertion_token_index == item.token.token_index;
|
|
}
|
|
|
|
// Whether a `]` or `}` was inserted directly before the current token. Such a
|
|
// closer repairs an illegal leaf adjacency (unlike `)`, these don't end a
|
|
// value). `Other` means no closer was inserted here.
|
|
static auto CloserFixesLeafAdjacency(Kind closer_inserted) -> bool {
|
|
return closer_inserted == Kind::CloseSquareBracket ||
|
|
closer_inserted == Kind::CloseCurlyBrace;
|
|
}
|
|
|
|
// The bracket-insertion rules below are expressed as data: each rule states
|
|
// the context it applies in and what that context costs, and the rules are
|
|
// tried in order so that the first (most specific) match wins. Two small
|
|
// categorical facts — a context category and the kind of the current token —
|
|
// form a bucket index, and a constexpr-built table maps each bucket to the
|
|
// bit-set of rules that can possibly apply there, so a lookup only tests the
|
|
// handful of rules relevant to the situation.
|
|
|
|
// Context categories for the closer and advance tables: the category of the
|
|
// innermost open bracket. `Struct` and `Scope` distinguish the two kinds of
|
|
// `{`. Each category has an index, which selects its row of bucket entries, and
|
|
// a bit, which is how a rule names it — so a rule can apply to several
|
|
// categories at once.
|
|
namespace {
|
|
|
|
namespace Top {
|
|
enum Index : int32_t {
|
|
ParenIndex,
|
|
SquareIndex,
|
|
StructIndex,
|
|
ScopeIndex,
|
|
// No open bracket at all. Only the advance table, which also scores tokens
|
|
// at the top level, uses this.
|
|
NoneIndex,
|
|
Count,
|
|
};
|
|
constexpr uint8_t Paren = 1 << ParenIndex;
|
|
constexpr uint8_t Square = 1 << SquareIndex;
|
|
constexpr uint8_t Struct = 1 << StructIndex;
|
|
constexpr uint8_t Scope = 1 << ScopeIndex;
|
|
constexpr uint8_t None = 1 << NoneIndex;
|
|
constexpr uint8_t ParenLike = Paren | Square;
|
|
constexpr uint8_t Any = ParenLike | Struct | Scope | None;
|
|
} // namespace Top
|
|
|
|
// Context categories for the opener table: which bracket would be inserted.
|
|
namespace Ins {
|
|
enum Index : int32_t {
|
|
ParenIndex,
|
|
SquareIndex,
|
|
ScopeBraceIndex,
|
|
StructBraceIndex,
|
|
Count,
|
|
};
|
|
constexpr uint8_t Paren = 1 << ParenIndex;
|
|
constexpr uint8_t Square = 1 << SquareIndex;
|
|
constexpr uint8_t ScopeBrace = 1 << ScopeBraceIndex;
|
|
constexpr uint8_t StructBrace = 1 << StructBraceIndex;
|
|
constexpr uint8_t ParenLike = Paren | Square;
|
|
} // namespace Ins
|
|
|
|
} // namespace
|
|
|
|
// Bucket rows are shared by all the tables, so there must be room for whichever
|
|
// set of context categories is larger.
|
|
constexpr int32_t NumContextCategories =
|
|
std::max<int32_t>(Top::Count, Ins::Count);
|
|
|
|
// The category of the innermost open bracket, which is always a real bracket.
|
|
static auto TopCategoryOf(const OpenBracketInfo& top) -> int32_t {
|
|
switch (top.kind) {
|
|
case Kind::OpenParen:
|
|
return Top::ParenIndex;
|
|
case Kind::OpenSquareBracket:
|
|
return Top::SquareIndex;
|
|
default:
|
|
return top.is_struct_brace ? Top::StructIndex : Top::ScopeIndex;
|
|
}
|
|
}
|
|
|
|
// As `TopCategoryOf`, but for a stack that may be empty.
|
|
static auto TopCategoryOfStack(const OpenStackStore& stacks, OpenStackId stack)
|
|
-> int32_t {
|
|
return stacks.IsEmpty(stack) ? Top::NoneIndex
|
|
: TopCategoryOf(stacks.Top(stack));
|
|
}
|
|
|
|
// The category of a synthetic opener of kind `kind`.
|
|
static auto InsCategoryOf(Kind kind, bool is_struct_brace) -> int32_t {
|
|
switch (kind) {
|
|
case Kind::OpenParen:
|
|
return Ins::ParenIndex;
|
|
case Kind::OpenSquareBracket:
|
|
return Ins::SquareIndex;
|
|
default:
|
|
return is_struct_brace ? Ins::StructBraceIndex : Ins::ScopeBraceIndex;
|
|
}
|
|
}
|
|
|
|
// Number of distinct `Kind` values.
|
|
constexpr int32_t NumKinds = static_cast<int32_t>(Kind::Other) + 1;
|
|
|
|
// A set of token kinds a rule can apply to. `Kind` values are the
|
|
// bit indexes, so there are no per-kind enumerators here to keep in sync with
|
|
// them; `Rule` takes the kinds themselves.
|
|
namespace {
|
|
|
|
enum class KindSet : uint32_t {
|
|
None = 0,
|
|
LLVM_MARK_AS_BITMASK_ENUM(uint32_t{1} << (NumKinds - 1))
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The set containing exactly `kinds`.
|
|
template <std::same_as<Kind>... KindT>
|
|
static constexpr auto KindSetOf(KindT... kinds) -> KindSet {
|
|
return (KindSet::None | ... |
|
|
static_cast<KindSet>(uint32_t{1} << static_cast<int>(kinds)));
|
|
}
|
|
|
|
// Sets of kinds that name a concept more than one rule shares. A rule wanting
|
|
// just one or two particular kinds names them directly instead.
|
|
namespace Kinds {
|
|
|
|
// Every kind: the default, for a rule that doesn't care.
|
|
constexpr KindSet Any = ~KindSet::None;
|
|
|
|
// A `(` or `[`, opening or closing: the bracket kinds written without a
|
|
// preceding space in formatted code.
|
|
constexpr KindSet OpenGroup =
|
|
KindSetOf(Kind::OpenParen, Kind::OpenSquareBracket);
|
|
constexpr KindSet CloseGroup =
|
|
KindSetOf(Kind::CloseParen, Kind::CloseSquareBracket);
|
|
|
|
// Any opening bracket, and everything that isn't one.
|
|
constexpr KindSet Opener = OpenGroup | KindSetOf(Kind::OpenCurlyBrace);
|
|
constexpr KindSet NonOpener = ~Opener;
|
|
|
|
// The statement-structuring operators together.
|
|
constexpr KindSet AnyStructural =
|
|
KindSetOf(Kind::Assignment, Kind::As, Kind::StructuralOp);
|
|
|
|
// A binary connector, which needs a left operand and so can't start a group.
|
|
constexpr KindSet BinaryConnector =
|
|
AnyStructural | KindSetOf(Kind::ComparisonOp);
|
|
|
|
// Everything that carries no bracket structure of its own: `Other` plus the
|
|
// operator and keyword classifications split out of it.
|
|
constexpr KindSet AnyOther =
|
|
AnyStructural |
|
|
KindSetOf(Kind::Other, Kind::ComparisonOp, Kind::ModifierKeyword);
|
|
|
|
// A token that could start a bracketed group.
|
|
constexpr KindSet GroupStarter =
|
|
Opener | KindSetOf(Kind::Leaf, Kind::Period, Kind::ModifierKeyword);
|
|
|
|
// A leaf, or a binding modifier keyword, which like a leaf can't directly
|
|
// follow a value-ending token.
|
|
constexpr KindSet LeafLike = KindSetOf(Kind::Leaf, Kind::ModifierKeyword);
|
|
|
|
// The kinds a dedent penalty can apply to: a closer is expected to dedent, and
|
|
// `FileEnd` isn't content.
|
|
constexpr KindSet Dedentable =
|
|
~(CloseGroup | KindSetOf(Kind::CloseCurlyBrace, Kind::FileEnd));
|
|
|
|
} // namespace Kinds
|
|
|
|
// A rule's `cost` when the move should not be considered at all.
|
|
constexpr int32_t DeclineCost = -1;
|
|
|
|
// A rule in a bracket-insertion table. The rule applies when the context
|
|
// category is in `ctx`, the current token's kind is in `kinds`, and all four
|
|
// cue conditions hold. Rules are tried in order and the first match wins, so
|
|
// earlier rules express stronger, more specific cues.
|
|
namespace {
|
|
|
|
struct BracketRule {
|
|
// Bit-set of context categories: `Top::` for the closer table (the category
|
|
// of the innermost open bracket), `Ins::` for the opener table (which bracket
|
|
// would be inserted).
|
|
uint8_t ctx;
|
|
// The token kinds this rule applies to.
|
|
KindSet kinds = Kinds::Any;
|
|
// Every cue listed here must hold.
|
|
CueSet when = CueSet::None;
|
|
// No cue listed here may hold.
|
|
CueSet unless = CueSet::None;
|
|
// The cues listed here must not all hold together.
|
|
CueSet not_all = CueSet::None;
|
|
// At least one cue listed here must hold, if any are listed.
|
|
CueSet any_of = CueSet::None;
|
|
// Cost of the insertion this rule proposes, or `DeclineCost`.
|
|
int32_t cost;
|
|
// See `BracketCorrection::rule_name`.
|
|
llvm::StringLiteral rule_name = "";
|
|
|
|
// Builders, so a rule reads as one sentence. Each returns an updated copy,
|
|
// and `Cost` or `Decline` finishes the rule.
|
|
template <typename... CueT>
|
|
constexpr auto When(CueT... cues) const -> BracketRule {
|
|
auto result = *this;
|
|
result.when = CueSetOf(cues...);
|
|
return result;
|
|
}
|
|
template <typename... CueT>
|
|
constexpr auto Unless(CueT... cues) const -> BracketRule {
|
|
auto result = *this;
|
|
result.unless = CueSetOf(cues...);
|
|
return result;
|
|
}
|
|
template <typename... CueT>
|
|
constexpr auto NotAll(CueT... cues) const -> BracketRule {
|
|
auto result = *this;
|
|
result.not_all = CueSetOf(cues...);
|
|
return result;
|
|
}
|
|
template <typename... CueT>
|
|
constexpr auto AnyOf(CueT... cues) const -> BracketRule {
|
|
auto result = *this;
|
|
result.any_of = CueSetOf(cues...);
|
|
return result;
|
|
}
|
|
constexpr auto Cost(int32_t rule_cost, llvm::StringLiteral name) const
|
|
-> BracketRule {
|
|
auto result = *this;
|
|
result.cost = rule_cost;
|
|
result.rule_name = name;
|
|
return result;
|
|
}
|
|
constexpr auto Decline() const -> BracketRule {
|
|
auto result = *this;
|
|
result.cost = DeclineCost;
|
|
return result;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Starts a rule that applies to context categories `ctx` and token kinds
|
|
// `kinds`.
|
|
static constexpr auto Rule(uint8_t ctx, KindSet kinds = Kinds::Any)
|
|
-> BracketRule {
|
|
return BracketRule{.ctx = ctx, .kinds = kinds, .cost = DeclineCost};
|
|
}
|
|
|
|
// As above, for a rule that applies to a few particular kinds. Taking the first
|
|
// kind separately keeps the no-kinds call unambiguous.
|
|
template <std::same_as<Kind>... RestT>
|
|
static constexpr auto Rule(uint8_t ctx, Kind kind, RestT... rest)
|
|
-> BracketRule {
|
|
return Rule(ctx, KindSetOf(kind, rest...));
|
|
}
|
|
|
|
// Whether `rule`'s cue conditions hold for the cue bit-set `cues`.
|
|
static constexpr auto Matches(const BracketRule& rule, CueSet cues) -> bool {
|
|
return (cues & rule.when) == rule.when &&
|
|
(cues & rule.unless) == CueSet::None &&
|
|
(rule.not_all == CueSet::None ||
|
|
(cues & rule.not_all) != rule.not_all) &&
|
|
(rule.any_of == CueSet::None || (cues & rule.any_of) != CueSet::None);
|
|
}
|
|
|
|
// Where to insert a synthetic closing bracket, and what that costs. All costs
|
|
// are relative; see the cost model comment at the top of this file for what
|
|
// they mean and how to change one.
|
|
constexpr BracketRule CloserRules[] = {
|
|
// `(` and `[`.
|
|
//
|
|
// An empty group: the opener is directly followed by a token that can't
|
|
// start group content, so the group's closer must have been right after
|
|
// the opener. Such a token is a `,` (`f(x(), y)` becoming `f(x(, y)`); a
|
|
// binary connector like `as`/`->`/`==`, which needs a left operand
|
|
// (`f() as T` becoming `f( as T`); or a spaced `.`, which as group content
|
|
// would be written unspaced (`f(.a = 1)`).
|
|
Rule(Top::ParenLike, Kind::Comma)
|
|
.When(Cue::AfterOpenTop)
|
|
.Cost(4, "Close_EmptyGroup"),
|
|
Rule(Top::ParenLike, Kinds::BinaryConnector)
|
|
.When(Cue::AfterOpenTop)
|
|
.Cost(4, "Close_EmptyGroup"),
|
|
Rule(Top::ParenLike, Kind::Period)
|
|
.When(Cue::AfterOpenTop, Cue::LeadingSpace)
|
|
.Cost(4, "Close_EmptyGroup"),
|
|
// A `;` can't appear inside parens or square brackets at all.
|
|
Rule(Top::ParenLike, Kind::Semi).Cost(6, "Close_ParenBeforeSemi"),
|
|
Rule(Top::ParenLike).When(Cue::Cascade).Cost(3, "Close_ParenCascade"),
|
|
// A `{` starting a block means the paren should have closed: `if (c) {`,
|
|
// `while (c) {`. A struct-literal `{...}` can legitimately sit inside a
|
|
// *call* paren (`f({.x = 1})`), but not inside a keyword or grouping paren
|
|
// (whose `{` — even an empty `{}` misread as a struct — is a block). This
|
|
// is not a cue for `[`: a `]` is essentially never immediately followed by
|
|
// `{` (only in `fn [captures] {...}`, which is unimplemented), so a `[`
|
|
// should close at an earlier cue, or not here at all.
|
|
Rule(Top::Paren, Kind::OpenCurlyBrace)
|
|
.NotAll(Cue::StructBrace, Cue::CallParenTop)
|
|
.Cost(11, "Close_ParenBeforeBrace"),
|
|
// `=` can directly follow both `)` and `]`, but `->` and `as` only
|
|
// plausibly follow `)`. An unspaced structural operator is not a cue:
|
|
// formatted code spaces these operators, and an unspaced `->` is a
|
|
// pointer member access (`p->x`).
|
|
Rule(Top::Paren, Kinds::AnyStructural)
|
|
.When(Cue::LeadingSpace)
|
|
.Cost(8, "Close_ParenBeforeStructuralOp"),
|
|
Rule(Top::Square, Kind::Assignment)
|
|
.When(Cue::LeadingSpace)
|
|
.Cost(8, "Close_ParenBeforeStructuralOp"),
|
|
// A leaf directly following a value-ending token is illegal, and a `]`
|
|
// between them fixes the adjacency (unlike `)`, `]` can be directly
|
|
// followed by a leaf, as in `impl forall [...] T as ...`).
|
|
Rule(Top::Square, Kind::Leaf)
|
|
.When(Cue::PrevValueEnding)
|
|
.Cost(4, "Close_SquareAtLeafAdjacency"),
|
|
// A `.` with whitespace before it mid-line suggests a closer was deleted
|
|
// right before it: member access is written without spaces, `x.y`.
|
|
Rule(Top::ParenLike, Kind::Period)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(6, "Close_ParenBeforeSpacedPeriod"),
|
|
// Similarly, a `(` or `[` with whitespace before it directly following a
|
|
// value-ending token: calls and indexing are written without spaces.
|
|
Rule(Top::ParenLike, Kinds::OpenGroup)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(6, "Close_ParenBeforeSpacedOpen"),
|
|
// A comparison or logical operator is unlikely inside square brackets or
|
|
// call/index argument lists (but common in `if (...)` etc.).
|
|
Rule(Top::Square, Kind::ComparisonOp)
|
|
.Cost(11, "Close_ParenBeforeComparison"),
|
|
Rule(Top::Paren, Kind::ComparisonOp)
|
|
.When(Cue::CallParenTop)
|
|
.Cost(11, "Close_ParenBeforeComparison"),
|
|
// A `,` with whitespace before it: formatted code has no space before a
|
|
// comma, so a closer was likely deleted in the gap.
|
|
Rule(Top::ParenLike, Kind::Comma)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(6, "Close_BeforeSpacedComma"),
|
|
// Likewise a `)` or `]` with whitespace before it: formatted code has no
|
|
// space before closers either.
|
|
Rule(Top::ParenLike, Kinds::CloseGroup)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(6, "Close_BeforeSpacedCloser"),
|
|
Rule(Top::ParenLike, Kind::FileEnd)
|
|
.Cost(CostCloseAtEnd, "Close_ParenAtFileEnd"),
|
|
// A wide whitespace gap mid-line suggests a deleted token in the gap.
|
|
Rule(Top::ParenLike)
|
|
.When(Cue::WideGap)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(CostCloseAtWideGap, "Close_ParenAtWideGap"),
|
|
// A `[` group rarely spans lines except in wrapped declaration headers
|
|
// (`impl forall [...]` etc.), where the line break follows the `]`.
|
|
Rule(Top::Square)
|
|
.When(Cue::FirstOnLine, Cue::NewLineFromTop)
|
|
.Cost(10, "Close_SquareAtContinuation"),
|
|
// A block `{` can't be content of a header/grouping `[` (only an index
|
|
// `arr[...]` could hold a lambda block, but a `[` after a keyword can't):
|
|
// the `]` must close before it. A last-resort bound, below the precise
|
|
// cues above, that stops the `[` from swallowing the block. Priced above
|
|
// the precise cues (so they win) but below closing at the region end, so
|
|
// an unclosed group can't swallow a whole block.
|
|
Rule(Top::Square, Kind::OpenCurlyBrace)
|
|
.Unless(Cue::StructBrace, Cue::CallParenTop)
|
|
.Cost(14, "Close_SquareBeforeBlock"),
|
|
// No positive cue that a `(`/`[` closes here. Closing before a bare
|
|
// dedent, statement introducer, or arbitrary token was never a correct
|
|
// guess in practice (it just closes too early), so decline: the search
|
|
// will close at a real cue, at the region end, or, failing both, replace
|
|
// the unmatched opener with an error token.
|
|
Rule(Top::ParenLike).Decline(),
|
|
|
|
// Struct `{`.
|
|
Rule(Top::Struct, Kind::Semi).Cost(6, "Close_StructBeforeSemi"),
|
|
Rule(Top::Struct).When(Cue::Cascade).Cost(6, "Close_StructCascade"),
|
|
// A block `{` can't be content of a struct literal/type `{...}` (a struct
|
|
// field is `.name = value`, and a bare `{` isn't a value here): the struct
|
|
// must close before it, as in `-> {.x: i32} { body }`.
|
|
Rule(Top::Struct, Kind::OpenCurlyBrace)
|
|
.Unless(Cue::StructBrace)
|
|
.Cost(14, "Close_StructBeforeBlock"),
|
|
Rule(Top::Struct)
|
|
.When(Cue::WideGap)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(CostCloseAtWideGap, "Close_StructAtWideGap"),
|
|
Rule(Top::Struct, Kind::FileEnd)
|
|
.Cost(CostCloseAtEnd, "Close_StructAtFileEnd"),
|
|
Rule(Top::Struct)
|
|
.When(Cue::FirstOnLine, Cue::DedentToHeader)
|
|
.Cost(12, "Close_StructAtDedent"),
|
|
Rule(Top::Struct).Cost(40, "Close_StructBaseline"),
|
|
|
|
// Scope `{`.
|
|
// A first-on-line `else` must have been preceded by the `}` closing the
|
|
// branch before it. Ordered ahead of Close_ScopeAtDedent, which would
|
|
// otherwise match first and charge more for the same insertion.
|
|
Rule(Top::Scope)
|
|
.When(Cue::ElseKeyword, Cue::FirstOnLine)
|
|
.Cost(4, "Close_ScopeBeforeElse"),
|
|
Rule(Top::Scope)
|
|
.When(Cue::FirstOnLine, Cue::DedentToHeader)
|
|
.Cost(6, "Close_ScopeAtDedent"),
|
|
Rule(Top::Scope).When(Cue::Cascade).Cost(6, "Close_ScopeCascade"),
|
|
Rule(Top::Scope, Kind::FileEnd)
|
|
.Cost(CostCloseAtEnd, "Close_ScopeAtFileEnd"),
|
|
Rule(Top::Scope).Cost(45, "Close_ScopeBaseline"),
|
|
};
|
|
|
|
// A bucket index: rules are identified by a bit in a `uint64_t`.
|
|
static_assert(std::size(CloserRules) <= 64);
|
|
|
|
// Maps each (context category, token kind) bucket to the bit-set of rules that
|
|
// can apply in it, so a lookup tests only those rules, in table order.
|
|
using RuleIndex =
|
|
std::array<std::array<uint64_t, NumKinds>, NumContextCategories>;
|
|
|
|
template <size_t N>
|
|
static constexpr auto BuildRuleIndex(const BracketRule (&rules)[N])
|
|
-> RuleIndex {
|
|
RuleIndex index = {};
|
|
for (size_t r = 0; r != N; ++r) {
|
|
for (int32_t t = 0; t != NumContextCategories; ++t) {
|
|
if ((rules[r].ctx & (1 << t)) == 0) {
|
|
continue;
|
|
}
|
|
for (int32_t k = 0; k != NumKinds; ++k) {
|
|
if ((rules[r].kinds & KindSetOf(static_cast<Kind>(k))) ==
|
|
KindSet::None) {
|
|
continue;
|
|
}
|
|
index[t][k] |= uint64_t{1} << r;
|
|
}
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
// The bit-set of rules that could apply in a bucket. A rule is identified by
|
|
// its bit position, which is its position in the table, so scanning the bits
|
|
// from lowest to highest visits the rules in table order.
|
|
static constexpr auto CandidateRules(const RuleIndex& index,
|
|
int32_t ctx_category, Kind kind)
|
|
-> uint64_t {
|
|
return index[ctx_category][static_cast<int32_t>(kind)];
|
|
}
|
|
|
|
// Returns the first rule that applies, for a first-match table, or null if
|
|
// none does.
|
|
template <size_t N>
|
|
static auto FindMatchingRule(const BracketRule (&rules)[N],
|
|
const RuleIndex& index, int32_t ctx_category,
|
|
Kind kind, CueSet cues) -> const BracketRule* {
|
|
uint64_t candidates = CandidateRules(index, ctx_category, kind);
|
|
while (candidates != 0) {
|
|
const auto& rule = rules[std::countr_zero(candidates)];
|
|
candidates &= candidates - 1;
|
|
if (Matches(rule, cues)) {
|
|
return &rule;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Returns the total cost of every rule that applies, for an additive table.
|
|
template <size_t N>
|
|
static auto SumMatchingRules(const BracketRule (&rules)[N],
|
|
const RuleIndex& index, int32_t ctx_category,
|
|
Kind kind, CueSet cues) -> int32_t {
|
|
int32_t total = 0;
|
|
uint64_t candidates = CandidateRules(index, ctx_category, kind);
|
|
while (candidates != 0) {
|
|
const auto& rule = rules[std::countr_zero(candidates)];
|
|
candidates &= candidates - 1;
|
|
if (Matches(rule, cues)) {
|
|
total += rule.cost;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static constexpr auto CloserRuleIndex = BuildRuleIndex(CloserRules);
|
|
|
|
// The cue for the previous token having kind `kind`, if any.
|
|
static constexpr auto PrevKindCue(Kind kind) -> CueSet {
|
|
switch (kind) {
|
|
case Kind::Period:
|
|
return CueSetOf(Cue::PrevIsPeriod);
|
|
case Kind::OpenParen:
|
|
case Kind::OpenSquareBracket:
|
|
return CueSetOf(Cue::PrevIsOpenBracket);
|
|
case Kind::OpenCurlyBrace:
|
|
return CueSetOf(Cue::PrevIsOpenCurly);
|
|
case Kind::CloseParen:
|
|
return CueSetOf(Cue::PrevIsCloseParen);
|
|
case Kind::CloseCurlyBrace:
|
|
return CueSetOf(Cue::PrevIsCloseCurly);
|
|
case Kind::CloseSquareBracket:
|
|
return CueSetOf(Cue::PrevIsCloseSquare);
|
|
case Kind::Comma:
|
|
return CueSetOf(Cue::PrevIsComma);
|
|
default:
|
|
return CueSet::None;
|
|
}
|
|
}
|
|
|
|
// Returns the set holding `cue` if `holds`, and the empty set otherwise.
|
|
static constexpr auto CueIf(bool holds, Cue cue) -> CueSet {
|
|
return holds ? CueSetOf(cue) : CueSet::None;
|
|
}
|
|
|
|
// Computes every cue that depends only on an item and its neighbours.
|
|
// `prev_token` is the token directly before the item and `prev_item` the item
|
|
// directly before it, both null at the start of the input. `context_cues` holds
|
|
// the cues the caller has already determined from the surrounding token arrays
|
|
// (`Cue::CollapsedBlock`, `Cue::FirstOnLine`, and so on).
|
|
static auto ComputeItemCues(const MismatchedBracketToken& token,
|
|
const MismatchedBracketToken* prev_token,
|
|
const Item* prev_item, CueSet context_cues)
|
|
-> CueSet {
|
|
CueSet cues = context_cues |
|
|
CueIf(token.has_leading_space, Cue::LeadingSpace) |
|
|
CueIf(token.has_wide_leading_space, Cue::WideGap) |
|
|
CueIf(token.is_else_keyword, Cue::ElseKeyword) |
|
|
CueIf(token.is_struct_brace, Cue::StructBrace) |
|
|
CueIf(PrevIsValueLike(prev_token), Cue::PrevValueLike);
|
|
if (prev_token != nullptr) {
|
|
// `forall` requires a following `[`; the other paren keywords (`if`,
|
|
// `while`, `for`, `match`) are statement introducers and require a `(`.
|
|
bool wants_paren = prev_token->kind == Kind::StatementIntroducer;
|
|
cues |= CueIf(prev_token->is_paren_keyword && wants_paren,
|
|
Cue::PrevKeywordWantsParen) |
|
|
CueIf(prev_token->is_paren_keyword && !wants_paren,
|
|
Cue::PrevKeywordWantsSquare) |
|
|
CueIf(prev_token->has_leading_space, Cue::PrevHasLeadingSpace) |
|
|
CueIf(IsValueEndingKind(prev_token->kind), Cue::PrevValueEnding) |
|
|
CueIf(prev_token->kind == Kind::As ||
|
|
prev_token->kind == Kind::StructuralOp,
|
|
Cue::PrevIntroducesType) |
|
|
PrevKindCue(prev_token->kind);
|
|
}
|
|
if (prev_item != nullptr) {
|
|
// A name directly following `as` or `->` is a type, not something
|
|
// callable, so empty parens after it are implausible.
|
|
cues |= CueIf(prev_item->token.kind == Kind::Leaf, Cue::PrevItemIsLeaf) |
|
|
CueIf(prev_item->HasAll(Cue::PrevIntroducesType),
|
|
Cue::PrevItemIsTypeName);
|
|
}
|
|
return cues;
|
|
}
|
|
|
|
// Computes the cues that depend on the innermost open bracket `top` and the
|
|
// search state, to combine with `item.cues`.
|
|
static auto ComputeTopCues(const OpenBracketInfo& top, const Item& item,
|
|
const OpenStackStore& stacks, OpenStackId stack)
|
|
-> CueSet {
|
|
const auto& token = item.token;
|
|
return CueIf(top.is_call_paren, Cue::CallParenTop) |
|
|
CueIf(MatchesDeeperOpener(stacks, stack, token.kind), Cue::Cascade) |
|
|
CueIf(top.token_pos == item.token_start_index - 1, Cue::AfterOpenTop) |
|
|
CueIf(token.line_indent <= top.effective_header_indent,
|
|
Cue::DedentToHeader) |
|
|
CueIf(token.line != top.line, Cue::NewLineFromTop);
|
|
}
|
|
|
|
// Computes the cost of inserting a synthetic closer for `top` directly before
|
|
// `item`, or nullopt if this insertion isn't worth exploring. Sets `rule_name`
|
|
// to the name of the rule that fired.
|
|
static auto ClassifyCloserInsertion(const OpenBracketInfo& top,
|
|
const Item& item,
|
|
const OpenStackStore& stacks,
|
|
OpenStackId stack,
|
|
llvm::StringLiteral& rule_name)
|
|
-> std::optional<int32_t> {
|
|
CueSet cues = item.cues | ComputeTopCues(top, item, stacks, stack);
|
|
const auto* rule = FindMatchingRule(
|
|
CloserRules, CloserRuleIndex, TopCategoryOf(top), item.token.kind, cues);
|
|
if (rule == nullptr || rule->cost == DeclineCost) {
|
|
return std::nullopt;
|
|
}
|
|
rule_name = rule->rule_name;
|
|
return rule->cost;
|
|
}
|
|
|
|
// Where to insert a synthetic opening bracket, and what that costs. A `(` or
|
|
// `[` can always be synthesized, just expensively without a cue, so those end
|
|
// in a baseline; a brace is only proposed where a cue supports it.
|
|
constexpr BracketRule OpenerRules[] = {
|
|
// `if`/`while`/`for`/`match` (statement introducers) require a following
|
|
// `(`; `forall` (an Other token) requires a following `[`.
|
|
Rule(Ins::Paren, Kinds::NonOpener)
|
|
.When(Cue::PrevKeywordWantsParen)
|
|
.Cost(3, "Open_AfterParenKeyword"),
|
|
Rule(Ins::Square, Kinds::NonOpener)
|
|
.When(Cue::PrevKeywordWantsSquare)
|
|
.Cost(3, "Open_AfterParenKeyword"),
|
|
// A leaf or binding modifier directly following a value-ending token is
|
|
// illegal; an opener here fixes the adjacency.
|
|
Rule(Ins::ParenLike, Kinds::LeafLike)
|
|
.When(Cue::PrevValueEnding)
|
|
.Cost(3, "Open_AtLeafAdjacency"),
|
|
// A `.` with whitespace before it directly following a value-ending
|
|
// token: likely a designator argument that lost its `(`, as in
|
|
// `ImplicitAs(.Self)`.
|
|
Rule(Ins::Paren, Kind::Period)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueEnding)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(CostCloseParenBeforeSpacedPeriod, "Open_BeforeSpacedPeriod"),
|
|
// A mid-line leaf with whitespace before it directly following an
|
|
// unspaced `.`: member access is written without spaces, `x.y`, so a
|
|
// bracket was likely deleted in the gap.
|
|
Rule(Ins::ParenLike, Kind::Leaf)
|
|
.When(Cue::LeadingSpace, Cue::PrevIsPeriod)
|
|
.Unless(Cue::FirstOnLine, Cue::PrevHasLeadingSpace)
|
|
.Cost(4, "Open_AfterPeriodGap"),
|
|
// A mid-line leaf with whitespace before it directly following an
|
|
// opener: formatted code has no space after `(` or `[`, so a bracket was
|
|
// likely deleted in the gap.
|
|
Rule(Ins::ParenLike, Kind::Leaf)
|
|
.When(Cue::LeadingSpace, Cue::PrevIsOpenBracket)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(4, "Open_AfterOpenGap"),
|
|
// A wide whitespace gap before a token that could start a group suggests
|
|
// an opener was deleted in the gap.
|
|
Rule(Ins::ParenLike, Kinds::GroupStarter)
|
|
.When(Cue::WideGap)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(CostCloseAtWideGap, "Open_AtWideGap"),
|
|
// An empty group directly after a name: `Op()`. Only applies after a
|
|
// leaf (a call of a just-computed value, `f(x)()`, is much rarer than a
|
|
// call of a name), and not when the name is a type after `as` or `->`,
|
|
// where a parenthesized group is more plausible than an empty call.
|
|
Rule(Ins::Paren, Kind::CloseParen)
|
|
.When(Cue::PrevItemIsLeaf, Cue::LeadingSpace)
|
|
.Unless(Cue::PrevItemIsTypeName)
|
|
.Cost(5, "Open_EmptyParens"),
|
|
// An empty `[]` is rarer than empty parens.
|
|
Rule(Ins::Square, Kind::CloseSquareBracket)
|
|
.When(Cue::PrevItemIsLeaf, Cue::LeadingSpace)
|
|
.Unless(Cue::PrevItemIsTypeName)
|
|
.Cost(12, "Open_EmptySquares"),
|
|
// An empty call of a just-computed value: `T.(Default.Op)()`. Only
|
|
// trusted when the `)` is spaced, marking the deletion gap. This looks at
|
|
// the last token of the previous item, so it fires after a collapsed
|
|
// `(...)` block too.
|
|
Rule(Ins::Paren, Kind::CloseParen)
|
|
.When(Cue::PrevIsCloseParen, Cue::LeadingSpace)
|
|
.Unless(Cue::FirstOnLine)
|
|
.Cost(8, "Open_EmptyParensAfterClose"),
|
|
// A `(` or `[` anywhere else an expression could start.
|
|
Rule(Ins::ParenLike).Cost(70, "Open_ParenBaseline"),
|
|
// A scope `{` is never inserted directly before another opener: the body
|
|
// it would open starts with that opener, so the `{` belongs before it only
|
|
// via one of the rules above.
|
|
Rule(Ins::ScopeBrace, Kinds::Opener).Decline(),
|
|
// A scope `{` between an unbraced declaration or statement header and its
|
|
// body, as in `if (c) return;`.
|
|
Rule(Ins::ScopeBrace)
|
|
.When(Cue::FollowsStatementHeader)
|
|
.Unless(Cue::HeaderHasOpenCurly)
|
|
.Cost(8, "Open_ScopeAfterHeader"),
|
|
Rule(Ins::ScopeBrace).Cost(30, "Open_ScopeBaseline"),
|
|
// A struct `{` before a `.` designator that isn't a member access.
|
|
Rule(Ins::StructBrace, Kind::Period)
|
|
.Unless(Cue::PrevValueEnding)
|
|
.Cost(5, "Open_StructBeforeDesignator"),
|
|
// A struct literal `{...}` that lost its `{`, leaving content directly
|
|
// before the `}`. Real content is required, so a stray `}` is reported as
|
|
// an error instead. Priced above Open_ScopeAfterHeader: when a single-line
|
|
// body lost its `{`, inserting it before the body beats making empty
|
|
// braces at the `}`.
|
|
Rule(Ins::StructBrace, Kind::CloseCurlyBrace)
|
|
.Unless(Cue::FirstOnLine)
|
|
.AnyOf(Cue::PrevValueEnding, Cue::PrevIsCloseCurly,
|
|
Cue::PrevIsCloseSquare, Cue::PrevIsComma)
|
|
.Cost(10, "Open_StructEmptyBraces"),
|
|
// Any other struct brace has no cue at all, and isn't worth proposing.
|
|
Rule(Ins::StructBrace).Decline(),
|
|
};
|
|
|
|
static_assert(std::size(OpenerRules) <= 64);
|
|
|
|
static constexpr auto OpenerRuleIndex = BuildRuleIndex(OpenerRules);
|
|
|
|
// Computes the cost of inserting a synthetic opener directly before `item`, or
|
|
// nullopt if this insertion isn't worth exploring. Sets `rule_name` to the name
|
|
// of the rule that fired.
|
|
static auto ClassifyOpenerInsertion(Kind kind, bool is_struct_brace,
|
|
const Item& item,
|
|
llvm::StringLiteral& rule_name)
|
|
-> std::optional<int32_t> {
|
|
const auto* rule = FindMatchingRule(OpenerRules, OpenerRuleIndex,
|
|
InsCategoryOf(kind, is_struct_brace),
|
|
item.token.kind, item.cues);
|
|
if (rule == nullptr || rule->cost == DeclineCost) {
|
|
return std::nullopt;
|
|
}
|
|
rule_name = rule->rule_name;
|
|
return rule->cost;
|
|
}
|
|
|
|
// Penalties for advancing over an item in a context where it doesn't belong.
|
|
// These make "close the group before this token" win over "swallow this token
|
|
// into the group". Unlike the tables above, this one is additive: every
|
|
// matching rule contributes, and the penalties sum.
|
|
constexpr BracketRule AdvanceRules[] = {
|
|
// A line that dedents to at-or-before the indentation of the enclosing
|
|
// brace's statement header, or of the enclosing paren's own line, while
|
|
// the bracket is still open. Closing brackets are excluded, since a `}`
|
|
// that closes its group is expected to dedent, and `FileEnd` isn't
|
|
// content at all.
|
|
Rule(Top::Struct | Top::Scope, Kinds::Dedentable)
|
|
.When(Cue::FirstOnLine, Cue::DedentToHeader)
|
|
.Cost(40, "Adv_DedentInScope"),
|
|
Rule(Top::ParenLike, Kinds::Dedentable)
|
|
.When(Cue::FirstOnLine, Cue::DedentToOpenerLine)
|
|
.Cost(25, "Adv_DedentInParen"),
|
|
// A scope `{` inside parens or a struct brace is a lambda, which is rare;
|
|
// a struct `{` there is a struct literal argument, which is common. The
|
|
// first rule covers a collapsed block that contains a scope brace.
|
|
Rule(Top::ParenLike | Top::Struct, Kinds::Opener)
|
|
.When(Cue::CollapsedBlock, Cue::ContainsScopeBrace)
|
|
.Cost(40, "Adv_ScopeBlockInParen"),
|
|
Rule(Top::ParenLike | Top::Struct, Kind::OpenCurlyBrace)
|
|
.Unless(Cue::CollapsedBlock, Cue::StructBrace)
|
|
.Cost(40, "Adv_ScopeBraceInParen"),
|
|
Rule(Top::ParenLike | Top::Struct, Kind::OpenCurlyBrace)
|
|
.When(Cue::StructBrace)
|
|
.Unless(Cue::CollapsedBlock)
|
|
.Cost(5, "Adv_StructBraceInParen"),
|
|
// A spaced `(` or `[` directly after a value: calls and indexing are
|
|
// written without spaces, so a bracket was likely deleted in the gap —
|
|
// unless this path already inserted a closer there.
|
|
Rule(Top::Any, Kinds::OpenGroup)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine, Cue::CollapsedBlock, Cue::CloserInserted)
|
|
.Cost(10, "Adv_SpacedOpenAfterValue"),
|
|
// Formatted code has no space before a closer either.
|
|
Rule(Top::Any, Kinds::CloseGroup)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine, Cue::BracketInsertedHere)
|
|
.Cost(8, "Adv_SpacedCloserUnexplained"),
|
|
// A `;` can't appear inside parens, square brackets, or a struct brace.
|
|
Rule(Top::ParenLike | Top::Struct, Kind::Semi).Cost(100, "Adv_SemiInParen"),
|
|
// A `,` at statement level: directly in a scope brace or at the top level.
|
|
Rule(Top::None | Top::Scope, Kind::Comma)
|
|
.Cost(50, "Adv_CommaAtStatementLevel"),
|
|
// A `,` directly following a still-open `(`/`[` is illegal.
|
|
Rule(Top::ParenLike | Top::Struct, Kind::Comma)
|
|
.When(Cue::AfterOpenTop)
|
|
.Cost(50, "Adv_CommaAfterOpen"),
|
|
// Formatted code has no space before a `,`, so a bracket was likely
|
|
// deleted in the gap.
|
|
Rule(Top::ParenLike | Top::Struct, Kind::Comma)
|
|
.When(Cue::LeadingSpace, Cue::PrevValueLike)
|
|
.Unless(Cue::FirstOnLine, Cue::CloserInserted, Cue::AfterOpenTop)
|
|
.Cost(8, "Adv_SpacedCommaInParen"),
|
|
// A statement introducer keyword inside parens or a struct brace.
|
|
Rule(Top::ParenLike | Top::Struct, Kind::StatementIntroducer)
|
|
.Cost(60, "Adv_IntroducerInParen"),
|
|
// A leaf, or a binding modifier keyword, directly following a value-ending
|
|
// token is an illegal adjacency — unless an opener synthesized here, or a
|
|
// `]`/`}` inserted here, repairs it.
|
|
Rule(Top::Any, Kinds::LeafLike)
|
|
.When(Cue::PrevValueEnding)
|
|
.Unless(Cue::OpenerHere, Cue::CloserFixesAdjacency)
|
|
.Cost(60, "Adv_LeafAdjacency"),
|
|
// `=`, `->`, or `as` inside parens or square brackets. These *can* occur
|
|
// there (default arguments, function types, casts), so this is mild; it
|
|
// serves to prefer the earliest sensible close point. Casts `(x as T)` are
|
|
// common enough that `as` keeps only a nominal preference.
|
|
Rule(Top::ParenLike, Kind::Assignment, Kind::StructuralOp)
|
|
.When(Cue::LeadingSpace)
|
|
.Cost(5, "Adv_StructuralOpInParen"),
|
|
Rule(Top::ParenLike, Kind::As)
|
|
.When(Cue::LeadingSpace)
|
|
.Cost(1, "Adv_AsOpInParen"),
|
|
// A comparison or logical operator inside square brackets or a call.
|
|
Rule(Top::Square, Kind::ComparisonOp).Cost(8, "Adv_ComparisonInSquare"),
|
|
Rule(Top::Paren, Kind::ComparisonOp)
|
|
.When(Cue::CallParenTop)
|
|
.Cost(8, "Adv_ComparisonInCall"),
|
|
// A wide mid-line whitespace gap suggests a deleted bracket that this path
|
|
// hasn't repaired.
|
|
Rule(Top::Any, Kinds::AnyOther)
|
|
.When(Cue::WideGap)
|
|
.Unless(Cue::FirstOnLine, Cue::BracketInsertedHere)
|
|
.Cost(10, "Adv_WideGapUnexplained"),
|
|
// A mid-line `.` with whitespace before it suggests a deleted bracket:
|
|
// member access is written without spaces. Prefer closing an open group
|
|
// before it, or opening one.
|
|
Rule(Top::Any, Kind::Period)
|
|
.When(Cue::LeadingSpace)
|
|
.Unless(Cue::FirstOnLine, Cue::BracketInsertedHere)
|
|
.AnyOf(Cue::PrevValueLike, Cue::PrevIsOpenBracket, Cue::PrevIsOpenCurly)
|
|
.Cost(10, "Adv_SpacedPeriodInParen"),
|
|
};
|
|
|
|
static_assert(std::size(AdvanceRules) <= 64);
|
|
|
|
// Declining is meaningless in an additive table: every matching rule
|
|
// contributes its cost, so a declining rule would subtract from the penalty.
|
|
// This also catches a rule that forgot its `Cost`.
|
|
static_assert([] {
|
|
for (const BracketRule& rule : AdvanceRules) {
|
|
if (rule.cost == DeclineCost) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}());
|
|
|
|
static constexpr auto AdvanceRuleIndex = BuildRuleIndex(AdvanceRules);
|
|
|
|
// Computes the cues for advancing over `item` in search state `node`.
|
|
static auto ComputeAdvanceCues(const OpenStackStore& stacks,
|
|
const SearchState& node, const Item& item)
|
|
-> CueSet {
|
|
bool opener_here = OpenerSynthesizedHere(stacks, node.stack, item);
|
|
bool closer_here = node.closer_inserted != Kind::Other;
|
|
CueSet cues = item.cues | CueIf(closer_here, Cue::CloserInserted) |
|
|
CueIf(CloserFixesLeafAdjacency(node.closer_inserted),
|
|
Cue::CloserFixesAdjacency) |
|
|
CueIf(opener_here, Cue::OpenerHere) |
|
|
CueIf(closer_here || opener_here, Cue::BracketInsertedHere);
|
|
if (stacks.IsEmpty(node.stack)) {
|
|
return cues;
|
|
}
|
|
const auto& top = stacks.Top(node.stack);
|
|
return cues | CueIf(top.is_call_paren, Cue::CallParenTop) |
|
|
CueIf(top.token_pos == item.token_start_index - 1, Cue::AfterOpenTop) |
|
|
CueIf(item.token.line_indent <= top.effective_header_indent,
|
|
Cue::DedentToHeader) |
|
|
CueIf(item.token.line_indent <= top.line_indent,
|
|
Cue::DedentToOpenerLine);
|
|
}
|
|
|
|
// Computes the total penalty for advancing over `item` in search state `node`,
|
|
// summing every `AdvanceRules` entry that matches.
|
|
static auto AdvancePenalty(const OpenStackStore& stacks,
|
|
const SearchState& node, const Item& item)
|
|
-> int32_t {
|
|
return SumMatchingRules(
|
|
AdvanceRules, AdvanceRuleIndex, TopCategoryOfStack(stacks, node.stack),
|
|
item.token.kind, ComputeAdvanceCues(stacks, node, item));
|
|
}
|
|
|
|
// Enumerates the distinct optimal repairs, by walking parent edges from each
|
|
// goal node back to the root and collecting the corrections along the way. At
|
|
// most `MaxOptimalPaths` are returned, so a correction that only a path beyond
|
|
// that cap would dispute stays untied.
|
|
static auto EnumerateOptimalPaths(llvm::ArrayRef<BeamNode> nodes,
|
|
llvm::ArrayRef<int32_t> goal_node_indices)
|
|
-> llvm::SmallVector<llvm::SmallVector<BracketCorrection>> {
|
|
llvm::SmallVector<llvm::SmallVector<BracketCorrection>> all_paths;
|
|
llvm::SmallVector<BracketCorrection> current_path;
|
|
struct StackFrame {
|
|
int32_t node_index;
|
|
int32_t edge_index = 0;
|
|
};
|
|
llvm::SmallVector<StackFrame> stack;
|
|
for (int32_t goal_idx : goal_node_indices) {
|
|
stack.push_back({.node_index = goal_idx, .edge_index = 0});
|
|
}
|
|
while (!stack.empty()) {
|
|
auto& frame = stack.back();
|
|
const auto& node = nodes[frame.node_index];
|
|
if (node.parent_edges.empty()) {
|
|
// The root, so `current_path` is now a complete repair, in reverse.
|
|
all_paths.push_back(current_path);
|
|
std::reverse(all_paths.back().begin(), all_paths.back().end());
|
|
stack.pop_back();
|
|
if (all_paths.size() >= MaxOptimalPaths) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (frame.edge_index > 0) {
|
|
const auto& prev_edge = node.parent_edges[frame.edge_index - 1];
|
|
if (prev_edge.has_correction) {
|
|
current_path.pop_back();
|
|
}
|
|
}
|
|
if (static_cast<size_t>(frame.edge_index) < node.parent_edges.size()) {
|
|
const auto& edge = node.parent_edges[frame.edge_index];
|
|
++frame.edge_index;
|
|
if (edge.has_correction) {
|
|
current_path.push_back(edge.correction);
|
|
}
|
|
stack.push_back({.node_index = edge.parent_node_index, .edge_index = 0});
|
|
} else {
|
|
stack.pop_back();
|
|
}
|
|
}
|
|
return all_paths;
|
|
}
|
|
|
|
// Finds which of the first optimal path's corrections the optimal repairs
|
|
// disagree about, treating the first path as the baseline. Two corrections
|
|
// make the same repair when `repair_key` gives them the same key. A baseline
|
|
// correction is agreed upon when every other path contains a counterpart
|
|
// correction making the same repair; if some path has no counterpart for it,
|
|
// it is marked as tied.
|
|
//
|
|
// Counterparts are found by counting corrections per key rather than by
|
|
// pairing corrections up individually: the baseline's `n`th correction with a
|
|
// given key has a counterpart in another path exactly when that path has at
|
|
// least `n` corrections with that key. So it suffices to know, for each key
|
|
// the baseline uses, the fewest corrections any path makes with that key.
|
|
static auto FindTiedCorrections(
|
|
llvm::ArrayRef<llvm::SmallVector<BracketCorrection>> all_paths,
|
|
llvm::function_ref<uint64_t(const BracketCorrection&)> repair_key)
|
|
-> llvm::BitVector {
|
|
llvm::ArrayRef<BracketCorrection> baseline_path = all_paths.front();
|
|
|
|
// Number the keys the baseline uses, and count how many of its corrections
|
|
// share each one. A key no baseline correction uses is never asked about, so
|
|
// it goes unnumbered.
|
|
Map<uint64_t, int32_t> key_ids;
|
|
llvm::SmallVector<int32_t> baseline_key_id(baseline_path.size());
|
|
// Which of the baseline's corrections with the same key this one is, counting
|
|
// from one: it needs the path to have at least this many.
|
|
llvm::SmallVector<int32_t> baseline_rank(baseline_path.size());
|
|
llvm::SmallVector<int32_t> baseline_count;
|
|
for (auto [corr_idx, corr] : llvm::enumerate(baseline_path)) {
|
|
auto result = key_ids.Insert(repair_key(corr),
|
|
static_cast<int32_t>(baseline_count.size()));
|
|
if (result.is_inserted()) {
|
|
baseline_count.push_back(0);
|
|
}
|
|
int32_t key_id = result.value();
|
|
baseline_key_id[corr_idx] = key_id;
|
|
baseline_rank[corr_idx] = ++baseline_count[key_id];
|
|
}
|
|
|
|
// The fewest corrections with each key that any path makes.
|
|
llvm::SmallVector<int32_t> min_count = baseline_count;
|
|
llvm::SmallVector<int32_t> path_count(baseline_count.size());
|
|
for (const auto& path : llvm::drop_begin(all_paths)) {
|
|
std::fill(path_count.begin(), path_count.end(), 0);
|
|
for (const auto& corr : path) {
|
|
if (int32_t* key_id = key_ids[repair_key(corr)]) {
|
|
++path_count[*key_id];
|
|
}
|
|
}
|
|
for (auto [key_id, count] : llvm::enumerate(path_count)) {
|
|
min_count[key_id] = std::min(min_count[key_id], count);
|
|
}
|
|
}
|
|
|
|
llvm::BitVector tied(baseline_path.size());
|
|
for (auto [corr_idx, key_id] : llvm::enumerate(baseline_key_id)) {
|
|
if (min_count[key_id] < baseline_rank[corr_idx]) {
|
|
tied.set(corr_idx);
|
|
}
|
|
}
|
|
return tied;
|
|
}
|
|
|
|
// From the optimal goal nodes, reconstructs the repair corrections and appends
|
|
// them to `corrections`. Every optimal repair is enumerated (up to a cap); a
|
|
// correction that the optimal repairs disagree about is marked tied, so the
|
|
// caller downgrades it to an error token rather than guessing. Falls back to
|
|
// naive recovery if no path can be reconstructed.
|
|
static auto ReconstructCorrections(
|
|
llvm::ArrayRef<BeamNode> nodes, llvm::ArrayRef<int32_t> goal_node_indices,
|
|
llvm::ArrayRef<Item> items, TokenIndex region_end_token,
|
|
llvm::SmallVectorImpl<BracketCorrection>& corrections) -> void {
|
|
auto all_paths = EnumerateOptimalPaths(nodes, goal_node_indices);
|
|
if (all_paths.empty()) {
|
|
SolveNaive(items, corrections);
|
|
return;
|
|
}
|
|
|
|
Map<int32_t, int32_t> token_to_item;
|
|
for (auto [idx, region_item] : llvm::enumerate(items)) {
|
|
token_to_item.Update(region_item.token.token_index.index,
|
|
static_cast<int32_t>(idx));
|
|
}
|
|
token_to_item.Update(region_end_token.index,
|
|
static_cast<int32_t>(items.size()));
|
|
|
|
// For each item, the first item of the run of consecutive items with its own
|
|
// kind that ends at it. Inserting a bracket anywhere within a run of that
|
|
// same bracket produces the same token sequence — a `)` on either side of an
|
|
// existing `)` reads the same — so an insertion point slides back to the
|
|
// start of such a run, and two that slide to the same place are the same
|
|
// repair. A collapsed item ends a run, since it stands for a whole bracketed
|
|
// block rather than one token.
|
|
llvm::SmallVector<int32_t> run_start(items.size(), 0);
|
|
for (auto [i, item] : llvm::enumerate(items)) {
|
|
auto idx = static_cast<int32_t>(i);
|
|
bool continues_run = idx > 0 && !item.HasAll(Cue::CollapsedBlock) &&
|
|
!items[idx - 1].HasAll(Cue::CollapsedBlock) &&
|
|
items[idx - 1].token.kind == item.token.kind;
|
|
run_start[idx] = continues_run ? run_start[idx - 1] : idx;
|
|
}
|
|
|
|
// Names the repair a correction makes, so that two corrections making the
|
|
// same repair get the same name. This reads only the fix, not the diagnosed
|
|
// bracket: two paths that blame different brackets but repair the token
|
|
// stream identically don't disagree about the repair.
|
|
//
|
|
// An insertion whose target is in this region is named by the item its
|
|
// insertion point slides back to; anything else is named by the token it
|
|
// fixes. Those are separate numbering spaces, so the name records which it
|
|
// used.
|
|
auto repair_key = [&](const BracketCorrection& c) -> uint64_t {
|
|
int32_t position = c.fix_token_index.index;
|
|
bool position_is_item = false;
|
|
if (c.fix_action == BracketFixAction::InsertBefore) {
|
|
if (int32_t* item = token_to_item[c.fix_token_index.index]) {
|
|
position_is_item = true;
|
|
position = *item;
|
|
if (position > 0 && !items[position - 1].HasAll(Cue::CollapsedBlock) &&
|
|
ToTokenKind(items[position - 1].token.kind) == c.fix_token_kind) {
|
|
position = run_start[position - 1];
|
|
}
|
|
}
|
|
}
|
|
return (static_cast<uint64_t>(c.fix_action) << 40) |
|
|
(static_cast<uint64_t>(c.fix_token_kind.AsInt()) << 33) |
|
|
(static_cast<uint64_t>(position_is_item) << 32) |
|
|
static_cast<uint32_t>(position);
|
|
};
|
|
|
|
llvm::BitVector tied = FindTiedCorrections(all_paths, repair_key);
|
|
for (auto [corr_idx, corr] : llvm::enumerate(all_paths.front())) {
|
|
corrections.push_back(corr);
|
|
corrections.back().is_tied = tied[corr_idx];
|
|
}
|
|
}
|
|
|
|
// Whether matching the real closer `item` directly against `top` is strictly
|
|
// better than synthesizing a duplicate closer in front of it. That's normally
|
|
// the case, but not when the closer isn't allowed to match `top` directly, and
|
|
// not when it has suspicious whitespace before it suggesting a closer was
|
|
// deleted in the gap.
|
|
static auto DirectMatchPreferred(const OpenBracketInfo& top, const Item& item)
|
|
-> bool {
|
|
auto kind = item.token.kind;
|
|
bool direct_match_ok = kind != Kind::CloseCurlyBrace || top.is_struct_brace ||
|
|
item.token.line == top.line ||
|
|
item.token.line_indent >= top.effective_header_indent;
|
|
bool spaced_suspicious = kind != Kind::CloseCurlyBrace &&
|
|
!item.HasAll(Cue::FirstOnLine) &&
|
|
item.HasAll(Cue::LeadingSpace, Cue::PrevValueLike);
|
|
return direct_match_ok && !spaced_suspicious;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// A flavor of synthetic opening bracket the search can insert.
|
|
struct SyntheticOpener {
|
|
Kind kind;
|
|
bool is_struct_brace;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// The synthetic openers the search considers inserting before a token, in the
|
|
// order they're proposed. Each is offered to `ClassifyOpenerInsertion`, which
|
|
// decides whether the context supports it.
|
|
constexpr SyntheticOpener SyntheticOpeners[] = {
|
|
{Kind::OpenParen, /*is_struct_brace=*/false},
|
|
{Kind::OpenSquareBracket, /*is_struct_brace=*/false},
|
|
{Kind::OpenCurlyBrace, /*is_struct_brace=*/false},
|
|
{Kind::OpenCurlyBrace, /*is_struct_brace=*/true},
|
|
};
|
|
|
|
// The stack entry for the real opening bracket `item`.
|
|
static auto RealOpener(const Item& item) -> OpenBracketInfo {
|
|
return OpenBracketInfo{OpenBracketKey{
|
|
.token_index = item.token.token_index,
|
|
.token_pos = item.token_start_index,
|
|
.line = item.token.line,
|
|
.line_indent = item.token.line_indent,
|
|
.effective_header_indent = item.effective_header_indent,
|
|
.kind = item.token.kind,
|
|
.is_struct_brace = item.token.is_struct_brace,
|
|
.is_call_paren = item.HasAll(Cue::PrevValueEnding),
|
|
}};
|
|
}
|
|
|
|
// The extra cost of matching the real closer `item` against `top`, or nullopt
|
|
// if the match isn't allowed. A multi-line scope close must not be dedented
|
|
// past its header, and pays for indentation disagreement with it.
|
|
static auto MatchClosePenalty(const OpenBracketInfo& top, const Item& item)
|
|
-> std::optional<int32_t> {
|
|
if (item.token.kind != Kind::CloseCurlyBrace || top.is_struct_brace ||
|
|
item.token.line == top.line) {
|
|
return 0;
|
|
}
|
|
if (item.token.line_indent < top.effective_header_indent) {
|
|
return std::nullopt;
|
|
}
|
|
if (item.HasAll(Cue::FirstOnLine) &&
|
|
item.token.line_indent != top.effective_header_indent) {
|
|
return CostBraceIndentMismatchBase +
|
|
CostBraceIndentMismatchPerColumn *
|
|
std::abs(top.effective_header_indent - item.token.line_indent);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The correction that records where the synthetic opener `opener`, which the
|
|
// real closer `closer` has just matched, would be inserted.
|
|
static auto InsertOpenerCorrection(const OpenBracketInfo& opener,
|
|
const MismatchedBracketToken& closer)
|
|
-> BracketCorrection {
|
|
return BracketCorrection{
|
|
.diagnostic_kind = BracketDiagnosticKind::UnmatchedClosing,
|
|
.diagnostic_token_index = closer.token_index,
|
|
.fix_action = BracketFixAction::InsertBefore,
|
|
.fix_token_index = opener.insertion_token_index,
|
|
.fix_token_kind = ToTokenKind(opener.kind),
|
|
.rule_name = opener.rule_name,
|
|
};
|
|
}
|
|
|
|
// The cost of closing an open bracket at the end of the file or region.
|
|
static auto CostToCloseAtEnd(const OpenBracketInfo& open) -> int32_t {
|
|
if (open.kind != Kind::OpenCurlyBrace) {
|
|
return CostCloseParenAtEnd;
|
|
}
|
|
return open.is_struct_brace ? CostCloseStructAtEnd : CostCloseAtEnd;
|
|
}
|
|
|
|
// A layered beam search over one damaged region.
|
|
//
|
|
// Each layer holds the states that survive just before one item. Expanding a
|
|
// layer first proposes bracket insertions before the item — epsilon moves,
|
|
// which stay within the layer — and then advances over the item into the next
|
|
// layer, pruning each layer back to `MaxBeamWidth`. States in a layer that
|
|
// agree on their open-bracket stack merge into one node, which keeps every
|
|
// cheapest way of reaching it so that ties can be found afterwards.
|
|
namespace {
|
|
|
|
class RegionSearch {
|
|
public:
|
|
// `region_end_token` is the token directly after the region, where any
|
|
// still-unclosed brackets are closed.
|
|
RegionSearch(llvm::ArrayRef<Item> items, TokenIndex region_end_token)
|
|
: items_(items), region_end_token_(region_end_token) {
|
|
nodes_.reserve(256);
|
|
}
|
|
|
|
// Runs the search and appends the corrections for the cheapest repair to
|
|
// `corrections`.
|
|
auto Solve(llvm::SmallVectorImpl<BracketCorrection>& corrections) -> void;
|
|
|
|
private:
|
|
// Adds the state reached by `edge` at total cost `next_cost` to `layer`,
|
|
// merging it into an equal state already there if there is one.
|
|
// `next_item_idx` is the item the state sits before. If `worklist` is given,
|
|
// any node that was added or became cheaper is appended to it, so that a
|
|
// further epsilon move can be applied to it.
|
|
auto AddToLayer(llvm::SmallVectorImpl<int32_t>& layer, int32_t next_item_idx,
|
|
OpenStackId next_stack, Kind closer_inserted,
|
|
int32_t next_cost, ParentEdge edge,
|
|
llvm::SmallVectorImpl<int32_t>* worklist = nullptr) -> void;
|
|
|
|
// Merges a newly-found way of reaching the state already in `nodes_[idx]`: a
|
|
// cheaper cost replaces what's recorded there, and an equal cost adds another
|
|
// parent edge, so that every cheapest path stays available.
|
|
auto MergeIntoNode(int32_t idx, int32_t cost, const ParentEdge& edge,
|
|
llvm::SmallVectorImpl<int32_t>* worklist) -> void;
|
|
|
|
// Keeps a layer within the beam width by discarding the costliest states.
|
|
auto PruneBeam(llvm::SmallVectorImpl<int32_t>& layer) -> void;
|
|
|
|
// Adds to the current layer every state reachable by inserting brackets
|
|
// directly before item `item_idx`.
|
|
auto InsertBracketsBefore(int32_t item_idx) -> void;
|
|
auto InsertSyntheticClosers(int32_t item_idx) -> void;
|
|
auto InsertSyntheticOpeners(int32_t item_idx) -> void;
|
|
|
|
// Advances the current layer over item `item_idx`, returning the layer that
|
|
// results.
|
|
auto AdvanceOverItem(int32_t item_idx) -> llvm::SmallVector<int32_t>;
|
|
auto AdvanceNode(int32_t node_idx, int32_t item_idx,
|
|
llvm::SmallVectorImpl<int32_t>& next_layer) -> void;
|
|
|
|
// Closes whatever is still open at the end of the region, returning the goal
|
|
// nodes of the cheapest complete repairs.
|
|
auto CloseAtRegionEnd() -> llvm::SmallVector<int32_t>;
|
|
|
|
llvm::ArrayRef<Item> items_;
|
|
TokenIndex region_end_token_;
|
|
|
|
// Every state the search has reached, in creation order; a layer names its
|
|
// states by their index here. Nodes are never removed, so pruning a layer
|
|
// leaves its states behind, unreachable.
|
|
llvm::SmallVector<BeamNode, 0> nodes_;
|
|
// The cost of the cheapest complete repair found so far, which bounds the
|
|
// cost of any state still worth expanding.
|
|
int32_t min_goal_cost_ = std::numeric_limits<int32_t>::max();
|
|
llvm::SmallVector<int32_t> current_layer_;
|
|
// The states in the layer currently being built, keyed on the state itself so
|
|
// that reaching a state already in the layer merges into it, and giving that
|
|
// state's index in `nodes_`. Kept across layers only to reuse its allocation.
|
|
Map<StateKey, int32_t> layer_dedup_;
|
|
// The open-bracket stacks every state in `nodes_` refers to.
|
|
OpenStackStore stacks_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
auto RegionSearch::AddToLayer(llvm::SmallVectorImpl<int32_t>& layer,
|
|
int32_t next_item_idx, OpenStackId next_stack,
|
|
Kind closer_inserted, int32_t next_cost,
|
|
ParentEdge edge,
|
|
llvm::SmallVectorImpl<int32_t>* worklist)
|
|
-> void {
|
|
if (next_cost > min_goal_cost_) {
|
|
return;
|
|
}
|
|
StateKey key = {.stack = next_stack, .closer_inserted = closer_inserted};
|
|
if (int32_t* existing = layer_dedup_[key]) {
|
|
MergeIntoNode(*existing, next_cost, edge, worklist);
|
|
return;
|
|
}
|
|
auto new_idx = static_cast<int32_t>(nodes_.size());
|
|
nodes_.push_back(BeamNode{
|
|
.item_index = next_item_idx,
|
|
.stack = next_stack,
|
|
.cost = next_cost,
|
|
.closer_inserted = closer_inserted,
|
|
.parent_edges = {edge},
|
|
});
|
|
layer.push_back(new_idx);
|
|
layer_dedup_.Insert(key, new_idx);
|
|
if (worklist) {
|
|
worklist->push_back(new_idx);
|
|
}
|
|
}
|
|
|
|
auto RegionSearch::MergeIntoNode(int32_t idx, int32_t cost,
|
|
const ParentEdge& edge,
|
|
llvm::SmallVectorImpl<int32_t>* worklist)
|
|
-> void {
|
|
BeamNode& node = nodes_[idx];
|
|
if (cost < node.cost) {
|
|
node.cost = cost;
|
|
node.parent_edges.clear();
|
|
node.parent_edges.push_back(edge);
|
|
if (worklist) {
|
|
worklist->push_back(idx);
|
|
}
|
|
} else if (cost == node.cost &&
|
|
llvm::none_of(node.parent_edges, [&](const ParentEdge& e) {
|
|
return EdgesEqual(e, edge);
|
|
})) {
|
|
node.parent_edges.push_back(edge);
|
|
}
|
|
}
|
|
|
|
auto RegionSearch::PruneBeam(llvm::SmallVectorImpl<int32_t>& layer) -> void {
|
|
if (layer.size() > MaxBeamWidth) {
|
|
llvm::stable_sort(layer, [&](int32_t a, int32_t b) {
|
|
return nodes_[a].cost < nodes_[b].cost;
|
|
});
|
|
layer.resize(MaxBeamWidth);
|
|
}
|
|
}
|
|
|
|
auto RegionSearch::InsertBracketsBefore(int32_t item_idx) -> void {
|
|
// Seed the dedup table with the states already in the layer, so that an
|
|
// insertion reaching one of them merges into it instead of duplicating it.
|
|
for (int32_t idx : current_layer_) {
|
|
layer_dedup_.Insert(
|
|
StateKey{.stack = nodes_[idx].stack,
|
|
.closer_inserted = nodes_[idx].closer_inserted},
|
|
idx);
|
|
}
|
|
InsertSyntheticClosers(item_idx);
|
|
InsertSyntheticOpeners(item_idx);
|
|
layer_dedup_.Clear();
|
|
PruneBeam(current_layer_);
|
|
}
|
|
|
|
auto RegionSearch::InsertSyntheticClosers(int32_t item_idx) -> void {
|
|
const Item& item = items_[item_idx];
|
|
// A worklist rather than one pass over the layer, so that several groups can
|
|
// be closed at the same point.
|
|
llvm::SmallVector<int32_t> worklist = current_layer_;
|
|
for (size_t head = 0; head < worklist.size(); ++head) {
|
|
const SearchState current = Snapshot(nodes_[worklist[head]]);
|
|
if (current.cost > min_goal_cost_ || stacks_.IsEmpty(current.stack)) {
|
|
continue;
|
|
}
|
|
const auto& top = stacks_.Top(current.stack);
|
|
// Synthetic openers exist only to consume real closers; closing one
|
|
// synthetically would insert a pointless empty pair.
|
|
if (top.is_synthetic) {
|
|
continue;
|
|
}
|
|
if (item.token.kind == MatchingClosingKind(top.kind) &&
|
|
DirectMatchPreferred(top, item)) {
|
|
continue;
|
|
}
|
|
llvm::StringLiteral rule_name = "";
|
|
auto cost =
|
|
ClassifyCloserInsertion(top, item, stacks_, current.stack, rule_name);
|
|
if (!cost) {
|
|
continue;
|
|
}
|
|
|
|
const OpenBracketInfo& popped = top;
|
|
OpenStackId next_stack = stacks_.Pop(current.stack);
|
|
auto closer_kind = MatchingClosingKind(popped.kind);
|
|
AddToLayer(
|
|
current_layer_, item_idx, next_stack, closer_kind, current.cost + *cost,
|
|
ParentEdge{
|
|
.parent_node_index = worklist[head],
|
|
.correction =
|
|
BracketCorrection{
|
|
.diagnostic_kind = BracketDiagnosticKind::UnmatchedOpening,
|
|
.diagnostic_token_index = popped.token_index,
|
|
.fix_action = BracketFixAction::InsertBefore,
|
|
.fix_token_index = item.token.token_index,
|
|
.fix_token_kind = ToTokenKind(closer_kind),
|
|
.rule_name = rule_name,
|
|
},
|
|
.has_correction = true,
|
|
},
|
|
&worklist);
|
|
}
|
|
}
|
|
|
|
auto RegionSearch::InsertSyntheticOpeners(int32_t item_idx) -> void {
|
|
const Item& item = items_[item_idx];
|
|
// Iterate only over the states present after the closer phase, so that
|
|
// synthetic openers don't chain onto each other. The bound is taken before
|
|
// the loop because the loop appends to the layer.
|
|
for (size_t idx : llvm::seq<size_t>(0, current_layer_.size())) {
|
|
int32_t node_idx = current_layer_[idx];
|
|
const SearchState current = Snapshot(nodes_[node_idx]);
|
|
if (current.cost > min_goal_cost_ ||
|
|
static_cast<size_t>(stacks_.Depth(current.stack)) >=
|
|
MaxSearchStackDepth) {
|
|
continue;
|
|
}
|
|
for (auto [open_kind, is_struct_brace] : SyntheticOpeners) {
|
|
llvm::StringLiteral rule_name = "";
|
|
auto cost =
|
|
ClassifyOpenerInsertion(open_kind, is_struct_brace, item, rule_name);
|
|
if (!cost) {
|
|
continue;
|
|
}
|
|
OpenStackId next_stack = stacks_.Push(
|
|
current.stack,
|
|
OpenBracketInfo{
|
|
OpenBracketKey{
|
|
.insertion_token_index = item.token.token_index,
|
|
.line = item.token.line,
|
|
.line_indent = item.token.line_indent,
|
|
.effective_header_indent = item.effective_header_indent,
|
|
.kind = open_kind,
|
|
.is_synthetic = true,
|
|
.is_struct_brace = is_struct_brace,
|
|
},
|
|
rule_name,
|
|
});
|
|
AddToLayer(current_layer_, item_idx, next_stack, current.closer_inserted,
|
|
current.cost + *cost,
|
|
ParentEdge{.parent_node_index = node_idx});
|
|
}
|
|
}
|
|
}
|
|
|
|
auto RegionSearch::AdvanceOverItem(int32_t item_idx)
|
|
-> llvm::SmallVector<int32_t> {
|
|
llvm::SmallVector<int32_t> next_layer;
|
|
for (int32_t node_idx : current_layer_) {
|
|
AdvanceNode(node_idx, item_idx, next_layer);
|
|
}
|
|
layer_dedup_.Clear();
|
|
PruneBeam(next_layer);
|
|
return next_layer;
|
|
}
|
|
|
|
auto RegionSearch::AdvanceNode(int32_t node_idx, int32_t item_idx,
|
|
llvm::SmallVectorImpl<int32_t>& next_layer)
|
|
-> void {
|
|
const SearchState current = Snapshot(nodes_[node_idx]);
|
|
if (current.cost > min_goal_cost_) {
|
|
return;
|
|
}
|
|
const Item& item = items_[item_idx];
|
|
auto kind = item.token.kind;
|
|
|
|
auto advance = [&](OpenStackId next_stack, int32_t add_cost,
|
|
BracketCorrection correction = {},
|
|
bool has_correction = false) {
|
|
AddToLayer(next_layer, item_idx + 1, next_stack, Kind::Other,
|
|
current.cost + add_cost,
|
|
ParentEdge{.parent_node_index = node_idx,
|
|
.correction = correction,
|
|
.has_correction = has_correction});
|
|
};
|
|
|
|
// What it costs to advance over this item in this state, per the
|
|
// `AdvanceRules` table.
|
|
int32_t penalty = AdvancePenalty(stacks_, current, item);
|
|
|
|
if (item.HasAll(Cue::CollapsedBlock)) {
|
|
advance(current.stack, penalty);
|
|
return;
|
|
}
|
|
|
|
if (IsOpeningBracket(kind)) {
|
|
// Advance, pushing the opener onto the stack.
|
|
if (static_cast<size_t>(stacks_.Depth(current.stack)) <
|
|
MaxSearchStackDepth) {
|
|
advance(stacks_.Push(current.stack, RealOpener(item)), penalty);
|
|
}
|
|
// Advance without pushing: replace the unmatched opener with an error
|
|
// token.
|
|
advance(
|
|
current.stack, CostReplaceOpening,
|
|
ReplaceWithError(item.token, BracketDiagnosticKind::UnmatchedOpening,
|
|
"Adv_ReplaceOpener"),
|
|
/*has_correction=*/true);
|
|
return;
|
|
}
|
|
|
|
if (IsClosingBracket(kind)) {
|
|
// Advance, popping the opener this closer matches.
|
|
if (!stacks_.IsEmpty(current.stack) &&
|
|
stacks_.Top(current.stack).kind == MatchingOpeningKind(kind)) {
|
|
const OpenBracketInfo& popped = stacks_.Top(current.stack);
|
|
if (auto match_cost = MatchClosePenalty(popped, item)) {
|
|
// Matching a synthetic opener finally pins down where it goes, so this
|
|
// is where it becomes a correction.
|
|
advance(stacks_.Pop(current.stack), penalty + *match_cost,
|
|
popped.is_synthetic ? InsertOpenerCorrection(popped, item.token)
|
|
: BracketCorrection{},
|
|
popped.is_synthetic);
|
|
}
|
|
}
|
|
// Advance without matching: replace the unmatched closer with an error
|
|
// token.
|
|
advance(
|
|
current.stack, CostReplaceClosing,
|
|
ReplaceWithError(item.token, BracketDiagnosticKind::UnmatchedClosing,
|
|
"Adv_ReplaceCloser"),
|
|
/*has_correction=*/true);
|
|
return;
|
|
}
|
|
|
|
// Any other token.
|
|
advance(current.stack, penalty);
|
|
}
|
|
|
|
auto RegionSearch::CloseAtRegionEnd() -> llvm::SmallVector<int32_t> {
|
|
llvm::SmallVector<int32_t> goal_node_indices;
|
|
for (int32_t node_idx : current_layer_) {
|
|
const SearchState current = Snapshot(nodes_[node_idx]);
|
|
if (current.cost > min_goal_cost_) {
|
|
continue;
|
|
}
|
|
// A synthetic opener that never matched a real closer is a meaningless
|
|
// insertion; reject such states rather than dropping it silently.
|
|
if (stacks_.HasSynthetic(current.stack)) {
|
|
continue;
|
|
}
|
|
// Close what's left innermost-first, one node per closer, so that each is
|
|
// reconstructed as its own correction.
|
|
int32_t finish_cost = current.cost;
|
|
int32_t parent = node_idx;
|
|
for (OpenStackId open_stack = current.stack; !stacks_.IsEmpty(open_stack);
|
|
open_stack = stacks_.Pop(open_stack)) {
|
|
const OpenBracketInfo& open = stacks_.Top(open_stack);
|
|
finish_cost += CostToCloseAtEnd(open);
|
|
nodes_.push_back(BeamNode{
|
|
.item_index = static_cast<int32_t>(items_.size()),
|
|
.stack = OpenStackId::Empty,
|
|
.cost = finish_cost,
|
|
.parent_edges = {{
|
|
.parent_node_index = parent,
|
|
.correction =
|
|
BracketCorrection{
|
|
.diagnostic_kind =
|
|
BracketDiagnosticKind::UnmatchedOpening,
|
|
.diagnostic_token_index = open.token_index,
|
|
.fix_action = BracketFixAction::InsertBefore,
|
|
.fix_token_index = region_end_token_,
|
|
.fix_token_kind =
|
|
ToTokenKind(MatchingClosingKind(open.kind)),
|
|
.rule_name = "Close_RegionEnd"},
|
|
.has_correction = true,
|
|
}},
|
|
});
|
|
parent = static_cast<int32_t>(nodes_.size()) - 1;
|
|
}
|
|
|
|
if (finish_cost < min_goal_cost_) {
|
|
min_goal_cost_ = finish_cost;
|
|
goal_node_indices.clear();
|
|
}
|
|
if (finish_cost == min_goal_cost_) {
|
|
goal_node_indices.push_back(parent);
|
|
}
|
|
}
|
|
return goal_node_indices;
|
|
}
|
|
|
|
auto RegionSearch::Solve(llvm::SmallVectorImpl<BracketCorrection>& corrections)
|
|
-> void {
|
|
nodes_.push_back(BeamNode{
|
|
.item_index = 0,
|
|
.stack = OpenStackId::Empty,
|
|
.cost = 0,
|
|
.parent_edges = {},
|
|
});
|
|
current_layer_ = {0};
|
|
|
|
for (auto [item_index, item] : llvm::enumerate(items_)) {
|
|
auto i = static_cast<int32_t>(item_index);
|
|
// Nothing is inserted before the end of the file; brackets still open there
|
|
// are closed by `CloseAtRegionEnd` instead.
|
|
if (item.token.kind != Kind::FileEnd) {
|
|
InsertBracketsBefore(i);
|
|
}
|
|
current_layer_ = AdvanceOverItem(i);
|
|
}
|
|
|
|
llvm::SmallVector<int32_t> goal_node_indices = CloseAtRegionEnd();
|
|
if (goal_node_indices.empty()) {
|
|
SolveNaive(items_, corrections);
|
|
return;
|
|
}
|
|
ReconstructCorrections(nodes_, goal_node_indices, items_, region_end_token_,
|
|
corrections);
|
|
}
|
|
|
|
// Solve a damaged region using layered beam search with tie detection.
|
|
// `region_end_token` is the token directly after the region, where any
|
|
// still-unclosed brackets are closed.
|
|
static auto SolveRegionCostBased(
|
|
llvm::ArrayRef<Item> items, TokenIndex region_end_token,
|
|
llvm::SmallVectorImpl<BracketCorrection>& corrections) -> void {
|
|
if (items.size() > static_cast<size_t>(MaxRegionItemsForSearch)) {
|
|
SolveNaive(items, corrections);
|
|
return;
|
|
}
|
|
RegionSearch(items, region_end_token).Solve(corrections);
|
|
}
|
|
|
|
// The passes below run in order over the whole token sequence, each consuming
|
|
// the results of the ones before it: analyze the tokens, decide which matched
|
|
// bracket pairs can be trusted and collapsed, build the item sequence from
|
|
// that, then split it into regions and search the damaged ones.
|
|
|
|
// Finds the stack position of the opener in `open_stack` that the closing
|
|
// bracket `tokens[closer_index]` should pair with, or -1 if none is plausible.
|
|
// A `}` matches a `{` only when they're on the same line, the `{` is a struct
|
|
// brace, or their line indentation agrees.
|
|
static auto FindMatchingOpener(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> open_stack,
|
|
int32_t closer_index) -> int32_t {
|
|
const auto& closer = tokens[closer_index];
|
|
auto num_open = static_cast<int32_t>(open_stack.size());
|
|
for (int32_t s : llvm::reverse(llvm::seq(0, num_open))) {
|
|
const auto& opener = tokens[open_stack[s]];
|
|
if (closer.kind != Kind::CloseCurlyBrace) {
|
|
if (opener.kind == MatchingOpeningKind(closer.kind)) {
|
|
return s;
|
|
}
|
|
} else if (opener.kind == Kind::OpenCurlyBrace &&
|
|
(opener.line == closer.line || opener.is_struct_brace ||
|
|
opener.line_indent == closer.line_indent)) {
|
|
return s;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Pairs up brackets by a stack walk, returning for each token the index of the
|
|
// bracket it pairs with, or -1 if it has none. A closer that doesn't match the
|
|
// top of the stack pops through to a plausible match if one exists (leaving the
|
|
// popped brackets unmatched), and is otherwise left unmatched without
|
|
// disturbing the stack.
|
|
static auto MatchBracketPairs(llvm::ArrayRef<MismatchedBracketToken> tokens)
|
|
-> llvm::SmallVector<int32_t> {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
llvm::SmallVector<int32_t> match_partner(num_tokens, -1);
|
|
llvm::SmallVector<int32_t> open_stack;
|
|
for (int32_t i : llvm::seq(0, num_tokens)) {
|
|
auto kind = tokens[i].kind;
|
|
if (IsOpeningBracket(kind)) {
|
|
open_stack.push_back(i);
|
|
} else if (IsClosingBracket(kind)) {
|
|
int32_t match_s = FindMatchingOpener(tokens, open_stack, i);
|
|
if (match_s != -1) {
|
|
match_partner[open_stack[match_s]] = i;
|
|
match_partner[i] = open_stack[match_s];
|
|
open_stack.resize(match_s);
|
|
}
|
|
}
|
|
}
|
|
return match_partner;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// The statement segments of the token sequence, which are separated by `;`,
|
|
// `{`, and `}`.
|
|
struct Segments {
|
|
// The id of each token's segment. Segments are numbered in order.
|
|
llvm::SmallVector<int32_t> id;
|
|
// The index of the first token of each token's segment.
|
|
llvm::SmallVector<int32_t> first;
|
|
};
|
|
|
|
// The indexes of the unmatched `(`, `[`, `)`, and `]` tokens, each list in
|
|
// increasing order so it can be binary-searched.
|
|
struct UnmatchedGroupBrackets {
|
|
llvm::SmallVector<int32_t> open_parens;
|
|
llvm::SmallVector<int32_t> open_squares;
|
|
llvm::SmallVector<int32_t> close_parens;
|
|
llvm::SmallVector<int32_t> close_squares;
|
|
|
|
// The openers and closers of the bracket kind that `open_kind` opens.
|
|
auto OpenersOfKind(Kind open_kind) const -> llvm::ArrayRef<int32_t> {
|
|
return open_kind == Kind::OpenParen ? open_parens : open_squares;
|
|
}
|
|
auto ClosersOfKind(Kind open_kind) const -> llvm::ArrayRef<int32_t> {
|
|
return open_kind == Kind::OpenParen ? close_parens : close_squares;
|
|
}
|
|
};
|
|
|
|
// Everything the later passes need to know about the token sequence as a whole,
|
|
// computed once up front by `AnalyzeTokens`. The vectors are all indexed by
|
|
// token index.
|
|
struct TokenAnalysis {
|
|
// The index of the bracket each token pairs with, or -1. See
|
|
// `MatchBracketPairs`.
|
|
llvm::SmallVector<int32_t> match_partner;
|
|
Segments segments;
|
|
UnmatchedGroupBrackets unmatched;
|
|
// See `ComputeAssociatedLineIndents`.
|
|
llvm::SmallVector<int32_t> effective_header_indent;
|
|
llvm::BitVector is_first_on_line;
|
|
// See `ComputeFollowsStatementHeader` and `ComputeHeaderHasOpenCurlyBrace`.
|
|
// The latter is only computed where the former holds.
|
|
llvm::BitVector follows_statement_header;
|
|
llvm::BitVector header_has_open_curly_brace;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static auto ComputeSegments(llvm::ArrayRef<MismatchedBracketToken> tokens)
|
|
-> Segments {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
Segments segments = {.id = llvm::SmallVector<int32_t>(num_tokens, 0),
|
|
.first = llvm::SmallVector<int32_t>(num_tokens, 0)};
|
|
for (int32_t i : llvm::seq(1, num_tokens)) {
|
|
auto prev_kind = tokens[i - 1].kind;
|
|
bool new_seg = prev_kind == Kind::Semi ||
|
|
prev_kind == Kind::OpenCurlyBrace ||
|
|
prev_kind == Kind::CloseCurlyBrace;
|
|
segments.id[i] = segments.id[i - 1] + (new_seg ? 1 : 0);
|
|
segments.first[i] = new_seg ? i : segments.first[i - 1];
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
static auto FindUnmatchedGroupBrackets(
|
|
llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<int32_t> match_partner) -> UnmatchedGroupBrackets {
|
|
UnmatchedGroupBrackets unmatched;
|
|
for (auto [i, token] : llvm::enumerate(tokens)) {
|
|
if (match_partner[i] != -1) {
|
|
continue;
|
|
}
|
|
auto index = static_cast<int32_t>(i);
|
|
switch (token.kind) {
|
|
case Kind::OpenParen:
|
|
unmatched.open_parens.push_back(index);
|
|
break;
|
|
case Kind::OpenSquareBracket:
|
|
unmatched.open_squares.push_back(index);
|
|
break;
|
|
case Kind::CloseParen:
|
|
unmatched.close_parens.push_back(index);
|
|
break;
|
|
case Kind::CloseSquareBracket:
|
|
unmatched.close_squares.push_back(index);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return unmatched;
|
|
}
|
|
|
|
static auto AnalyzeTokens(llvm::ArrayRef<MismatchedBracketToken> tokens)
|
|
-> TokenAnalysis {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
auto match_partner = MatchBracketPairs(tokens);
|
|
auto segments = ComputeSegments(tokens);
|
|
auto unmatched = FindUnmatchedGroupBrackets(tokens, match_partner);
|
|
|
|
auto effective_header_indent =
|
|
ComputeAssociatedLineIndents(tokens, match_partner);
|
|
llvm::BitVector is_first_on_line(num_tokens);
|
|
llvm::BitVector follows_statement_header(num_tokens);
|
|
llvm::BitVector header_has_open_curly_brace(num_tokens);
|
|
for (int32_t i : llvm::seq(0, num_tokens)) {
|
|
is_first_on_line[i] = (i == 0 || tokens[i].line != tokens[i - 1].line);
|
|
follows_statement_header[i] =
|
|
ComputeFollowsStatementHeader(tokens, match_partner, i);
|
|
if (follows_statement_header[i]) {
|
|
header_has_open_curly_brace[i] =
|
|
ComputeHeaderHasOpenCurlyBrace(tokens, match_partner, i);
|
|
}
|
|
}
|
|
|
|
return TokenAnalysis{
|
|
.match_partner = std::move(match_partner),
|
|
.segments = std::move(segments),
|
|
.unmatched = std::move(unmatched),
|
|
.effective_header_indent = std::move(effective_header_indent),
|
|
.is_first_on_line = std::move(is_first_on_line),
|
|
.follows_statement_header = std::move(follows_statement_header),
|
|
.header_has_open_curly_brace = std::move(header_has_open_curly_brace),
|
|
};
|
|
}
|
|
|
|
// Whether the sorted list `list` contains an element in [lo, hi].
|
|
static auto ContainsInRange(llvm::ArrayRef<int32_t> list, int32_t lo,
|
|
int32_t hi) -> bool {
|
|
const auto* it = std::lower_bound(list.begin(), list.end(), lo);
|
|
return it != list.end() && *it <= hi;
|
|
}
|
|
|
|
// Whether a matched `{`...`}` pair is trustworthy, judging by how the `}` lines
|
|
// up and by the separators directly inside the pair.
|
|
static auto BracePairIsClean(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
const TokenAnalysis& analysis, int32_t open_idx,
|
|
int32_t close_idx) -> bool {
|
|
const auto& open = tokens[open_idx];
|
|
int32_t header_indent = analysis.effective_header_indent[open_idx];
|
|
if (open.line != tokens[close_idx].line) {
|
|
if (!open.is_struct_brace &&
|
|
(!open.is_at_end_of_line ||
|
|
header_indent != tokens[close_idx].line_indent)) {
|
|
return false;
|
|
}
|
|
if (open.is_struct_brace && tokens[close_idx].line_indent < header_indent) {
|
|
return false;
|
|
}
|
|
}
|
|
// A `;` directly inside a struct brace, or a `,` directly inside a scope
|
|
// brace, is illegal; the brace pairing has likely captured too much.
|
|
auto bad_kind = open.is_struct_brace ? Kind::Semi : Kind::Comma;
|
|
int32_t depth = 0;
|
|
for (int32_t j : llvm::seq(open_idx + 1, close_idx)) {
|
|
if (IsOpeningBracket(tokens[j].kind)) {
|
|
++depth;
|
|
} else if (IsClosingBracket(tokens[j].kind)) {
|
|
--depth;
|
|
} else if (tokens[j].kind == bad_kind && depth == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Whether a matched `(`...`)` or `[`...`]` pair is trustworthy. An unmatched
|
|
// opener of the same kind earlier in the same statement segment could really
|
|
// own our closer, and an unmatched closer of the same kind later in the same
|
|
// segment could really own our opener; both make the pairing suspect.
|
|
static auto GroupPairIsClean(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
const TokenAnalysis& analysis, int32_t open_idx,
|
|
int32_t close_idx) -> bool {
|
|
auto kind = tokens[open_idx].kind;
|
|
if (ContainsInRange(analysis.unmatched.OpenersOfKind(kind),
|
|
analysis.segments.first[open_idx], open_idx - 1)) {
|
|
return false;
|
|
}
|
|
// A scan for an unmatched closer would be bounded by the end of the closer's
|
|
// segment, so it's enough to check whether the first one after `close_idx` is
|
|
// still in that segment.
|
|
llvm::ArrayRef<int32_t> closers = analysis.unmatched.ClosersOfKind(kind);
|
|
const auto* it = std::upper_bound(closers.begin(), closers.end(), close_idx);
|
|
return it == closers.end() ||
|
|
analysis.segments.id[*it] != analysis.segments.id[close_idx];
|
|
}
|
|
|
|
// Whether everything between a matched pair is itself safe to collapse: every
|
|
// bracket inside pairs up within the pair and is clean, and for a scope brace,
|
|
// no line inside is dedented to or past the header.
|
|
static auto PairInteriorIsClean(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
const TokenAnalysis& analysis,
|
|
const llvm::BitVector& is_clean_range,
|
|
int32_t open_idx, int32_t close_idx) -> bool {
|
|
const auto& open = tokens[open_idx];
|
|
for (int32_t j : llvm::seq(open_idx + 1, close_idx)) {
|
|
int32_t partner = analysis.match_partner[j];
|
|
if (partner == -1) {
|
|
// An unmatched bracket inside means the pair can't be trusted.
|
|
if (IsOpeningBracket(tokens[j].kind) ||
|
|
IsClosingBracket(tokens[j].kind)) {
|
|
return false;
|
|
}
|
|
} else if (partner < open_idx || partner > close_idx) {
|
|
return false;
|
|
}
|
|
if (IsOpeningBracket(tokens[j].kind) && !is_clean_range[j]) {
|
|
return false;
|
|
}
|
|
if (open.kind == Kind::OpenCurlyBrace && !open.is_struct_brace &&
|
|
analysis.is_first_on_line[j] &&
|
|
tokens[j].line != tokens[close_idx].line &&
|
|
tokens[j].line_indent <= analysis.effective_header_indent[open_idx]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Marks the matched pairs that can be trusted, so that the search can collapse
|
|
// each into a single item instead of reconsidering brackets that clearly pair
|
|
// up. Pairs are visited in reverse order, so an inner pair is decided before
|
|
// the pairs enclosing it.
|
|
//
|
|
// Note: an illegal leaf adjacency inside a *matched* pair is treated as invalid
|
|
// user code, not a bracket error; the pair is still trusted and collapsed. The
|
|
// adjacency cue only guides where to insert brackets in regions that are
|
|
// already unbalanced.
|
|
static auto MarkCleanRanges(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
const TokenAnalysis& analysis) -> llvm::BitVector {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
llvm::BitVector is_clean_range(num_tokens);
|
|
for (int32_t i : llvm::reverse(llvm::seq(0, num_tokens))) {
|
|
// Only consider a pair from its opener; this also skips the unmatched
|
|
// tokens, whose partner is -1.
|
|
int32_t close_idx = analysis.match_partner[i];
|
|
if (close_idx <= i) {
|
|
continue;
|
|
}
|
|
bool clean = tokens[i].kind == Kind::OpenCurlyBrace
|
|
? BracePairIsClean(tokens, analysis, i, close_idx)
|
|
: GroupPairIsClean(tokens, analysis, i, close_idx);
|
|
is_clean_range[i] =
|
|
clean &&
|
|
PairInteriorIsClean(tokens, analysis, is_clean_range, i, close_idx);
|
|
}
|
|
return is_clean_range;
|
|
}
|
|
|
|
// Builds the item sequence the search runs over: one item per token, except
|
|
// that each clean matched pair collapses into a single item spanning it.
|
|
static auto BuildItems(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
const TokenAnalysis& analysis,
|
|
const llvm::BitVector& is_clean_range)
|
|
-> llvm::SmallVector<Item> {
|
|
auto num_tokens = static_cast<int32_t>(tokens.size());
|
|
llvm::SmallVector<Item> items;
|
|
auto make_item = [&](int32_t start, int32_t end, bool collapsed,
|
|
bool has_scope) {
|
|
// Cues the surrounding analysis has already determined; the rest follow
|
|
// from the tokens themselves.
|
|
CueSet context_cues =
|
|
CueIf(collapsed, Cue::CollapsedBlock) |
|
|
CueIf(has_scope, Cue::ContainsScopeBrace) |
|
|
CueIf(analysis.is_first_on_line[start], Cue::FirstOnLine) |
|
|
CueIf(analysis.follows_statement_header[start],
|
|
Cue::FollowsStatementHeader) |
|
|
CueIf(analysis.header_has_open_curly_brace[start],
|
|
Cue::HeaderHasOpenCurly);
|
|
items.push_back(Item{
|
|
.token_start_index = start,
|
|
.token_end_index = end,
|
|
.token = tokens[start],
|
|
.effective_header_indent = analysis.effective_header_indent[start],
|
|
.cues = ComputeItemCues(
|
|
tokens[start], start > 0 ? &tokens[start - 1] : nullptr,
|
|
items.empty() ? nullptr : &items.back(), context_cues),
|
|
});
|
|
};
|
|
for (int32_t i = 0; i < num_tokens;) {
|
|
if (!is_clean_range[i]) {
|
|
make_item(i, i, /*collapsed=*/false,
|
|
tokens[i].kind == Kind::OpenCurlyBrace ||
|
|
tokens[i].kind == Kind::CloseCurlyBrace);
|
|
++i;
|
|
continue;
|
|
}
|
|
int32_t close_idx = analysis.match_partner[i];
|
|
bool has_scope = llvm::any_of(llvm::seq(i, close_idx + 1), [&](int32_t j) {
|
|
return tokens[j].kind == Kind::OpenCurlyBrace &&
|
|
!tokens[j].is_struct_brace;
|
|
});
|
|
make_item(i, close_idx, /*collapsed=*/true, has_scope);
|
|
i = close_idx + 1;
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// Finds the item indexes where each independently-solved region starts,
|
|
// beginning with 0 and ending with the number of items, so that consecutive
|
|
// boundaries delimit the regions. A region ends at a top-level declaration
|
|
// boundary: a statement introducer at zero indentation whose predecessor ended
|
|
// a statement. This bounds how far a single mistake can smear, and gives
|
|
// unclosed brackets a natural place to be closed (the region end).
|
|
static auto FindRegionBoundaries(llvm::ArrayRef<MismatchedBracketToken> tokens,
|
|
llvm::ArrayRef<Item> items)
|
|
-> llvm::SmallVector<int32_t> {
|
|
llvm::SmallVector<int32_t> boundaries = {0};
|
|
for (auto [item_index, item] : llvm::enumerate(items)) {
|
|
auto i = static_cast<int32_t>(item_index);
|
|
// Note: line_indent is a 1-based column number, so top-level tokens have
|
|
// line_indent 1.
|
|
if (i == 0 || item.HasAll(Cue::CollapsedBlock) ||
|
|
item.token.kind != Kind::StatementIntroducer ||
|
|
item.token.is_else_keyword || item.token.line_indent > 1 ||
|
|
!item.HasAll(Cue::FirstOnLine)) {
|
|
continue;
|
|
}
|
|
auto prev_end_kind = tokens[items[i - 1].token_end_index].kind;
|
|
if ((prev_end_kind == Kind::Semi ||
|
|
prev_end_kind == Kind::CloseCurlyBrace) &&
|
|
boundaries.back() != i) {
|
|
boundaries.push_back(i);
|
|
}
|
|
}
|
|
if (boundaries.back() != static_cast<int32_t>(items.size())) {
|
|
boundaries.push_back(static_cast<int32_t>(items.size()));
|
|
}
|
|
return boundaries;
|
|
}
|
|
|
|
// Whether a region's loose (non-collapsed) brackets already form a balanced,
|
|
// well-nested sequence. Such a region needs no solving: it has no unmatched
|
|
// bracket, so the search would simply match everything and emit no corrections.
|
|
// Skipping it avoids running the beam search over the many regions whose
|
|
// matched pairs merely weren't collapsed.
|
|
static auto RegionIsBalanced(llvm::ArrayRef<Item> region) -> bool {
|
|
llvm::SmallVector<Kind> open_kinds;
|
|
for (const Item& item : region) {
|
|
if (item.HasAll(Cue::CollapsedBlock)) {
|
|
continue;
|
|
}
|
|
auto kind = item.token.kind;
|
|
if (IsOpeningBracket(kind)) {
|
|
open_kinds.push_back(kind);
|
|
} else if (IsClosingBracket(kind)) {
|
|
if (open_kinds.empty() ||
|
|
MatchingClosingKind(open_kinds.back()) != kind) {
|
|
return false;
|
|
}
|
|
open_kinds.pop_back();
|
|
}
|
|
}
|
|
return open_kinds.empty();
|
|
}
|
|
|
|
auto FixMismatchedBrackets(llvm::ArrayRef<MismatchedBracketToken> tokens)
|
|
-> llvm::SmallVector<BracketCorrection> {
|
|
llvm::SmallVector<BracketCorrection> corrections;
|
|
if (tokens.empty()) {
|
|
return corrections;
|
|
}
|
|
|
|
TokenAnalysis analysis = AnalyzeTokens(tokens);
|
|
llvm::BitVector is_clean_range = MarkCleanRanges(tokens, analysis);
|
|
llvm::SmallVector<Item> items = BuildItems(tokens, analysis, is_clean_range);
|
|
llvm::SmallVector<int32_t> region_boundaries =
|
|
FindRegionBoundaries(tokens, items);
|
|
|
|
// Each region runs between consecutive boundaries.
|
|
for (auto [start, end] :
|
|
llvm::zip(region_boundaries, llvm::drop_begin(region_boundaries))) {
|
|
if (start >= end) {
|
|
continue;
|
|
}
|
|
auto region = llvm::ArrayRef<Item>(items).slice(start, end - start);
|
|
if (RegionIsBalanced(region)) {
|
|
continue;
|
|
}
|
|
// Any bracket still open at the end of the region is closed before the
|
|
// token the next region starts at, or before the final `FileEnd`.
|
|
TokenIndex region_end_token = end < static_cast<int32_t>(items.size())
|
|
? items[end].token.token_index
|
|
: tokens.back().token_index;
|
|
SolveRegionCostBased(region, region_end_token, corrections);
|
|
}
|
|
|
|
llvm::stable_sort(
|
|
corrections, [](const BracketCorrection& a, const BracketCorrection& b) {
|
|
if (a.diagnostic_token_index != b.diagnostic_token_index) {
|
|
return a.diagnostic_token_index < b.diagnostic_token_index;
|
|
}
|
|
return a.fix_token_index < b.fix_token_index;
|
|
});
|
|
|
|
return corrections;
|
|
}
|
|
|
|
} // namespace Carbon::Lex
|