Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit mutation of container-typed fields to the scope of the composite #1267

Merged
merged 36 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1dbd114
demonstration of external mutation error
dsainati1 Nov 22, 2021
e30e922
add tests
dsainati1 Nov 22, 2021
02736cc
improve tests to include acceess(contract)
dsainati1 Nov 23, 2021
4680fd9
add contract test
dsainati1 Nov 23, 2021
bc66e50
test for mutating struct field within defining contract
dsainati1 Nov 23, 2021
8cf2b38
rename tests
dsainati1 Nov 23, 2021
348d347
add external mutation error when calling mutative methods on arrays a…
dsainati1 Nov 23, 2021
7d09f1e
pub(set) access mode allows mutation of fields anywhere
dsainati1 Nov 23, 2021
1e7cd60
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Nov 23, 2021
6391f16
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Nov 24, 2021
f9c6c3c
add test for structure created in its own scope
dsainati1 Nov 24, 2021
c4b0ef3
test for mutating value through a reference
dsainati1 Nov 24, 2021
8bcf40e
add rfc for mutability feature
dsainati1 Nov 24, 2021
d8143c6
iterate on rfc, add another test for code included there
dsainati1 Nov 26, 2021
fb7cfac
add prior art section for Swift
dsainati1 Nov 26, 2021
0825e3f
remove rfc, as it is a FLIP now
dsainati1 Nov 29, 2021
91a4dec
update docs
dsainati1 Dec 9, 2021
eb4482f
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Dec 9, 2021
c353c04
clarify meaning of 'mutate' in docs
dsainati1 Dec 10, 2021
1d1610c
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Jan 4, 2022
1b40fb4
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Jan 20, 2022
c0f5d20
Apply suggestions from code review
dsainati1 Jan 24, 2022
29767a9
Merge branch 'mutability' of github.com:onflow/cadence into mutability
dsainati1 Jan 24, 2022
14465b1
update documentation to better point out mutating methods
dsainati1 Jan 24, 2022
9fe9347
improve tests
dsainati1 Jan 24, 2022
ce61db4
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Jan 24, 2022
8a36307
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Jan 25, 2022
937eb88
Apply suggestions from code review
dsainati1 Jan 25, 2022
ecf58c1
Merge branch 'mutability' of github.com:onflow/cadence into mutability
dsainati1 Jan 25, 2022
4ed47d3
fix lint
dsainati1 Jan 25, 2022
a424dda
add test
dsainati1 Feb 1, 2022
797ec72
add comments
dsainati1 Feb 1, 2022
01dbc6a
Merge branch 'master' of github.com:onflow/cadence into mutability
dsainati1 Feb 1, 2022
1d6b965
add test for public key immutability
dsainati1 Feb 2, 2022
32d59bf
add test for contract names immutability
dsainati1 Feb 2, 2022
55be095
fix indentation
dsainati1 Feb 2, 2022
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
41 changes: 28 additions & 13 deletions docs/language/access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ will be covered in a later section.
Top-level declarations
(variables, constants, functions, structures, resources, interfaces)
and fields (in structures, and resources) are always only able to be written
to in the scope where it is defined (self).
to and mutated in the scope where it is defined (self).

There are four levels of access control defined in the code that specify where
a declaration can be accessed or called.
Expand Down Expand Up @@ -74,21 +74,21 @@ a declaration can be accessed or called.

**Access level must be specified for each declaration**

The `(set)` suffix can be used to make variables also publicly writable.
The `(set)` suffix can be used to make variables also publicly writable and mutable.

To summarize the behavior for variable declarations, constant declarations, and fields:

| Declaration kind | Access modifier | Read scope | Write scope |
|:-----------------|:-------------------------|:-----------------------------------------------------|:------------------|
| `let` | `priv` / `access(self)` | Current and inner | *None* |
| `let` | `access(contract)` | Current, inner, and containing contract | *None* |
| `let` | `access(account)` | Current, inner, and other contracts in same account | *None* |
| `let` | `pub`,`access(all)` | **All** | *None* |
| `var` | `access(self)` | Current and inner | Current and inner |
| `var` | `access(contract)` | Current, inner, and containing contract | Current and inner |
| `var` | `access(account)` | Current, inner, and other contracts in same account | Current and inner |
| `var` | `pub` / `access(all)` | **All** | Current and inner |
| `var` | `pub(set)` | **All** | **All** |
| Declaration kind | Access modifier | Read scope | Write scope | Mutate scope |
|:-----------------|:-------------------------|:-----------------------------------------------------|:------------------|:------------------|
| `let` | `priv` / `access(self)` | Current and inner | *None* | Current and inner |
| `let` | `access(contract)` | Current, inner, and containing contract | *None* | Current and inner |
| `let` | `access(account)` | Current, inner, and other contracts in same account | *None* | Current and inner |
| `let` | `pub`,`access(all)` | **All** | *None* | Current and inner |
| `var` | `access(self)` | Current and inner | Current and inner | Current and inner |
| `var` | `access(contract)` | Current, inner, and containing contract | Current and inner | Current and inner |
| `var` | `access(account)` | Current, inner, and other contracts in same account | Current and inner | Current and inner |
| `var` | `pub` / `access(all)` | **All** | Current and inner | Current and inner |
| `var` | `pub(set)` | **All** | **All** | **All** |

