Skip to content
Open
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
8 changes: 8 additions & 0 deletions cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3508,6 +3508,14 @@ func TestOptionalValuesEvalUnknowns(t *testing.T) {
},
out: types.IntOne,
},
{
expr: `[optional.of(1), optional.none()].filter(x, x.hasValue()).map(x, x.value())`,
out: types.DefaultTypeAdapter.NativeToValue([]int64{1}),
},
{
expr: `[1, 2].map(x, optional.of(x)).filter(x, x.hasValue()).map(x, x.value())`,
out: types.DefaultTypeAdapter.NativeToValue([]int64{1, 2}),
},
}
for i, tst := range tests {
tc := tst
Expand Down
85 changes: 84 additions & 1 deletion checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ func (c *checker) checkCreateList(e ast.Expr) {
for _, optInd := range optionalIndices {
optionals[optInd] = true
}
var mSnapshot *mapping
for i, e := range create.Elements() {
c.check(e)
elemType := c.getType(e)
Expand All @@ -440,24 +441,36 @@ func (c *checker) checkCreateList(e ast.Expr) {
c.errors.typeMismatch(e.ID(), c.location(e), types.NewOptionalType(elemType), elemType)
}
}
if mSnapshot == nil && (hasTypeParam(elemsType) || hasTypeParam(elemType)) {
mSnapshot = c.mappings.copy()
}
elemsType = c.joinTypes(e, elemsType, elemType)
}
if elemsType == nil {
// If the list is empty, assign free type var to elem type.
elemsType = c.newTypeVar()
}
if mSnapshot != nil && isDyn(elemsType) {
c.mappings = mSnapshot
}
c.setType(e, types.NewListType(elemsType))
}

func (c *checker) checkCreateMap(e ast.Expr) {
mapVal := e.AsMap()
var mapKeyType *types.Type
var mapValueType *types.Type
var mSnapshotKey *mapping
var mSnapshotVal *mapping
for _, e := range mapVal.Entries() {
entry := e.AsMapEntry()
key := entry.Key()
c.check(key)
mapKeyType = c.joinTypes(key, mapKeyType, c.getType(key))
keyType := c.getType(key)
if mSnapshotKey == nil && (hasTypeParam(mapKeyType) || hasTypeParam(keyType)) {
mSnapshotKey = c.mappings.copy()
}
mapKeyType = c.joinTypes(key, mapKeyType, keyType)

val := entry.Value()
c.check(val)
Expand All @@ -469,16 +482,40 @@ func (c *checker) checkCreateMap(e ast.Expr) {
c.errors.typeMismatch(val.ID(), c.location(val), types.NewOptionalType(valType), valType)
}
}
if mSnapshotVal == nil && (hasTypeParam(mapValueType) || hasTypeParam(valType)) {
mSnapshotVal = c.mappings.copy()
}
mapValueType = c.joinTypes(val, mapValueType, valType)
}
if mapKeyType == nil {
// If the map is empty, assign free type variables to typeKey and value type.
mapKeyType = c.newTypeVar()
mapValueType = c.newTypeVar()
}
if mSnapshotKey != nil && isDyn(mapKeyType) {
c.mappings = mSnapshotKey
}
if mSnapshotVal != nil && isDyn(mapValueType) {
c.mappings = mSnapshotVal
}
c.setType(e, types.NewMapType(mapKeyType, mapValueType))
}

func hasTypeParam(t *types.Type) bool {
if t == nil {
return false
}
if t.Kind() == types.TypeParamKind {
return true
}
for _, p := range t.Parameters() {
if hasTypeParam(p) {
return true
}
}
return false
}

func (c *checker) checkCreateStruct(e ast.Expr) {
msgVal := e.AsStruct()
// Determine the type of the message.
Expand Down Expand Up @@ -620,13 +657,59 @@ func (c *checker) joinTypes(e ast.Expr, previous, current *types.Type) *types.Ty
if c.isAssignable(previous, current) {
return mostGeneral(previous, current)
}
if c.isAssignable(current, previous) {
return mostGeneral(current, previous)
}
if t := maybeJoinNullable(previous, current); t != nil {
return t
}
if c.dynAggregateLiteralElementTypesEnabled() {
return types.DynType
}
c.errors.typeMismatch(e.ID(), c.location(e), previous, current)
return types.ErrorType
}

func isPrimitiveType(t *types.Type) bool {
switch t.Kind() {
case types.BoolKind, types.BytesKind, types.DoubleKind, types.IntKind, types.StringKind, types.UintKind:
return !t.IsAssignableType(types.NullType)
default:
return false
}
}

