Skip to content

Commit b57f54d

Browse files
Read-only/Write-only extern properties, Read-only vector properties (luau-lang#2071)
Implements the `read` and `write` property attributes for external type definitions, which the embedded `vector` type now makes use of: **Before:** ```luau declare extern type vector with x: number y: number z: number end ``` **After:** ```luau declare extern type vector with read x: number read y: number read z: number end ``` The following code now creates type-errors in the expected places: ```luau --!strict local function increment(v: vector) v.x += 1 -- TE v.x -= 1 -- TE v.y *= 1 -- TE v.z /= 1 -- TE print(v.x) -- No TE print(v.x > 5) -- No TE v.x = 15 -- TE end increment(vector.create(1, 2, 3)) ``` This PR also supports different read/write types: ```luau -- Definition declare extern type Foo with read Bar: number write Bar: string end -- Script local f: Foo local b: number = f.Bar f.Bar = "Hello, World!" ``` Additionally, * Adds two new tests for the implemented syntax * Adds the `LuauLValueCompoundAssignmentVisitLhs`, `LuauExternReadWriteAttributes` and `LuauTypeCheckerVectorReadOnly` flags Closes luau-lang#2062
1 parent 2771874 commit b57f54d

7 files changed

Lines changed: 296 additions & 31 deletions

File tree

Analysis/src/ConstraintGenerator.cpp

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ LUAU_FASTFLAG(LuauCaptureRecursiveCallsForTablesAndGlobals2)
4646
LUAU_FASTFLAGVARIABLE(LuauForwardPolarityForFunctionTypes)
4747
LUAU_FASTFLAGVARIABLE(LuauKeepExplicitMapForGlobalTypes)
4848
LUAU_FASTFLAGVARIABLE(LuauRefinementTypeVector)
49+
LUAU_FASTFLAG(LuauExternReadWriteAttributes)
4950

5051
namespace Luau
5152
{
@@ -2061,16 +2062,16 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte
20612062
}
20622063
}
20632064

2064-
for (const AstDeclaredExternTypeProperty& prop : declaredExternType->props)
2065+
for (const AstDeclaredExternTypeProperty& externProp : declaredExternType->props)
20652066
{
2066-
Name propName(prop.name.value);
2067-
TypeId propTy = resolveType(scope, prop.ty, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Mixed);
2067+
Name propName(externProp.name.value);
2068+
TypeId propTy = resolveType(scope, externProp.ty, /* inTypeArguments */ false, /* replaceErrorWithFresh */ false, /* initialPolarity */ Polarity::Mixed);
20682069

20692070
bool assignToMetatable = isMetamethod(propName);
20702071

20712072
// Function typeArguments always take 'self', but this isn't reflected in the
20722073
// parsed annotation. Add it here.
2073-
if (prop.isMethod)
2074+
if (externProp.isMethod)
20742075
{
20752076
if (FunctionType* ftv = getMutable<FunctionType>(propTy))
20762077
{
@@ -2082,9 +2083,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte
20822083
FunctionDefinition defn;
20832084

20842085
defn.definitionModuleName = module->name;
2085-
defn.definitionLocation = prop.location;
2086+
defn.definitionLocation = externProp.location;
20862087
// No data is preserved for varargLocation
2087-
defn.originalNameLocation = prop.nameLocation;
2088+
defn.originalNameLocation = externProp.nameLocation;
20882089

20892090
ftv->definition = defn;
20902091
}
@@ -2094,11 +2095,30 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte
20942095

20952096
if (props.count(propName) == 0)
20962097
{
2097-
props[propName] = {propTy, /*deprecated*/ false, /*deprecatedSuggestion*/ "", prop.location};
2098+
Property tableProp;
2099+
2100+
if (FFlag::LuauExternReadWriteAttributes)
2101+
{
2102+
if (externProp.access == AstTableAccess::Read)
2103+
tableProp = Property::readonly(propTy);
2104+
else if (externProp.access == AstTableAccess::Write)
2105+
tableProp = Property::writeonly(propTy);
2106+
else
2107+
tableProp = Property::rw(propTy);
2108+
2109+
tableProp.location = externProp.location;
2110+
}
2111+
else
2112+
{
2113+
tableProp = {propTy, /*deprecated*/ false, /*deprecatedSuggestion*/ "", externProp.location};
2114+
}
2115+
2116+
props[propName] = tableProp;
20982117
}
20992118
else
21002119
{
21012120
Luau::Property& prop = props[propName];
2121+
bool addedWriteTypeByOverload = false;
21022122

21032123
if (auto readTy = prop.readTy)
21042124
{
@@ -2120,14 +2140,30 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte
21202140
}
21212141
else
21222142
{
2123-
reportError(
2124-
declaredExternType->location,
2125-
GenericError{format("Cannot overload read type of non-function class member '%s'", propName.c_str())}
2126-
);
2143+
if (FFlag::LuauExternReadWriteAttributes)
2144+
{
2145+
if (externProp.access == AstTableAccess::Write && !prop.writeTy.has_value())
2146+
{
2147+
prop.writeTy = propTy;
2148+
addedWriteTypeByOverload = true;
2149+
}
2150+
else
2151+
reportError(
2152+
declaredExternType->location,
2153+
GenericError{format("Cannot overload read type of non-function extern type member '%s'", propName.c_str())}
2154+
);
2155+
}
2156+
else
2157+
{
2158+
reportError(
2159+
declaredExternType->location,
2160+
GenericError{format("Cannot overload read type of non-function extern type member '%s'", propName.c_str())}
2161+
);
2162+
}
21272163
}
21282164
}
21292165

2130-
if (auto writeTy = prop.writeTy)
2166+
if (auto writeTy = prop.writeTy; writeTy && !addedWriteTypeByOverload)
21312167
{
21322168
// We special-case this logic to keep the intersection flat; otherwise we
21332169
// would create a ton of nested intersection typeArguments.
@@ -2147,10 +2183,23 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareExte
21472183
}
21482184
else
21492185
{
2150-
reportError(
2151-
declaredExternType->location,
2152-
GenericError{format("Cannot overload write type of non-function class member '%s'", propName.c_str())}
2153-
);
2186+
if (FFlag::LuauExternReadWriteAttributes)
2187+
{
2188+
if (externProp.access == AstTableAccess::Read && !prop.readTy.has_value())
2189+
prop.readTy = propTy;
2190+
else
2191+
reportError(
2192+
declaredExternType->location,
2193+
GenericError{format("Cannot overload write type of non-function extern type member '%s'", propName.c_str())}
2194+
);
2195+
}
2196+
else
2197+
{
2198+
reportError(
2199+
declaredExternType->location,
2200+
GenericError{format("Cannot overload write type of non-function extern type member '%s'", propName.c_str())}
2201+
);
2202+
}
21542203
}
21552204
}
21562205
}