To summarize the behavior for functions:

Expand Down Expand Up @@ -143,6 +143,10 @@ pub struct SomeStruct {
//
pub(set) var e: Int

// Arrays and dictionaries declared without (set) cannot be
// mutated in external scopes
pub let arr: [Int]

// The initializer is omitted for brevity.

// Declare a private function which is only callable
Expand Down Expand Up @@ -203,4 +207,15 @@ some.e
// Valid: can set publicly settable variable field in outer scope.
//
some.e = 5

// Invalid: cannot mutate a public field in outer scope.
//
some.f.append(0)

// Invalid: cannot mutate a public field in outer scope.
//
some.f[3] = 1

// Valid: can call non-mutating methods on a public field in outer scope
some.f.contains(0)
```
89 changes: 65 additions & 24 deletions runtime/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1289,24 +1289,16 @@ func TestRuntimePublicKey(t *testing.T) {
storage: storage,
}

value, err := executeScript(script, runtimeInterface)
require.NoError(t, err)

expected := cadence.Struct{
StructType: PublicKeyType,
Fields: []cadence.Value{
// Public key (bytes)
newBytesValue([]byte{1, 2}),
_, err := executeScript(script, runtimeInterface)

// Signature Algo
newSignAlgoValue(sema.SignatureAlgorithmECDSA_P256),
require.Error(t, err)

// valid
cadence.Bool(false),
},
}
var checkerErr *sema.CheckerError
require.ErrorAs(t, err, &checkerErr)
errs := checkerErr.Errors
require.Len(t, errs, 1)

assert.Equal(t, expected, value)
assert.IsType(t, &sema.ExternalMutationError{}, errs[0])
})

}
Expand Down Expand Up @@ -1368,7 +1360,6 @@ func TestAuthAccountContracts(t *testing.T) {
transaction {
prepare(signer: AuthAccount) {
signer.contracts.names[0] = "baz"
assert(signer.contracts.names[0] == "foo")
}
}
`)
Expand All @@ -1393,8 +1384,14 @@ func TestAuthAccountContracts(t *testing.T) {
Location: nextTransactionLocation(),
},
)
require.Error(t, err)

require.NoError(t, err)
var checkerErr *sema.CheckerError
require.ErrorAs(t, err, &checkerErr)
errs := checkerErr.Errors
require.Len(t, errs, 1)

assert.IsType(t, &sema.ExternalMutationError{}, errs[0])
})
}

Expand Down Expand Up @@ -1571,7 +1568,7 @@ func TestPublicAccountContracts(t *testing.T) {

nextTransactionLocation := newTransactionLocationGenerator()

result, err := rt.ExecuteScript(
_, err := rt.ExecuteScript(
Script{
Source: script,
},
Expand All @@ -1581,14 +1578,58 @@ func TestPublicAccountContracts(t *testing.T) {
},
)

require.NoError(t, err)
require.Error(t, err)

require.IsType(t, cadence.Array{}, result)
array := result.(cadence.Array)
var checkerErr *sema.CheckerError
require.ErrorAs(t, err, &checkerErr)
errs := checkerErr.Errors
require.Len(t, errs, 1)

require.Len(t, array.Values, 2)
assert.Equal(t, cadence.String("foo"), array.Values[0])
assert.Equal(t, cadence.String("bar"), array.Values[1])
assert.IsType(t, &sema.ExternalMutationError{}, errs[0])
})

t.Run("append names", func(t *testing.T) {
t.Parallel()

rt := newTestInterpreterRuntime()

script := []byte(`
pub fun main(): [String] {
let acc = getAccount(0x02)
acc.contracts.names.append("baz")
return acc.contracts.names
}
`)

runtimeInterface := &testRuntimeInterface{
getSigningAccounts: func() ([]Address, error) {
return []Address{{42}}, nil
},
getAccountContractNames: func(_ Address) ([]string, error) {
return []string{"foo", "bar"}, nil
},
}

nextTransactionLocation := newTransactionLocationGenerator()

_, err := rt.ExecuteScript(
Script{
Source: script,
},
Context{
Interface: runtimeInterface,
Location: nextTransactionLocation(),
},
)

require.Error(t, err)

var checkerErr *sema.CheckerError
require.ErrorAs(t, err, &checkerErr)
errs := checkerErr.Errors
require.Len(t, errs, 1)

assert.IsType(t, &sema.ExternalMutationError{}, errs[0])
})
}

