Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions indra/alscript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ target_sources(
luau/alluaufrontend.cpp
luau/alluaunavigation.cpp
luau/alluauservice.cpp
luau/alluaushadowed.cpp
luau/alluausharedstart.cpp
luau/alluautaskpool.cpp
luau/alluautypes.cpp
Expand Down Expand Up @@ -153,6 +154,7 @@ target_sources(
luau/alluaufrontend.h
luau/alluaunavigation.h
luau/alluauservice.h
luau/alluaushadowed.h
luau/alluausharedstart.h
luau/alluautaskpool.h
luau/alluautypes.h
Expand Down
8 changes: 7 additions & 1 deletion indra/alscript/luau/alluaucompletion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#include "alluaufragment.h"
#include "alluaufrontend.h"
#include "alluaushadowed.h"
#include "alluautypes.h"
#include "alscriptlexicon.h"

Expand Down Expand Up @@ -340,7 +341,12 @@ std::vector<ALScriptCompletion> ALLuauCompletion::complete(std::string_view sour
{
return {};
}
return answer(Luau::autocomplete(*mFront.frontend, mFront.moduleName, at, strings), *module, at);
Luau::AutocompleteResult found;
{
const ALLuauShadowed shadowed(*module, at);
found = Luau::autocomplete(*mFront.frontend, mFront.moduleName, at, strings);
}
return answer(found, *module, at);
}

std::vector<ALScriptCompletion> ALLuauCompletion::answer(const Luau::AutocompleteResult& found, const Luau::Module& module, Luau::Position at) const
Expand Down
14 changes: 12 additions & 2 deletions indra/alscript/luau/alluaufragment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "alluaufragment.h"

#include "alluaufrontend.h"
#include "alluaushadowed.h"
#include "alscriptlexicon.h"

#include "Luau/Allocator.h"
Expand Down Expand Up @@ -350,7 +351,8 @@ ALLuauFragment::Completion ALLuauFragment::complete(std::string_view source, Lua
Luau::FragmentAutocompleteStatusResult made{ Luau::FragmentAutocompleteStatus::Success, std::nullopt };
bool timedOut = false;
{
const Restored restored(*mFront.frontend, mFront.moduleName, *mBase);
const Restored restored(*mFront.frontend, mFront.moduleName, *mBase);
const ALLuauShadowed shadowed(*mBase, at);
made = Luau::tryFragmentAutocomplete(*mFront.frontend, mFront.moduleName, at, context, std::move(callback));
timedOut = restored.timedOut();
}
Expand Down Expand Up @@ -392,6 +394,13 @@ ALLuauFragment::Completion ALLuauFragment::complete(std::string_view source, Lua
{
return answer;
}
// A name the fragment declares again beside the last check's of it,
// both in the fragment's own scope, where Luau took whichever its table
// gave first: the whole script, whose scopes are set right first.
if (made.result->freshScope && ALLuauShadowed::ambiguous(*made.result->freshScope, at, found))
{
return answer;
}
++mFront.fragments;
answer.outcome = Outcome::Answered;
answer.found = std::move(made.result->acResults);
Expand Down Expand Up @@ -429,7 +438,8 @@ ALLuauFragment::Typed ALLuauFragment::typecheck(std::string_view source, Luau::P
std::pair<Luau::FragmentTypeCheckStatus, Luau::FragmentTypeCheckResult> made{ Luau::FragmentTypeCheckStatus::SkipAutocomplete, {} };
bool timedOut = false;
{
const Restored restored(*mFront.frontend, mFront.moduleName, *mBase);
const Restored restored(*mFront.frontend, mFront.moduleName, *mBase);
const ALLuauShadowed shadowed(*mBase, at);
try
{
// To the end of the call at the least, rather than Luau's own
Expand Down
194 changes: 194 additions & 0 deletions indra/alscript/luau/alluaushadowed.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @file alluaushadowed.cpp
* @brief Of a name a scope declares more than once, only the declaration in effect at a place, while Luau is asked about it.
*
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
* Alchemy Viewer Source Code
* Copyright (C) 2026, Rye <rye@alchemyviewer.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* $/LicenseInfo$
*/

#include "linden_common.h"

#include "alluaushadowed.h"

#include "Luau/Ast.h"
#include "Luau/AstQuery.h"
#include "Luau/Module.h"

#include <algorithm>
#include <string_view>

namespace
{
// A local a scope declares.
struct Declared
{
std::string_view name;
Luau::Symbol symbol;
Luau::Location location;
};

// Each run of one name among locals in order of their names, `each`
// given its first and last.
template<typename Each>
void eachName(const std::vector<Declared>& locals, Each each)
{
for (auto first = locals.begin(); first != locals.end();)
{
const auto last = std::find_if(first, locals.end(), [&first](const Declared& local) { return local.name != first->name; });
each(first, last);
first = last;
}
}

// The locals of a scope that another of its locals shares a name with,
// each name's together in the order they were declared. Most scopes
// declare no name twice, and have none.
void sharedNames(const Luau::Scope& scope, std::vector<Declared>& out)
{
out.clear();
if (scope.bindings.size() < 2)
{
return;
}
for (const auto& [symbol, binding] : scope.bindings)
{
if (symbol.local)
{
out.push_back({ symbol.local->name.value, symbol, binding.location });
}
}
std::sort(out.begin(), out.end(), [](const Declared& a, const Declared& b) {
return a.name != b.name ? a.name < b.name : a.location.begin < b.location.begin;
});
size_t shared = 0;
for (size_t first = 0; first < out.size();)
{
size_t last = first + 1;
while (last < out.size() && out[last].name == out[first].name)
{
++last;
}
for (size_t i = first; last - first > 1 && i < last; ++i)
{
out[shared++] = out[i];
}
first = last;
}
out.resize(shared);
}

// Whether the place `ancestry` was found at is in the statement that
// declares `local`, as Luau's autocomplete asks (isBeingDefined): the
// `local` the name is in, its value being written.
bool declaring(const Luau::AstLocal* local, const std::vector<Luau::AstNode*>& ancestry)
{
for (const Luau::AstNode* node : ancestry)
{
if (const Luau::AstStatLocal* statement = node->as<Luau::AstStatLocal>())
{
if (std::find(statement->vars.begin(), statement->vars.end(), local) != statement->vars.end())
{
return true;
}
}
}
return false;
}
}

ALLuauShadowed::ALLuauShadowed(const Luau::Module& module, Luau::Position at)
{
std::vector<Declared> shared;
std::vector<Luau::AstNode*> ancestry;
bool found = false;
for (const auto& [where, scope] : module.scopes)
{
sharedNames(*scope, shared);
if (shared.empty())
{
continue;
}
// What `at` is in, found as Luau's autocomplete finds it: only
// where a name is declared again at all.
if (!found && module.root)
{
found = true;
ancestry = Luau::findAncestryAtPositionForAutocomplete(module.root, at);
}
eachName(shared, [&](auto first, auto last) {
// The one in effect: declared before `at`, as Luau's own test
// has it (isBindingLegalAtCurrentPosition), the last of those
// but one whose statement `at` is in.
auto kept = last;
for (auto it = first; it != last; ++it)
{
if (it->location.end < at && !declaring(it->symbol.local, ancestry))
{
kept = it;
}
}
for (auto it = first; it != last; ++it)
{
if (it == kept)
{
continue;
}
// Taken out by where it is: by its key is ambiguous to
// libc++, a Symbol being constructible, if deleted, from
// anything.
const Bindings::const_iterator bound = scope->bindings.find(it->symbol);
if (bound != scope->bindings.end())
{
mAside.emplace_back(scope.get(), scope->bindings.extract(bound));
}
}
});
}
}

ALLuauShadowed::~ALLuauShadowed()
{
for (auto it = mAside.rbegin(); it != mAside.rend(); ++it)
{
it->first->bindings.insert(std::move(it->second));
}
}

// static
bool ALLuauShadowed::ambiguous(const Luau::Scope& scope, Luau::Position at, const Luau::AutocompleteResult& found)
{
// Autocomplete reads a scope's bindings by name for an expression,
// passing over a local whose statement the place is in, and for a
// statement, not.
const bool expression = found.context == Luau::AutocompleteContext::Expression;
if (!expression && found.context != Luau::AutocompleteContext::Statement)
{
return false;
}
std::vector<Declared> shared;
sharedNames(scope, shared);
bool told = false;
eachName(shared, [&](auto first, auto last) {
const auto offered = std::count_if(first, last, [&](const Declared& local) {
return local.location.end < at && !(expression && declaring(local.symbol.local, found.ancestry));
});
told = told || offered > 1;
});
return told;
}
72 changes: 72 additions & 0 deletions indra/alscript/luau/alluaushadowed.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @file alluaushadowed.h
* @brief Of a name a scope declares more than once, only the declaration in effect at a place, while Luau is asked about it.
*
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
* Alchemy Viewer Source Code
* Copyright (C) 2026, Rye <rye@alchemyviewer.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* $/LicenseInfo$
*/

#pragma once

#include "Luau/AutocompleteTypes.h"
#include "Luau/Location.h"
#include "Luau/Scope.h"

#include <utility>
#include <vector>

namespace Luau
{
struct Module;
}

// A module's scopes as Lua has them at a place, while this lives: of the
// locals a scope declares more than once under one name, only the one in
// effect there -- the last declared before it, but for one whose statement
// the place is in -- and none where none is. The others are put back as
// this goes.
//
// Luau keeps each declaration a scope makes among its bindings, a table
// ordered by where the parser put each. Where it looks a local up by its
// name -- autocomplete offering what is in scope, a fragment's check
// bringing the last check's type of each name it uses -- it takes the first
// the table gives: for the same script at the same place, one run offers
// `local function test` and the next the `local test = 1` declared after
// it. Where it looks one up by its declaration, as a check does each use,
// there is no such choice.
//
// Over a module of one thread's front end, as the questions are.
class ALLuauShadowed
{
public:
ALLuauShadowed(const Luau::Module& module, Luau::Position at);
~ALLuauShadowed();
ALLuauShadowed(const ALLuauShadowed&) = delete;
ALLuauShadowed& operator=(const ALLuauShadowed&) = delete;

// Whether what autocomplete `found` at `at` took of `scope` by the
// table's order: two of its locals of one name it would offer there.
// For a scope Luau makes and reads in the one call, which nothing can
// set right before it is read: a fragment's own.
static bool ambiguous(const Luau::Scope& scope, Luau::Position at, const Luau::AutocompleteResult& found);

private:
using Bindings = decltype(Luau::Scope::bindings);
std::vector<std::pair<Luau::Scope*, Bindings::node_type>> mAside;
};
Loading
Loading