Skip to content

Commit

Permalink
Allow non-contiguous state values. (#1279)
Browse files Browse the repository at this point in the history
Allow non-contiguous state values.

Requiring the states to be a zero-based and contiguous makes it harder
for some pieces to support different sets of different states based on
what they contain. For example, an ImportPiece only supports states for
splitting the names inside a single combinator when there is more than
one combinator.

To model that with contiguous states requires the piece to carefully
track how state indexes are shifted or splitting pieces into different
classes to handle the different sets of states.

Instead, with this change, we simply allow the piece to return a list
of states that may be non-contiguous. They can then omit states that
don't make sense given the piece's contents. This makes ImportPiece
simpler and I think will be useful for other pieces that have a variety
of ways they can split.

Also wrap state values in a class to make things a little clearer and
more type safe.
  • Loading branch information
munificent authored Oct 13, 2023
1 parent 8f52f1e commit 35a26b2
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 179 deletions.
2 changes: 1 addition & 1 deletion lib/src/back_end/code_writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class CodeWriter {

// TODO(tall): Support pieces with different split costs, and possibly
// different costs for each state value.
if (state != 0) _cost++;
if (state != State.initial) _cost++;

// TODO(perf): Memoize this. Might want to create a nested PieceWriter
// instead of passing in `this` so we can better control what state needs
Expand Down
18 changes: 9 additions & 9 deletions lib/src/back_end/solution.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class PieceStateSet {
/// there's nothing to solve for them.
final List<Piece> _pieces;

final Map<Piece, int> _pieceStates;
final Map<Piece, State> _pieceStates;

/// Creates a new [PieceStateSet] with no pieces set to any state (which
/// implicitly means they have state 0).
Expand All @@ -26,7 +26,7 @@ class PieceStateSet {
PieceStateSet._(this._pieces, this._pieceStates);

/// The state this solution selects for [piece].
int pieceState(Piece piece) => _pieceStates[piece] ?? 0;
State pieceState(Piece piece) => _pieceStates[piece] ?? State.initial;

/// Gets the first piece that doesn't have a state selected yet, or `null` if
/// all pieces have selected states.
Expand All @@ -42,7 +42,7 @@ class PieceStateSet {
}

/// Creates a clone of this state with [piece] bound to [state].
PieceStateSet cloneWith(Piece piece, int state) {
PieceStateSet cloneWith(Piece piece, State state) {
return PieceStateSet._(_pieces, {..._pieceStates, piece: state});
}

Expand Down Expand Up @@ -85,13 +85,13 @@ class Solution implements Comparable<Solution> {
var piece = _state.firstUnsolved();
if (piece == null) return const [];

var result = <Solution>[];
for (var i = 0; i < piece.stateCount; i++) {
var solution = Solution(root, pageWidth, _state.cloneWith(piece, i));
result.add(solution);
}
return [
// All pieces support a default state.
Solution(root, pageWidth, _state.cloneWith(piece, State.initial)),

return result;
for (var state in piece.states)
Solution(root, pageWidth, _state.cloneWith(piece, state))
];
}

/// Compares two solutions where a more desirable solution comes first.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/back_end/solver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Solver {

void traverse(Piece piece) {
// We don't need to worry about selecting pieces that have only one state.
if (piece.stateCount > 1) unsolvedPieces.add(piece);
if (piece.states.isNotEmpty) unsolvedPieces.add(piece);
piece.forEachChild(traverse);
}

Expand Down
11 changes: 2 additions & 9 deletions lib/src/front_end/piece_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,10 @@ mixin PieceFactory implements CommentWriter {
}
}

var combinator = switch (combinators.length) {
0 => null,
1 => OneCombinatorPiece(combinators[0]),
2 => TwoCombinatorPiece(combinators),
_ => throw StateError('Directives can only have up to two combinators.'),
};

token(directive.semicolon);

writer.push(
ImportPiece(directivePiece, configurationsPiece, asClause, combinator));
writer.push(ImportPiece(
directivePiece, configurationsPiece, asClause, combinators));
}

/// Creates a single infix operation.
Expand Down
6 changes: 3 additions & 3 deletions lib/src/piece/block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ class BlockPiece extends Piece {
: _alwaysSplit = alwaysSplit;

@override
int get stateCount => _alwaysSplit ? 1 : 2;
List<State> get states => _alwaysSplit ? const [] : const [State.split];

@override
void format(CodeWriter writer, int state) {
void format(CodeWriter writer, State state) {
writer.format(leftBracket);

if (_alwaysSplit || state == 1) {
if (_alwaysSplit || state == State.split) {
writer.setIndent(Indent.block);
writer.newline();
writer.format(contents);
Expand Down
251 changes: 116 additions & 135 deletions lib/src/piece/import.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,169 +6,150 @@ import '../back_end/code_writer.dart';
import '../constants.dart';
import 'piece.dart';

/// An import or export directive.
/// An import or export directive and its `show` and `hide` combinators.
///
/// Contains pieces for the keyword and URI, the optional `as` clause for
/// imports, the configurations (`if` clauses), and combinators (`show` and
/// `hide`).
/// imports, and the configurations (`if` clauses).
///
/// Combinators can be split like so:
///
/// [State.initial] All on one line:
///
/// ```
/// import 'animals.dart' show Ant, Bat hide Cat, Dog;
/// ```
///
/// [_beforeCombinators] Wrap before each keyword:
///
/// ```
/// import 'animals.dart'
/// show Ant, Bat
/// hide Cat, Dog;
/// ```
///
/// [_firstCombinator] Wrap before each keyword and split the first list of
/// names (only used when there are multiple combinators):
///
/// ```
/// import 'animals.dart'
/// show
/// Ant,
/// Bat
/// hide Cat, Dog;
/// ```
///
/// [_secondCombinator]: Wrap before each keyword and split the second list of
/// names (only used when there are multiple combinators):
///
/// ```
/// import 'animals.dart'
/// show Ant, Bat
/// hide
/// Cat,
/// Dog;
/// ```
///
/// [State.split] Wrap before each keyword and split both lists of names:
///
/// ```
/// import 'animals.dart'
/// show
/// Ant,
/// Bat
/// hide
/// Cat,
/// Dog;
/// ```
///
/// These are not allowed:
///
/// ```
/// // Wrap list but not keyword:
/// import 'animals.dart' show
/// Ant,
/// Bat
/// hide Cat, Dog;
///
/// // Wrap one keyword but not both:
/// import 'animals.dart'
/// show Ant, Bat hide Cat, Dog;
///
/// import 'animals.dart' show Ant, Bat
/// hide Cat, Dog;
/// ```
///
/// This ensures that when any wrapping occurs, the keywords are always at the
/// beginning of the line.
class ImportPiece extends Piece {
/// Split before combinator keywords.
static const _beforeCombinators = State(1);

/// Split before each name in the first combinator.
static const _firstCombinator = State(2);

/// Split before each name in the second combinator.
static const _secondCombinator = State(3);

/// The main directive and its URI.
final Piece directive;
final Piece _directive;

/// If the directive has `if` configurations, this is them.
final Piece? configurations;
final Piece? _configurations;

/// The `as` clause for this directive.
///
/// Null if this is not an import or it has no library prefix.
final Piece? asClause;

/// The piece for the `show` and/or `hide` combinators.
final Piece? combinator;

ImportPiece(
this.directive, this.configurations, this.asClause, this.combinator);
final Piece? _asClause;

@override
int get stateCount => 1;
final List<ImportCombinator> _combinators;

@override
void format(CodeWriter writer, int state) {
writer.format(directive);
writer.formatOptional(configurations);
writer.formatOptional(asClause);
writer.formatOptional(combinator);
ImportPiece(this._directive, this._configurations, this._asClause,
this._combinators) {
assert(_combinators.length <= 2);
}

@override
void forEachChild(void Function(Piece piece) callback) {
callback(directive);
if (configurations case var configurations?) callback(configurations);
if (asClause case var asClause?) callback(asClause);
if (combinator case var combinator?) callback(combinator);
}
List<State> get states => [
_beforeCombinators,
if (_combinators.length > 1) ...[
_firstCombinator,
_secondCombinator,
],
State.split
];

@override
String toString() => 'Directive';
}

/// The combinator on a directive with only one combinator. It can be split:
///
/// // 0: All on one line:
/// import 'animals.dart' show Ant, Bat, Cat;
///
/// // 1: Split before the keyword:
/// import 'animals.dart'
/// show Ant, Bat, Cat;
///
/// // 2: Split before the keyword and each name:
/// import 'animals.dart'
/// show
/// Ant,
/// Bat,
/// Cat;
class OneCombinatorPiece extends Piece {
final ImportCombinator combinator;

OneCombinatorPiece(this.combinator);

/// 0: No splits anywhere.
/// 1: Split before combinator keyword.
/// 2: Split before combinator keyword and before each name.
@override
int get stateCount => 3;
void format(CodeWriter writer, State state) {
writer.format(_directive);
writer.formatOptional(_configurations);
writer.formatOptional(_asClause);

if (_combinators.isNotEmpty) {
_combinators[0].format(writer,
splitKeyword: state != State.initial,
splitNames: state == _firstCombinator || state == State.split);
}

@override
void format(CodeWriter writer, int state) {
combinator.format(writer, splitKeyword: state != 0, splitNames: state == 2);
if (_combinators.length > 1) {
_combinators[1].format(writer,
splitKeyword: state != State.initial,
splitNames: state == _secondCombinator || state == State.split);
}
}

@override
void forEachChild(void Function(Piece piece) callback) {
combinator.forEachChild(callback);
}

@override
String toString() => '1Comb';
}

/// The combinators on a directive with two combinators. It can be split:
///
/// // 0: All on one line:
/// import 'animals.dart' show Ant, Bat hide Cat, Dog;
///
/// // 1: Wrap before each keyword:
/// import 'animals.dart'
/// show Ant, Bat
/// hide Cat, Dog;
///
/// // 2: Wrap before each keyword and split the first list of names:
/// import 'animals.dart'
/// show
/// Ant,
/// Bat
/// hide Cat, Dog;
///
/// // 3: Wrap before each keyword and split the second list of names:
/// import 'animals.dart'
/// show Ant, Bat
/// hide
/// Cat,
/// Dog;
///
/// // 4: Wrap before each keyword and split both lists of names:
/// import 'animals.dart'
/// show
/// Ant,
/// Bat
/// hide
/// Cat,
/// Dog;
///
/// These are not allowed:
///
/// // Wrap list but not keyword:
/// import 'animals.dart' show
/// Ant,
/// Bat
/// hide Cat, Dog;
///
/// // Wrap one keyword but not both:
/// import 'animals.dart'
/// show Ant, Bat hide Cat, Dog;
///
/// import 'animals.dart' show Ant, Bat
/// hide Cat, Dog;
///
/// This ensures that when any wrapping occurs, the keywords are always at
/// the beginning of the line.
class TwoCombinatorPiece extends Piece {
final List<ImportCombinator> combinators;

TwoCombinatorPiece(this.combinators);

@override
int get stateCount => 5;

@override
void format(CodeWriter writer, int state) {
assert(combinators.length == 2);
callback(_directive);
if (_configurations case var configurations?) callback(configurations);
if (_asClause case var asClause?) callback(asClause);

combinators[0].format(writer,
splitKeyword: state != 0, splitNames: state == 2 || state == 4);
combinators[1].format(writer,
splitKeyword: state != 0, splitNames: state == 3 || state == 4);
}

@override
void forEachChild(void Function(Piece piece) callback) {
for (var combinator in combinators) {
for (var combinator in _combinators) {
combinator.forEachChild(callback);
}
}

@override
String toString() => '2Comb';
String toString() => 'Import';
}

/// A single `show` or `hide` combinator within an import or export directive.
Expand Down
Loading

0 comments on commit 35a26b2

Please sign in to comment.