Skip to content

Commit 60c1d94

Browse files
committed
feature: Implement a static require tracer in order to resolve relative requires for bytecode compilation
1 parent 59b1beb commit 60c1d94

File tree

11 files changed

+425
-1
lines changed

11 files changed

+425
-1
lines changed

lute/cli/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ add_library(Lute.CLI.lib STATIC)
2525
target_sources(Lute.CLI.lib PRIVATE
2626
include/lute/climain.h
2727
include/lute/compile.h
28+
include/lute/staticrequires.h
2829
include/lute/tc.h
2930

3031
src/climain.cpp
3132
src/compile.cpp
33+
src/staticrequires.cpp
3234
src/tc.cpp
3335
)
3436

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <vector>
5+
#include <set>
6+
#include <map>
7+
#include <optional>
8+
9+
class StaticRequireTracer
10+
{
11+
public:
12+
StaticRequireTracer() = default;
13+
14+
// Trace dependencies starting from an entry point file
15+
// rootDirectory: Base directory for resolving all requires
16+
// entryPoint: Path to entry point file (relative to rootDirectory)
17+
// Returns list of all files in dependency order (entry point first)
18+
std::vector<std::string> trace(const std::string& rootDirectory, const std::string& entryPoint);
19+
20+
// Get the require graph built during the last trace
21+
// Maps each file to the list of files it requires
22+
const std::map<std::string, std::vector<std::string>>& getRequireGraph() const { return requireGraph; }
23+
24+
private:
25+
std::set<std::string> visited;
26+
std::vector<std::string> discovered;
27+
std::map<std::string, std::vector<std::string>> requireGraph;
28+
std::string rootDirectory;
29+
30+
// Extract all require() paths from source code
31+
std::vector<std::string> extractRequires(const std::string& source);
32+
33+
// Resolve a require path relative to the requiring file
34+
std::optional<std::string> resolveRequire(const std::string& requirer, const std::string& required);
35+
};

