Skip to content

Commit 4e1f4ff

Browse files
committed
feat: Add virtual nevermore loader support
1 parent d96e3bc commit 4e1f4ff

File tree

10 files changed

+332
-1
lines changed

10 files changed

+332
-1
lines changed

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "")
44
option(LUAU_ENABLE_TIME_TRACE "Build with Luau TimeTrace" OFF)
55
option(LSP_BUILD_ASAN "Build with ASAN" OFF)
66
option(LSP_STATIC_CRT "Link with the static CRT (/MT)" OFF)
7+
option(NEVERMORE_STRING_REQUIRE "Build with Nevermore String Requires" ON)
78

89
if (LSP_STATIC_CRT)
910
cmake_policy(SET CMP0091 NEW)
@@ -15,6 +16,11 @@ if (LUAU_ENABLE_TIME_TRACE)
1516
add_definitions(-DLUAU_ENABLE_TIME_TRACE)
1617
endif ()
1718

19+
if (NEVERMORE_STRING_REQUIRE)
20+
add_definitions(-DNEVERMORE_STRING_REQUIRE)
21+
endif ()
22+
23+
1824
project(Luau.LanguageServer LANGUAGES CXX)
1925
add_subdirectory(luau)
2026
add_library(Luau.LanguageServer STATIC)
@@ -38,6 +44,7 @@ target_sources(Luau.LanguageServer PRIVATE
3844
src/CliConfigurationParser.cpp
3945
src/platform/AutoImports.cpp
4046
src/platform/LSPPlatform.cpp
47+
src/platform/roblox/NevermoreStringRequire.cpp
4148
src/platform/StringRequireAutoImporter.cpp
4249
src/platform/StringRequireSuggester.cpp
4350
src/platform/roblox/RobloxCodeAction.cpp

luau

Submodule luau updated from 72f6c8b to 220f3a5

src/LuauExt.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ std::optional<Luau::AstExpr*> matchRequire(const Luau::AstExprCall& call)
167167
if (call.args.size != 1)
168168
return std::nullopt;
169169

170+
#ifdef NEVERMORE_STRING_REQUIRE
171+
const Luau::AstExprLocal* local = call.func->as<Luau::AstExprLocal>();
172+
if (local && local->local->name == require)
173+
{
174+
return call.args.data[0];
175+
}
176+
#endif
177+
170178
const Luau::AstExprGlobal* funcAsGlobal = call.func->as<Luau::AstExprGlobal>();
171179
if (!funcAsGlobal || funcAsGlobal->name != require)
172180
return std::nullopt;
@@ -727,6 +735,13 @@ bool isRequire(const Luau::AstExpr* expr)
727735
{
728736
if (auto funcAsGlobal = call->func->as<Luau::AstExprGlobal>(); funcAsGlobal && funcAsGlobal->name == "require")
729737
return true;
738+
739+
#ifdef NEVERMORE_STRING_REQUIRE
740+
if (const Luau::AstExprLocal* local = call->as<Luau::AstExprLocal>(); local && local->local->name == "require")
741+
{
742+
return true;
743+
}
744+
#endif
730745
}
731746
else if (auto assertion = expr->as<Luau::AstExprTypeAssertion>())
732747
{

src/WorkspaceFileResolver.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ std::optional<Luau::SourceCode> WorkspaceFileResolver::readSource(const Luau::Mo
7272
if (platform->isVirtualPath(name))
7373
{
7474
auto filePath = platform->resolveToRealPath(name);
75+
76+
#ifdef NEVERMORE_STRING_REQUIRE
77+
// If we are missing a file path, then try to resolve into a virtual node that we generate
78+
if (!filePath.has_value())
79+
{
80+
std::optional<Luau::SourceCode> sourceCode = platform->resolveToVirtualSourceCode(name);
81+
if (sourceCode.has_value())
82+
{
83+
return sourceCode;
84+
}
85+
}
86+
#endif
87+
7588
if (!filePath)
7689
return std::nullopt;
7790

src/include/Platform/LSPPlatform.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class LSPPlatform
5454
return name;
5555
}
5656

57+
#ifdef NEVERMORE_STRING_REQUIRE
58+
[[nodiscard]] virtual std::optional<Luau::SourceCode> resolveToVirtualSourceCode(const Luau::ModuleName& name) const
59+
{
60+
return std::nullopt;
61+
}
62+
#endif
63+
5764
[[nodiscard]] virtual Luau::SourceCode::Type sourceCodeTypeFromPath(const std::filesystem::path& path) const
5865
{
5966
return Luau::SourceCode::Type::Module;

src/include/Platform/RobloxPlatform.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ struct SourceNode
8080
// A different TypeId is created for each type checker (frontend.typeChecker and frontend.typeCheckerForAutocomplete)
8181
std::unordered_map<Luau::GlobalTypes const*, Luau::TypeId> tys{}; // NB: NOT POPULATED BY SOURCEMAP, created manually. Can be null!
8282

83+
#ifdef NEVERMORE_STRING_REQUIRE
84+
bool isVirtualNevermoreLoader = false;
85+
// The corresponding TypeId for this sourcemap node
86+
// A different TypeId is created for each type checker (frontend.typeChecker and frontend.typeCheckerForAutocomplete)
87+
std::unordered_map<Luau::GlobalTypes const*, Luau::TypeId> stringRequireTypes{}; // NB: NOT POPULATED BY SOURCEMAP, created manually. Can be null!
88+
#endif
89+
8390
bool isScript();
8491
std::optional<std::filesystem::path> getScriptFilePath();
8592
Luau::SourceCode::Type sourceCodeType() const;
@@ -136,6 +143,10 @@ class RobloxPlatform : public LSPPlatform
136143
mutable std::unordered_map<std::string, SourceNodePtr> realPathsToSourceNodes{};
137144
mutable std::unordered_map<Luau::ModuleName, SourceNodePtr> virtualPathsToSourceNodes{};
138145

146+
#ifdef NEVERMORE_STRING_REQUIRE
147+
mutable std::unordered_map<std::string, SourceNodePtr> moduleNameToSourceNode{};
148+
#endif
149+
139150
std::optional<SourceNodePtr> getSourceNodeFromVirtualPath(const Luau::ModuleName& name) const;
140151
std::optional<SourceNodePtr> getSourceNodeFromRealPath(const std::string& name) const;
141152

@@ -170,6 +181,12 @@ class RobloxPlatform : public LSPPlatform
170181

171182
std::optional<std::filesystem::path> resolveToRealPath(const Luau::ModuleName& name) const override;
172183

184+
#ifdef NEVERMORE_STRING_REQUIRE
185+
std::optional<Luau::SourceCode> resolveToVirtualSourceCode(const Luau::ModuleName& name) const override;
186+
Luau::TypeId getStringRequireType(const Luau::GlobalTypes& globals, Luau::TypeArena& arena, const SourceNodePtr& node) const;
187+
std::optional<SourceNodePtr> findStringModule(const std::string& moduleName) const;
188+
#endif
189+
173190
Luau::SourceCode::Type sourceCodeTypeFromPath(const std::filesystem::path& path) const override;
174191

175192
std::optional<std::string> readSourceCode(const Luau::ModuleName& name, const std::filesystem::path& path) const override;
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include "Platform/RobloxPlatform.hpp"
2+
#include "LSP/JsonTomlSyntaxParser.hpp"
3+
#include "Luau/BuiltinDefinitions.h"
4+
#include "Luau/ConstraintSolver.h"
5+
#include "Luau/TypeInfer.h"
6+
7+
#include <filesystem>
8+
#include <queue>
9+
10+
#ifdef NEVERMORE_STRING_REQUIRE
11+
12+
struct MagicStringRequireLookup final : Luau::MagicFunction
13+
{
14+
const Luau::GlobalTypes& globals;
15+
const RobloxPlatform& platform;
16+
Luau::TypeArena& arena;
17+
SourceNodePtr node;
18+
19+
MagicStringRequireLookup(const Luau::GlobalTypes& globals, const RobloxPlatform& platform, Luau::TypeArena& arena, SourceNodePtr node)
20+
: globals(globals)
21+
, platform(platform)
22+
, arena(arena)
23+
, node(std::move(node))
24+
{
25+
}
26+
27+
std::optional<Luau::WithPredicate<Luau::TypePackId>> handleOldSolver(Luau::TypeChecker& typeChecker, const Luau::ScopePtr& scope,
28+
const Luau::AstExprCall& expr, Luau::WithPredicate<Luau::TypePackId> withPredicate) override;
29+
bool infer(const Luau::MagicFunctionCallContext& context) override;
30+
};
31+
32+
std::optional<Luau::WithPredicate<Luau::TypePackId>> MagicStringRequireLookup::handleOldSolver(
33+
Luau::TypeChecker& typeChecker, const Luau::ScopePtr& scope, const Luau::AstExprCall& expr, Luau::WithPredicate<Luau::TypePackId>)
34+
{
35+
if (expr.args.size < 1)
36+
{
37+
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{}});
38+
return std::nullopt;
39+
}
40+
41+
auto str = expr.args.data[0]->as<Luau::AstExprConstantString>();
42+
if (!str)
43+
{
44+
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{}});
45+
return std::nullopt;
46+
}
47+
48+
auto moduleName = std::string(str->value.data, str->value.size);
49+
50+
if (node->name == moduleName)
51+
{
52+
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{ moduleName }});
53+
return std::nullopt;
54+
}
55+
56+
auto module = platform.findStringModule(moduleName);
57+
if (!module.has_value())
58+
{
59+
typeChecker.reportError(Luau::TypeError{expr.args.data[0]->location, Luau::UnknownRequire{ moduleName }});
60+
return std::nullopt;
61+
}
62+
63+
Luau::ModuleInfo moduleInfo;
64+
moduleInfo.name = module.value()->virtualPath;
65+
66+
return Luau::WithPredicate<Luau::TypePackId>{arena.addTypePack({typeChecker.checkRequire(scope, moduleInfo, expr.args.data[0]->location)})};
67+
}
68+
69+
bool MagicStringRequireLookup::infer(const Luau::MagicFunctionCallContext& context)
70+
{
71+
// TODO: Actually like, do something here
72+
if (context.callSite->args.size < 1)
73+
return false;
74+
75+
auto str = context.callSite->args.data[0]->as<Luau::AstExprConstantString>();
76+
if (!str)
77+
return false;
78+
79+
auto moduleName = std::string(str->value.data, str->value.size);
80+
auto module = platform.findStringModule(moduleName);
81+
if (!module.has_value())
82+
{
83+
context.solver->reportError(Luau::UnknownRequire{ moduleName }, context.callSite->args.data[0]->location);
84+
return false;
85+
}
86+
87+
88+
Luau::ModuleInfo moduleInfo;
89+
moduleInfo.name = module.value()->virtualPath;
90+
91+
asMutable(context.result)->ty.emplace<Luau::BoundTypePack>(context.solver->arena->addTypePack({
92+
context.solver->resolveModule(moduleInfo, context.callSite->args.data[0]->location)
93+
}));
94+
95+
return true;
96+
}
97+
98+
static void attachMagicStringRequireLookupFunction(const Luau::GlobalTypes& globals, const RobloxPlatform& platform, Luau::TypeArena& arena, const SourceNodePtr& node, Luau::TypeId lookupFuncTy)
99+
{
100+
101+
Luau::attachMagicFunction(
102+
lookupFuncTy, std::make_shared<MagicStringRequireLookup>(globals, platform, arena, node));
103+
Luau::attachTag(lookupFuncTy, kSourcemapGeneratedTag);
104+
Luau::attachTag(lookupFuncTy, "StringRequires");
105+
Luau::attachTag(lookupFuncTy, "require"); // Magic tag
106+
}
107+
108+
Luau::TypeId RobloxPlatform::getStringRequireType(const Luau::GlobalTypes& globals, Luau::TypeArena& arena, const SourceNodePtr& node) const
109+
{
110+
// Gets the type corresponding to the sourcemap node if it exists
111+
// Make sure to use the correct ty version (base typeChecker vs autocomplete typeChecker)
112+
if (node->stringRequireTypes.find(&globals) != node->stringRequireTypes.end())
113+
return node->stringRequireTypes.at(&globals);
114+
115+
// TODO: Memory safety for RobloxPlatform this
116+
117+
Luau::LazyType lazyTypeValue(
118+
[&globals, this, &arena, node](Luau::LazyType& lazyTypeValue) -> void
119+
{
120+
// Check if the lazy type value already has an unwrapped type
121+
if (lazyTypeValue.unwrapped.load())
122+
return;
123+
124+
// Handle if the node is no longer valid
125+
if (!node)
126+
{
127+
lazyTypeValue.unwrapped = globals.builtinTypes->anyType;
128+
return;
129+
}
130+
131+
// TODO: Resolve name to lazy instance
132+
// Or type union
133+
Luau::TypePackId argTypes = arena.addTypePack({ globals.builtinTypes->stringType });
134+
Luau::TypePackId retTypes = arena.addTypePack({ globals.builtinTypes->anyType }); // This should be overriden by the type checker
135+
Luau::FunctionType functionCtv(argTypes, retTypes);
136+
137+
auto typeId = arena.addType(std::move(functionCtv));
138+
attachMagicStringRequireLookupFunction(globals, *this, arena, node, typeId);
139+
140+
lazyTypeValue.unwrapped = typeId;
141+
return;
142+
});
143+
144+
auto ty = arena.addType(std::move(lazyTypeValue));
145+
node->stringRequireTypes.insert_or_assign(&globals, ty);
146+
147+
return ty;
148+
}
149+
150+
std::optional<SourceNodePtr> RobloxPlatform::findStringModule(const std::string& moduleName) const
151+
{
152+
// TODO: Use "node_modules" as a project scope and handle duplications
153+
auto result = this->moduleNameToSourceNode.find(moduleName);
154+
if (result != this->moduleNameToSourceNode.end())
155+
return result->second;
156+
157+
return std::nullopt;
158+
}
159+
160+
std::optional<Luau::SourceCode> RobloxPlatform::resolveToVirtualSourceCode(const Luau::ModuleName& name) const
161+
{
162+
if (!isVirtualPath(name))
163+
{
164+
return std::nullopt;
165+
}
166+
167+
auto sourceNode = getSourceNodeFromVirtualPath(name);
168+
if (!sourceNode || !sourceNode.value()->isVirtualNevermoreLoader)
169+
{
170+
return std::nullopt;
171+
}
172+
173+
std::string source = R"lua(
174+
--!strict
175+
176+
local loader = {}
177+
178+
function loader.load(thisScript: ModuleScript): typeof(StringRequire)
179+
return nil :: never
180+
end
181+
182+
return loader
183+
)lua";
184+
185+
return Luau::SourceCode {
186+
source,
187+
Luau::SourceCode::Type::Module,
188+
};
189+
}
190+
191+
#endif

