Skip to content
This repository was archived by the owner on May 24, 2023. It is now read-only.

null safety #52

Merged
merged 20 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ env:

jobs:
# Check code formatting and static analysis on a single OS (linux)
# against Dart dev.
# against Dart beta.
analyze:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sdk: [dev]
sdk: [beta]
steps:
- uses: actions/checkout@v2
- uses: dart-lang/[email protected]
Expand All @@ -38,7 +38,7 @@ jobs:

# Run tests on a matrix consisting of two dimensions:
# 1. OS: ubuntu-latest, (macos-latest, windows-latest)
# 2. release channel: dev
# 2. release channel: beta
test:
needs: analyze
runs-on: ${{ matrix.os }}
Expand All @@ -47,7 +47,7 @@ jobs:
matrix:
# Add macos-latest and/or windows-latest if relevant for this package.
os: [ubuntu-latest]
sdk: [2.9.0, dev]
sdk: [beta]
steps:
- uses: actions/checkout@v2
- uses: dart-lang/[email protected]
Expand Down
7 changes: 2 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
# 1.0.0-dev


- Migrate to null safety.
- **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to
reduce memory consumption of the algorithm to O(n).
- Require Dart SDK `>=2.9.0 <3.0.0`.

# 0.2.1

- Require Dart SDK `>=2.2.0 <3.0.0`.

# 0.2.0

Expand Down
3 changes: 2 additions & 1 deletion benchmark/connected_components_benchmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ void main() {
while (watch.elapsed < duration) {
count++;
final length =
stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? []).length;
stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? <Never>[])
.length;
assert(length == 244, '$length');
}

Expand Down
6 changes: 3 additions & 3 deletions benchmark/shortest_path_benchmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ void main() {
}
}

int minTicks;
int? minTicks;
var maxIteration = 0;

final testOutput =
shortestPath(0, size - 1, (e) => graph[e] ?? []).toString();
shortestPath(0, size - 1, (e) => graph[e] ?? <Never>[]).toString();
print(testOutput);
assert(testOutput == '(258, 252, 819, 999)', testOutput);

Expand All @@ -34,7 +34,7 @@ void main() {
watch
..reset()
..start();
final result = shortestPath(0, size - 1, (e) => graph[e] ?? []);
final result = shortestPath(0, size - 1, (e) => graph[e] ?? <Never>[])!;
final length = result.length;
final first = result.first;
watch.stop();
Expand Down
6 changes: 3 additions & 3 deletions benchmark/shortest_path_worst_case_benchmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ void main() {
}
}

int minTicks;
int? minTicks;
var maxIteration = 0;

final testOutput =
shortestPath(0, size - 1, (e) => graph[e] ?? []).toString();
shortestPath(0, size - 1, (e) => graph[e] ?? <Never>[]).toString();
print(testOutput);
assert(testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(),
'$testOutput');
Expand All @@ -36,7 +36,7 @@ void main() {
watch
..reset()
..start();
final result = shortestPath(0, size - 1, (e) => graph[e] ?? []);
final result = shortestPath(0, size - 1, (e) => graph[e] ?? <Never>[])!;
final length = result.length;
final first = result.first;
watch.stop();
Expand Down
12 changes: 7 additions & 5 deletions example/crawl_async_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ Future<Null> main() async {
print(allImports.map((s) => s.uri).toList());
}

AnalysisContext _analysisContext;
AnalysisContext? _analysisContext;

Future<AnalysisContext> get analysisContext async {
if (_analysisContext == null) {
var context = _analysisContext;
if (context == null) {
var libUri = Uri.parse('package:graphs/');
var libPath = await pathForUri(libUri);
var packagePath = p.dirname(libPath);
Expand All @@ -39,16 +40,17 @@ Future<AnalysisContext> get analysisContext async {
throw StateError('Expected to find exactly one context root, got $roots');
}

_analysisContext = ContextBuilder().createContext(contextRoot: roots[0]);
context = _analysisContext =
ContextBuilder().createContext(contextRoot: roots[0]);
}

return _analysisContext;
return context;
}

Future<Iterable<Uri>> findImports(Uri from, Source source) async {
return source.unit.directives
.whereType<UriBasedDirective>()
.map((d) => d.uri.stringValue)
.map((d) => d.uri.stringValue!)
.where((uri) => !uri.startsWith('dart:'))
.map((import) => resolveImport(import, from));
}
Expand Down
2 changes: 1 addition & 1 deletion example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ void main() {
});

var components = stronglyConnectedComponents<Node>(
graph.nodes.keys, (node) => graph.nodes[node]);
graph.nodes.keys, (node) => graph.nodes[node] ?? []);

