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

Static check to prevent nested references #2970

Merged
merged 4 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions runtime/sema/check_reference_expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ func (checker *Checker) VisitReferenceExpression(referenceExpression *ast.Refere

referencedType, actualType := checker.visitExpression(referencedExpression, expectedLeftType)

// check that the type of the referenced value is not itself a reference
var requireNoReferenceNesting func(actualType Type)
requireNoReferenceNesting = func(actualType Type) {
switch nestedReference := actualType.(type) {
case *ReferenceType:
checker.report(&NestedReferenceError{
Type: nestedReference,
Range: checker.expressionRange(referenceExpression),
})
case *OptionalType:
requireNoReferenceNesting(nestedReference.Type)
}
}
requireNoReferenceNesting(actualType)

hasErrors := len(checker.errors) > beforeErrors
if !hasErrors {
// If the reference type was an optional type,
Expand Down
17 changes: 17 additions & 0 deletions runtime/sema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4694,3 +4694,20 @@ func (*InvalidTypeParameterizedNonNativeFunctionError) IsUserError() {}
func (e *InvalidTypeParameterizedNonNativeFunctionError) Error() string {
return "invalid type parameters in non-native function"
}

// NestedReferenceError
type NestedReferenceError struct {
Type *ReferenceType
ast.Range
}

var _ SemanticError = &NestedReferenceError{}
var _ errors.UserError = &NestedReferenceError{}

func (*NestedReferenceError) isSemanticError() {}

func (*NestedReferenceError) IsUserError() {}

func (e *NestedReferenceError) Error() string {
return fmt.Sprintf("cannot create a nested reference to value of type %s", e.Type.QualifiedString())
}
60 changes: 60 additions & 0 deletions runtime/tests/checker/reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3061,3 +3061,63 @@ func TestCheckResourceReferenceIndexNilAssignment(t *testing.T) {
require.IsType(t, &sema.InvalidResourceAssignmentError{}, errors[2])
})
}

func TestCheckNestedReference(t *testing.T) {
t.Parallel()

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

_, err := ParseAndCheck(t, `
fun main() {
let x = &1 as &Int
let y = &x as & &Int
}
`)

errors := RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errors[0])
})

t.Run("type of underlying value checked", func(t *testing.T) {
t.Parallel()

_, err := ParseAndCheck(t, `
fun main() {
let x = &1 as &Int
let y = &x as &AnyStruct
}
`)

errors := RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errors[0])
})

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

_, err := ParseAndCheck(t, `
fun main() {
let x: &Int? = &1 as &Int
let y = &x as &AnyStruct?
}
`)

errors := RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errors[0])
})

t.Run("nested optional", func(t *testing.T) {
t.Parallel()

_, err := ParseAndCheck(t, `
fun main() {
let x: &Int?? = &1 as &Int
let y = &x as &AnyStruct?
}
`)

errors := RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errors[0])
})
}
37 changes: 33 additions & 4 deletions runtime/tests/interpreter/reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1739,14 +1739,20 @@ func TestInterpretReferenceToReference(t *testing.T) {

t.Parallel()

inter := parseCheckAndInterpret(t, `
inter, err := parseCheckAndInterpretWithOptions(t, `
fun main() {
let x = &1 as &Int
let y = &x as & &Int
}
`)
`, ParseCheckAndInterpretOptions{
HandleCheckerError: func(err error) {
errs := checker.RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errs[0])
},
})
require.NoError(t, err)

_, err := inter.Invoke("main")
_, err = inter.Invoke("main")
RequireError(t, err)

require.ErrorAs(t, err, &interpreter.NestedReferenceError{})
Expand All @@ -1773,11 +1779,34 @@ func TestInterpretReferenceToReference(t *testing.T) {

t.Parallel()

inter := parseCheckAndInterpret(t, `
inter, err := parseCheckAndInterpretWithOptions(t, `
fun main() {
let x: (&Int)? = &1 as &Int
let y: (&(&Int))? = &x
}
`, ParseCheckAndInterpretOptions{
HandleCheckerError: func(err error) {
errs := checker.RequireCheckerErrors(t, err, 1)
require.IsType(t, &sema.NestedReferenceError{}, errs[0])
},
})
require.NoError(t, err)

_, err = inter.Invoke("main")
RequireError(t, err)

require.ErrorAs(t, err, &interpreter.NestedReferenceError{})
})

t.Run("upcast to optional anystruct", func(t *testing.T) {

t.Parallel()

inter := parseCheckAndInterpret(t, `
fun main() {
let x = &1 as &Int as AnyStruct?
let y = &x as &AnyStruct?
}
`)

_, err := inter.Invoke("main")
Expand Down
Loading