Skip to content

Commit

Permalink
Merge pull request #41784 from poorna2152/open_record_spread
Browse files Browse the repository at this point in the history
Fix using of the spread field with an open record to construct a closed record
  • Loading branch information
gimantha authored Feb 27, 2024
2 parents d539027 + fd71780 commit bb0abfc
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ public enum DiagnosticErrorCode implements DiagnosticCode {
CANNOT_SPECIFY_NAMED_ARG_FOR_FIELD_OF_INCLUDED_RECORD_WHEN_ARG_SPECIFIED_FOR_INCLUDED_RECORD("BCE2137",
"cannot.specify.named.argument.for.field.of.included.record.when.arg.specified.for.included.record"),
CYCLIC_TYPE_REFERENCE_NOT_YET_SUPPORTED("BCE2138", "cyclic.type.reference.not.yet.supported"),
INVALID_SPREAD_FIELD_TO_CREATE_CLOSED_RECORD_FROM_OPEN_RECORD("BCE2139",
"invalid.spread.field.to.create.closed.record.from.open.record"),
INVALID_SPREAD_FIELD_REST_FIELD_MISMATCH("BCE2140", "invalid.spread.field.rest.field.mismatch"),

//Transaction related error codes
ROLLBACK_CANNOT_BE_OUTSIDE_TRANSACTION_BLOCK("BCE2300", "rollback.cannot.be.outside.transaction.block"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7622,10 +7622,11 @@ private BType checkMappingField(RecordLiteralNode.RecordField field, BType mappi
BLangExpression spreadExpr = ((BLangRecordLiteral.BLangRecordSpreadOperatorField) field).expr;
checkExpr(spreadExpr, data);

BRecordType mappingRecordType = (BRecordType) mappingType;
BType spreadExprType = Types.getImpliedType(spreadExpr.getBType());
if (spreadExprType.tag == TypeTags.MAP) {
return types.checkType(spreadExpr.pos, ((BMapType) spreadExprType).constraint,
getAllFieldType((BRecordType) mappingType),
getAllFieldType(mappingRecordType),
DiagnosticErrorCode.INCOMPATIBLE_TYPES);
}

Expand All @@ -7635,16 +7636,17 @@ private BType checkMappingField(RecordLiteralNode.RecordField field, BType mappi
return symTable.semanticError;
}

BRecordType spreadRecordType = (BRecordType) spreadExprType;
boolean errored = false;
for (BField bField : ((BRecordType) spreadExprType).fields.values()) {
for (BField bField : spreadRecordType.fields.values()) {
BType specFieldType = bField.type;
if (types.isNeverTypeOrStructureTypeWithARequiredNeverMember(specFieldType)) {
continue;
}
BSymbol fieldSymbol = symResolver.resolveStructField(spreadExpr.pos, data.env, bField.name,
mappingType.tsymbol);
BType expectedFieldType = checkRecordLiteralKeyByName(spreadExpr.pos, fieldSymbol, bField.name,
(BRecordType) mappingType);
mappingRecordType);
if (expectedFieldType != symTable.semanticError &&
!types.isAssignable(specFieldType, expectedFieldType)) {
dlog.error(spreadExpr.pos, DiagnosticErrorCode.INCOMPATIBLE_TYPES_FIELD,
Expand All @@ -7654,6 +7656,21 @@ private BType checkMappingField(RecordLiteralNode.RecordField field, BType mappi
}
}
}
if (!spreadRecordType.sealed) {
if (mappingRecordType.sealed) {
dlog.error(spreadExpr.pos,
DiagnosticErrorCode.INVALID_SPREAD_FIELD_TO_CREATE_CLOSED_RECORD_FROM_OPEN_RECORD,
spreadRecordType);
errored = true;
} else if (!types.isAssignable(spreadRecordType.restFieldType,
mappingRecordType.restFieldType)) {
dlog.error(spreadExpr.pos,
DiagnosticErrorCode.INVALID_SPREAD_FIELD_REST_FIELD_MISMATCH,
spreadRecordType, spreadRecordType.restFieldType,
mappingRecordType.restFieldType);
errored = true;
}
}
return errored ? symTable.semanticError : symTable.noType;
} else {
BLangRecordVarNameField varNameField = (BLangRecordVarNameField) field;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,13 @@ error.spread.field.may.duplicate.already.specified.keys=\
error.multiple.inclusive.types=\
invalid usage of mapping constructor expression: multiple spread fields of inclusive mapping types are not allowed

error.invalid.spread.field.to.create.closed.record.from.open.record=\
invalid usage of spread field with open record of type ''{0}'', that may have rest fields, to construct a closed record

error.invalid.spread.field.rest.field.mismatch=\
invalid usage of spread field with open record of type ''{0}'', that may have rest fields of type ''{1}'', \
to construct a record that allows only ''{2}'' rest fields

error.invalid.array.literal=\
invalid usage of array literal with type ''{0}''

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ public void testSpreadOpFieldSemanticAnalysisNegative() {
validateError(result, i++, "incompatible types: expected 'int' for field 'i', found 'float'", 41, 17);
validateError(result, i++, "incompatible types: expected 'string' for field 's', found 'int'", 41, 17);
validateError(result, i++, "incompatible types: expected 'int' for field 'i', found 'boolean'", 41, 29);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| boolean i; anydata...; |}', that " +
"may have rest fields, to construct a closed record", 41, 29);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| int i; boolean x; anydata...; |}'," +
" that may have rest fields, to construct a closed record", 49, 29);
validateError(result, i++, "undefined field 'x' in record 'Foo'", 49, 29);
validateError(result, i++, "incompatible types: expected a map or a record, found 'other'", 53, 26);
validateError(result, i++, "undefined symbol 'b'", 53, 26);
Expand All @@ -156,19 +162,63 @@ public void testSpreadOpFieldSemanticAnalysisNegative() {
validateError(result, i++, "missing non-defaultable required record field 'i'", 64, 13);
validateError(result, i++, "missing non-defaultable required record field 'i'", 67, 13);
validateError(result, i++, "missing non-defaultable required record field 'i'", 71, 13);
validateError(result, i++, "incompatible types: expected a map or a record, found 'int'", 78, 28);
validateError(result, i++, "incompatible types: expected 'string', found '(int|float)'", 86, 25);
validateError(result, i++, "incompatible types: expected 'string', found 'anydata'", 86, 39);
validateError(result, i++, "incompatible types: expected a map or a record, found 'other'", 90, 38);
validateError(result, i++, "undefined symbol 'b'", 90, 38);
validateError(result, i++, "incompatible types: expected a map or a record, found 'other'", 90, 44);
validateError(result, i++, "undefined function 'getFoo'", 90, 44);
validateError(result, i++, "incompatible types: expected 'json', found 'any'", 100, 18);
validateError(result, i++, "incompatible types: expected 'json', found 'anydata'", 100, 30);
validateError(result, i++, "incompatible types: expected 'json', found 'any'", 101, 30);
validateError(result, i++, "incompatible types: expected 'json', found 'anydata'", 101, 36);
validateError(result, i++, "incompatible types: expected 'int', found 'string'", 114, 18);
validateError(result, i++, "incompatible types: expected '(int|float)', found 'string'", 115, 32);
validateError(result, i++,
"invalid usage of spread field with open record of type 'Address', that may have rest fields, to " +
"construct a closed record", 92, 39);
validateError(result, i++,
"invalid usage of spread field with open record of type 'Address', that may have rest fields, to " +
"construct a closed record", 95, 20);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string street; anydata...; |}', " +
"that may have rest fields, to construct a closed record", 98, 20);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string s; anydata...; |}', that " +
"may have rest fields, to construct a closed record", 102, 17);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| int i; anydata...; |}', that may " +
"have rest fields, to construct a closed record", 102, 26);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| int j; anydata...; |}', that may " +
"have rest fields of type 'anydata', to construct a record that allows only 'string' rest " +
"fields", 108, 36);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| boolean b; anydata...; |}', that " +
"may have rest fields of type 'anydata', to construct a record that allows only 'string' rest" +
" fields", 108, 45);
validateError(result, i++,
"incompatible types: expected 'string' for field 'population', found 'int'", 119, 21);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string name; string continent; int" +
" population; anydata...; |}', that may have rest fields of type 'anydata', to construct a " +
"record that allows only 'string' rest fields", 119, 21);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string name; string continent; " +
"anydata...; |}', that may have rest fields of type 'anydata', to construct a record that " +
"allows only 'string' rest fields", 122, 21);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string name; anydata...; |}', that" +
" may have rest fields of type 'anydata', to construct a record that allows only 'string' " +
"rest fields", 125, 40);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string name; string continent; " +
"string...; |}', that may have rest fields of type 'string', to construct a record that " +
"allows only 'int' rest fields", 128, 64);
validateError(result, i++,
"invalid usage of spread field with open record of type 'record {| string name; string continent; " +
"error...; |}', that may have rest fields, to construct a closed record", 131, 56);
validateError(result, i++, "incompatible types: expected a map or a record, found 'int'", 138, 28);
validateError(result, i++, "incompatible types: expected 'string', found '(int|float)'", 146, 25);
validateError(result, i++, "incompatible types: expected 'string', found 'anydata'", 146, 39);
validateError(result, i++, "incompatible types: expected a map or a record, found 'other'", 150, 38);
validateError(result, i++, "undefined symbol 'b'", 150, 38);
validateError(result, i++, "incompatible types: expected a map or a record, found 'other'", 150, 44);
validateError(result, i++, "undefined function 'getFoo'", 150, 44);
validateError(result, i++, "incompatible types: expected 'json', found 'any'", 160, 18);
validateError(result, i++, "incompatible types: expected 'json', found 'anydata'", 160, 30);
validateError(result, i++, "incompatible types: expected 'json', found 'any'", 161, 30);
validateError(result, i++, "incompatible types: expected 'json', found 'anydata'", 161, 36);
validateError(result, i++, "incompatible types: expected 'int', found 'string'", 174, 18);
validateError(result, i++, "incompatible types: expected '(int|float)', found 'string'", 175, 32);
Assert.assertEquals(result.getErrorCount(), i);
}