lute/cli/src/staticrequires.cpp

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#include "lute/staticrequires.h"
2+
3+
#include "Luau/Ast.h"
4+
#include "Luau/Parser.h"
5+
#include "Luau/FileUtils.h"
6+
7+
#include "lute/modulepath.h"
8+
9+
#include <cstdio>
10+
#include <filesystem>
11+
#include <queue>
12+
13+
namespace fs = std::filesystem;
14+
15+
// AST visitor to extract require() calls
16+
class RequireExtractor : public Luau::AstVisitor
17+
{
18+
public:
19+
std::vector<std::string> requirePaths;
20+
21+
bool visit(Luau::AstExprCall* call) override
22+
{
23+
// Check if the function being called is 'require'
24+
if (auto global = call->func->as<Luau::AstExprGlobal>())
25+
{
26+
if (global->name == "require" && call->args.size > 0)
27+
{
28+
// Extract string literal argument
29+
if (auto str = call->args.data[0]->as<Luau::AstExprConstantString>())
30+
{
31+
requirePaths.emplace_back(str->value.data, str->value.size);
32+
}
33+
}
34+
}
35+
return true;
36+
}
37+
};
38+
39+
std::vector<std::string> StaticRequireTracer::trace(const std::string& rootDirectory, const std::string& entryPoint)
40+
{
41+
visited.clear();
42+
discovered.clear();
43+
requireGraph.clear();
44+
this->rootDirectory = rootDirectory;
45+
46+
std::queue<std::string> toProcess;
47+
toProcess.push(entryPoint);
48+
49+
while (!toProcess.empty())
50+
{
51+
std::string filePath = toProcess.front();
52+
toProcess.pop();
53+
54+
// Get absolute path for visited tracking
55+
std::string absPath;
56+
try
57+
{
58+
std::string fullPath = joinPaths(rootDirectory, filePath);
59+
absPath = fs::absolute(fullPath).string();
60+
}
61+
catch (const std::exception& e)
62+
{
63+
fprintf(stderr, "Warning: Could not resolve path '%s': %s\n", filePath.c_str(), e.what());
64+
continue;
65+
}
66+
67+
// Skip if already visited (handles circular dependencies)
68+
if (visited.count(absPath))
69+
continue;
70+
71+
visited.insert(absPath);
72+
73+
// Read the file
74+
std::string fullPath = joinPaths(rootDirectory, filePath);
75+
std::optional<std::string> source = readFile(fullPath);
76+
if (!source)
77+
{
78+
fprintf(stderr, "Warning: Could not read file '%s'\n", fullPath.c_str());
79+
continue;
80+
}
81+
82+
// Add to discovered list (use the relative path from rootDirectory)
83+
discovered.push_back(filePath);
84+
85+
// Extract require calls
86+
std::vector<std::string> requiresInFile = extractRequires(*source);
87+
88+
// Build the require graph for this file
89+
std::vector<std::string> resolvedDeps;
90+
91+
// Add each required module to the queue
92+
for (const auto& req : requiresInFile)
93+
{
94+
std::optional<std::string> resolvedPath = resolveRequire(filePath, req);
95+
if (resolvedPath)
96+
{
97+
toProcess.push(*resolvedPath);
98+
resolvedDeps.push_back(*resolvedPath);
99+
}
100+
else
101+
{
102+
fprintf(stderr, "Warning: Could not resolve require('%s') from '%s'\n", req.c_str(), filePath.c_str());
103+
}
104+
}
105+
106+
// Store the resolved dependencies in the graph
107+
requireGraph[filePath] = std::move(resolvedDeps);
108+
}
109+
110+
return discovered;
111+
}
112+
113+
std::vector<std::string> StaticRequireTracer::extractRequires(const std::string& source)
114+
{
115+
Luau::Allocator allocator;
116+
Luau::AstNameTable names(allocator);
117+
118+
Luau::ParseOptions options;
119+
Luau::ParseResult result = Luau::Parser::parse(source.c_str(), source.size(), names, allocator, options);
120+
121+
if (result.errors.size() > 0)
122+
{
123+
// Even with parse errors, we can still try to extract requires
124+
// Parse errors might be from type errors or other issues that don't prevent require extraction
125+
}
126+
127+
RequireExtractor extractor;
128+
result.root->visit(&extractor);
129+
130+
return extractor.requirePaths;
131+
}
132+
133+
std::optional<std::string> StaticRequireTracer::resolveRequire(const std::string& requirer, const std::string& required)
134+
{
135+
// Get the directory containing the requiring file (relative to rootDirectory)
136+
std::string requirerDir;
137+
size_t lastSlash = requirer.find_last_of("/\\");
138+
if (lastSlash != std::string::npos)
139+
{
140+
requirerDir = requirer.substr(0, lastSlash);
141+
}
142+
else
143+
{
144+
requirerDir = "";
145+
}
146+
147+
// Helper functions for ModulePath - paths are relative to rootDirectory
148+
auto isFile = [this](const std::string& path) -> bool {
149+
std::string fullPath = joinPaths(rootDirectory, path);
150+
return fs::is_regular_file(fullPath);
151+
};
152+
153+
auto isDir = [this](const std::string& path) -> bool {
154+
std::string fullPath = joinPaths(rootDirectory, path);
155+
return fs::is_directory(fullPath);
156+
};
157+
158+
// Create a ModulePath with root as rootDirectory, starting at requirer's directory
159+
// This allows us to navigate up with .. beyond requirerDir, but not beyond rootDirectory
160+
std::optional<ModulePath> modulePath = ModulePath::create(
161+
"", // Root is empty (relative path base)
162+
requirerDir, // Start at the requirer's directory
163+
isFile,
164+
isDir
165+
);
166+
167+
if (!modulePath)
168+
return std::nullopt;
169+
170+
// Navigate according to the require path
171+
// Split require path by '/' and navigate
172+
std::string reqPath = required;
173+
size_t pos = 0;
174+
175+
while (pos < reqPath.size())
176+
{
177+
size_t nextSlash = reqPath.find('/', pos);
178+
std::string component;
179+
180+
if (nextSlash == std::string::npos)
181+
{
182+
component = reqPath.substr(pos);
183+
pos = reqPath.size();
184+
}
185+
else
186+
{
187+
component = reqPath.substr(pos, nextSlash - pos);
188+
pos = nextSlash + 1;
189+
}
190+
191+
if (component.empty() || component == ".")
192+
continue;
193+
194+
if (component == "..")
195+
{
196+
if (modulePath->toParent() != NavigationStatus::Success)
197+
return std::nullopt;
198+
}
199+
else
200+
{
201+
if (modulePath->toChild(component) != NavigationStatus::Success)
202+
return std::nullopt;
203+
}
204+
}
205+
206+
// Get the resolved path (relative to rootDirectory)
207+
ResolvedRealPath resolved = modulePath->getRealPath();
208+
if (resolved.status != NavigationStatus::Success)
209+
return std::nullopt;
210+
211+
// Strip leading slash if present (occurs when root is empty)
212+
std::string result = resolved.realPath;
213+
if (!result.empty() && result[0] == '/')
214+
result = result.substr(1);
215+
216+
return result;
217+
}

tests/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ target_sources(Lute.Test PRIVATE
1111

1212
src/modulepath.test.cpp
1313
src/require.test.cpp
14-
src/stdsystem.test.cpp)
14+
src/stdsystem.test.cpp
15+
src/staticrequires.test.cpp)
1516

1617
set_target_properties(Lute.Test PROPERTIES OUTPUT_NAME lute-tests)
1718
target_compile_features(Lute.Test PUBLIC cxx_std_17)

0 commit comments

Comments
 (0)