blob: e990b3494593637f71e51711c264ef3574db10af [file] [log] [blame]
// Copyright (c) 2025, 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:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
import 'package:front_end/src/util/parser_ast.dart';
import 'package:front_end/src/util/parser_ast_helper.dart';
import '../interval_list.dart';
import 'compile_helper.dart';
import 'stacktrace_utils.dart';
const bool debug = false;
Future<void> main(List<String> args) async {
if (args.length != 1) throw "Needs exactly 1 input.";
Helper helper = new Helper();
await helper.setup();
Uint8List rawBytes = new File(args.single).readAsBytesSync();
String sourceAsString = utf8.decode(rawBytes);
(Object, StackTrace)? compile = await helper.compile(sourceAsString);
if (compile == null) throw "Input doesn't crash.";
String categorized = categorize(compile.$2);
CompilationUnitEnd ast = getAST(
rawBytes,
includeComments: true,
enableTripleShift: true,
allowPatterns: true,
);
ast.debugPrint();
Visitor v = new Visitor(helper, categorized, sourceAsString);
Token firstToken = (ast.children!.first as CompilationUnitBegin).token;
await v.walkTokens(firstToken);
await ast.accept(v);
String? minimized = v.getMinimizedSource();
if (minimized == null) {
print("Couldn't minimize.");
} else {
print("Minimized to: ");
print(minimized);
}
}
class Visitor extends RecursiveParserAstVisitorWithDefaultNodeAsync {
final Helper helper;
final String wantedCategorizedCrash;
final String initialSource;
List<(int, int)> removeable = [];
Visitor(this.helper, this.wantedCategorizedCrash, this.initialSource) {
prevBraceInfo = initialSource.getBraceCountString();
print("Initial source brace info: $prevBraceInfo");
}
Future<void> walkTokens(Token token) async {
Token t = token;
while (!t.isEof) {
if (t.endGroup != null) {
if (await _canRemove("Token range", t.runtimeType, t, t.endGroup!)) {
t = t.endGroup!.next!;
continue;
}
}
t = t.next!;
}
}
@override
Future<void> defaultNode(ParserAstNode node) async {
if (node is BeginAndEndTokenParserAstNode) {
if (await _canRemove(
node.what,
node.runtimeType,
node.beginToken,
node.endToken,
)) {
return;
}
} else if (node is BinaryExpressionEnd) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.endToken,
)) {
return;
}
} else if (node is ThrowExpressionHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.throwToken,
node.endToken,
)) {
return;
}
} else if (node is LabelHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token.previous!,
node.token,
)) {
return;
}
} else if (node is CaseExpressionEnd) {
if (await _canRemove(
node.what,
node.runtimeType,
node.caseKeyword,
node.colon,
)) {
return;
}
} else if (node is UnaryPrefixAssignmentExpressionHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.token,
)) {
return;
}
} else if (node is UnaryPrefixExpressionHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.token,
)) {
return;
}
} else if (node is EmptyStatementHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.token,
)) {
return;
}
} else if (node is AssignmentExpressionHandle) {
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.endToken,
)) {
return;
}
} else if (node is ConditionalExpressionEnd) {
if (await _canRemove(
node.what,
node.runtimeType,
node.question.next!,
node.colon.previous!,
)) {
// Fine, but we still have to recurse.
}
if (await _canRemove(
node.what,
node.runtimeType,
node.colon.next!,
node.endToken,
)) {
// Fine, but we still have to recurse.
}
} else if (node is BinaryPatternEnd) {
// This seems a little dicy.
if (await _canRemove(
node.what,
node.runtimeType,
node.token,
node.token,
)) {
return;
}
} else if (node is PartEnd) {
if (await _canRemove(
node.what,
node.runtimeType,
node.partKeyword,
node.semicolon,
)) {
return;
}
} else if (node is TypedefEnd) {
if (await _canRemove(
node.what,
node.runtimeType,
node.augmentToken ?? node.typedefKeyword,
node.endToken,
)) {
return;
}
}
// We can't remove the node (or it isn't a Begin/End one) - recurse.
await super.defaultNode(node);
}
Future<bool> _canRemove(
String what,
Type type,
Token beginToken,
Token endToken,
) async {
int length = endToken.charEnd - beginToken.offset;
if (length > 0) {
String newSource = _constructSourceFromRemovableAnd((
type,
beginToken,
endToken,
));
print("Compile (${newSource.length} vs ${initialSource.length})");
(Object, StackTrace)? compile = await helper.compile(newSource);
if (compile != null) {
String categorized = categorize(compile.$2);
if (categorized == wantedCategorizedCrash) {
print(" => Can remove $what with length $length");
// We can actually do this.
removeable.add((beginToken.charOffset, endToken.charEnd));
return true;
}
}
}
return false;
}
String _constructSourceFromRemovableAnd(
(Type type, Token beginToken, Token endToken)? nodeInfo,
) {
IntervalListBuilder intervalListBuilder = new IntervalListBuilder();
if (nodeInfo != null) {
intervalListBuilder.addIntervalExcludingEnd(
nodeInfo.$2.charOffset,
nodeInfo.$3.charEnd,
);
}
for ((int, int) removeThis in removeable) {
intervalListBuilder.addIntervalExcludingEnd(removeThis.$1, removeThis.$2);
}
IntervalList intervalList = intervalListBuilder.buildIntervalList();
int from = 0;
StringBuffer sb = new StringBuffer();
intervalList.forEach((int open, int close) {
sb.write(initialSource.substring(from, open));
from = close;
});
sb.write(initialSource.substring(from, initialSource.length));
String output = sb.toString();
if (debug) {
String braceInfo = output.getBraceCountString();
if (braceInfo != prevBraceInfo) {
print(
"After cutting out ${nodeInfo?.$1} at "
"${nodeInfo?.$2.charOffset} brace info: "
"$braceInfo",
);
prevBraceInfo = braceInfo;
}
}
return output;
}
String? prevBraceInfo;
String? getMinimizedSource() {
if (removeable.isEmpty) return null;
return _constructSourceFromRemovableAnd(null);
}
}
extension on String {
List<int> count() {
List<int> result = List.filled(257, 0);
List<int> codeUnits = this.codeUnits;
for (int i = 0; i < codeUnits.length; i++) {
int unit = codeUnits[i];
if (unit < 0 || unit > 255) {
result[256]++;
} else {
result[unit]++;
}
}
return result;
}
String getBraceCountString() {
List<int> counts = count();
StringBuffer sb = new StringBuffer();
for (int codeUnit in [40, 41, 91, 93, 123, 125]) {
if (sb.isNotEmpty) sb.write(", ");
sb.writeCharCode(codeUnit);
sb.write(": ");
sb.write(counts[codeUnit]);
}
return sb.toString();
}
}