Analysis/src/EmbeddedBuiltinDefinitions.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
LUAU_FASTFLAGVARIABLE(LuauTypeCheckerUdtfRenameClassToExtern)
55
LUAU_FASTFLAGVARIABLE(LuauNewMathConstantsAnalysis)
6+
LUAU_FASTFLAGVARIABLE(LuauTypeCheckerVectorReadOnly)
67

78
namespace Luau
89
{
@@ -329,6 +330,37 @@ declare buffer: {
329330

330331
static const char* const kBuiltinDefinitionVectorSrc = R"BUILTIN_SRC(
331332
333+
-- While vector would have been better represented as a built-in primitive type, type solver extern type handling covers most of the properties
334+
declare extern type vector with
335+
read x: number
336+
read y: number
337+
read z: number
338+
end
339+
340+
declare vector: {
341+
create: @checked (x: number, y: number, z: number?) -> vector,
342+
magnitude: @checked (vec: vector) -> number,
343+
normalize: @checked (vec: vector) -> vector,
344+
cross: @checked (vec1: vector, vec2: vector) -> vector,
345+
dot: @checked (vec1: vector, vec2: vector) -> number,
346+
angle: @checked (vec1: vector, vec2: vector, axis: vector?) -> number,
347+
floor: @checked (vec: vector) -> vector,
348+
ceil: @checked (vec: vector) -> vector,
349+
abs: @checked (vec: vector) -> vector,
350+
sign: @checked (vec: vector) -> vector,
351+
clamp: @checked (vec: vector, min: vector, max: vector) -> vector,
352+
max: @checked (vector, ...vector) -> vector,
353+
min: @checked (vector, ...vector) -> vector,
354+
lerp: @checked (vec1: vector, vec2: vector, t: number) -> vector,
355+
356+
zero: vector,
357+
one: vector,
358+
}
359+
360+
)BUILTIN_SRC";
361+
362+
static const char* const kBuiltinDefinitionVectorSrc_DEPRECATED = R"BUILTIN_SRC(
363+
332364
-- While vector would have been better represented as a built-in primitive type, type solver extern type handling covers most of the properties
333365
declare extern type vector with
334366
x: number
@@ -373,7 +405,14 @@ std::string getBuiltinDefinitionSource()
373405
result += kBuiltinDefinitionDebugSrc;
374406
result += kBuiltinDefinitionUtf8Src;
375407
result += kBuiltinDefinitionBufferSrc;
376-
result += kBuiltinDefinitionVectorSrc;
408+
if (FFlag::LuauTypeCheckerVectorReadOnly)
409+
{
410+
result += kBuiltinDefinitionVectorSrc;
411+
}
412+
else
413+
{
414+
result += kBuiltinDefinitionVectorSrc_DEPRECATED;
415+
}
377416

378417
return result;
379418
}

