// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/ToString.h" #include "Luau/Constraint.h" #include "Luau/Location.h" #include "Luau/Scope.h" #include "Luau/TxnLog.h" #include "Luau/TypeInfer.h" #include "Luau/TypePack.h" #include "Luau/Type.h" #include "Luau/TypeFamily.h" #include "Luau/VisitType.h" #include #include #include LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAGVARIABLE(LuauToStringPrettifyLocation, false) /* * Enables increasing levels of verbosity for Luau type names when stringifying. * After level 2, test cases will break unpredictably because a pointer to their * scope will be included in the stringification of generic and free types. * * Supported values: * * 0: Disabled, no changes. * * 1: Prefix free/generic types with free- and gen-, respectively. Also reveal * hidden variadic tails. * * 2: Suffix free/generic types with their scope depth. * * 3: Suffix free/generic types with their scope pointer, if present. */ LUAU_FASTINTVARIABLE(DebugLuauVerboseTypeNames, 0) LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort, false) namespace Luau { namespace { struct FindCyclicTypes final : TypeVisitor { FindCyclicTypes() = default; FindCyclicTypes(const FindCyclicTypes&) = delete; FindCyclicTypes& operator=(const FindCyclicTypes&) = delete; bool exhaustive = false; std::unordered_set visited; std::unordered_set visitedPacks; std::set cycles; std::set cycleTPs; void cycle(TypeId ty) override { cycles.insert(ty); } void cycle(TypePackId tp) override { cycleTPs.insert(tp); } bool visit(TypeId ty) override { return visited.insert(ty).second; } bool visit(TypePackId tp) override { return visitedPacks.insert(tp).second; } bool visit(TypeId ty, const TableType& ttv) override { if (!visited.insert(ty).second) return false; if (ttv.name || ttv.syntheticName) { for (TypeId itp : ttv.instantiatedTypeParams) traverse(itp); for (TypePackId itp : ttv.instantiatedTypePackParams) traverse(itp); return exhaustive; } return true; } bool visit(TypeId ty, const ClassType&) override { return false; } bool visit(TypeId, const PendingExpansionType&) override { return false; } }; template void findCyclicTypes(std::set& cycles, std::set& cycleTPs, TID ty, bool exhaustive) { FindCyclicTypes fct; fct.exhaustive = exhaustive; fct.traverse(ty); cycles = std::move(fct.cycles); cycleTPs = std::move(fct.cycleTPs); } } // namespace static std::pair> canUseTypeNameInScope(ScopePtr scope, const std::string& name) { for (ScopePtr curr = scope; curr; curr = curr->parent) { for (const auto& [importName, nameTable] : curr->importedTypeBindings) { if (nameTable.count(name)) return {true, importName}; } if (curr->exportedTypeBindings.count(name)) return {true, std::nullopt}; } return {false, std::nullopt}; } struct StringifierState { ToStringOptions& opts; ToStringResult& result; std::unordered_map cycleNames; std::unordered_map cycleTpNames; std::unordered_set seen; std::unordered_set usedNames; size_t indentation = 0; bool exhaustive; StringifierState(ToStringOptions& opts, ToStringResult& result) : opts(opts) , result(result) , exhaustive(opts.exhaustive) { for (const auto& [_, v] : opts.nameMap.types) usedNames.insert(v); for (const auto& [_, v] : opts.nameMap.typePacks) usedNames.insert(v); } bool hasSeen(const void* tv) { void* ttv = const_cast(tv); if (seen.find(ttv) != seen.end()) return true; seen.insert(ttv); return false; } void unsee(const void* tv) { void* ttv = const_cast(tv); auto iter = seen.find(ttv); if (iter != seen.end()) seen.erase(iter); } std::string getName(TypeId ty) { const size_t s = opts.nameMap.types.size(); std::string& n = opts.nameMap.types[ty]; if (!n.empty()) return n; for (int count = 0; count < 256; ++count) { std::string candidate = generateName(usedNames.size() + count); if (!usedNames.count(candidate)) { usedNames.insert(candidate); n = candidate; return candidate; } } return generateName(s); } int previousNameIndex = 0; std::string getName(TypePackId ty) { const size_t s = opts.nameMap.typePacks.size(); std::string& n = opts.nameMap.typePacks[ty]; if (!n.empty()) return n; for (int count = 0; count < 256; ++count) { std::string candidate = generateName(previousNameIndex + count); if (!usedNames.count(candidate)) { previousNameIndex += count; usedNames.insert(candidate); n = candidate; return candidate; } } return generateName(s); } void emit(const std::string& s) { if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength) return; result.name += s; } void emitLevel(Scope* scope) { size_t count = 0; for (Scope* s = scope; s; s = s->parent.get()) ++count; emit(count); if (FInt::DebugLuauVerboseTypeNames >= 3) { emit("-"); char buffer[16]; uint32_t s = uint32_t(intptr_t(scope) & 0xFFFFFF); snprintf(buffer, sizeof(buffer), "0x%x", s); emit(buffer); } } void emit(TypeLevel level) { emit(std::to_string(level.level)); emit("-"); emit(std::to_string(level.subLevel)); } void emit(const char* s) { if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength) return; result.name += s; } void emit(int i) { emit(std::to_string(i).c_str()); } void emit(size_t i) { emit(std::to_string(i).c_str()); } void indent() { indentation += 4; } void dedent() { indentation -= 4; } void newline() { if (!opts.useLineBreaks) return emit(" "); emit("\n"); emitIndentation(); } private: void emitIndentation() { if (!opts.useLineBreaks) return; emit(std::string(indentation, ' ')); } }; struct TypeStringifier { StringifierState& state; explicit TypeStringifier(StringifierState& state) : state(state) { } void stringify(TypeId tv) { if (state.opts.maxTypeLength > 0 && state.result.name.length() > state.opts.maxTypeLength) return; if (tv->ty.valueless_by_exception()) { state.result.error = true; state.emit("* VALUELESS BY EXCEPTION *"); return; } auto it = state.cycleNames.find(tv); if (it != state.cycleNames.end()) { state.emit(it->second); return; } Luau::visit( [this, tv](auto&& t) { return (*this)(tv, t); }, tv->ty); } void stringify(const std::string& name, const Property& prop) { if (isIdentifier(name)) state.emit(name); else { state.emit("[\""); state.emit(escape(name)); state.emit("\"]"); } state.emit(": "); if (FFlag::DebugLuauReadWriteProperties) { // We special case the stringification if the property's read and write types are shared. if (prop.isShared()) return stringify(*prop.readType()); // Otherwise emit them separately. if (auto ty = prop.readType()) { state.emit("read "); stringify(*ty); } if (prop.readType() && prop.writeType()) state.emit(" + "); if (auto ty = prop.writeType()) { state.emit("write "); stringify(*ty); } } else stringify(prop.type()); } void stringify(TypePackId tp); void stringify(TypePackId tpid, const std::vector>& names); void stringify(const std::vector& types, const std::vector& typePacks) { if (types.size() == 0 && typePacks.size() == 0) return; if (types.size() || typePacks.size()) state.emit("<"); bool first = true; for (TypeId ty : types) { if (!first) state.emit(", "); first = false; stringify(ty); } bool singleTp = typePacks.size() == 1; for (TypePackId tp : typePacks) { if (isEmpty(tp) && singleTp) continue; if (!first) state.emit(", "); else first = false; bool wrap = !singleTp && get(follow(tp)); if (wrap) state.emit("("); stringify(tp); if (wrap) state.emit(")"); } if (types.size() || typePacks.size()) state.emit(">"); } void operator()(TypeId ty, const FreeType& ftv) { state.result.invalid = true; if (FInt::DebugLuauVerboseTypeNames >= 1) state.emit("free-"); state.emit(state.getName(ty)); if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); if (FFlag::DebugLuauDeferredConstraintResolution) state.emitLevel(ftv.scope); else state.emit(ftv.level); } } void operator()(TypeId, const BoundType& btv) { stringify(btv.boundTo); } void operator()(TypeId ty, const GenericType& gtv) { if (FInt::DebugLuauVerboseTypeNames >= 1) state.emit("gen-"); if (gtv.explicitName) { state.usedNames.insert(gtv.name); state.opts.nameMap.types[ty] = gtv.name; state.emit(gtv.name); } else state.emit(state.getName(ty)); if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); if (FFlag::DebugLuauDeferredConstraintResolution) state.emitLevel(gtv.scope); else state.emit(gtv.level); } } void operator()(TypeId, const BlockedType& btv) { state.emit("*blocked-"); state.emit(btv.index); state.emit("*"); } void operator()(TypeId ty, const PendingExpansionType& petv) { state.emit("*pending-expansion-"); state.emit(petv.index); state.emit("*"); } void operator()(TypeId, const PrimitiveType& ptv) { switch (ptv.type) { case PrimitiveType::NilType: state.emit("nil"); return; case PrimitiveType::Boolean: state.emit("boolean"); return; case PrimitiveType::Number: state.emit("number"); return; case PrimitiveType::String: state.emit("string"); return; case PrimitiveType::Thread: state.emit("thread"); return; case PrimitiveType::Function: state.emit("function"); return; case PrimitiveType::Table: state.emit("table"); return; default: LUAU_ASSERT(!"Unknown primitive type"); throw InternalCompilerError("Unknown primitive type " + std::to_string(ptv.type)); } } void operator()(TypeId, const SingletonType& stv) { if (const BooleanSingleton* bs = Luau::get(&stv)) state.emit(bs->value ? "true" : "false"); else if (const StringSingleton* ss = Luau::get(&stv)) { state.emit("\""); state.emit(escape(ss->value)); state.emit("\""); } else { LUAU_ASSERT(!"Unknown singleton type"); throw InternalCompilerError("Unknown singleton type"); } } void operator()(TypeId, const FunctionType& ftv) { if (state.hasSeen(&ftv)) { state.result.cycle = true; state.emit("*CYCLE*"); return; } // We should not be respecting opts.hideNamedFunctionTypeParameters here. if (ftv.generics.size() > 0 || ftv.genericPacks.size() > 0) { state.emit("<"); bool comma = false; for (auto it = ftv.generics.begin(); it != ftv.generics.end(); ++it) { if (comma) state.emit(", "); comma = true; stringify(*it); } for (auto it = ftv.genericPacks.begin(); it != ftv.genericPacks.end(); ++it) { if (comma) state.emit(", "); comma = true; stringify(*it); } state.emit(">"); } state.emit("("); if (state.opts.functionTypeArguments) stringify(ftv.argTypes, ftv.argNames); else stringify(ftv.argTypes); state.emit(") -> "); bool plural = true; auto retBegin = begin(ftv.retTypes); auto retEnd = end(ftv.retTypes); if (retBegin != retEnd) { ++retBegin; if (retBegin == retEnd && !retBegin.tail()) plural = false; } if (plural) state.emit("("); stringify(ftv.retTypes); if (plural) state.emit(")"); state.unsee(&ftv); } void operator()(TypeId, const TableType& ttv) { if (ttv.boundTo) return stringify(*ttv.boundTo); if (!state.exhaustive) { if (ttv.name) { // If scope if provided, add module name and check visibility if (state.opts.scope) { auto [success, moduleName] = canUseTypeNameInScope(state.opts.scope, *ttv.name); if (!success) state.result.invalid = true; if (moduleName) { state.emit(*moduleName); state.emit("."); } } state.emit(*ttv.name); stringify(ttv.instantiatedTypeParams, ttv.instantiatedTypePackParams); return; } if (ttv.syntheticName) { state.result.invalid = true; state.emit(*ttv.syntheticName); stringify(ttv.instantiatedTypeParams, ttv.instantiatedTypePackParams); return; } } if (state.hasSeen(&ttv)) { state.result.cycle = true; state.emit("*CYCLE*"); return; } std::string openbrace = "@@@"; std::string closedbrace = "@@@?!"; switch (state.opts.hideTableKind ? TableState::Unsealed : ttv.state) { case TableState::Sealed: state.result.invalid = true; openbrace = "{|"; closedbrace = "|}"; break; case TableState::Unsealed: openbrace = "{"; closedbrace = "}"; break; case TableState::Free: state.result.invalid = true; openbrace = "{-"; closedbrace = "-}"; break; case TableState::Generic: state.result.invalid = true; openbrace = "{+"; closedbrace = "+}"; break; } // If this appears to be an array, we want to stringify it using the {T} syntax. if (ttv.indexer && ttv.props.empty() && isNumber(ttv.indexer->indexType)) { state.emit("{"); stringify(ttv.indexer->indexResultType); state.emit("}"); state.unsee(&ttv); return; } state.emit(openbrace); state.indent(); bool comma = false; if (ttv.indexer) { state.newline(); state.emit("["); stringify(ttv.indexer->indexType); state.emit("]: "); stringify(ttv.indexer->indexResultType); comma = true; } size_t index = 0; size_t oldLength = state.result.name.length(); for (const auto& [name, prop] : ttv.props) { if (comma) { state.emit(","); state.newline(); } else state.newline(); size_t length = state.result.name.length() - oldLength; if (state.opts.maxTableLength > 0 && (length - 2 * index) >= state.opts.maxTableLength) { state.emit("... "); state.emit(std::to_string(ttv.props.size() - index)); state.emit(" more ..."); break; } stringify(name, prop); comma = true; ++index; } state.dedent(); if (comma) state.newline(); else state.emit(" "); state.emit(closedbrace); state.unsee(&ttv); } void operator()(TypeId, const MetatableType& mtv) { state.result.invalid = true; if (!state.exhaustive && mtv.syntheticName) { state.emit(*mtv.syntheticName); return; } state.emit("{ @metatable "); stringify(mtv.metatable); state.emit(","); state.newline(); stringify(mtv.table); state.emit(" }"); } void operator()(TypeId, const ClassType& ctv) { state.emit(ctv.name); } void operator()(TypeId, const AnyType&) { state.emit("any"); } void operator()(TypeId, const UnionType& uv) { if (state.hasSeen(&uv)) { state.result.cycle = true; state.emit("*CYCLE*"); return; } bool optional = false; bool hasNonNilDisjunct = false; std::vector results = {}; for (auto el : &uv) { el = follow(el); if (isNil(el)) { optional = true; continue; } else { hasNonNilDisjunct = true; } std::string saved = std::move(state.result.name); bool needParens = !state.cycleNames.count(el) && (get(el) || get(el)); if (needParens) state.emit("("); stringify(el); if (needParens) state.emit(")"); results.push_back(std::move(state.result.name)); state.result.name = std::move(saved); } state.unsee(&uv); if (!FFlag::DebugLuauToStringNoLexicalSort) std::sort(results.begin(), results.end()); if (optional && results.size() > 1) state.emit("("); bool first = true; for (std::string& ss : results) { if (!first) { state.newline(); state.emit("| "); } state.emit(ss); first = false; } if (optional) { const char* s = "?"; if (results.size() > 1) s = ")?"; if (!hasNonNilDisjunct) s = "nil"; state.emit(s); } } void operator()(TypeId, const IntersectionType& uv) { if (state.hasSeen(&uv)) { state.result.cycle = true; state.emit("*CYCLE*"); return; } std::vector results = {}; for (auto el : uv.parts) { el = follow(el); std::string saved = std::move(state.result.name); bool needParens = !state.cycleNames.count(el) && (get(el) || get(el)); if (needParens) state.emit("("); stringify(el); if (needParens) state.emit(")"); results.push_back(std::move(state.result.name)); state.result.name = std::move(saved); } state.unsee(&uv); if (!FFlag::DebugLuauToStringNoLexicalSort) std::sort(results.begin(), results.end()); bool first = true; for (std::string& ss : results) { if (!first) { state.newline(); state.emit("& "); } state.emit(ss); first = false; } } void operator()(TypeId, const ErrorType& tv) { state.result.error = true; state.emit("*error-type*"); } void operator()(TypeId, const LazyType& ltv) { if (TypeId unwrapped = ltv.unwrapped.load()) { stringify(unwrapped); } else { state.result.invalid = true; state.emit("lazy?"); } } void operator()(TypeId, const UnknownType& ttv) { state.emit("unknown"); } void operator()(TypeId, const NeverType& ttv) { state.emit("never"); } void operator()(TypeId, const NegationType& ntv) { state.emit("~"); // The precedence of `~` should be less than `|` and `&`. TypeId followed = follow(ntv.ty); bool parens = get(followed) || get(followed); if (parens) state.emit("("); stringify(ntv.ty); if (parens) state.emit(")"); } void operator()(TypeId, const TypeFamilyInstanceType& tfitv) { state.emit(tfitv.family->name); state.emit("<"); bool comma = false; for (TypeId ty : tfitv.typeArguments) { if (comma) state.emit(", "); comma = true; stringify(ty); } for (TypePackId tp : tfitv.packArguments) { if (comma) state.emit(", "); comma = true; stringify(tp); } state.emit(">"); } }; struct TypePackStringifier { StringifierState& state; const std::vector> elemNames; static inline const std::vector> dummyElemNames = {}; unsigned elemIndex = 0; explicit TypePackStringifier(StringifierState& state, const std::vector>& elemNames) : state(state) , elemNames(elemNames) { } explicit TypePackStringifier(StringifierState& state) : state(state) , elemNames(dummyElemNames) { } void stringify(TypeId tv) { TypeStringifier tvs{state}; tvs.stringify(tv); } void stringify(TypePackId tp) { if (state.opts.maxTypeLength > 0 && state.result.name.length() > state.opts.maxTypeLength) return; if (tp->ty.valueless_by_exception()) { state.result.error = true; state.emit("* VALUELESS TP BY EXCEPTION *"); return; } auto it = state.cycleTpNames.find(tp); if (it != state.cycleTpNames.end()) { state.emit(it->second); return; } Luau::visit( [this, tp](auto&& t) { return (*this)(tp, t); }, tp->ty); } void operator()(TypePackId, const TypePack& tp) { if (state.hasSeen(&tp)) { state.result.cycle = true; state.emit("*CYCLETP*"); return; } bool first = true; for (const auto& typeId : tp.head) { if (first) first = false; else state.emit(", "); // Do not respect opts.namedFunctionOverrideArgNames here if (elemIndex < elemNames.size() && elemNames[elemIndex]) { state.emit(elemNames[elemIndex]->name); state.emit(": "); } elemIndex++; stringify(typeId); } if (tp.tail && !isEmpty(*tp.tail)) { TypePackId tail = follow(*tp.tail); if (auto vtp = get(tail); !vtp || (FInt::DebugLuauVerboseTypeNames < 1 && !vtp->hidden)) { if (first) first = false; else state.emit(", "); stringify(tail); } } state.unsee(&tp); } void operator()(TypePackId, const Unifiable::Error& error) { state.result.error = true; state.emit("*error-type*"); } void operator()(TypePackId, const VariadicTypePack& pack) { state.emit("..."); if (FInt::DebugLuauVerboseTypeNames >= 1 && pack.hidden) { state.emit("*hidden*"); } stringify(pack.ty); } void operator()(TypePackId tp, const GenericTypePack& pack) { if (FInt::DebugLuauVerboseTypeNames >= 1) state.emit("gen-"); if (pack.explicitName) { state.usedNames.insert(pack.name); state.opts.nameMap.typePacks[tp] = pack.name; state.emit(pack.name); } else { state.emit(state.getName(tp)); } if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); if (FFlag::DebugLuauDeferredConstraintResolution) state.emitLevel(pack.scope); else state.emit(pack.level); } state.emit("..."); } void operator()(TypePackId tp, const FreeTypePack& pack) { state.result.invalid = true; if (FInt::DebugLuauVerboseTypeNames >= 1) state.emit("free-"); state.emit(state.getName(tp)); if (FInt::DebugLuauVerboseTypeNames >= 2) { state.emit("-"); if (FFlag::DebugLuauDeferredConstraintResolution) state.emitLevel(pack.scope); else state.emit(pack.level); } state.emit("..."); } void operator()(TypePackId, const BoundTypePack& btv) { stringify(btv.boundTo); } void operator()(TypePackId, const BlockedTypePack& btp) { state.emit("*blocked-tp-"); state.emit(btp.index); state.emit("*"); } void operator()(TypePackId, const TypeFamilyInstanceTypePack& tfitp) { state.emit(tfitp.family->name); state.emit("<"); bool comma = false; for (TypeId p : tfitp.typeArguments) { if (comma) state.emit(", "); comma = true; stringify(p); } for (TypePackId p : tfitp.packArguments) { if (comma) state.emit(", "); comma = true; stringify(p); } state.emit(">"); } }; void TypeStringifier::stringify(TypePackId tp) { TypePackStringifier tps(state); tps.stringify(tp); } void TypeStringifier::stringify(TypePackId tpid, const std::vector>& names) { TypePackStringifier tps(state, names); tps.stringify(tpid); } static void assignCycleNames(const std::set& cycles, const std::set& cycleTPs, std::unordered_map& cycleNames, std::unordered_map& cycleTpNames, bool exhaustive) { int nextIndex = 1; for (TypeId cycleTy : cycles) { std::string name; // TODO: use the stringified type list if there are no cycles if (auto ttv = get(follow(cycleTy)); !exhaustive && ttv && (ttv->syntheticName || ttv->name)) { // If we have a cycle type in type parameters, assign a cycle name for this named table if (std::find_if(ttv->instantiatedTypeParams.begin(), ttv->instantiatedTypeParams.end(), [&](auto&& el) { return cycles.count(follow(el)); }) != ttv->instantiatedTypeParams.end()) cycleNames[cycleTy] = ttv->name ? *ttv->name : *ttv->syntheticName; continue; } name = "t" + std::to_string(nextIndex); ++nextIndex; cycleNames[cycleTy] = std::move(name); } for (TypePackId tp : cycleTPs) { std::string name = "tp" + std::to_string(nextIndex); ++nextIndex; cycleTpNames[tp] = std::move(name); } } ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts) { /* * 1. Walk the Type and track seen TypeIds. When you reencounter a TypeId, add it to a set of seen cycles. * 2. Generate some names for each cycle. For a starting point, we can just call them t0, t1 and so on. * 3. For each seen cycle, stringify it like we do now, but replace each known cycle with its name. * 4. Print out the root of the type using the same algorithm as step 3. */ ty = follow(ty); ToStringResult result; StringifierState state{opts, result}; std::set cycles; std::set cycleTPs; findCyclicTypes(cycles, cycleTPs, ty, opts.exhaustive); assignCycleNames(cycles, cycleTPs, state.cycleNames, state.cycleTpNames, opts.exhaustive); TypeStringifier tvs{state}; if (!opts.exhaustive) { if (auto ttv = get(ty); ttv && (ttv->name || ttv->syntheticName)) { if (ttv->syntheticName) result.invalid = true; // If scope if provided, add module name and check visibility if (ttv->name && opts.scope) { auto [success, moduleName] = canUseTypeNameInScope(opts.scope, *ttv->name); if (!success) result.invalid = true; if (moduleName) result.name = format("%s.", moduleName->c_str()); } result.name += ttv->name ? *ttv->name : *ttv->syntheticName; tvs.stringify(ttv->instantiatedTypeParams, ttv->instantiatedTypePackParams); return result; } else if (auto mtv = get(ty); mtv && mtv->syntheticName) { result.invalid = true; result.name = *mtv->syntheticName; return result; } } /* If the root itself is a cycle, we special case a little. * We go out of our way to print the following: * * t1 where t1 = the_whole_root_type */ auto it = state.cycleNames.find(ty); if (it != state.cycleNames.end()) state.emit(it->second); else tvs.stringify(ty); if (!state.cycleNames.empty() || !state.cycleTpNames.empty()) { result.cycle = true; state.emit(" where "); } state.exhaustive = true; std::vector> sortedCycleNames{state.cycleNames.begin(), state.cycleNames.end()}; std::sort(sortedCycleNames.begin(), sortedCycleNames.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); bool semi = false; for (const auto& [cycleTy, name] : sortedCycleNames) { if (semi) state.emit(" ; "); state.emit(name); state.emit(" = "); Luau::visit( [&tvs, cycleTy = cycleTy](auto&& t) { return tvs(cycleTy, t); }, cycleTy->ty); semi = true; } std::vector> sortedCycleTpNames(state.cycleTpNames.begin(), state.cycleTpNames.end()); std::sort(sortedCycleTpNames.begin(), sortedCycleTpNames.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); TypePackStringifier tps{state}; for (const auto& [cycleTp, name] : sortedCycleTpNames) { if (semi) state.emit(" ; "); state.emit(name); state.emit(" = "); Luau::visit( [&tps, cycleTy = cycleTp](auto&& t) { return tps(cycleTy, t); }, cycleTp->ty); semi = true; } if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength) { result.truncated = true; result.name += "... *TRUNCATED*"; } return result; } ToStringResult toStringDetailed(TypePackId tp, ToStringOptions& opts) { /* * 1. Walk the Type and track seen TypeIds. When you reencounter a TypeId, add it to a set of seen cycles. * 2. Generate some names for each cycle. For a starting point, we can just call them t0, t1 and so on. * 3. For each seen cycle, stringify it like we do now, but replace each known cycle with its name. * 4. Print out the root of the type using the same algorithm as step 3. */ ToStringResult result; StringifierState state{opts, result}; std::set cycles; std::set cycleTPs; findCyclicTypes(cycles, cycleTPs, tp, opts.exhaustive); assignCycleNames(cycles, cycleTPs, state.cycleNames, state.cycleTpNames, opts.exhaustive); TypeStringifier tvs{state}; /* If the root itself is a cycle, we special case a little. * We go out of our way to print the following: * * t1 where t1 = the_whole_root_type */ auto it = state.cycleTpNames.find(tp); if (it != state.cycleTpNames.end()) state.emit(it->second); else tvs.stringify(tp); if (!cycles.empty()) { result.cycle = true; state.emit(" where "); } state.exhaustive = true; std::vector> sortedCycleNames{state.cycleNames.begin(), state.cycleNames.end()}; std::sort(sortedCycleNames.begin(), sortedCycleNames.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); bool semi = false; for (const auto& [cycleTy, name] : sortedCycleNames) { if (semi) state.emit(" ; "); state.emit(name); state.emit(" = "); Luau::visit( [&tvs, cycleTy = cycleTy](auto t) { return tvs(cycleTy, t); }, cycleTy->ty); semi = true; } if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength) { result.name += "... *TRUNCATED*"; } return result; } std::string toString(TypeId ty, ToStringOptions& opts) { return toStringDetailed(ty, opts).name; } std::string toString(TypePackId tp, ToStringOptions& opts) { return toStringDetailed(tp, opts).name; } std::string toString(const Type& tv, ToStringOptions& opts) { return toString(const_cast(&tv), opts); } std::string toString(const TypePackVar& tp, ToStringOptions& opts) { return toString(const_cast(&tp), opts); } std::string toStringNamedFunction(const std::string& funcName, const FunctionType& ftv, ToStringOptions& opts) { ToStringResult result; StringifierState state{opts, result}; TypeStringifier tvs{state}; state.emit(funcName); if (!opts.hideNamedFunctionTypeParameters) tvs.stringify(ftv.generics, ftv.genericPacks); state.emit("("); auto argPackIter = begin(ftv.argTypes); bool first = true; size_t idx = 0; while (argPackIter != end(ftv.argTypes)) { // ftv takes a self parameter as the first argument, skip it if specified in option if (idx == 0 && ftv.hasSelf && opts.hideFunctionSelfArgument) { ++argPackIter; ++idx; continue; } if (!first) state.emit(", "); first = false; // We don't respect opts.functionTypeArguments if (idx < opts.namedFunctionOverrideArgNames.size()) { state.emit(opts.namedFunctionOverrideArgNames[idx] + ": "); } else if (idx < ftv.argNames.size() && ftv.argNames[idx]) { state.emit(ftv.argNames[idx]->name + ": "); } else { state.emit("_: "); } tvs.stringify(*argPackIter); ++argPackIter; ++idx; } if (argPackIter.tail()) { if (auto vtp = get(*argPackIter.tail()); !vtp || !vtp->hidden) { if (!first) state.emit(", "); state.emit("...: "); if (vtp) tvs.stringify(vtp->ty); else tvs.stringify(*argPackIter.tail()); } } state.emit("): "); size_t retSize = size(ftv.retTypes); bool hasTail = !finite(ftv.retTypes); bool wrap = get(follow(ftv.retTypes)) && (hasTail ? retSize != 0 : retSize != 1); if (wrap) state.emit("("); tvs.stringify(ftv.retTypes); if (wrap) state.emit(")"); return result.name; } static ToStringOptions& dumpOptions() { static ToStringOptions opts = ([]() { ToStringOptions o; o.exhaustive = true; o.functionTypeArguments = true; o.maxTableLength = 0; o.maxTypeLength = 0; return o; })(); return opts; } std::string dump(TypeId ty) { std::string s = toString(ty, dumpOptions()); printf("%s\n", s.c_str()); return s; } std::string dump(const std::optional& ty) { if (ty) return dump(*ty); printf("nullopt\n"); return "nullopt"; } std::string dump(TypePackId ty) { std::string s = toString(ty, dumpOptions()); printf("%s\n", s.c_str()); return s; } std::string dump(const std::optional& ty) { if (ty) return dump(*ty); printf("nullopt\n"); return "nullopt"; } std::string dump(const ScopePtr& scope, const char* name) { auto binding = scope->linearSearchForBinding(name); if (!binding) { printf("No binding %s\n", name); return {}; } TypeId ty = binding->typeId; std::string s = toString(ty, dumpOptions()); printf("%s\n", s.c_str()); return s; } std::string generateName(size_t i) { std::string n; n = char('a' + i % 26); if (i >= 26) n += std::to_string(i / 26); return n; } std::string toString(const Constraint& constraint, ToStringOptions& opts) { auto go = [&opts](auto&& c) -> std::string { using T = std::decay_t; auto tos = [&opts](auto&& a) { return toString(a, opts); }; if constexpr (std::is_same_v) { std::string subStr = tos(c.subType); std::string superStr = tos(c.superType); return subStr + " <: " + superStr; } else if constexpr (std::is_same_v) { std::string subStr = tos(c.subPack); std::string superStr = tos(c.superPack); return subStr + " <: " + superStr; } else if constexpr (std::is_same_v) { std::string subStr = tos(c.generalizedType); std::string superStr = tos(c.sourceType); return subStr + " ~ gen " + superStr; } else if constexpr (std::is_same_v) { std::string subStr = tos(c.subType); std::string superStr = tos(c.superType); return subStr + " ~ inst " + superStr; } else if constexpr (std::is_same_v) { std::string resultStr = tos(c.resultType); std::string operandStr = tos(c.operandType); return resultStr + " ~ Unary<" + toString(c.op) + ", " + operandStr + ">"; } else if constexpr (std::is_same_v) { std::string resultStr = tos(c.resultType); std::string leftStr = tos(c.leftType); std::string rightStr = tos(c.rightType); return resultStr + " ~ Binary<" + toString(c.op) + ", " + leftStr + ", " + rightStr + ">"; } else if constexpr (std::is_same_v) { std::string iteratorStr = tos(c.iterator); std::string variableStr = tos(c.variables); return variableStr + " ~ Iterate<" + iteratorStr + ">"; } else if constexpr (std::is_same_v) { std::string namedStr = tos(c.namedType); return "@name(" + namedStr + ") = " + c.name; } else if constexpr (std::is_same_v) { std::string targetStr = tos(c.target); return "expand " + targetStr; } else if constexpr (std::is_same_v) { return "call " + tos(c.fn) + "( " + tos(c.argsPack) + " )" + " with { result = " + tos(c.result) + " }"; } else if constexpr (std::is_same_v) { return tos(c.resultType) + " ~ prim " + tos(c.expectedType) + ", " + tos(c.singletonType) + ", " + tos(c.multitonType); } else if constexpr (std::is_same_v) { return tos(c.resultType) + " ~ hasProp " + tos(c.subjectType) + ", \"" + c.prop + "\""; } else if constexpr (std::is_same_v) { const std::string pathStr = c.path.size() == 1 ? "\"" + c.path[0] + "\"" : "[\"" + join(c.path, "\", \"") + "\"]"; return tos(c.resultType) + " ~ setProp " + tos(c.subjectType) + ", " + pathStr + " " + tos(c.propType); } else if constexpr (std::is_same_v) { return tos(c.resultType) + " ~ setIndexer " + tos(c.subjectType) + " [ " + tos(c.indexType) + " ] " + tos(c.propType); } else if constexpr (std::is_same_v) { std::string result = tos(c.resultType); std::string discriminant = tos(c.discriminantType); if (c.negated) return result + " ~ if isSingleton D then ~D else unknown where D = " + discriminant; else return result + " ~ if isSingleton D then D else unknown where D = " + discriminant; } else if constexpr (std::is_same_v) return tos(c.resultPack) + " ~ unpack " + tos(c.sourcePack); else if constexpr (std::is_same_v) { const char* op = c.mode == RefineConstraint::Union ? "union" : "intersect"; return tos(c.resultType) + " ~ refine " + tos(c.type) + " " + op + " " + tos(c.discriminant); } else if constexpr (std::is_same_v) return "reduce " + tos(c.ty); else if constexpr (std::is_same_v) { return "reduce " + tos(c.tp); } else static_assert(always_false_v, "Non-exhaustive constraint switch"); }; return visit(go, constraint.c); } std::string toString(const Constraint& constraint) { return toString(constraint, ToStringOptions{}); } std::string dump(const Constraint& c) { ToStringOptions opts; opts.exhaustive = true; opts.functionTypeArguments = true; std::string s = toString(c, opts); printf("%s\n", s.c_str()); return s; } std::optional getFunctionNameAsString(const AstExpr& expr) { const AstExpr* curr = &expr; std::string s; for (;;) { if (auto local = curr->as()) return local->local->name.value + s; if (auto global = curr->as()) return global->name.value + s; if (auto indexname = curr->as()) { curr = indexname->expr; s = "." + std::string(indexname->index.value) + s; } else if (auto group = curr->as()) { curr = group->expr; } else { return std::nullopt; } } return s; } std::string toString(const Position& position) { return "{ line = " + std::to_string(position.line) + ", col = " + std::to_string(position.column) + " }"; } std::string toString(const Location& location, int offset, bool useBegin) { if (FFlag::LuauToStringPrettifyLocation) { return "(" + std::to_string(location.begin.line + offset) + ", " + std::to_string(location.begin.column + offset) + ") - (" + std::to_string(location.end.line + offset) + ", " + std::to_string(location.end.column + offset) + ")"; } else { return "Location { " + toString(location.begin) + ", " + toString(location.end) + " }"; } } } // namespace Luau