Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/CLI/src/Reduce.cpp
2725 views
1
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
3
#include "Luau/Ast.h"
4
#include "Luau/Common.h"
5
#include "Luau/Parser.h"
6
#include "Luau/PrettyPrinter.h"
7
8
#include "Luau/FileUtils.h"
9
10
#include <algorithm>
11
#include <stdio.h>
12
#include <string>
13
#include <string_view>
14
#include <queue>
15
16
#define VERBOSE 0 // 1 - print out commandline invocations. 2 - print out stdout
17
18
#if defined(_WIN32) && !defined(__MINGW32__)
19
20
const auto popen = &_popen;
21
const auto pclose = &_pclose;
22
23
#endif
24
25
using namespace Luau;
26
27
enum class TestResult
28
{
29
BugFound, // We encountered the bug we are trying to isolate
30
NoBug, // We did not encounter the bug we are trying to isolate
31
};
32
33
struct Enqueuer : public AstVisitor
34
{
35
std::queue<AstStatBlock*>* queue;
36
37
explicit Enqueuer(std::queue<AstStatBlock*>* queue)
38
: queue(queue)
39
{
40
LUAU_ASSERT(queue);
41
}
42
43
bool visit(AstStatBlock* block) override
44
{
45
queue->push(block);
46
return false;
47
}
48
};
49
50
struct Reducer
51
{
52
Allocator allocator;
53
AstNameTable nameTable{allocator};
54
ParseOptions parseOptions;
55
56
ParseResult parseResult;
57
CstNodeMap cstNodeMap{nullptr};
58
AstStatBlock* root;
59
60
std::string scriptName;
61
62
std::string command;
63
std::string_view searchText;
64
65
Reducer()
66
{
67
parseOptions.captureComments = true;
68
parseOptions.storeCstData = true;
69
}
70
71
std::string readLine(FILE* f)
72
{
73
std::string line = "";
74
char buffer[256];
75
while (fgets(buffer, sizeof(buffer), f))
76
{
77
auto len = strlen(buffer);
78
line += std::string(buffer, len);
79
if (buffer[len - 1] == '\n')
80
break;
81
}
82
83
return line;
84
}
85
86
void writeTempScript(bool minify = false)
87
{
88
std::string source = prettyPrintWithTypes(*root, cstNodeMap);
89
90
if (minify)
91
{
92
size_t pos = 0;
93
do
94
{
95
pos = source.find("\n\n", pos);
96
if (pos == std::string::npos)
97
break;
98
99
source.erase(pos, 1);
100
} while (true);
101
}
102
103
FILE* f = fopen(scriptName.c_str(), "w");
104
if (!f)
105
{
106
printf("Unable to open temp script to %s\n", scriptName.c_str());
107
exit(2);
108
}
109
110
for (const HotComment& comment : parseResult.hotcomments)
111
fprintf(f, "--!%s\n", comment.content.c_str());
112
113
auto written = fwrite(source.data(), 1, source.size(), f);
114
if (written != source.size())
115
{
116
printf("??? %zu %zu\n", written, source.size());
117
printf("Unable to write to temp script %s\n", scriptName.c_str());
118
exit(3);
119
}
120
121
fclose(f);
122
}
123
124
int step = 0;
125
126
std::string escape(const std::string& s)
127
{
128
std::string result;
129
result.reserve(s.size() + 20); // guess
130
result += '"';
131
for (char c : s)
132
{
133
if (c == '"')
134
result += '\\';
135
result += c;
136
}
137
result += '"';
138
139
return result;
140
}
141
142
TestResult run()
143
{
144
writeTempScript();
145
146
std::string cmd = command;
147
while (true)
148
{
149
auto pos = cmd.find("{}");
150
if (std::string::npos == pos)
151
break;
152
153
cmd = cmd.substr(0, pos) + escape(scriptName) + cmd.substr(pos + 2);
154
}
155
156
#if VERBOSE >= 1
157
printf("running %s\n", cmd.c_str());
158
#endif
159
160
TestResult result = TestResult::NoBug;
161
162
++step;
163
printf("Step %4d...\n", step);
164
165
FILE* p = popen(cmd.c_str(), "r");
166
167
while (!feof(p))
168
{
169
std::string s = readLine(p);
170
#if VERBOSE >= 2
171
printf("%s", s.c_str());
172
#endif
173
if (std::string::npos != s.find(searchText))
174
{
175
result = TestResult::BugFound;
176
break;
177
}
178
}
179
180
pclose(p);
181
182
return result;
183
}
184
185
std::vector<AstStat*> getNestedStats(AstStat* stat)
186
{
187
std::vector<AstStat*> result;
188
189
auto append = [&](AstStatBlock* block)
190
{
191
if (block)
192
result.insert(result.end(), block->body.data, block->body.data + block->body.size);
193
};
194
195
if (auto block = stat->as<AstStatBlock>())
196
append(block);
197
else if (auto ifs = stat->as<AstStatIf>())
198
{
199
append(ifs->thenbody);
200
if (ifs->elsebody)
201
{
202
if (AstStatBlock* elseBlock = ifs->elsebody->as<AstStatBlock>())
203
append(elseBlock);
204
else if (AstStatIf* elseIf = ifs->elsebody->as<AstStatIf>())
205
{
206
auto innerStats = getNestedStats(elseIf);
207
result.insert(end(result), begin(innerStats), end(innerStats));
208
}
209
else
210
{
211
printf("AstStatIf's else clause can have more statement types than I thought\n");
212
LUAU_ASSERT(0);
213
}
214
}
215
}
216
else if (auto w = stat->as<AstStatWhile>())
217
append(w->body);
218
else if (auto r = stat->as<AstStatRepeat>())
219
append(r->body);
220
else if (auto f = stat->as<AstStatFor>())
221
append(f->body);
222
else if (auto f = stat->as<AstStatForIn>())
223
append(f->body);
224
else if (auto f = stat->as<AstStatFunction>())
225
append(f->func->body);
226
else if (auto f = stat->as<AstStatLocalFunction>())
227
append(f->func->body);
228
229
return result;
230
}
231
232
// Move new body data into allocator-managed storage so that it's safe to keep around longterm.
233
AstStat** reallocateStatements(const std::vector<AstStat*>& statements)
234
{
235
AstStat** newData = static_cast<AstStat**>(allocator.allocate(sizeof(AstStat*) * statements.size()));
236
std::copy(statements.data(), statements.data() + statements.size(), newData);
237
238
return newData;
239
}
240
241
// Semiopen interval
242
using Span = std::pair<size_t, size_t>;
243
244
// Generates 'chunks' semiopen spans of equal-ish size to span the indeces running from 0 to 'size'
245
// Also inverses.
246
std::vector<std::pair<Span, Span>> generateSpans(size_t size, size_t chunks)
247
{
248
if (size <= 1)
249
return {};
250
251
LUAU_ASSERT(chunks > 0);
252
size_t chunkLength = std::max<size_t>(1, size / chunks);
253
254
std::vector<std::pair<Span, Span>> result;
255
256
auto append = [&result](Span a, Span b)
257
{
258
if (a.first == a.second && b.first == b.second)
259
return;
260
else
261
result.emplace_back(a, b);
262
};
263
264
size_t i = 0;
265
while (i < size)
266
{
267
size_t end = std::min(i + chunkLength, size);
268
append(Span{0, i}, Span{end, size});
269
270
i = end;
271
}
272
273
i = 0;
274
while (i < size)
275
{
276
size_t end = std::min(i + chunkLength, size);
277
append(Span{i, end}, Span{size, size});
278
279
i = end;
280
}
281
282
return result;
283
}
284
285
// Returns the statements of block within span1 and span2
286
// Also has the hokey restriction that span1 must come before span2
287
std::vector<AstStat*> prunedSpan(AstStatBlock* block, Span span1, Span span2)
288
{
289
std::vector<AstStat*> result;
290
291
for (size_t i = span1.first; i < span1.second; ++i)
292
result.push_back(block->body.data[i]);
293
294
for (size_t i = span2.first; i < span2.second; ++i)
295
result.push_back(block->body.data[i]);
296
297
return result;
298
}
299
300
// returns true if anything was culled plus the chunk count
301
std::pair<bool, size_t> deleteChildStatements(AstStatBlock* block, size_t chunkCount)
302
{
303
if (block->body.size == 0)
304
return {false, chunkCount};
305
306
do
307
{
308
auto permutations = generateSpans(block->body.size, chunkCount);
309
for (const auto& [span1, span2] : permutations)
310
{
311
auto tempStatements = prunedSpan(block, span1, span2);
312
AstArray<AstStat*> backupBody{tempStatements.data(), tempStatements.size()};
313
314
std::swap(block->body, backupBody);
315
TestResult result = run();
316
if (result == TestResult::BugFound)
317
{
318
// The bug still reproduces without the statements we've culled. Commit.
319
block->body.data = reallocateStatements(tempStatements);
320
return {true, std::max<size_t>(2, chunkCount - 1)};
321
}
322
else
323
{
324
// The statements we've culled are critical for the reproduction of the bug.
325
// TODO try promoting its contents into this scope
326
std::swap(block->body, backupBody);
327
}
328
}
329
330
chunkCount *= 2;
331
} while (chunkCount <= block->body.size);
332
333
return {false, block->body.size};
334
}
335
336
bool deleteChildStatements(AstStatBlock* b)
337
{
338
bool result = false;
339
340
size_t chunkCount = 2;
341
while (true)
342
{
343
auto [workDone, newChunkCount] = deleteChildStatements(b, chunkCount);
344
if (workDone)
345
{
346
result = true;
347
chunkCount = newChunkCount;
348
continue;
349
}
350
else
351
break;
352
}
353
354
return result;
355
}
356
357
bool tryPromotingChildStatements(AstStatBlock* b, size_t index)
358
{
359
std::vector<AstStat*> tempStats(b->body.data, b->body.data + b->body.size);
360
AstStat* removed = tempStats.at(index);
361
tempStats.erase(begin(tempStats) + index);
362
363
std::vector<AstStat*> nestedStats = getNestedStats(removed);
364
tempStats.insert(begin(tempStats) + index, begin(nestedStats), end(nestedStats));
365
366
AstArray<AstStat*> tempArray{tempStats.data(), tempStats.size()};
367
std::swap(b->body, tempArray);
368
369
TestResult result = run();
370
371
if (result == TestResult::BugFound)
372
{
373
b->body.data = reallocateStatements(tempStats);
374
return true;
375
}
376
else
377
{
378
std::swap(b->body, tempArray);
379
return false;
380
}
381
}
382
383
// We live with some weirdness because I'm kind of lazy: If a statement's
384
// contents are promoted, we try promoting those prometed statements right
385
// away. I don't think it matters: If we can delete a statement and still
386
// exhibit the bug, we should do so. The order isn't so important.
387
bool tryPromotingChildStatements(AstStatBlock* b)
388
{
389
size_t i = 0;
390
while (i < b->body.size)
391
{
392
bool promoted = tryPromotingChildStatements(b, i);
393
if (!promoted)
394
++i;
395
}
396
397
return false;
398
}
399
400
void walk(AstStatBlock* block)
401
{
402
std::queue<AstStatBlock*> queue;
403
Enqueuer enqueuer{&queue};
404
405
queue.push(block);
406
407
while (!queue.empty())
408
{
409
AstStatBlock* b = queue.front();
410
queue.pop();
411
412
bool result = false;
413
do
414
{
415
result = deleteChildStatements(b);
416
417
/* Try other reductions here before we walk into child statements
418
* Other reductions to try someday:
419
*
420
* Promoting a statement's children to the enclosing block.
421
* Deleting type annotations
422
* Deleting parts of type annotations
423
* Replacing subexpressions with ({} :: any)
424
* Inlining type aliases
425
* Inlining constants
426
* Inlining functions
427
*/
428
result |= tryPromotingChildStatements(b);
429
} while (result);
430
431
for (AstStat* stat : b->body)
432
stat->visit(&enqueuer);
433
}
434
}
435
436
void run(const std::string scriptName, const std::string command, std::string_view source, std::string_view searchText)
437
{
438
this->scriptName = scriptName;
439
440
#if 0
441
// Handy debugging trick: VS Code will update its view of the file in realtime as it is edited.
442
std::string wheee = "code " + scriptName;
443
system(wheee.c_str());
444
#endif
445
446
printf("Script: %s\n", scriptName.c_str());
447
448
this->command = command;
449
this->searchText = searchText;
450
451
parseResult = Parser::parse(source.data(), source.size(), nameTable, allocator, parseOptions);
452
if (!parseResult.errors.empty())
453
{
454
printf("Parse errors\n");
455
exit(1);
456
}
457
458
root = parseResult.root;
459
cstNodeMap = std::move(parseResult.cstNodeMap);
460
461
const TestResult initialResult = run();
462
if (initialResult == TestResult::NoBug)
463
{
464
printf("Could not find failure string in the unmodified script! Check your commandline arguments\n");
465
exit(2);
466
}
467
468
walk(root);
469
470
writeTempScript(/* minify */ true);
471
472
printf("Done! Check %s\n", scriptName.c_str());
473
}
474
};
475
476
[[noreturn]] void help(const std::vector<std::string_view>& args)
477
{
478
printf("Syntax: %s script command \"search text\"\n", args[0].data());
479
printf(" Within command, use {} as a stand-in for the script being reduced\n");
480
exit(1);
481
}
482
483
int main(int argc, char** argv)
484
{
485
const std::vector<std::string_view> args(argv, argv + argc);
486
487
if (args.size() != 4)
488
help(args);
489
490
for (size_t i = 1; i < args.size(); ++i)
491
{
492
if (args[i] == "--help")
493
help(args);
494
}
495
496
std::string scriptName = argv[1];
497
std::string appName = argv[2];
498
const std::string searchText = argv[3];
499
500
std::optional<std::string> source = readFile(scriptName);
501
502
if (!source)
503
{
504
printf("Could not read source %s\n", argv[1]);
505
exit(1);
506
}
507
508
Reducer reducer;
509
reducer.run(std::move(scriptName), std::move(appName), *source, searchText);
510
}
511
512