Analysis/src/TypeChecker2.cpp

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ LUAU_FASTFLAG(LuauExternTypesNormalizeWithShapes)
4242
LUAU_FASTFLAGVARIABLE(LuauCheckFunctionStatementTypes)
4343
LUAU_FASTFLAGVARIABLE(LuauComparisonToNilsIsAlwaysOk2)
4444
LUAU_FASTFLAGVARIABLE(LuauLValueCompoundAssignmentVisitLhs)
45+
LUAU_FASTFLAG(LuauExternReadWriteAttributes)
4546

4647
namespace Luau
4748
{
@@ -3661,17 +3662,22 @@ void TypeChecker2::checkIndexTypeFromType(
36613662
// because extern typeArguments come into being with full knowledge of their
36623663
// shape. We instead want to report the unknown property error of
36633664
// the `else` branch.
3664-
else if (context == ValueContext::LValue && !get<ExternType>(tableTy))
3665+
else if (context == ValueContext::LValue && (FFlag::LuauExternReadWriteAttributes || !get<ExternType>(tableTy)))
36653666
{
36663667
const auto lvPropTypes = lookupProp(norm.get(), prop, ValueContext::RValue, location, astIndexExprType, dummy);
36673668
if (lvPropTypes.foundOneProp() && lvPropTypes.noneMissingProp())
36683669
reportError(PropertyAccessViolation{tableTy, prop, PropertyAccessViolation::CannotWrite}, location);
36693670
else if (get<PrimitiveType>(tableTy) || get<FunctionType>(tableTy))
36703671
reportError(NotATable{tableTy}, location);
36713672
else
3672-
reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location);
3673+
{
3674+
if (FFlag::LuauExternReadWriteAttributes && get<ExternType>(tableTy))
3675+
reportError(UnknownProperty{tableTy, prop}, location);
3676+
else
3677+
reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location);
3678+
}
36733679
}
3674-
else if (context == ValueContext::RValue && !get<ExternType>(tableTy))
3680+
else if (context == ValueContext::RValue && (FFlag::LuauExternReadWriteAttributes || !get<ExternType>(tableTy)))
36753681
{
36763682
const auto rvPropTypes = lookupProp(norm.get(), prop, ValueContext::LValue, location, astIndexExprType, dummy);
36773683
if (rvPropTypes.foundOneProp() && rvPropTypes.noneMissingProp())
@@ -3733,7 +3739,14 @@ PropertyType TypeChecker2::hasIndexTypeFromType(
37333739
// is compatible with the indexer's indexType
37343740
// Construct the intersection and test inhabitedness!
37353741
if (auto property = lookupExternTypeProp(cls, prop))
3736-
return {NormalizationResult::True, context == ValueContext::LValue ? property->writeTy : property->readTy};
3742+
{
3743+
if (FFlag::LuauExternReadWriteAttributes
3744+
&& ((context == ValueContext::LValue && !property->writeTy) || (context == ValueContext::RValue && !property->readTy))
3745+
)
3746+
return {NormalizationResult::False, {}};
3747+
else
3748+
return {NormalizationResult::True, context == ValueContext::LValue ? property->writeTy : property->readTy};
3749+
}
37373750
if (cls->indexer)
37383751
{
37393752
TypeId inhabitedTestType = module->internalTypes.addType(IntersectionType{{cls->indexer->indexType, astIndexExprType}});

Ast/include/Luau/Ast.h

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,20 +1052,21 @@ class AstStatDeclareFunction : public AstStat
10521052
AstTypePack* retTypes;
10531053
};
10541054

1055+
enum class AstTableAccess
1056+
{
1057+
Read = 0b01,
1058+
Write = 0b10,
1059+
ReadWrite = 0b11,
1060+
};
1061+
10551062
struct AstDeclaredExternTypeProperty
10561063
{
10571064
AstName name;
10581065
Location nameLocation;
10591066
AstType* ty = nullptr;
10601067
bool isMethod = false;
10611068
Location location;
1062-
};
1063-
1064-
enum class AstTableAccess
1065-
{
1066-
Read = 0b01,
1067-
Write = 0b10,
1068-
ReadWrite = 0b11,
1069+
AstTableAccess access = AstTableAccess::ReadWrite;
10691070
};
10701071

10711072
struct AstTableIndexer

Ast/src/Parser.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, f
2222
LUAU_FASTFLAGVARIABLE(DesugaredArrayTypeReferenceIsEmpty)
2323
LUAU_FASTFLAGVARIABLE(LuauConst2)
2424
LUAU_FASTFLAGVARIABLE(DebugLuauNoInline)
25+
LUAU_FASTFLAGVARIABLE(LuauExternReadWriteAttributes)
2526

2627
// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix
2728
bool luau_telemetry_parsed_return_type_variadic_with_type_suffix = false;
@@ -1632,6 +1633,30 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray<AstAttr*
16321633
}
16331634
else
16341635
{
1636+
AstTableAccess access = AstTableAccess::ReadWrite;
1637+
1638+
if (FFlag::LuauExternReadWriteAttributes)
1639+
{
1640+
if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':')
1641+
{
1642+
if (AstName(lexer.current().name) == "read")
1643+
{
1644+
access = AstTableAccess::Read;
1645+
lexer.next();
1646+
}
1647+
else if (AstName(lexer.current().name) == "write")
1648+
{
1649+
access = AstTableAccess::Write;
1650+
lexer.next();
1651+
}
1652+
else
1653+
{
1654+
report(lexer.current().location, "Expected blank or 'read' or 'write' attribute, got '%s'", lexer.current().name);
1655+
lexer.next();
1656+
}
1657+
}
1658+
}
1659+
16351660
Location propStart = lexer.current().location;
16361661
std::optional<Name> propName = parseNameOpt("property name");
16371662

@@ -1641,7 +1666,7 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray<AstAttr*
16411666
expectAndConsume(':', "property type annotation");
16421667
AstType* propType = parseType();
16431668
props.push_back(
1644-
AstDeclaredExternTypeProperty{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())}
1669+
AstDeclaredExternTypeProperty{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation()), access}
16451670
);
16461671
}
16471672
}

