mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-12 13:00:38 +00:00
3b0e93bec9
# What's changed? Add program argument passing to scripts run using the Luau REPL! You can now pass `--program-args` (or shorthand `-a`) to the REPL which will treat all remaining arguments as arguments to pass to executed scripts. These values can be accessed through variadic argument expansion. You can read these values like so: ``` local args = {...} -- gets you an array of all the arguments ``` For example if we run the following script like `luau test.lua -a test1 test2 test3`: ``` -- test.lua print(...) ``` you should get the output: ``` test1 test2 test3 ``` ### Native Code Generation * Improve A64 lowering for vector operations by using vector instructions * Fix lowering issue in IR value location tracking! - A developer reported a divergence between code run in the VM and Native Code Generation which we have now fixed ### New Type Solver * Apply substitution to type families, and emit new constraints to reduce those further * More progress on reducing comparison (`lt/le`)type families * Resolve two major sources of cyclic types in the new solver ### Miscellaneous * Turned internal compiler errors (ICE's) into warnings and errors ------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
505 lines
14 KiB
C++
505 lines
14 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Luau/Ast.h"
|
|
#include "Luau/Common.h"
|
|
#include "Luau/Parser.h"
|
|
#include "Luau/Transpiler.h"
|
|
|
|
#include "FileUtils.h"
|
|
|
|
#include <algorithm>
|
|
#include <stdio.h>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <queue>
|
|
|
|
#define VERBOSE 0 // 1 - print out commandline invocations. 2 - print out stdout
|
|
|
|
#if defined(_WIN32) && !defined(__MINGW32__)
|
|
|
|
const auto popen = &_popen;
|
|
const auto pclose = &_pclose;
|
|
|
|
#endif
|
|
|
|
using namespace Luau;
|
|
|
|
enum class TestResult
|
|
{
|
|
BugFound, // We encountered the bug we are trying to isolate
|
|
NoBug, // We did not encounter the bug we are trying to isolate
|
|
};
|
|
|
|
struct Enqueuer : public AstVisitor
|
|
{
|
|
std::queue<AstStatBlock*>* queue;
|
|
|
|
explicit Enqueuer(std::queue<AstStatBlock*>* queue)
|
|
: queue(queue)
|
|
{
|
|
LUAU_ASSERT(queue);
|
|
}
|
|
|
|
bool visit(AstStatBlock* block) override
|
|
{
|
|
queue->push(block);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
struct Reducer
|
|
{
|
|
Allocator allocator;
|
|
AstNameTable nameTable{allocator};
|
|
ParseOptions parseOptions;
|
|
|
|
ParseResult parseResult;
|
|
AstStatBlock* root;
|
|
|
|
std::string scriptName;
|
|
|
|
std::string command;
|
|
std::string_view searchText;
|
|
|
|
Reducer()
|
|
{
|
|
parseOptions.captureComments = true;
|
|
}
|
|
|
|
std::string readLine(FILE* f)
|
|
{
|
|
std::string line = "";
|
|
char buffer[256];
|
|
while (fgets(buffer, sizeof(buffer), f))
|
|
{
|
|
auto len = strlen(buffer);
|
|
line += std::string(buffer, len);
|
|
if (buffer[len - 1] == '\n')
|
|
break;
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
void writeTempScript(bool minify = false)
|
|
{
|
|
std::string source = transpileWithTypes(*root);
|
|
|
|
if (minify)
|
|
{
|
|
size_t pos = 0;
|
|
do
|
|
{
|
|
pos = source.find("\n\n", pos);
|
|
if (pos == std::string::npos)
|
|
break;
|
|
|
|
source.erase(pos, 1);
|
|
} while (true);
|
|
}
|
|
|
|
FILE* f = fopen(scriptName.c_str(), "w");
|
|
if (!f)
|
|
{
|
|
printf("Unable to open temp script to %s\n", scriptName.c_str());
|
|
exit(2);
|
|
}
|
|
|
|
for (const HotComment& comment : parseResult.hotcomments)
|
|
fprintf(f, "--!%s\n", comment.content.c_str());
|
|
|
|
auto written = fwrite(source.data(), 1, source.size(), f);
|
|
if (written != source.size())
|
|
{
|
|
printf("??? %zu %zu\n", written, source.size());
|
|
printf("Unable to write to temp script %s\n", scriptName.c_str());
|
|
exit(3);
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
int step = 0;
|
|
|
|
std::string escape(const std::string& s)
|
|
{
|
|
std::string result;
|
|
result.reserve(s.size() + 20); // guess
|
|
result += '"';
|
|
for (char c : s)
|
|
{
|
|
if (c == '"')
|
|
result += '\\';
|
|
result += c;
|
|
}
|
|
result += '"';
|
|
|
|
return result;
|
|
}
|
|
|
|
TestResult run()
|
|
{
|
|
writeTempScript();
|
|
|
|
std::string cmd = command;
|
|
while (true)
|
|
{
|
|
auto pos = cmd.find("{}");
|
|
if (std::string::npos == pos)
|
|
break;
|
|
|
|
cmd = cmd.substr(0, pos) + escape(scriptName) + cmd.substr(pos + 2);
|
|
}
|
|
|
|
#if VERBOSE >= 1
|
|
printf("running %s\n", cmd.c_str());
|
|
#endif
|
|
|
|
TestResult result = TestResult::NoBug;
|
|
|
|
++step;
|
|
printf("Step %4d...\n", step);
|
|
|
|
FILE* p = popen(cmd.c_str(), "r");
|
|
|
|
while (!feof(p))
|
|
{
|
|
std::string s = readLine(p);
|
|
#if VERBOSE >= 2
|
|
printf("%s", s.c_str());
|
|
#endif
|
|
if (std::string::npos != s.find(searchText))
|
|
{
|
|
result = TestResult::BugFound;
|
|
break;
|
|
}
|
|
}
|
|
|
|
pclose(p);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<AstStat*> getNestedStats(AstStat* stat)
|
|
{
|
|
std::vector<AstStat*> result;
|
|
|
|
auto append = [&](AstStatBlock* block) {
|
|
if (block)
|
|
result.insert(result.end(), block->body.data, block->body.data + block->body.size);
|
|
};
|
|
|
|
if (auto block = stat->as<AstStatBlock>())
|
|
append(block);
|
|
else if (auto ifs = stat->as<AstStatIf>())
|
|
{
|
|
append(ifs->thenbody);
|
|
if (ifs->elsebody)
|
|
{
|
|
if (AstStatBlock* elseBlock = ifs->elsebody->as<AstStatBlock>())
|
|
append(elseBlock);
|
|
else if (AstStatIf* elseIf = ifs->elsebody->as<AstStatIf>())
|
|
{
|
|
auto innerStats = getNestedStats(elseIf);
|
|
result.insert(end(result), begin(innerStats), end(innerStats));
|
|
}
|
|
else
|
|
{
|
|
printf("AstStatIf's else clause can have more statement types than I thought\n");
|
|
LUAU_ASSERT(0);
|
|
}
|
|
}
|
|
}
|
|
else if (auto w = stat->as<AstStatWhile>())
|
|
append(w->body);
|
|
else if (auto r = stat->as<AstStatRepeat>())
|
|
append(r->body);
|
|
else if (auto f = stat->as<AstStatFor>())
|
|
append(f->body);
|
|
else if (auto f = stat->as<AstStatForIn>())
|
|
append(f->body);
|
|
else if (auto f = stat->as<AstStatFunction>())
|
|
append(f->func->body);
|
|
else if (auto f = stat->as<AstStatLocalFunction>())
|
|
append(f->func->body);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Move new body data into allocator-managed storage so that it's safe to keep around longterm.
|
|
AstStat** reallocateStatements(const std::vector<AstStat*>& statements)
|
|
{
|
|
AstStat** newData = static_cast<AstStat**>(allocator.allocate(sizeof(AstStat*) * statements.size()));
|
|
std::copy(statements.data(), statements.data() + statements.size(), newData);
|
|
|
|
return newData;
|
|
}
|
|
|
|
// Semiopen interval
|
|
using Span = std::pair<size_t, size_t>;
|
|
|
|
// Generates 'chunks' semiopen spans of equal-ish size to span the indeces running from 0 to 'size'
|
|
// Also inverses.
|
|
std::vector<std::pair<Span, Span>> generateSpans(size_t size, size_t chunks)
|
|
{
|
|
if (size <= 1)
|
|
return {};
|
|
|
|
LUAU_ASSERT(chunks > 0);
|
|
size_t chunkLength = std::max<size_t>(1, size / chunks);
|
|
|
|
std::vector<std::pair<Span, Span>> result;
|
|
|
|
auto append = [&result](Span a, Span b) {
|
|
if (a.first == a.second && b.first == b.second)
|
|
return;
|
|
else
|
|
result.emplace_back(a, b);
|
|
};
|
|
|
|
size_t i = 0;
|
|
while (i < size)
|
|
{
|
|
size_t end = std::min(i + chunkLength, size);
|
|
append(Span{0, i}, Span{end, size});
|
|
|
|
i = end;
|
|
}
|
|
|
|
i = 0;
|
|
while (i < size)
|
|
{
|
|
size_t end = std::min(i + chunkLength, size);
|
|
append(Span{i, end}, Span{size, size});
|
|
|
|
i = end;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Returns the statements of block within span1 and span2
|
|
// Also has the hokey restriction that span1 must come before span2
|
|
std::vector<AstStat*> prunedSpan(AstStatBlock* block, Span span1, Span span2)
|
|
{
|
|
std::vector<AstStat*> result;
|
|
|
|
for (size_t i = span1.first; i < span1.second; ++i)
|
|
result.push_back(block->body.data[i]);
|
|
|
|
for (size_t i = span2.first; i < span2.second; ++i)
|
|
result.push_back(block->body.data[i]);
|
|
|
|
return result;
|
|
}
|
|
|
|
// returns true if anything was culled plus the chunk count
|
|
std::pair<bool, size_t> deleteChildStatements(AstStatBlock* block, size_t chunkCount)
|
|
{
|
|
if (block->body.size == 0)
|
|
return {false, chunkCount};
|
|
|
|
do
|
|
{
|
|
auto permutations = generateSpans(block->body.size, chunkCount);
|
|
for (const auto& [span1, span2] : permutations)
|
|
{
|
|
auto tempStatements = prunedSpan(block, span1, span2);
|
|
AstArray<AstStat*> backupBody{tempStatements.data(), tempStatements.size()};
|
|
|
|
std::swap(block->body, backupBody);
|
|
TestResult result = run();
|
|
if (result == TestResult::BugFound)
|
|
{
|
|
// The bug still reproduces without the statements we've culled. Commit.
|
|
block->body.data = reallocateStatements(tempStatements);
|
|
return {true, std::max<size_t>(2, chunkCount - 1)};
|
|
}
|
|
else
|
|
{
|
|
// The statements we've culled are critical for the reproduction of the bug.
|
|
// TODO try promoting its contents into this scope
|
|
std::swap(block->body, backupBody);
|
|
}
|
|
}
|
|
|
|
chunkCount *= 2;
|
|
} while (chunkCount <= block->body.size);
|
|
|
|
return {false, block->body.size};
|
|
}
|
|
|
|
bool deleteChildStatements(AstStatBlock* b)
|
|
{
|
|
bool result = false;
|
|
|
|
size_t chunkCount = 2;
|
|
while (true)
|
|
{
|
|
auto [workDone, newChunkCount] = deleteChildStatements(b, chunkCount);
|
|
if (workDone)
|
|
{
|
|
result = true;
|
|
chunkCount = newChunkCount;
|
|
continue;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool tryPromotingChildStatements(AstStatBlock* b, size_t index)
|
|
{
|
|
std::vector<AstStat*> tempStats(b->body.data, b->body.data + b->body.size);
|
|
AstStat* removed = tempStats.at(index);
|
|
tempStats.erase(begin(tempStats) + index);
|
|
|
|
std::vector<AstStat*> nestedStats = getNestedStats(removed);
|
|
tempStats.insert(begin(tempStats) + index, begin(nestedStats), end(nestedStats));
|
|
|
|
AstArray<AstStat*> tempArray{tempStats.data(), tempStats.size()};
|
|
std::swap(b->body, tempArray);
|
|
|
|
TestResult result = run();
|
|
|
|
if (result == TestResult::BugFound)
|
|
{
|
|
b->body.data = reallocateStatements(tempStats);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
std::swap(b->body, tempArray);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// We live with some weirdness because I'm kind of lazy: If a statement's
|
|
// contents are promoted, we try promoting those prometed statements right
|
|
// away. I don't think it matters: If we can delete a statement and still
|
|
// exhibit the bug, we should do so. The order isn't so important.
|
|
bool tryPromotingChildStatements(AstStatBlock* b)
|
|
{
|
|
size_t i = 0;
|
|
while (i < b->body.size)
|
|
{
|
|
bool promoted = tryPromotingChildStatements(b, i);
|
|
if (!promoted)
|
|
++i;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void walk(AstStatBlock* block)
|
|
{
|
|
std::queue<AstStatBlock*> queue;
|
|
Enqueuer enqueuer{&queue};
|
|
|
|
queue.push(block);
|
|
|
|
while (!queue.empty())
|
|
{
|
|
AstStatBlock* b = queue.front();
|
|
queue.pop();
|
|
|
|
bool result = false;
|
|
do
|
|
{
|
|
result = deleteChildStatements(b);
|
|
|
|
/* Try other reductions here before we walk into child statements
|
|
* Other reductions to try someday:
|
|
*
|
|
* Promoting a statement's children to the enclosing block.
|
|
* Deleting type annotations
|
|
* Deleting parts of type annotations
|
|
* Replacing subexpressions with ({} :: any)
|
|
* Inlining type aliases
|
|
* Inlining constants
|
|
* Inlining functions
|
|
*/
|
|
result |= tryPromotingChildStatements(b);
|
|
} while (result);
|
|
|
|
for (AstStat* stat : b->body)
|
|
stat->visit(&enqueuer);
|
|
}
|
|
}
|
|
|
|
void run(const std::string scriptName, const std::string command, std::string_view source, std::string_view searchText)
|
|
{
|
|
this->scriptName = scriptName;
|
|
|
|
#if 0
|
|
// Handy debugging trick: VS Code will update its view of the file in realtime as it is edited.
|
|
std::string wheee = "code " + scriptName;
|
|
system(wheee.c_str());
|
|
#endif
|
|
|
|
printf("Script: %s\n", scriptName.c_str());
|
|
|
|
this->command = command;
|
|
this->searchText = searchText;
|
|
|
|
parseResult = Parser::parse(source.data(), source.size(), nameTable, allocator, parseOptions);
|
|
if (!parseResult.errors.empty())
|
|
{
|
|
printf("Parse errors\n");
|
|
exit(1);
|
|
}
|
|
|
|
root = parseResult.root;
|
|
|
|
const TestResult initialResult = run();
|
|
if (initialResult == TestResult::NoBug)
|
|
{
|
|
printf("Could not find failure string in the unmodified script! Check your commandline arguments\n");
|
|
exit(2);
|
|
}
|
|
|
|
walk(root);
|
|
|
|
writeTempScript(/* minify */ true);
|
|
|
|
printf("Done! Check %s\n", scriptName.c_str());
|
|
}
|
|
};
|
|
|
|
[[noreturn]] void help(const std::vector<std::string_view>& args)
|
|
{
|
|
printf("Syntax: %s script command \"search text\"\n", args[0].data());
|
|
printf(" Within command, use {} as a stand-in for the script being reduced\n");
|
|
exit(1);
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
const std::vector<std::string_view> args(argv, argv + argc);
|
|
|
|
if (args.size() != 4)
|
|
help(args);
|
|
|
|
for (size_t i = 1; i < args.size(); ++i)
|
|
{
|
|
if (args[i] == "--help")
|
|
help(args);
|
|
}
|
|
|
|
const std::string scriptName = argv[1];
|
|
const std::string appName = argv[2];
|
|
const std::string searchText = argv[3];
|
|
|
|
std::optional<std::string> source = readFile(scriptName);
|
|
|
|
if (!source)
|
|
{
|
|
printf("Could not read source %s\n", argv[1]);
|
|
exit(1);
|
|
}
|
|
|
|
Reducer reducer;
|
|
reducer.run(scriptName, appName, *source, searchText);
|
|
}
|