src/platform/roblox/RobloxCompletion.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ std::optional<Luau::AutocompleteEntryMap> RobloxPlatform::completionCallback(
121121
}
122122
}
123123
}
124+
#ifdef NEVERMORE_STRING_REQUIRE
125+
else if (tag == "StringRequires")
126+
{
127+
Luau::AutocompleteEntryMap result;
128+
for (const auto& pair : this->moduleNameToSourceNode)
129+
result.insert_or_assign(
130+
pair.first, Luau::AutocompleteEntry{Luau::AutocompleteEntryKind::String, workspaceFolder->frontend.builtinTypes->stringType, false,
131+
false, Luau::TypeCorrectKind::Correct});
132+
133+
return result;
134+
}
135+
#endif
124136
else if (tag == "Properties")
125137
{
126138
if (ctx && ctx.value())

src/platform/roblox/RobloxFileResolver.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ static std::string mapContext(const std::string& context)
104104

105105
std::optional<Luau::ModuleInfo> RobloxPlatform::resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) {
106106

107+
#ifdef NEVERMORE_STRING_REQUIRE
108+
// Resolve Nevermore string require before the platform tries to resolve the require path
109+
if (auto* str = node->as<Luau::AstExprConstantString>())
110+
{
111+
auto module = this->findStringModule(std::string(str->value.data, str->value.size));
112+
if (module.has_value())
113+
{
114+
Luau::ModuleName virtualPath = getVirtualPathFromSourceNode(module.value());
115+
return Luau::ModuleInfo{virtualPath};
116+
}
117+
}
118+
#endif
119+
107120
if (auto parentResult = LSPPlatform::resolveModule(context, node))
108121
return parentResult;
109122

0 commit comments

Comments
 (0)