func isWrapperType(t *types.Type) bool {
switch t.Kind() {
case types.BoolKind, types.BytesKind, types.DoubleKind, types.IntKind, types.StringKind, types.UintKind:
return t.IsAssignableType(types.NullType)
default:
return false
}
}

func maybeJoinNullable(t1, t2 *types.Type) *types.Type {
if t1.Kind() == types.NullTypeKind {
if isPrimitiveType(t2) || isWrapperType(t2) {
return types.NewNullableType(t2)
}
}
if t2.Kind() == types.NullTypeKind {
if isPrimitiveType(t1) || isWrapperType(t1) {
return types.NewNullableType(t1)
}
}
if t1.Kind() == t2.Kind() {
if isPrimitiveType(t1) && isWrapperType(t2) {
return t2
}
if isWrapperType(t1) && isPrimitiveType(t2) {
return t1
}
}
return nil
}

func (c *checker) dynAggregateLiteralElementTypesEnabled() bool {
return c.env.aggLitElemType == dynElementType
}
Expand Down
159 changes: 158 additions & 1 deletion checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2452,6 +2452,157 @@ _&&_(_==_(list~type(list(dyn))^list,
}
}

func TestListUnification(t *testing.T) {
tests := []struct {
in string
env testEnv
outType *types.Type
}{
{
in: `[1, null_int, null]`,
env: testEnv{
idents: []*decls.VariableDecl{
decls.NewVariable("null_int", types.NewNullableType(types.IntType)),
},
},
outType: types.NewListType(types.NewNullableType(types.IntType)),
},
{
in: `[null, 1, null_int]`,
env: testEnv{
idents: []*decls.VariableDecl{
decls.NewVariable("null_int", types.NewNullableType(types.IntType)),
},
},
outType: types.NewListType(types.NewNullableType(types.IntType)),
},
{
in: `[null_int, null, 1]`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want an explicit test to check that different nullables don't coalesce [m.string_wrapper, null, m.int64_wrapper] -> list(dyn)

env: testEnv{
idents: []*decls.VariableDecl{
decls.NewVariable("null_int", types.NewNullableType(types.IntType)),
},
},
outType: types.NewListType(types.NewNullableType(types.IntType)),
},
{
in: `[1, null]`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are a bit interesting, do you think we should automatically promote to a union type based on component elements, or require that one of the elements already inferred to be a union? (maybe less interesting for just nullables right now, but becomes more interesting if we add a JSON value type union or something like any of from JSON schema and have to decide if we keep a set of type union candidates or allow arbitrary ones).

outType: types.NewListType(types.NewNullableType(types.IntType)),
},
{
in: `[null, 1]`,
outType: types.NewListType(types.NewNullableType(types.IntType)),
},
{
in: `['foo', null]`,
outType: types.NewListType(types.NewNullableType(types.StringType)),
},
{
in: `[null, 'foo']`,
outType: types.NewListType(types.NewNullableType(types.StringType)),
},
}

p, err := parser.NewParser(parser.Macros(parser.AllMacros...))
if err != nil {
t.Fatalf("parser.NewParser() failed: %v", err)
}

for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
src := common.NewTextSource(tc.in)
pAst, errors := p.Parse(src)
if len(errors.GetErrors()) > 0 {
t.Fatalf("Parse(%s) failed: %v", tc.in, errors.ToDisplayString())
}
reg, err := types.NewProtoRegistry()
if err != nil {
t.Fatalf("types.NewProtoRegistry() failed: %v", err)
}
cont, err := containers.NewContainer()
if err != nil {
t.Fatalf("containers.NewContainer() failed: %v", err)
}
env, err := NewEnv(cont, reg)
if err != nil {
t.Fatalf("NewEnv failed: %v", err)
}
if len(tc.env.idents) > 0 {
for _, id := range tc.env.idents {
env.AddIdents(id)
}
}
cAst, errs := Check(pAst, src, env)
if len(errs.GetErrors()) > 0 {
t.Fatalf("Check(%s) failed: %v", tc.in, errs.ToDisplayString())
}
gotType := cAst.GetType(cAst.Expr().ID())
if !gotType.IsExactType(tc.outType) {
t.Errorf("Check(%s) type = %v, want %v", tc.in, gotType, tc.outType)
}
})
}
}