tests/Parser.test.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ LUAU_FASTINT(LuauParseErrorLimit)
1919
LUAU_DYNAMIC_FASTFLAG(DebugLuauReportReturnTypeVariadicWithTypeSuffix)
2020
LUAU_FASTFLAG(LuauConst2)
2121
LUAU_FASTFLAG(DebugLuauNoInline)
22+
LUAU_FASTFLAG(LuauExternReadWriteAttributes)
2223

2324
// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix
2425
extern bool luau_telemetry_parsed_return_type_variadic_with_type_suffix;
@@ -4780,4 +4781,37 @@ TEST_CASE_FIXTURE(Fixture, "explicit_type_instantiation_errors")
47804781
matchParseError("local a = x:a<<T>>", "Expected '(', '{' or <string> when parsing function call, got <eof>");
47814782
}
47824783

4784+
TEST_CASE_FIXTURE(Fixture, "extern_read_write_attributes")
4785+
{
4786+
ScopedFastFlag _[] = {
4787+
{FFlag::DebugLuauForceOldSolver, false},
4788+
{FFlag::LuauExternReadWriteAttributes, true}
4789+
};
4790+
4791+
ParseResult result = tryParse(R"(
4792+
declare extern type Foo with
4793+
read ReadOnlyMember: string
4794+
write WriteOnlyMember: number
4795+
ReadWriteMember: vector
4796+
wRITE BadAttributeMember: buffer
4797+
end
4798+
)");
4799+
4800+
REQUIRE_EQ(result.errors.size(), 1);
4801+
CHECK_EQ(result.errors[0].getLocation().begin.line, 5);
4802+
CHECK_EQ(result.errors[0].getMessage(), "Expected blank or 'read' or 'write' attribute, got 'wRITE'");
4803+
4804+
AstStatBlock* stat = result.root;
4805+
4806+
REQUIRE_EQ(stat->body.size, 1);
4807+
4808+
AstStatDeclareExternType* declaredExternType = stat->body.data[0]->as<AstStatDeclareExternType>();
4809+
CHECK_EQ(declaredExternType->props.size, 4);
4810+
4811+
CHECK_EQ(declaredExternType->props.data[0].access, AstTableAccess::Read);
4812+
CHECK_EQ(declaredExternType->props.data[1].access, AstTableAccess::Write);
4813+
CHECK_EQ(declaredExternType->props.data[2].access, AstTableAccess::ReadWrite);
4814+
CHECK_EQ(declaredExternType->props.data[3].access, AstTableAccess::ReadWrite);
4815+
}
4816+
47834817
TEST_SUITE_END();

0 commit comments

Comments
 (0)