Skip to content

Commit

Permalink
Add support for type expressions in intersections (#7172)
Browse files Browse the repository at this point in the history
Add support for type expressions in:
- intersection: `select A[is B | C & D]`
- intersection on backlink: `SELECT A.<a[is B | C]`
- polymorphism: `SELECT A {[is B & C].b}`
- type checking: `SELECT A is B | C`

Replaces previous logic of `apply_intersection` with a more general approach which handles intersections and general type expressions.
- Relies on new logic of type_to_typeref to deal with resolving the actual underlying types.
  • Loading branch information
dnwpark committed May 14, 2024
1 parent c2c6d60 commit b642dfa
Show file tree
Hide file tree
Showing 11 changed files with 726 additions and 125 deletions.
60 changes: 25 additions & 35 deletions edb/edgeql/compiler/schemactx.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,6 @@ def apply_intersection(
) -> TypeIntersectionResult:
"""Compute an intersection of two types: *left* and *right*.
In theory, this should handle all combinations of unions and intersections
recursively, but currently this handles only the common case of
intersecting a regular type or a union type with a regular type.
Returns:
A :class:`~TypeIntersectionResult` named tuple containing the
result intersection type, whether the type system considers
Expand All @@ -552,40 +548,34 @@ def apply_intersection(
# of the argument, then this is, effectively, a NOP.
return TypeIntersectionResult(stype=left)

is_subtype = False
empty_intersection = False
union = left.get_union_of(ctx.env.schema)
if union:
# If the argument type is a union type, then we
# narrow it by the intersection type.
narrowed_union = []
for component_type in union.objects(ctx.env.schema):
if component_type.issubclass(ctx.env.schema, right):
narrowed_union.append(component_type)
elif right.issubclass(ctx.env.schema, component_type):
narrowed_union.append(right)

if len(narrowed_union) == 0:
int_type = get_intersection_type((left, right), ctx=ctx)
is_subtype = int_type.issubclass(ctx.env.schema, left)
assert isinstance(right, s_obj.InheritingObject)
empty_intersection = not any(
c.issubclass(ctx.env.schema, left)
for c in right.descendants(ctx.env.schema)
)
elif len(narrowed_union) == 1:
int_type = narrowed_union[0]
is_subtype = int_type.issubclass(ctx.env.schema, left)
else:
int_type = get_union_type(narrowed_union, ctx=ctx)
else:
is_subtype = right.issubclass(ctx.env.schema, left)
empty_intersection = not is_subtype
int_type = get_intersection_type((left, right), ctx=ctx)
if right.issubclass(ctx.env.schema, left):
# The intersection type is a proper *subclass* and can be directly
# narrowed.
return TypeIntersectionResult(
stype=right,
is_empty=False,
is_subtype=True,
)

if (
left.get_is_opaque_union(ctx.env.schema)
and (left_union := left.get_union_of(ctx.env.schema))
):
# Expose any opaque union types before continuing with the intersection.
# The schema does not yet fully implement type intersections since there
# is no `IntersectionTypeShell`. As a result, some intersections
# produced while compiling the standard library cannot be resolved.
left = get_union_type(left_union.objects(ctx.env.schema), ctx=ctx)

int_type: s_types.Type = get_intersection_type([left, right], ctx=ctx)
is_empty: bool = (
not s_utils.expand_type_expr_descendants(int_type, ctx.env.schema)
)
is_subtype: bool = int_type.issubclass(ctx.env.schema, left)

return TypeIntersectionResult(
stype=int_type,
is_empty=empty_intersection,
is_empty=is_empty,
is_subtype=is_subtype,
)

Expand Down
8 changes: 1 addition & 7 deletions edb/edgeql/compiler/setgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,13 +532,7 @@ def compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set:
f'it is not an object type',
span=step.span)

if not isinstance(step.type, qlast.TypeName):
raise errors.QueryError(
f'complex type expressions are not supported here',
span=step.span,
)

typ = schemactx.get_schema_type(step.type.maintype, ctx=ctx)
typ: s_types.Type = typegen.ql_typeexpr_to_type(step.type, ctx=ctx)

try:
path_tip = type_intersection_set(
Expand Down
67 changes: 50 additions & 17 deletions edb/edgeql/compiler/typegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,29 @@ def ql_typeexpr_to_type(
ql_t: qlast.TypeExpr, *, ctx: context.ContextLevel
) -> s_types.Type:

types = _ql_typeexpr_to_type(ql_t, ctx=ctx)
if len(types) > 1:
return schemactx.get_union_type(types, ctx=ctx, span=ql_t.span)
else:
(op, _, types) = (
_ql_typeexpr_get_types(ql_t, ctx=ctx)
)
return _ql_typeexpr_combine_types(op, types, ctx=ctx)


def _ql_typeexpr_combine_types(
op: Optional[str], types: List[s_types.Type], *,
ctx: context.ContextLevel
) -> s_types.Type:
if len(types) == 1:
return types[0]
elif op == '|':
return schemactx.get_union_type(types, ctx=ctx)
elif op == '&':
return schemactx.get_intersection_type(types, ctx=ctx)
else:
raise errors.InternalServerError('This should never happen')


def _ql_typeexpr_to_type(
def _ql_typeexpr_get_types(
ql_t: qlast.TypeExpr, *, ctx: context.ContextLevel
) -> List[s_types.Type]:
) -> Tuple[Optional[str], bool, List[s_types.Type]]:

if isinstance(ql_t, qlast.TypeOf):
with ctx.new() as subctx:
Expand All @@ -184,38 +197,58 @@ def _ql_typeexpr_to_type(
stype = setgen.get_set_type(ir_set, ctx=subctx)
ctx.env.type_rewrites = orig_rewrites

return [stype]
return (None, True, [stype])

elif isinstance(ql_t, qlast.TypeOp):
if ql_t.op == '|':
if ql_t.op in ['|', '&']:
(left_op, left_leaf, left_types) = (
_ql_typeexpr_get_types(ql_t.left, ctx=ctx)
)
(right_op, right_leaf, right_types) = (
_ql_typeexpr_get_types(ql_t.right, ctx=ctx)
)

# We need to validate that type ops are applied only to
# object types. So we check the base case here, when the
# left or right operand is a single type, because if it's
# a longer list, then we know that it was already composed
# of "|" or "&", or it is the result of inference by
# "typeof" and is a list of object types anyway.
left = _ql_typeexpr_to_type(ql_t.left, ctx=ctx)
right = _ql_typeexpr_to_type(ql_t.right, ctx=ctx)

if len(left) == 1 and not left[0].is_object_type():
if left_leaf and not left_types[0].is_object_type():
raise errors.UnsupportedFeatureError(
f'cannot use type operator {ql_t.op!r} with non-object '
f'type {left[0].get_displayname(ctx.env.schema)}',
f'type {left_types[0].get_displayname(ctx.env.schema)}',
span=ql_t.left.span)
if len(right) == 1 and not right[0].is_object_type():
if right_leaf and not right_types[0].is_object_type():
raise errors.UnsupportedFeatureError(
f'cannot use type operator {ql_t.op!r} with non-object '
f'type {right[0].get_displayname(ctx.env.schema)}',
f'type {right_types[0].get_displayname(ctx.env.schema)}',
span=ql_t.right.span)

return left + right
# if an operand is either a single type or uses the same operator,
# flatten it into the result types list.
# if an operand has a different operator is used, its types should
# be combined into a new type before appending to the result types.
types: List[s_types.Type] = []
types += (
left_types
if left_op is None or left_op == ql_t.op else
[_ql_typeexpr_combine_types(left_op, left_types, ctx=ctx)]
)
types += (
right_types
if right_op is None or right_op == ql_t.op else
[_ql_typeexpr_combine_types(right_op, right_types, ctx=ctx)]
)

return (ql_t.op, False, types)

raise errors.UnsupportedFeatureError(
f'type operator {ql_t.op!r} is not implemented',
span=ql_t.span)

elif isinstance(ql_t, qlast.TypeName):
return [_ql_typename_to_type(ql_t, ctx=ctx)]
return (None, True, [_ql_typename_to_type(ql_t, ctx=ctx)])

else:
raise errors.EdgeQLSyntaxError("Unexpected type expression",
Expand Down
7 changes: 1 addition & 6 deletions edb/edgeql/compiler/viewgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,12 +699,7 @@ def _shape_el_ql_to_shape_el_desc(
source_intersection = [steps[0]]
lexpr = steps[1]
ptype = steps[0].type
if not isinstance(ptype, qlast.TypeName):
raise errors.QueryError(
'complex type expressions are not supported here',
span=ptype.span,
)
source_spec = schemactx.get_schema_type(ptype.maintype, ctx=ctx)
source_spec = typegen.ql_typeexpr_to_type(ptype, ctx=ctx)
if not isinstance(source_spec, s_objtypes.ObjectType):
raise errors.QueryError(
f"expected object type, got "
Expand Down
24 changes: 13 additions & 11 deletions edb/schema/objtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def get_displayname(self, schema: s_schema.Schema) -> str:
comp_dns = sorted(
(c.get_displayname(schema)
for c in union_of.objects(schema)))
return ' | '.join(comp_dns)
return '(' + ' | '.join(comp_dns) + ')'
else:
intersection_of = mtype.get_intersection_of(schema)
if intersection_of:
Expand All @@ -172,9 +172,9 @@ def get_displayname(self, schema: s_schema.Schema) -> str:
for c in intersection_of.objects(schema)))
# Elide BaseObject from display, because `& BaseObject`
# is a nop.
return ' & '.join(
return '(' + ' & '.join(
dn for dn in comp_dns if dn != 'std::BaseObject'
)
) + ')'
elif mtype == self:
return super().get_displayname(schema)
else:
Expand Down Expand Up @@ -276,17 +276,18 @@ def _issubclass(
if self == parent:
return True

my_union = self.get_union_of(schema)
if my_union and not self.get_is_opaque_union(schema):
if (
(my_union := self.get_union_of(schema))
and not self.get_is_opaque_union(schema)
):
# A union is considered a subclass of a type, if
# ALL its components are subclasses of that type.
return all(
t._issubclass(schema, parent)
for t in my_union.objects(schema)
)

my_intersection = self.get_intersection_of(schema)
if my_intersection:
if my_intersection := self.get_intersection_of(schema):
# An intersection is considered a subclass of a type, if
# ANY of its components are subclasses of that type.
return any(
Expand All @@ -299,8 +300,10 @@ def _issubclass(
return True

elif isinstance(parent, ObjectType):
parent_union = parent.get_union_of(schema)
if parent_union:
if (
(parent_union := parent.get_union_of(schema))
and not parent.get_is_opaque_union(schema)
):
# A type is considered a subclass of a union type,
# if it is a subclass of ANY of the union components.
return (
Expand All @@ -311,8 +314,7 @@ def _issubclass(
)
)

parent_intersection = parent.get_intersection_of(schema)
if parent_intersection:
if parent_intersection := parent.get_intersection_of(schema):
# A type is considered a subclass of an intersection type,
# if it is a subclass of ALL of the intersection components.
return all(
Expand Down
14 changes: 14 additions & 0 deletions tests/schemas/advtypes.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,17 @@ type CBaBc extending Ba, Bc;
type CBbBc extending Bb, Bc;

type CBaBbBc extending Ba, Bb, Bc;

# 3 types which resemble the base types

type XBa {
required property ba -> str;
}

type XBb {
required property bb -> int64;
}

type XBc {
required property bc -> float64;
}

0 comments on commit b642dfa

Please sign in to comment.