Skip to content

Commit 12b037a

Browse files
committed
feat: Add virtual nevermore loader support
1 parent 9308d00 commit 12b037a

File tree

10 files changed

+325
-1
lines changed

10 files changed

+325
-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)
@@ -37,6 +43,7 @@ target_sources(Luau.LanguageServer PRIVATE
3743
src/JsonTomlSyntaxParser.cpp
3844
src/CliConfigurationParser.cpp
3945
src/platform/LSPPlatform.cpp
46+
src/platform/roblox/NevermoreStringRequire.cpp
4047
src/platform/roblox/RobloxCodeAction.cpp
4148
src/platform/roblox/RobloxColorProvider.cpp
4249
src/platform/roblox/RobloxCompletion.cpp

luau

Submodule luau updated from 5f42e63 to 931bd85

src/LuauExt.cpp

Lines changed: 8 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+
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;

src/WorkspaceFileResolver.cpp

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

src/include/Platform/LSPPlatform.hpp

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

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

src/include/Platform/RobloxPlatform.hpp

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

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

149+
#ifdef NEVERMORE_STRING_REQUIRE
150+
mutable std::unordered_map<std::string, SourceNodePtr> moduleNameToSourceNode{};
151+
#endif
152+
142153
std::optional<SourceNodePtr> getSourceNodeFromVirtualPath(const Luau::ModuleName& name) const;
143154
std::optional<SourceNodePtr> getSourceNodeFromRealPath(const std::string& name) const;
144155

@@ -173,6 +184,12 @@ class RobloxPlatform : public LSPPlatform
173184

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

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

178195
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
@@ -159,6 +159,18 @@ std::optional<Luau::AutocompleteEntryMap> RobloxPlatform::completionCallback(
159159
}
160160
}
161161
}
162+
#ifdef NEVERMORE_STRING_REQUIRE
163+
else if (tag == "StringRequires")
164+
{
165+
Luau::AutocompleteEntryMap result;
166+
for (const auto& pair : this->moduleNameToSourceNode)
167+
result.insert_or_assign(
168+
pair.first, Luau::AutocompleteEntry{Luau::AutocompleteEntryKind::String, workspaceFolder->frontend.builtinTypes->stringType, false,
169+
false, Luau::TypeCorrectKind::Correct});
170+
171+
return result;
172+
}
173+
#endif
162174
else if (tag == "Properties")
163175
{
164176
if (ctx && ctx.value())

src/platform/roblox/RobloxFileResolver.cpp

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

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

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

0 commit comments

Comments
 (0)