print(components);
}
6 changes: 4 additions & 2 deletions lib/src/crawl_async.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ final _empty = Future<Null>.value(null);
/// Crawling is eager, so calls to [edges] may overlap with other calls that
/// have not completed. If the [edges] callback needs to be limited or throttled
/// that must be done by wrapping it before calling [crawlAsync].
Stream<V> crawlAsync<K, V>(Iterable<K> roots, FutureOr<V> Function(K) readNode,
Stream<V> crawlAsync<K extends Object, V>(
Iterable<K> roots,
FutureOr<V> Function(K) readNode,
FutureOr<Iterable<K>> Function(K, V) edges) {
final crawl = _CrawlAsync(roots, readNode, edges)..run();
return crawl.result.stream;
Expand Down Expand Up @@ -69,7 +71,7 @@ class _CrawlAsync<K, V> {
if (value == null) return;
if (result.isClosed) return;
result.add(value);
var next = await edges(key, value) ?? const [];
var next = await edges(key, value);
await Future.wait(next.map(_visit), eagerError: true);
}

Expand Down
71 changes: 31 additions & 40 deletions lib/src/shortest_path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import 'dart:collection';
/// If [start] `==` [target], an empty [List] is returned and [edges] is never
/// called.
///
/// [start], [target] and all values returned by [edges] must not be `null`.
/// If asserts are enabled, an [AssertionError] is raised if these conditions
/// are not met. If asserts are not enabled, violations result in undefined
/// behavior.
///
/// If [equals] is provided, it is used to compare nodes in the graph. If
/// [equals] is omitted, the node's own [Object.==] is used instead.
///
Expand All @@ -24,12 +19,12 @@ import 'dart:collection';
///
/// If you supply one of [equals] or [hashCode], you should generally also to
/// supply the other.
Iterable<T> shortestPath<T>(
Iterable<T>? shortestPath<T extends Object>(
T start,
T target,
Iterable<T> Function(T) edges, {
bool Function(T, T) equals,
int Function(T) hashCode,
bool Function(T, T)? equals,
int Function(T)? hashCode,
}) =>
_shortestPaths<T>(
start,
Expand Down Expand Up @@ -58,11 +53,11 @@ Iterable<T> shortestPath<T>(
///
/// If you supply one of [equals] or [hashCode], you should generally also to
/// supply the other.
Map<T, Iterable<T>> shortestPaths<T>(
Map<T, Iterable<T>> shortestPaths<T extends Object>(
T start,
Iterable<T> Function(T) edges, {
bool Function(T, T) equals,
int Function(T) hashCode,
bool Function(T, T)? equals,
int Function(T)? hashCode,
}) =>
_shortestPaths<T>(
start,
Expand All @@ -71,37 +66,35 @@ Map<T, Iterable<T>> shortestPaths<T>(
hashCode: hashCode,
);

Map<T, Iterable<T>> _shortestPaths<T>(
Map<T, Iterable<T>> _shortestPaths<T extends Object>(
T start,
Iterable<T> Function(T) edges, {
T target,
bool Function(T, T) equals,
int Function(T) hashCode,
T? target,
bool Function(T, T)? equals,
int Function(T)? hashCode,
}) {
assert(start != null, '`start` cannot be null');
assert(edges != null, '`edges` cannot be null');

final distances = HashMap<T, _Tail<T>>(equals: equals, hashCode: hashCode);
distances[start] = _Tail<T>();

equals ??= _defaultEquals;
if (equals(start, target)) {
final nonNullEquals = equals ??= _defaultEquals;
final isTarget =
target == null ? _neverTarget : (T node) => nonNullEquals(node, target);
if (isTarget(start)) {
return distances;
}

final toVisit = ListQueue<T>()..add(start);

while (toVisit.isNotEmpty) {
final current = toVisit.removeFirst();
final currentPath = distances[current];
final currentPath = distances[current]!;

for (var edge in edges(current)) {
assert(edge != null, '`edges` cannot return null values.');
final existingPath = distances[edge];

if (existingPath == null) {
distances[edge] = currentPath.append(edge);
if (equals(edge, target)) {
if (isTarget(edge)) {
return distances;
}
toVisit.add(edge);
Expand All @@ -113,6 +106,7 @@ Map<T, Iterable<T>> _shortestPaths<T>(
}

bool _defaultEquals(Object a, Object b) => a == b;
bool _neverTarget(Object _) => false;

/// An immutable iterable that can efficiently return a copy with a value
/// appended.
Expand All @@ -123,9 +117,9 @@ bool _defaultEquals(Object a, Object b) => a == b;
/// space because it copies all the values to a new list and uses that
/// iterator in order to avoid stack overflows for large paths. This copy is
/// cached for subsequent calls.
class _Tail<T> extends Iterable<T> {
final T /*?*/ tail;
final _Tail<T> /*?*/ head;
class _Tail<T extends Object> extends Iterable<T> {
final T? tail;
final _Tail<T>? head;
@override
final int length;
_Tail()
Expand All @@ -135,19 +129,16 @@ class _Tail<T> extends Iterable<T> {
_Tail._(this.tail, this.head, this.length);
_Tail<T> append(T value) => _Tail._(value, this, length + 1);

Iterator<T> /*?*/ _iterator;

@override
Iterator<T> get iterator {
if (_iterator == null) {
var /*_Tail<T>?*/ next = this;
var values = List<T>.generate(length, (_) {
var val = next.tail;
next = next.head;
return val;
});
_iterator = values.reversed.iterator;
}
return _iterator;
}
Iterator<T> get iterator => _asIterable.iterator;

late final _asIterable = () {
_Tail<T>? next = this;
var reversed = List.generate(length, (_) {
var val = next!.tail;
next = next!.head;
return val as T;
});
return reversed.reversed;
}();
}
19 changes: 9 additions & 10 deletions lib/src/strongly_connected_components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,34 @@ import 'dart:math' show min;
///
/// If you supply one of [equals] or [hashCode], you should generally also to
/// supply the other.
List<List<T>> stronglyConnectedComponents<T>(
List<List<T>> stronglyConnectedComponents<T extends Object>(
Iterable<T> nodes,
Iterable<T> Function(T) edges, {
bool Function(T, T) equals,
int Function(T) hashCode,
bool Function(T, T)? equals,
int Function(T)? hashCode,
}) {
final result = <List<T>>[];
final lowLinks = HashMap<T, int>(equals: equals, hashCode: hashCode);
final indexes = HashMap<T, int>(equals: equals, hashCode: hashCode);
final onStack = HashSet<T>(equals: equals, hashCode: hashCode);

equals ??= _defaultEquals;
final nonNullEquals = equals ?? _defaultEquals;

var index = 0;
var lastVisited = Queue<T>();

void strongConnect(T node) {
indexes[node] = index;
lowLinks[node] = index;
var lowLink = lowLinks[node] = index;
index++;
lastVisited.addLast(node);
onStack.add(node);
// ignore: omit_local_variable_types
for (final T next in edges(node) ?? const []) {
for (final next in edges(node)) {
if (!indexes.containsKey(next)) {
strongConnect(next);
lowLinks[node] = min(lowLinks[node], lowLinks[next]);
lowLink = lowLinks[node] = min(lowLink, lowLinks[next]!);
} else if (onStack.contains(next)) {
lowLinks[node] = min(lowLinks[node], indexes[next]);
lowLink = lowLinks[node] = min(lowLink, indexes[next]!);
}
}
if (lowLinks[node] == indexes[node]) {
Expand All @@ -67,7 +66,7 @@ List<List<T>> stronglyConnectedComponents<T>(
next = lastVisited.removeLast();
onStack.remove(next);
component.add(next);
} while (!equals(next, node));
} while (!nonNullEquals(next, node));
result.add(component);
}
}
Expand Down
12 changes: 6 additions & 6 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ description: Graph algorithms that operate on graphs in any representation
homepage: https://github.com/dart-lang/graphs

environment:
sdk: '>=2.9.0 <3.0.0'
sdk: '>=2.12.0-0 <3.0.0'

dev_dependencies:
pedantic: ^1.3.0
test: ^1.5.1
pedantic: ^1.10.0
test: ^1.16.0

# For examples
analyzer: '>=0.35.0 <0.42.0'
path: ^1.1.0
pool: ^1.3.0
path: ^1.8.0
pool: ^1.5.0
analyzer: ^1.0.0
Loading