Expand Down Expand Up @@ -248,7 +298,8 @@ public Object[][] spreadOpFieldTests() {
{ "testSpreadOpInConstMap" },
{ "testSpreadOpInGlobalMap" },
{ "testMappingConstrExprAsSpreadExpr" },
{ "testSpreadFieldWithRecordTypeHavingNeverField" }
{ "testSpreadFieldWithRecordTypeHavingNeverField" },
{ "testSpreadFieldWithRecordTypeHavingRestDescriptor" }
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ public void testImmutableTypesNegative() {
"'PersonalDetails'", 112, 18);
validateError(result, index++, "incompatible types: expected '(Department & readonly)' for field 'dept', " +
"found 'Department'", 113, 12);
validateError(result, index++,
"invalid usage of spread field with open record of type 'record {| Department dept; anydata...; |}', " +
"that may have rest fields, to construct a closed record", 113, 12);
validateError(result, index++,
"invalid usage of spread field with open record of type 'record {| readonly (Department & readonly) " +
"dept; (anydata & readonly)...; |} & readonly', that may have rest fields, to construct a " +
"closed record", 133, 12);

// Updates.
validateError(result, index++, "cannot update 'readonly' record field 'details' in 'Employee'", 136, 5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function testSpreadFieldWithRecordTypeHavingNeverField() {
assertEquality("Main Street", location["street"]);

Candidate candidate = {name: "Jack"};
record {|string name;|} candidateInLine = {...candidate};
record {string name;} candidateInLine = {...candidate};
assertEquality ("Jack", candidateInLine.name);

record {|int i; string s; never n?;|} bar1InLine = {i: 1, s: "s"};
Expand Down Expand Up @@ -246,6 +246,58 @@ function testSpreadFieldWithRecordTypeHavingNeverField() {
assertEquality(8, bar8.i);
}

type Vehicle record {|
int year;
string manufacturer;
string model;
anydata...;
|};

type Truck record {
int year;
string manufacturer;
string model;
int loadCapacity;
};

type RecA record {|
int i;
string s;
string m;
any|error...;
|};

type RecB record {
int i;
string s;
string m;
error e;
};

function testSpreadFieldWithRecordTypeHavingRestDescriptor() {
Truck truck = {year: 2023, manufacturer: "Tesla", model: "Cybertruck", loadCapacity: 15000};
Vehicle vehicle = {...truck};
assertEquality("Cybertruck", vehicle.model);
assertEquality("Tesla", vehicle.manufacturer);
assertEquality(2023, vehicle.year);
assertEquality(15000, vehicle["loadCapacity"]);

RecB recB = {i: 1, s: "s", m: "m", e: error("e")};
RecA recA = {...recB};
assertEquality(1, recA.i);
assertEquality("s", recA.s);
assertEquality("m", recA.m);
assertEquality("e", (<error>recA["e"]).message());

record {|int i; int...;|} r1 = {i: 1};
record {|int i; string|int...;|} r2 = {...r1};
assertEquality(1, r2.i);

record {|string s; never...;|} r3 = {s: "s"};
record {|string s; int...;|} r4 = {...r3};
assertEquality("s", r4.s);
}

function assertEquality(any|error expected, any|error actual) {
if expected is anydata && actual is anydata && expected == actual {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,66 @@ function testFieldWithNeverType() {
Foo _ = {...rec5, ...rec6};
}

type Address record {
string street;
};

type Street record {|
string street;
|};

type Foz record {|
string s;
int i;
int j;
boolean b;
string...;
|};

function testSpreadOfOpenRecordToCreateClosedRecord() {
Address address1 = {street: "Main Street"};
record {|string street;|} _ = {...address1};

Address address2 = {street: "Maple street" };
Street _ = {...address2};

record {string street;} address3 = {street: "Jump Street"};
Street _ = {...address3};

record {string s;} foo1 = {s: "S"};
record {int i;} foo2 = {i: 2};
Foo _ = {...foo1, ...foo2};

record {|string s;|} foz1 = {s: "s"};
record {|int i;|} foz2 = {i: 1};
record {int j;} foz3 = {j: 2};
record {boolean b;} foz4 = {b: true};
Foz _ = {...foz1, ...foz2, ...foz3, ...foz4};
}

type Country record {|
string name;
string continent;
string...;
|};

function testSpreadOpFieldOfMismatchingRestType() {
record {string name; string continent; int population;} country1 = {name: "China", continent: "Asia", population: 1400};
Country _ = {...country1};

record {string name; string continent;} country2 = {name: "Sri Lanka", continent: "Asia"};
Country _ = {...country2};

record {string name;} country3 = {name: "India"};
Country _ = {continent: "Asia", ...country3};

record {|string name; string continent; string...;|} country4 = {name: "Sri Lanka", continent: "Asia"};
record {|string name; string continent; int...;|} _ = {...country4};

record {|string name; string continent; error...;|} country5 = {name: "India", continent: "Asia"};
record {|string name; string continent;|} _ = {...country5};
}

///////////////////////// Map Tests /////////////////////////

function testMapSpreadOpFieldOfIncorrectType() {
Expand Down

0 comments on commit bb0abfc

Please sign in to comment.