blob: 660b8d0bf7400ea4dff083998357dfcb8773088b [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import '../chunk.dart';
import '../fast_hash.dart';
/// A constraint that determines the different ways a related set of chunks may
/// be split.
class Rule extends FastHash {
/// Rule value that splits no chunks.
/// Every rule is required to treat this value as fully unsplit.
static const unsplit = 0;
/// Rule constraint value that means "any value as long as something splits".
/// It disallows [unsplit] but allows any other value.
static const mustSplit = -1;
/// The number of different states this rule can be in.
/// Each state determines which set of chunks using this rule are split and
/// which aren't. Values range from zero to one minus this. Value zero
/// always means "no chunks are split" and increasing values by convention
/// mean increasingly undesirable splits.
/// By default, a rule has two values: fully unsplit and fully split.
int get numValues => 2;
/// The rule value that forces this rule into its maximally split state.
/// By convention, this is the highest of the range of allowed values.
int get fullySplitValue => numValues - 1;
int get cost => _cost;
final int _cost;
/// During line splitting [LineSplitter] sets this to the index of this
/// rule in its list of rules.
int? index;
/// If `true`, the rule has been "hardened" meaning it's been placed into a
/// permanent "must fully split" state.
bool get isHardened => _isHardened;
bool _isHardened = false;
/// The other [Rule]s that are implied this one.
/// In many cases, if a split occurs inside an expression, surrounding rules
/// also want to split too. For example, a split in the middle of an argument
/// forces the entire argument list to also split.
/// This tracks those relationships. If this rule splits, (sets its value to
/// [fullySplitValue]) then all of the surrounding implied rules are also set
/// to their fully split value.
/// This contains all direct as well as transitive relationships. If A
/// contains B which contains C, C's outerRules contains both B and A.
final Set<Rule> _implied = <Rule>{};
/// Marks [other] as implied by this one.
/// That means that if this rule splits, then [other] is force to split too.
void imply(Rule other) {
/// Whether this rule cares about rules that it contains.
/// If `true` then inner rules will constrain this one and force it to split
/// when they split. Otherwise, it can split independently of any contained
/// rules.
bool get splitsOnInnerRules => true;
Rule([int? cost]) : _cost = cost ?? Cost.normal;
/// Creates a new rule that is already fully split.
Rule.hard() : _cost = 0 {
// Set the cost to zero since it will always be applied, so there's no
// point in penalizing it.
// Also, this avoids doubled counting in literal blocks where there is both
// a split in the outer chunk containing the block and the inner hard split
// between the elements or statements.
/// Fixes this rule into a "fully split" state.
void harden() {
_isHardened = true;
/// Returns `true` if [chunk] should split when this rule has [value].
bool isSplit(int value, Chunk chunk) {
if (_isHardened) return true;
if (value == Rule.unsplit) return false;
// Let the subclass decide.
return isSplitAtValue(value, chunk);
/// Subclasses can override this to determine which values split which chunks.
/// By default, this assumes every chunk splits.
bool isSplitAtValue(int value, Chunk chunk) => true;
/// Given that this rule has [value], determine if [other]'s value should be
/// constrained.
/// Allows relationships between rules like "if I split, then this should
/// split too". Returns a non-negative value to force [other] to take that
/// value. Returns -1 to allow [other] to take any non-zero value. Returns
/// `null` to not constrain other.
int? constrain(int value, Rule other) {
// By default, any containing rule will be fully split if this one is split.
if (value == Rule.unsplit) return null;
if (_implied.contains(other)) return other.fullySplitValue;
return null;
/// A protected method for subclasses to add the rules that they constrain
/// to [rules].
/// Called by [Rule] the first time [constrainedRules] is accessed.
void addConstrainedRules(Set<Rule> rules) {}
/// Discards constraints on any rule that doesn't have an index.
/// This is called by [LineSplitter] after it has indexed all of the in-use
/// rules. A rule may end up with a constraint on a rule that's no longer
/// used by any chunk. This can happen if the rule gets hardened, or if it
/// simply never got used by a chunk. For example, a rule for splitting an
/// empty list of metadata annotations.
/// This removes all of those.
void forgetUnusedRules() {
_implied.retainWhere((rule) => rule.index != null);
// Clear the cached ones too.
_constrainedRules = null;
_allConstrainedRules = null;
/// The other [Rule]s that this rule places immediate constraints on.
Set<Rule> get constrainedRules {
// Lazy initialize this on first use. Note: Assumes this is only called
// after the chunks have been written and any constraints have been wired
// up.
var rules = _constrainedRules;
if (rules != null) return rules;
rules = _implied.toSet();
_constrainedRules = rules;
return rules;
Set<Rule>? _constrainedRules;
/// The transitive closure of all of the rules this rule places constraints
/// on, directly or indirectly, including itself.
Set<Rule> get allConstrainedRules {
var rules = _allConstrainedRules;
if (rules != null) return rules;
rules = {};
_traverseConstraints(rules, this);
_allConstrainedRules = rules;
return rules;
/// Traverses the constraint graph of [rule] adding everything to [rules].
void _traverseConstraints(Set<Rule> rules, Rule rule) {
if (rules.contains(rule)) return;
for (var rule in rule.constrainedRules) {
_traverseConstraints(rules, rule);
Set<Rule>? _allConstrainedRules;
String toString() => '$id';