func BenchmarkListUnification(b *testing.B) {
p, err := parser.NewParser(parser.Macros(parser.AllMacros...))
if err != nil {
b.Fatalf("parser.NewParser() failed: %v", err)
}
src := common.NewTextSource(`[1, null_int, null]`)
pAst, errors := p.Parse(src)
if len(errors.GetErrors()) > 0 {
b.Fatalf("Parse failed: %v", errors.ToDisplayString())
}
reg, err := types.NewProtoRegistry()
if err != nil {
b.Fatalf("types.NewProtoRegistry() failed: %v", err)
}
cont, err := containers.NewContainer()
if err != nil {
b.Fatalf("containers.NewContainer() failed: %v", err)
}
env, err := NewEnv(cont, reg)
if err != nil {
b.Fatalf("NewEnv failed: %v", err)
}
env.AddIdents(decls.NewVariable("null_int", types.NewNullableType(types.IntType)))

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Check(pAst, src, env)
}
}

func BenchmarkConcreteList(b *testing.B) {
p, err := parser.NewParser(parser.Macros(parser.AllMacros...))
if err != nil {
b.Fatalf("parser.NewParser() failed: %v", err)
}
src := common.NewTextSource(`[1, 2, 3]`)
pAst, errors := p.Parse(src)
if len(errors.GetErrors()) > 0 {
b.Fatalf("Parse failed: %v", errors.ToDisplayString())
}
reg, err := types.NewProtoRegistry()
if err != nil {
b.Fatalf("types.NewProtoRegistry() failed: %v", err)
}
cont, err := containers.NewContainer()
if err != nil {
b.Fatalf("containers.NewContainer() failed: %v", err)
}
env, err := NewEnv(cont, reg)
if err != nil {
b.Fatalf("NewEnv failed: %v", err)
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Check(pAst, src, env)
}
}

func testEnvs(t testing.TB) map[string]testEnv {
return map[string]testEnv{
"default": {
Expand Down Expand Up @@ -2654,7 +2805,10 @@ func BenchmarkCheck(b *testing.B) {
if len(errors.GetErrors()) > 0 {
b.Fatalf("Unexpected parse errors: %v", errors.ToDisplayString())
}
reg, err := types.NewProtoRegistry(types.ProtoTypeDefs(&proto2pb.TestAllTypes{}, &proto3pb.TestAllTypes{}))
reg, err := types.NewProtoRegistry(
types.JSONFieldNames(tc.env.jsonFieldNames),
types.ProtoTypeDefs(&proto2pb.TestAllTypes{}, &proto3pb.TestAllTypes{}),
)
if err != nil {
b.Fatalf("types.NewProtoRegistry() failed: %v", err)
}
Expand All @@ -2671,6 +2825,9 @@ func BenchmarkCheck(b *testing.B) {
if len(tc.opts) != 0 {
opts = tc.opts
}
if tc.env.jsonFieldNames {
opts = append(opts, JSONFieldNames(true))
}
env, err := NewEnv(cont, reg, opts...)
if err != nil {
b.Fatalf("NewEnv(cont, reg) failed: %v", err)
Expand Down
15 changes: 15 additions & 0 deletions checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,26 @@ func isEqualOrLessSpecific(t1, t2 *types.Type) bool {
if isDyn(t2) || kind2 == types.TypeParamKind {
return false
}
// Nullable types are less specific than NullType.
if kind2 == types.NullTypeKind && t1.IsAssignableType(types.NullType) {
return true
}
if kind1 == types.NullTypeKind && t2.IsAssignableType(types.NullType) {
return false
}
// Types must be of the same kind to be equal.
if kind1 != kind2 {
return false
}

// Wrapper types are less specific than their underlying primitive types.
if t1.IsAssignableType(types.NullType) && !t2.IsAssignableType(types.NullType) {
return true
}
if !t1.IsAssignableType(types.NullType) && t2.IsAssignableType(types.NullType) {
return false
}

// With limited exceptions for ANY and JSON values, the types must agree and be equivalent in
// order to return true.
switch kind1 {
Expand Down
1 change: 0 additions & 1 deletion conformance/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ _TESTS_TO_SKIP = [
"enums/strong_proto3",

# Type deductions
"type_deductions/wrappers/wrapper_promotion_2",
"type_deductions/legacy_nullable_types/null_assignable_to_message_parameter_candidate",
"type_deductions/legacy_nullable_types/null_assignable_to_duration_parameter_candidate",
"type_deductions/legacy_nullable_types/null_assignable_to_timestamp_parameter_candidate",
Expand Down
Loading