diff --git a/.gitignore b/.gitignore index 5852b330c..366cf8f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ __pycache__ .cache .clangd +compile_commands.json +flake.nix +flake.lock diff --git a/Analysis/include/Luau/ToString.h b/Analysis/include/Luau/ToString.h index 25d89de95..069bd7a07 100644 --- a/Analysis/include/Luau/ToString.h +++ b/Analysis/include/Luau/ToString.h @@ -47,6 +47,7 @@ struct ToStringOptions bool hideFunctionSelfArgument = false; // If true, `self: X` will be omitted from the function signature if the function has self bool hideTableAliasExpansions = false; // If true, all table aliases will not be expanded bool useQuestionMarks = true; // If true, use a postfix ? for options, else write them out as unions that include nil. + bool useTruthyFalsy = true; // If true, `~(false?)` and `false?` will emit `truthy` and `falsy` respectively. bool ignoreSyntheticName = false; // If true, ignore synthetic names on table types. size_t maxTableLength = size_t(FInt::LuauTableTypeMaximumStringifierLength); // Only applied to TableTypes size_t maxTypeLength = size_t(FInt::LuauTypeMaximumStringifierLength); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index fc88b6a94..5cb98fa3e 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -12,6 +12,7 @@ #include "Luau/TypePack.h" #include "Luau/Type.h" #include "Luau/TypeFunction.h" +#include "Luau/TypeUtils.h" #include "Luau/VisitType.h" #include "Luau/TypeOrPack.h" @@ -39,6 +40,7 @@ LUAU_FASTFLAG(LuauIntegerType2) */ LUAU_FASTINTVARIABLE(DebugLuauVerboseTypeNames, 0) LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort) +LUAU_FASTFLAGVARIABLE(LuauToStringTruthyFalsy) namespace Luau { @@ -884,8 +886,14 @@ struct TypeStringifier state.emit("*no-refine*"); } - void operator()(TypeId, const UnionType& uv) + void operator()(TypeId ty, const UnionType& uv) { + if (FFlag::LuauToStringTruthyFalsy && state.opts.useTruthyFalsy && isApproximatelyFalsyType(ty)) + { + state.emit("falsy"); + return; + } + if (state.hasSeen(&uv)) { state.result.cycle = true; @@ -1016,7 +1024,10 @@ struct TypeStringifier std::string saved = std::move(state.result.name); size_t savedSpansSize = state.result.typeSpans.size(); - bool needParens = !state.cycleNames.contains(el) && (get(el) != nullptr || get(el) != nullptr); + // soooo 'falsy' is technically a UnionType :( + bool isNonFalsyUnion = + get(el) != nullptr && (FFlag::LuauToStringTruthyFalsy && state.opts.useTruthyFalsy ? !isApproximatelyFalsyType(el) : true); + bool needParens = !state.cycleNames.contains(el) && (isNonFalsyUnion || get(el) != nullptr); if (needParens) state.emit("("); @@ -1115,8 +1126,14 @@ struct TypeStringifier state.emit("never"); } - void operator()(TypeId, const NegationType& ntv) + void operator()(TypeId ty, const NegationType& ntv) { + if (FFlag::LuauToStringTruthyFalsy && state.opts.useTruthyFalsy && isApproximatelyTruthyType(ty)) + { + state.emit("truthy"); + return; + } + state.emit("~"); // The precedence of `~` should be less than `|` and `&`. diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 9394028a7..f165548c3 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -14,6 +14,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauIntegerType2) LUAU_FASTFLAG(DebugLuauForceOldSolver) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) using namespace Luau; @@ -1111,8 +1112,9 @@ TEST_CASE_FIXTURE(NormalizeFixture, "truthy_table_property_and_optional_table_wi TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") { - ScopedFastFlag sff[] = { + ScopedFastFlag sffs[] = { {FFlag::DebugLuauForceOldSolver, false}, // Only because it affects the stringification of free types + {FFlag::LuauToStringTruthyFalsy, true}, }; TypeId freeTy = arena.freshType(getBuiltins(), getGlobalScope()); @@ -1125,7 +1127,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") TypeId result = typeFromNormal(*norm); - CHECK("'a & (false?)" == toString(result)); + CHECK("'a & falsy" == toString(result)); } TEST_CASE_FIXTURE(NormalizeFixture, "free_type_intersection_ordering") diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index 458fab443..49d13d1c9 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -9,6 +9,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauForceOldSolver) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) LUAU_DYNAMIC_FASTINT(LuauSimplificationComplexityLimit) namespace @@ -482,10 +483,11 @@ TEST_CASE_FIXTURE(SimplifyFixture, "union") TEST_CASE_FIXTURE(SimplifyFixture, "two_unions") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; ScopedFastInt sfi{DFInt::LuauSimplificationComplexityLimit, 10}; TypeId t1 = arena->addType(UnionType{{numberTy, booleanTy, stringTy, nilTy, tableTy}}); - CHECK("false?" == intersectStr(t1, falsyTy)); + CHECK("falsy" == intersectStr(t1, falsyTy)); } TEST_CASE_FIXTURE(SimplifyFixture, "curious_union") diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index f337b4661..5870d11da 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -13,6 +13,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauForceOldSolver) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) TEST_SUITE_BEGIN("ToString"); @@ -1062,4 +1063,69 @@ TEST_CASE_FIXTURE(Fixture, "record_type_compositions_generic") CHECK_EQ(recordedTyObject, requireTypeAlias("Object")); } +TEST_CASE_FIXTURE(BuiltinsFixture, "tostring_truthy_falsy") +{ + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, + }; + + CheckResult result = check(R"( + type function negate(t: type) + return types.negationof(t) + end + type meow = false? + type mrrp = negate + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(toString(requireTypeAlias("meow")), "falsy"); + CHECK_EQ(toString(requireTypeAlias("mrrp")), "truthy"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "tostring_truthy_falsy_expanded") +{ + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, + }; + + CheckResult result = check(R"( + type function negate(t: type) + return types.negationof(t) + end + type kya = (false | nil) | false + type purr = negate<(false | nil) | false> + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(toString(requireTypeAlias("kya")), "falsy"); + CHECK_EQ(toString(requireTypeAlias("purr")), "truthy"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "tostring_truthy_falsy_no_parenthesis") +{ + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, + }; + + ToStringOptions opts; + + CheckResult result = check(R"( + type function negate(t: type) + return types.negationof(t) + end + type hiss = unknown & (false?) + type scratch = unknown & negate + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ(toString(requireTypeAlias("hiss")), "falsy & unknown"); + CHECK_EQ(toString(requireTypeAlias("scratch")), "truthy & unknown"); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index e3f1f05fd..7fc3129cd 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -25,6 +25,7 @@ LUAU_FASTFLAG(LuauBidirectionalInferenceVariadics) LUAU_FASTFLAG(LuauConstraintGraph) LUAU_FASTFLAG(LuauBidirectionalInferenceBetterLambdaHandling) LUAU_FASTFLAG(LuauHigherOrderGenericInference) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -2888,6 +2889,7 @@ TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types") ScopedFastFlag sffs[] = { {FFlag::LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier, true}, {FFlag::LuauConstraintGraph, true}, + {FFlag::LuauToStringTruthyFalsy, true}, }; CheckResult result = check(R"( @@ -2922,7 +2924,7 @@ TEST_CASE_FIXTURE(Fixture, "unifier_should_not_bind_free_types") auto tm2 = get(result.errors[1]); REQUIRE(tm2); CHECK(toString(tm2->wantedType) == "string"); - CHECK(toString(tm2->givenType) == "unknown & ~(false?)"); + CHECK(toString(tm2->givenType) == "truthy & unknown"); } } diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 607e9944a..3daf18446 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -13,6 +13,7 @@ LUAU_FASTFLAG(LuauCheckFunctionStatementTypes) LUAU_FASTFLAG(DebugLuauForceOldSolver) LUAU_FASTFLAG(LuauPropagateFreeTypesIntoUnionAndIntersectionBounds) LUAU_FASTFLAG(LuauDropUnionSubtypeReasoning) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) TEST_SUITE_BEGIN("IntersectionTypes"); @@ -1498,7 +1499,10 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections") TEST_CASE_FIXTURE(BuiltinsFixture, "narrow_intersection_nevers") { - ScopedFastFlag sffs{FFlag::DebugLuauForceOldSolver, false}; + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, + }; loadDefinition(R"( declare extern type Player with @@ -1513,7 +1517,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "narrow_intersection_nevers") end )")); - CHECK_EQ("Player & { read Character: ~(false?) }", toString(requireTypeAtPosition({3, 23}))); + CHECK_EQ("Player & { read Character: truthy }", toString(requireTypeAtPosition({3, 23}))); } TEST_CASE_FIXTURE(BuiltinsFixture, "bounds_propagate_into_free_intersection_bounds") diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index b3d288ad3..e2ffbb70a 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -20,11 +20,14 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauForceOldSolver) LUAU_FASTFLAG(LuauSolverAgnosticStringification) LUAU_FASTFLAG(LuauConcatDoesntAlwaysReturnString) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) TEST_SUITE_BEGIN("TypeInferOperators"); TEST_CASE_FIXTURE(Fixture, "or_joins_types") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; + CheckResult result = check(R"( local s = "a" or 10 local x:string|number = s @@ -34,7 +37,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types") if (!FFlag::DebugLuauForceOldSolver) { // FIXME: Regression - CHECK("(string & ~(false?)) | number" == toString(*requireType("s"))); + CHECK("(string & truthy) | number" == toString(*requireType("s"))); CHECK("number | string" == toString(*requireType("x"))); } else @@ -46,6 +49,8 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types") TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; + CheckResult result = check(R"( local s = "a" or 10 local x:number|string = s @@ -56,7 +61,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") if (!FFlag::DebugLuauForceOldSolver) { // FIXME: Regression. - CHECK("(string & ~(false?)) | number" == toString(*requireType("s"))); + CHECK("(string & truthy) | number" == toString(*requireType("s"))); CHECK("number | string" == toString(*requireType("y"))); } else @@ -68,6 +73,8 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_extras") TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; + CheckResult result = check(R"( local s = "a" or "b" local x:string = s @@ -77,7 +84,7 @@ TEST_CASE_FIXTURE(Fixture, "or_joins_types_with_no_superfluous_union") if (!FFlag::DebugLuauForceOldSolver) { // FIXME: Regression - CHECK("(string & ~(false?)) | string" == toString(requireType("s"))); + CHECK("(string & truthy) | string" == toString(requireType("s"))); } else CHECK("string" == toString(requireType("s"))); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index b65e40b2a..202aae3e5 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -12,6 +12,7 @@ LUAU_FASTFLAG(DebugLuauForceOldSolver) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) LUAU_FASTFLAG(LuauIndexingIntoErrorGivesError); +LUAU_FASTFLAG(LuauToStringTruthyFalsy) using namespace Luau; @@ -1035,6 +1036,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "either_number_or_string") TEST_CASE_FIXTURE(Fixture, "not_t_or_some_prop_of_t") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; + CheckResult result = check(R"( local function f(t: {x: boolean}?) if not t or t.x then @@ -1054,7 +1057,7 @@ TEST_CASE_FIXTURE(Fixture, "not_t_or_some_prop_of_t") // ... which we can't _quite_ refine into the type it ought to be: // // { write x: boolean, read x: true } | nil - CHECK_EQ("({ read x: ~(false?) } & { x: boolean })?", toString(requireTypeAtPosition({3, 28}))); + CHECK_EQ("({ read x: truthy } & { x: boolean })?", toString(requireTypeAtPosition({3, 28}))); } else CHECK_EQ("{ x: boolean }?", toString(requireTypeAtPosition({3, 28}))); @@ -1663,6 +1666,7 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "isa_type_refinement_must_be_know TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_optional_properties_should_not_refine_extern_types_to_never") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; CheckResult result = check(R"( local weld: WeldConstraint = nil :: any @@ -1676,7 +1680,7 @@ TEST_CASE_FIXTURE(RefinementExternTypeFixture, "asserting_optional_properties_sh LUAU_REQUIRE_NO_ERRORS(result); if (!FFlag::DebugLuauForceOldSolver) - CHECK_EQ("WeldConstraint & { read Part1: ~(false?) }", toString(requireTypeAtPosition({3, 15}))); + CHECK_EQ("WeldConstraint & { read Part1: truthy }", toString(requireTypeAtPosition({3, 15}))); else CHECK_EQ("WeldConstraint", toString(requireTypeAtPosition({3, 15}))); CHECK_EQ("Vector3", toString(requireTypeAtPosition({6, 29}))); @@ -2566,6 +2570,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonnil_refinement_on_generic") TEST_CASE_FIXTURE(BuiltinsFixture, "truthy_refinement_on_generic") { + ScopedFastFlag sff{FFlag::LuauToStringTruthyFalsy, true}; CheckResult result = check(R"( local function printOptional(item: T?, printer: (T) -> string): string if item then @@ -2578,7 +2583,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "truthy_refinement_on_generic") LUAU_REQUIRE_NO_ERRORS(result); if (!FFlag::DebugLuauForceOldSolver) - CHECK_EQ("T & ~(false?)", toString(requireTypeAtPosition({3, 31}))); + CHECK_EQ("T & truthy", toString(requireTypeAtPosition({3, 31}))); else CHECK_EQ("T", toString(requireTypeAtPosition({3, 31}))); } @@ -2883,6 +2888,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinements_from_and_should_not_refine_to_ne { ScopedFastFlag sffs[] = { {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, }; loadDefinition(R"( @@ -2906,7 +2912,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinements_from_and_should_not_refine_to_ne LUAU_REQUIRE_NO_ERRORS(results); - CHECK_EQ("(Config & { read KeyboardEnabled: false? }) | (Config & { read MouseEnabled: false? })", toString(requireTypeAtPosition({6, 24}))); + CHECK_EQ("(Config & { read KeyboardEnabled: falsy }) | (Config & { read MouseEnabled: falsy })", toString(requireTypeAtPosition({6, 24}))); } TEST_CASE_FIXTURE(Fixture, "force_simplify_constraint_doesnt_drop_blocked_type") diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index b9e79a5eb..621c03694 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -29,6 +29,7 @@ LUAU_FASTFLAG(LuauPropertyModifierMismatchErrors) LUAU_FASTFLAG(LuauReadOnlyIndexers) LUAU_FASTFLAG(LuauRemoveConstraintSolverEmplace) LUAU_FASTFLAG(LuauRemovePrimitiveTypeConstraintAndSubtypingUnifier) +LUAU_FASTFLAG(LuauToStringTruthyFalsy) TEST_SUITE_BEGIN("TableTests"); @@ -5263,8 +5264,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "subtyping_with_a_metatable_table_path") TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type") { - - ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false}; + ScopedFastFlag sffs[] = { + {FFlag::DebugLuauForceOldSolver, false}, + {FFlag::LuauToStringTruthyFalsy, true}, + }; // This will have one (legitimate) error but previously would crash. auto result = check(R"( @@ -5281,7 +5284,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ( - "Cannot add indexer to table '{ @metatable t1, (nil & ~(false?)) | { } } where t1 = { new: (a) -> { @metatable t1, (a & ~(false?)) | { " + "Cannot add indexer to table '{ @metatable t1, (nil & truthy) | { } } where t1 = { new: (a) -> { @metatable t1, (a & truthy) | { " "} } }'", toString(result.errors[0]) );