Expand Down
8 changes: 4 additions & 4 deletions runtime/resourcedictionary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const resourceDictionaryContract = `

pub resource C {

pub let rs: @{String: R}
pub(set) var rs: @{String: R}

init() {
self.rs <- {}
Expand Down Expand Up @@ -410,7 +410,7 @@ func TestRuntimeResourceDictionaryValues_Nested(t *testing.T) {

pub resource C2 {

pub let rs: @{String: R}
pub(set) var rs: @{String: R}

init() {
self.rs <- {}
Expand All @@ -431,7 +431,7 @@ func TestRuntimeResourceDictionaryValues_Nested(t *testing.T) {

pub resource C {

pub let c2s: @{String: C2}
pub(set) var c2s: @{String: C2}

init() {
self.c2s <- {}
Expand Down Expand Up @@ -602,7 +602,7 @@ func TestRuntimeResourceDictionaryValues_DictionaryTransfer(t *testing.T) {

pub resource C {

pub let rs: @{String: R}
pub(set) var rs: @{String: R}

init() {
self.rs <- {}
Expand Down
2 changes: 1 addition & 1 deletion runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,7 @@ func TestRuntimeStorageMultipleTransactionsResourceWithArray(t *testing.T) {

container := []byte(`
pub resource Container {
pub let values: [Int]
pub(set) var values: [Int]

init() {
self.values = []
Expand Down
16 changes: 16 additions & 0 deletions runtime/sema/check_assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,22 @@ func (checker *Checker) visitIndexExpressionAssignment(

elementType = checker.visitIndexExpression(target, true)

targetExpression := target.TargetExpression
switch targetExpression := targetExpression.(type) {
case *ast.MemberExpression:
_, member, _ := checker.visitMember(targetExpression)
if !checker.isMutatableMember(member) {
checker.report(
&ExternalMutationError{
Name: member.Identifier.Identifier,
DeclarationKind: member.DeclarationKind,
Range: ast.NewRangeFromPositioned(targetExpression.Identifier),
EnclosingType: member.ContainerType,
},
)
}
}

if elementType == nil {
return InvalidType
}
Expand Down
22 changes: 22 additions & 0 deletions runtime/sema/check_member_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@ func (checker *Checker) visitMember(expression *ast.MemberExpression) (accessedT
}
targetRange := ast.NewRangeFromPositioned(expression.Expression)
member = resolver.Resolve(identifier, targetRange, checker.report)
switch targetExpression := accessedExpression.(type) {
case *ast.MemberExpression:
_, m, _ := checker.visitMember(targetExpression)
if !checker.isMutatableMember(m) && resolver.Mutating {
checker.report(
&ExternalMutationError{
Name: m.Identifier.Identifier,
DeclarationKind: m.DeclarationKind,
Range: ast.NewRangeFromPositioned(targetRange),
EnclosingType: m.ContainerType,
},
)
}
}
}

// Get the member from the accessed value based
Expand Down Expand Up @@ -312,6 +326,14 @@ func (checker *Checker) isWriteableMember(member *Member) bool {
checker.containerTypes[member.ContainerType]
}

// isMutatableMember returns true if the given member can be mutated
// in the current location of the checker
//
func (checker *Checker) isMutatableMember(member *Member) bool {
return checker.isWriteableAccess(member.Access) ||
checker.containerTypes[member.ContainerType]
}

// containingContractKindedType returns the containing contract-kinded type
// of the given type, if any.
//
Expand Down
28 changes: 28 additions & 0 deletions runtime/sema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2966,3 +2966,31 @@ func (e *InvalidEntryPointTypeError) Error() string {
e.Type.QualifiedString(),
)
}

// ImportedProgramError

type ExternalMutationError struct {
Name string
EnclosingType Type
DeclarationKind common.DeclarationKind
ast.Range
}

func (e *ExternalMutationError) Error() string {
return fmt.Sprintf(
"cannot mutate `%s`: %s was defined inside `%s`",
e.Name,
e.DeclarationKind.Name(),
e.EnclosingType.QualifiedString(),
)
}

func (e *ExternalMutationError) SecondaryError() string {
return fmt.Sprintf(
"Consider adding a setter for `%s` to `%s`",
e.Name,
e.EnclosingType.QualifiedString(),
)
}

func (*ExternalMutationError) isSemanticError() {}
Loading