Skip to content

Commit

Permalink
Merge pull request #1189 from onflow/issue473
Browse files Browse the repository at this point in the history
Add `isSubtype` function for runtime values
  • Loading branch information
dsainati1 committed Oct 25, 2021
2 parents fea33af + 863598e commit aafc32b
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 4 deletions.
10 changes: 10 additions & 0 deletions docs/language/run-time-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ Type<Int>() == Type<Int>()
Type<Int>() != Type<String>()
```

The method `fun isSubtype(of otherType: Type): Bool` can be used to compare the run-time types of values.

```cadence
Type<Int>().isSubtype(of: Type<Int>()) // true
Type<Int>().isSubtype(of: Type<String>()) // false
Type<Int>().isSubtype(of: Type<Int?>()) // true
```

To get the run-time type's fully qualified type identifier, use the `let identifier: String` field:

```cadence
Expand Down
18 changes: 18 additions & 0 deletions runtime/interpreter/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,24 @@ func (v TypeValue) GetMember(inter *Interpreter, _ func() LocationRange, name st
typeID = string(inter.ConvertStaticToSemaType(staticType).ID())
}
return NewStringValue(typeID)
case "isSubtype":
return NewHostFunctionValue(
func(invocation Invocation) Value {
staticType := v.Type
otherStaticType := invocation.Arguments[0].(TypeValue).Type

// if either type is unknown, the subtype relation is false, as it doesn't make sense to even ask this question
if staticType == nil || otherStaticType == nil {
return BoolValue(false)
}
result := sema.IsSubType(
inter.ConvertStaticToSemaType(staticType),
inter.ConvertStaticToSemaType(otherStaticType),
)
return BoolValue(result)
},
sema.MetaTypeIsSubtypeFunctionType,
)
}

return nil
Expand Down
39 changes: 35 additions & 4 deletions runtime/sema/meta_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ import (
"github.com/onflow/cadence/runtime/common"
)

