| // Copyright (c) 2022, 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 'dart:math' as math; | |
| import 'package:heap_snapshot/analysis.dart'; | |
| import 'package:vm_service/vm_service.dart'; | |
| import 'completion.dart'; | |
| abstract class SetExpression { | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output); | |
| } | |
| class FilterExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<String> patterns; | |
| FilterExpression(this.expr, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = expr.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| final patterns = this | |
| .patterns | |
| .map((String pattern) { | |
| if (pattern == 'String') { | |
| return ['_OneByteString', '_TwoByteString']; | |
| } else if (pattern == 'List') { | |
| return ['_List', '_GrowableList', '_ImmutableList']; | |
| } else if (pattern == 'Map') { | |
| return ['_HashMap', '_Map', '_ConstMap']; | |
| } else if (pattern == 'Set') { | |
| return ['_HashSet', '_Set', '_ConstSet']; | |
| } | |
| return [pattern]; | |
| }) | |
| .expand((l) => l) | |
| .toList(); | |
| return analysis.filterByClassPatterns(oids, patterns); | |
| } | |
| } | |
| class DFilterExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<String> patterns; | |
| DFilterExpression(this.expr, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = expr.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| final predicates = patterns.map((String pattern) { | |
| final l = pattern.startsWith('<'); | |
| final le = pattern.startsWith('<='); | |
| final e = pattern.startsWith('=='); | |
| final ge = pattern.startsWith('>='); | |
| final g = pattern.startsWith('>'); | |
| if (l || le || e || ge || g) { | |
| final value = pattern.substring((le || e || ge) ? 2 : 1); | |
| int limit = int.parse(value); | |
| if (l) | |
| return (o) { | |
| final len = analysis.variableLengthOf(o); | |
| return len != -1 && len < limit; | |
| }; | |
| if (le) | |
| return (o) { | |
| final len = analysis.variableLengthOf(o); | |
| return len != -1 && len <= limit; | |
| }; | |
| if (e) | |
| return (o) { | |
| final len = analysis.variableLengthOf(o); | |
| return len != -1 && len == limit; | |
| }; | |
| if (ge) | |
| return (o) { | |
| final len = analysis.variableLengthOf(o); | |
| return len != -1 && len >= limit; | |
| }; | |
| if (g) | |
| return (o) { | |
| final len = analysis.variableLengthOf(o); | |
| return len != -1 && len > limit; | |
| }; | |
| throw 'unreachable'; | |
| } | |
| if (pattern.startsWith('^')) { | |
| pattern = pattern.substring(1); | |
| final regexp = RegExp(pattern); | |
| return (HeapSnapshotObject object) { | |
| final data = object.data; | |
| if (data is String) { | |
| return !regexp.hasMatch(data); | |
| } | |
| return false; | |
| }; | |
| } | |
| final regexp = RegExp(pattern); | |
| return (HeapSnapshotObject object) { | |
| final data = object.data; | |
| if (data is String) { | |
| return regexp.hasMatch(data); | |
| } | |
| return false; | |
| }; | |
| }).toList(); | |
| return analysis.filter( | |
| oids, (object) => !predicates.any((p) => !p(object))); | |
| } | |
| } | |
| class MinusExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<SetExpression> operands; | |
| MinusExpression(this.expr, this.operands); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final result = expr.evaluate(namedSets, analysis, output)?.toSet(); | |
| if (result == null) return null; | |
| for (int i = 0; i < operands.length; ++i) { | |
| final oids = operands[i].evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| result.removeAll(oids); | |
| } | |
| return result; | |
| } | |
| } | |
| class OrExpression extends SetExpression { | |
| final List<SetExpression> exprs; | |
| OrExpression(this.exprs); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final result = SpecializedIntSet(analysis.graph.objects.length); | |
| for (int i = 0; i < exprs.length; ++i) { | |
| final oids = exprs[i].evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| result.addAll(oids); | |
| } | |
| return result; | |
| } | |
| } | |
| class AndExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<SetExpression> operands; | |
| AndExpression(this.expr, this.operands); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final nullableResult = expr.evaluate(namedSets, analysis, output)?.toSet(); | |
| if (nullableResult == null) return null; | |
| IntSet result = nullableResult; | |
| for (int i = 0; i < operands.length; ++i) { | |
| final oids = operands[i].evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| result = result.intersection(oids); | |
| } | |
| return result; | |
| } | |
| } | |
| class SampleExpression extends SetExpression { | |
| static final _random = math.Random(); | |
| final SetExpression expr; | |
| final int count; | |
| SampleExpression(this.expr, this.count); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = expr.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| if (oids.isEmpty) return oids; | |
| final result = IntSet(); | |
| final l = oids.toList(); | |
| while (result.length < count && result.length < oids.length) { | |
| result.add(l[_random.nextInt(oids.length)]); | |
| } | |
| return result; | |
| } | |
| } | |
| class ClosureExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<String> patterns; | |
| ClosureExpression(this.expr, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final roots = expr.evaluate(namedSets, analysis, output); | |
| if (roots == null) return null; | |
| final filter = analysis.parseTraverseFilter(patterns); | |
| if (filter == null && | |
| roots.length == analysis.roots.length && | |
| roots.intersection(analysis.roots).length == roots.length) { | |
| // The analysis needs to calculate the set of reachable objects | |
| // already, so we re-use it instead of computing it again. | |
| return analysis.reachableObjects; | |
| } | |
| return analysis.transitiveGraph(roots, filter); | |
| } | |
| } | |
| class UserClosureExpression extends SetExpression { | |
| final SetExpression expr; | |
| final List<String> patterns; | |
| UserClosureExpression(this.expr, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final roots = expr.evaluate(namedSets, analysis, output); | |
| if (roots == null) return null; | |
| final filter = analysis.parseTraverseFilter(patterns); | |
| return analysis.reverseTransitiveGraph(roots, filter); | |
| } | |
| } | |
| class FollowExpression extends SetExpression { | |
| final SetExpression objs; | |
| final List<String> patterns; | |
| FollowExpression(this.objs, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = objs.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| return analysis.findReferences(oids, patterns); | |
| } | |
| } | |
| class UserFollowExpression extends SetExpression { | |
| final SetExpression objs; | |
| final List<String> patterns; | |
| UserFollowExpression(this.objs, this.patterns); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = objs.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| return analysis.findUsers(oids, patterns); | |
| } | |
| } | |
| class NamedExpression extends SetExpression { | |
| final String name; | |
| NamedExpression(this.name); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = namedSets.getSet(name); | |
| if (oids == null) { | |
| output.printError('"$name" does not refer to a command or named set.'); | |
| return null; | |
| } | |
| return oids; | |
| } | |
| } | |
| class SetNameExpression extends SetExpression { | |
| final String name; | |
| final SetExpression expr; | |
| SetNameExpression(this.name, this.expr); | |
| IntSet? evaluate(NamedSets namedSets, Analysis analysis, Output output) { | |
| final oids = expr.evaluate(namedSets, analysis, output); | |
| if (oids == null) return null; | |
| namedSets.nameSet(oids, name); | |
| return oids; | |
| } | |
| } | |
| IntSet? parseAndEvaluate( | |
| NamedSets namedSets, Analysis analysis, String text, Output output) { | |
| final sexpr = parseExpression(text, output, namedSets.names.toSet()); | |
| if (sexpr == null) return null; | |
| return sexpr.evaluate(namedSets, analysis, output); | |
| } | |
| SetExpression? parseExpression( | |
| String text, Output output, Set<String> namedSets) { | |
| const help = 'See `help eval` for available expression types and arguments.'; | |
| final tokens = _TokenIterator(text); | |
| final sexpr = parse(tokens, output, namedSets); | |
| if (sexpr == null) { | |
| output.printError(help); | |
| return null; | |
| } | |
| if (tokens.moveNext()) { | |
| tokens.movePrev(); | |
| output.printError( | |
| 'Found unexpected "${tokens.remaining}" after ${sexpr.runtimeType}.'); | |
| output.printError(help); | |
| return null; | |
| } | |
| return sexpr; | |
| } | |
| final Map<String, SetExpression? Function(_TokenIterator, Output, Set<String>)> | |
| parsingFunctions = { | |
| // Filtering expressions. | |
| 'filter': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return FilterExpression(s, patterns); | |
| }, | |
| 'dfilter': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return DFilterExpression(s, patterns); | |
| }, | |
| // Traversing expressions. | |
| 'follow': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final objs = parse(tokens, output, namedSets); | |
| if (objs == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return FollowExpression(objs, patterns); | |
| }, | |
| 'users': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final objs = parse(tokens, output, namedSets); | |
| if (objs == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return UserFollowExpression(objs, patterns); | |
| }, | |
| 'closure': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return ClosureExpression(s, patterns); | |
| }, | |
| 'uclosure': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final patterns = _parsePatterns(tokens, output); | |
| return UserClosureExpression(s, patterns); | |
| }, | |
| // Set operations | |
| 'minus': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final operands = _parseExpressions(tokens, output, namedSets); | |
| if (operands == null) return null; | |
| return MinusExpression(s, operands); | |
| }, | |
| 'or': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final operands = _parseExpressions(tokens, output, namedSets); | |
| if (operands == null) return null; | |
| return OrExpression(operands); | |
| }, | |
| 'and': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| final operands = _parseExpressions(tokens, output, namedSets); | |
| if (operands == null) return null; | |
| return AndExpression(s, operands); | |
| }, | |
| // Sample expression | |
| 'sample': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final s = parse(tokens, output, namedSets); | |
| if (s == null) return null; | |
| int count = 1; | |
| if (tokens.moveNext()) { | |
| if (tokens.current == ')') { | |
| tokens.movePrev(); | |
| } else { | |
| tokens.current; | |
| final value = int.tryParse(tokens.current); | |
| if (value == null) { | |
| output.printError( | |
| '"sample" expression expects integer as 2nd argument.'); | |
| return null; | |
| } | |
| count = value; | |
| } | |
| } | |
| return SampleExpression(s, count); | |
| }, | |
| // Sub-expression | |
| '(': (_TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final expr = parse(tokens, output, namedSets); | |
| if (expr == null) return null; | |
| if (!tokens.moveNext()) { | |
| output.printError('Expected closing ")" after "${tokens._text}".'); | |
| return null; | |
| } | |
| if (tokens.current != ')') { | |
| output.printError('Expected closing ")" but found "${tokens.current}".'); | |
| tokens.movePrev(); | |
| return null; | |
| } | |
| return expr; | |
| }, | |
| }; | |
| SetExpression? parse( | |
| _TokenIterator tokens, Output output, Set<String> namedSets) { | |
| if (!tokens.moveNext()) { | |
| output.printError('Reached end of input: expected expression'); | |
| return null; | |
| } | |
| final current = tokens.current; | |
| final parserFun = parsingFunctions[current]; | |
| if (parserFun != null) return parserFun(tokens, output, namedSets); | |
| if (current == ')') { | |
| output.printError('Unexpected ).'); | |
| return null; | |
| } | |
| if (tokens.moveNext()) { | |
| if (tokens.current == '=') { | |
| final expr = parse(tokens, output, namedSets); | |
| if (expr == null) return null; | |
| return SetNameExpression(current, expr); | |
| } | |
| tokens.movePrev(); | |
| } | |
| if (!namedSets.contains(current)) { | |
| output.printError('There is no set with name "$current". See `info`.'); | |
| // We're at the end - it may be beneficial to suggest completion. | |
| if (tokens.isAtEnd && tokens._text.endsWith(current)) { | |
| final pc = PostfixCompleter(tokens._text); | |
| final candidate = pc.tryComplete(current, namedSets.toList()) ?? | |
| pc.tryComplete(current, parsingFunctions.keys.toList()); | |
| if (candidate != null) { | |
| output.suggestCompletion(candidate); | |
| } | |
| } | |
| return null; | |
| } | |
| return NamedExpression(current); | |
| } | |
| class _TokenIterator { | |
| final String _text; | |
| String? _current = null; | |
| int _index = 0; | |
| _TokenIterator(this._text); | |
| String get current => _current!; | |
| bool get isAtEnd => _index == _text.length; | |
| bool moveNextPattern() { | |
| _current = null; | |
| int start = _index; | |
| while ( | |
| start < _text.length && _text.codeUnitAt(start) == ' '.codeUnitAt(0)) { | |
| start++; | |
| } | |
| if (start == _text.length) return false; | |
| int openCount = 0; | |
| int end = start; | |
| while (end < _text.length) { | |
| final char = _text.codeUnitAt(end); | |
| if (char == '('.codeUnitAt(0)) { | |
| openCount++; | |
| end++; | |
| continue; | |
| } | |
| if (char == ')'.codeUnitAt(0)) { | |
| openCount--; | |
| if (openCount >= 0) { | |
| end++; | |
| continue; | |
| } | |
| // This ) has no corresponding (. | |
| if (start == end) return false; | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| if (char == ' '.codeUnitAt(0)) { | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| end++; | |
| } | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| bool moveNext() { | |
| int start = _index; | |
| while ( | |
| start < _text.length && _text.codeUnitAt(start) == ' '.codeUnitAt(0)) { | |
| start++; | |
| } | |
| if (start == _text.length) return false; | |
| int end = start + 1; | |
| final firstChar = _text.codeUnitAt(start); | |
| if (firstChar == '('.codeUnitAt(0) || firstChar == ')'.codeUnitAt(0)) { | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| if (firstChar == '=') { | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| while (end < _text.length && _text.codeUnitAt(end) != ' '.codeUnitAt(0)) { | |
| final char = _text.codeUnitAt(end); | |
| if (char == '('.codeUnitAt(0) || char == ')'.codeUnitAt(0)) { | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| if (char == '=') { | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| end++; | |
| } | |
| _current = _text.substring(start, end); | |
| _index = end; | |
| return true; | |
| } | |
| void movePrev() { | |
| _index -= current.length; | |
| _current = null; | |
| } | |
| String? peek() { | |
| if (!moveNext()) return null; | |
| final peek = current; | |
| movePrev(); | |
| return peek; | |
| } | |
| String get remaining => _text.substring(_index); | |
| } | |
| List<SetExpression>? _parseExpressions( | |
| _TokenIterator tokens, Output output, Set<String> namedSets) { | |
| final all = <SetExpression>[]; | |
| while (true) { | |
| final peek = tokens.peek(); | |
| if (peek == null || peek == ')') break; | |
| final e = parse(tokens, output, namedSets); | |
| if (e == null) return null; | |
| all.add(e); | |
| } | |
| return all; | |
| } | |
| List<String> _parsePatterns(_TokenIterator tokens, Output output) { | |
| final patterns = <String>[]; | |
| while (tokens.moveNextPattern()) { | |
| if (tokens.current == ')') { | |
| tokens.movePrev(); | |
| } | |
| patterns.add(tokens.current); | |
| } | |
| return patterns; | |
| } | |
| class NamedSets { | |
| final Map<String, IntSet> _namedSets = {}; | |
| int _varIndex = 0; | |
| List<String> get names => _namedSets.keys.toList(); | |
| String nameSet(IntSet oids, [String? id]) { | |
| id ??= _generateName(); | |
| _namedSets[id] = oids; | |
| return id; | |
| } | |
| IntSet? getSet(String name) => _namedSets[name]; | |
| bool hasSetName(String name) => _namedSets.containsKey(name); | |
| void clear(String name) { | |
| _namedSets.remove(name); | |
| } | |
| void clearWhere(bool Function(String) cond) { | |
| _namedSets.removeWhere((name, _) => cond(name)); | |
| } | |
| void forEach(void Function(String, IntSet) fun) { | |
| _namedSets.forEach(fun); | |
| } | |
| String _generateName() => '\$${_varIndex++}'; | |
| } | |
| abstract class Output { | |
| void print(String message) {} | |
| void printError(String message) {} | |
| void suggestCompletion(String text) {} | |
| } | |
| const dslDescription = ''' | |
| An `<expr>` can be | |
| Filtering a set of objects based on class/field or data: | |
| filter <expr> $dslFilter | |
| dfilter <expr> [{<,<=,==,>=,>}NUM]* [content-pattern]* | |
| Traversing to references or uses of the set of objects: | |
| follow <expr> $dslFilter | |
| users <expr> $dslFilter | |
| closure <expr> $dslFilter | |
| uclosure <expr> $dslFilter | |
| Performing a set operation on multiple sets: | |
| or <expr>* | |
| and <expr>* | |
| sub <expr> <expr>* | |
| Sample a random element from a set: | |
| sample <expr> <num>? | |
| Name a set of objects or retrieve the objects for a given name: | |
| <name> | |
| <name> = <expr> | |
| '''; | |
| const dslFilter = '[class-pattern]* [class-pattern:field-pattern]*'; |