const typeIdentifierDocString = `
const metaTypeIdentifierDocString = `
The fully-qualified identifier of the type
`

const metaTypeSubtypeDocString = `
Returns true if this type is a subtype of the given type at run-time
`

// MetaType represents the type of a type.
//
var MetaType = &SimpleType{
Expand All @@ -39,7 +43,23 @@ var MetaType = &SimpleType{
Equatable: true,
ExternallyReturnable: true,
Importable: true,
Members: func(t *SimpleType) map[string]MemberResolver {
}

var MetaTypeIsSubtypeFunctionType = &FunctionType{
Parameters: []*Parameter{
{
Label: "of",
Identifier: "otherType",
TypeAnnotation: NewTypeAnnotation(MetaType),
},
},
ReturnTypeAnnotation: NewTypeAnnotation(
BoolType,
),
}

func init() {
MetaType.Members = func(t *SimpleType) map[string]MemberResolver {
return map[string]MemberResolver{
"identifier": {
Kind: common.DeclarationKindField,
Expand All @@ -48,10 +68,21 @@ var MetaType = &SimpleType{
t,
identifier,
StringType,
typeIdentifierDocString,
metaTypeIdentifierDocString,
)
},
},
"isSubtype": {
Kind: common.DeclarationKindFunction,
Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member {
return NewPublicFunctionMember(
t,
identifier,
MetaTypeIsSubtypeFunctionType,
metaTypeSubtypeDocString,
)
},
},
}
},
}
}
87 changes: 87 additions & 0 deletions runtime/tests/checker/metatype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,93 @@ func TestCheckIsInstance(t *testing.T) {
}
}

func TestCheckIsSubtype(t *testing.T) {

t.Parallel()

cases := []struct {
name string
code string
valid bool
}{
{
name: "string is a subtype of string",
code: `
let stringType = Type<String>()
let result = stringType.isSubtype(of: stringType)
`,
valid: true,
},
{
name: "int is a subtype of int",
code: `
let intType = Type<Int>()
let result = intType.isSubtype(of: intType)
`,
valid: true,
},
{
name: "resource is a subtype of resource",
code: `
resource R {}
let rType = Type<@R>()
let result = rType.isSubtype(of: rType)
`,
valid: true,
},
{
name: "Int is an instance of Int?",
code: `
let result = Type<Int>().isSubtype(of: Type<Int?>())
`,
valid: true,
},
{
name: "isSubtype must take a type",
code: `
let result = Type<Int>().isSubtype(of: 3)
`,
valid: false,
},
{
name: "isSubtype must take an argument",
code: `
let result = Type<Int>().isSubtype()
`,
valid: false,
},
{
name: "isSubtype argument must be named",
code: `
let result = Type<Int>().isSubtype(Type<Int?>())
`,
valid: false,
},
{
name: "isSubtype must take fewer than two arguments",
code: `
let result = Type<Int>().isSubtype(of: Type<Int?>(), Type<Int?>())
`,
valid: false,
},
}

for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
checker, err := ParseAndCheck(t, testCase.code)
if testCase.valid {
require.NoError(t, err)
assert.Equal(t,
sema.BoolType,
RequireGlobalValue(t, checker.Elaboration, "result"),
)
} else {
require.Error(t, err)
}
})
}
}

func TestCheckIsInstance_Redeclaration(t *testing.T) {

t.Parallel()
Expand Down
137 changes: 137 additions & 0 deletions runtime/tests/interpreter/metatype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,143 @@ func TestInterpretIsInstance(t *testing.T) {
}
}

func TestInterpretIsSubtype(t *testing.T) {

t.Parallel()

cases := []struct {
name string
code string
result bool
}{
{
name: "String is a subtype of String",
code: `
let result = Type<String>().isSubtype(of: Type<String>())
`,
result: true,
},
{
name: "Int is a subtype of Int",
code: `
let result = Type<Int>().isSubtype(of: Type<Int>())
`,
result: true,
},
{
name: "Int is a subtype of Int?",
code: `
let result = Type<Int>().isSubtype(of: Type<Int?>())
`,
result: true,
},
{
name: "Int? is a subtype of Int",
code: `
let result = Type<Int?>().isSubtype(of: Type<Int>())
`,
result: false,
},
{
name: "resource is a subtype of AnyResource",
code: `
resource R {}
let result = Type<@R>().isSubtype(of: Type<@AnyResource>())
`,
result: true,
},
{
name: "struct is a subtype of AnyStruct",
code: `
struct S {}
let result = Type<S>().isSubtype(of: Type<AnyStruct>())
`,
result: true,
},
{
name: "Int is not a subtype of resource",
code: `
resource R {}
let result = Type<Int>().isSubtype(of: Type<@R>())
`,
result: false,
},
{
name: "resource is not a subtype of String",
code: `
resource R {}
let result = Type<@R>().isSubtype(of: Type<String>())
`,
result: false,
},
{
name: "resource R is not a subtype of resource S",
code: `
resource R {}
resource S {}
let result = Type<@R>().isSubtype(of: Type<@S>())
`,
result: false,
},
{
name: "resource R is not a subtype of resource S",
code: `
resource R {}
resource S {}
let result = Type<@R>().isSubtype(of: Type<@S>())
`,
result: false,
},
{
name: "Int is not a subtype of an unknown type",
code: `
let result = Type<Int>().isSubtype(of: unknownType)
`,
result: false,
},
{
name: "unknown type is not a subtype of Int",
code: `
let result = unknownType.isSubtype(of: Type<Int>())
`,
result: false,
},
}

valueDeclarations := stdlib.StandardLibraryValues{
{
Name: "unknownType",
Type: sema.MetaType,
Value: interpreter.TypeValue{
Type: nil,
},
Kind: common.DeclarationKindConstant,
},
}

semaValueDeclarations := valueDeclarations.ToSemaValueDeclarations()
interpreterValueDeclarations := valueDeclarations.ToInterpreterValueDeclarations()

for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
inter, err := parseCheckAndInterpretWithOptions(t, testCase.code, ParseCheckAndInterpretOptions{
CheckerOptions: []sema.Option{
sema.WithPredeclaredValues(semaValueDeclarations),
},
Options: []interpreter.Option{
interpreter.WithPredeclaredValues(interpreterValueDeclarations),
},
})
require.NoError(t, err)

assert.Equal(t,
interpreter.BoolValue(testCase.result),
inter.Globals["result"].GetValue(),
)
})
}
}

func TestInterpretGetType(t *testing.T) {

t.Parallel()
Expand Down

0 comments on commit aafc32b

Please sign in to comment.