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

Support interfaces having multiple base interfaces #2711

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
23 changes: 13 additions & 10 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7696,23 +7696,26 @@ export class Compiler extends DiagnosticEmitter {
), false // managedness is irrelevant here, isn't interrupted
)
);
let allInstances: Set<Class> | null;
let allInstances: Class[] | null;
if (instance.isInterface) {
allInstances = instance.implementers;
let implementers = instance.implementers;
// Ensure interfaces are filtered out, since their class IDs will never be
// seen in actual objects.
allInstances = implementers
? Set_values(implementers).filter(implementer => implementer.kind == ElementKind.Class)
: null;
} else {
allInstances = new Set();
allInstances.add(instance);
let extenders = instance.extenders;
if (extenders) {
for (let _values = Set_values(extenders), i = 0, k = _values.length; i < k; ++i) {
let extender = _values[i];
allInstances.add(extender);
}
allInstances = Set_values(extenders);
allInstances.push(instance);
} else {
allInstances = [instance];
}
}
if (allInstances) {
for (let _values = Set_values(allInstances), i = 0, k = _values.length; i < k; ++i) {
let instance = _values[i];
for (let i = 0, k = allInstances.length; i < k; ++i) {
let instance = unchecked(allInstances[i]);
stmts.push(
module.br("is_instance",
module.binary(BinaryOp.EqI32,
Expand Down
12 changes: 8 additions & 4 deletions src/extra/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,12 +1297,16 @@ export class ASTBuilder {
}
sb.push(">");
}
let extendsType = node.extendsType;
if (extendsType) {
let implementsTypes = node.implementsTypes;
if (implementsTypes && implementsTypes.length > 0) {
sb.push(" extends ");
this.visitTypeNode(extendsType);
this.visitTypeNode(implementsTypes[0]);
for (let i = 1, k = implementsTypes.length; i < k; ++i) {
sb.push(", ");
this.visitTypeNode(implementsTypes[i]);
}
}
// must not have implementsTypes
// must not have extendsType
sb.push(" {\n");
let indentLevel = ++this.indentLevel;
let members = node.members;
Expand Down
44 changes: 32 additions & 12 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1708,20 +1708,40 @@ export class Parser extends DiagnosticEmitter {
}

let extendsType: NamedTypeNode | null = null;
let implementsTypes: NamedTypeNode[] | null = null;
if (tn.skip(Token.Extends)) {
let type = this.parseType(tn);
if (!type) return null;
if (type.kind != NodeKind.NamedType) {
this.error(
DiagnosticCode.Identifier_expected,
type.range
);
return null;
if (isInterface) {
do {
let type = this.parseType(tn);
if (!type) return null;
if (type.kind != NodeKind.NamedType) {
this.error(
DiagnosticCode.Identifier_expected,
type.range
);
return null;
}
// Note: Even though the keyword is "extends", the base interfaces are stored in
// the implementsTypes field, as that's already an array that can be used.
// When an InterfacePrototype is created, the base InterfacePrototypes are
// stored in the interfacePrototypes field for the same reason.
if (!implementsTypes) implementsTypes = [<NamedTypeNode>type];
else implementsTypes.push(<NamedTypeNode>type);
} while (tn.skip(Token.Comma));
} else {
let type = this.parseType(tn);
if (!type) return null;
if (type.kind != NodeKind.NamedType) {
this.error(
DiagnosticCode.Identifier_expected,
type.range
);
return null;
}
extendsType = <NamedTypeNode>type;
}
extendsType = <NamedTypeNode>type;
}

let implementsTypes: NamedTypeNode[] | null = null;
if (tn.skip(Token.Implements)) {
if (isInterface) {
this.error(
Expand Down Expand Up @@ -1757,14 +1777,14 @@ export class Parser extends DiagnosticEmitter {
let members = new Array<DeclarationStatement>();
let declaration: ClassDeclaration;
if (isInterface) {
assert(!implementsTypes);
assert(!extendsType);
declaration = Node.createInterfaceDeclaration(
identifier,
decorators,
flags,
typeParameters,
extendsType,
null,
implementsTypes,
members,
tn.range(startPos, tn.pos)
);
Expand Down
143 changes: 84 additions & 59 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ export class Program extends DiagnosticEmitter {
break;
}
case NodeKind.InterfaceDeclaration: {
this.initializeInterface(<InterfaceDeclaration>statement, file, queuedExtends);
this.initializeInterface(<InterfaceDeclaration>statement, file, queuedImplements);
break;
}
case NodeKind.NamespaceDeclaration: {
Expand Down Expand Up @@ -1303,64 +1303,45 @@ export class Program extends DiagnosticEmitter {
}
}

// resolve prototypes of extended classes or interfaces
// resolve prototypes of extended classes
let resolver = this.resolver;
for (let i = 0, k = queuedExtends.length; i < k; ++i) {
let thisPrototype = queuedExtends[i];
assert(thisPrototype.kind == ElementKind.ClassPrototype);
let extendsNode = assert(thisPrototype.extendsNode); // must be present if in queuedExtends
let baseElement = resolver.resolveTypeName(extendsNode.name, null, thisPrototype.parent);
if (!baseElement) continue;
if (thisPrototype.kind == ElementKind.ClassPrototype) {
if (baseElement.kind == ElementKind.ClassPrototype) {
let basePrototype = <ClassPrototype>baseElement;
if (basePrototype.hasDecorator(DecoratorFlags.Final)) {
this.error(
DiagnosticCode.Class_0_is_final_and_cannot_be_extended,
extendsNode.range, basePrototype.identifierNode.text
);
}
if (
basePrototype.hasDecorator(DecoratorFlags.Unmanaged) !=
thisPrototype.hasDecorator(DecoratorFlags.Unmanaged)
) {
this.error(
DiagnosticCode.Unmanaged_classes_cannot_extend_managed_classes_and_vice_versa,
Range.join(thisPrototype.identifierNode.range, extendsNode.range)
);
}
if (!thisPrototype.extends(basePrototype)) {
thisPrototype.basePrototype = basePrototype;
} else {
this.error(
DiagnosticCode._0_is_referenced_directly_or_indirectly_in_its_own_base_expression,
basePrototype.identifierNode.range,
basePrototype.identifierNode.text,
);
}
} else {
if (baseElement.kind == ElementKind.ClassPrototype) {
let basePrototype = <ClassPrototype>baseElement;
if (basePrototype.hasDecorator(DecoratorFlags.Final)) {
this.error(
DiagnosticCode.A_class_may_only_extend_another_class,
extendsNode.range
DiagnosticCode.Class_0_is_final_and_cannot_be_extended,
extendsNode.range, basePrototype.identifierNode.text
);
}
} else if (thisPrototype.kind == ElementKind.InterfacePrototype) {
if (baseElement.kind == ElementKind.InterfacePrototype) {
const basePrototype = <InterfacePrototype>baseElement;
if (!thisPrototype.extends(basePrototype)) {
thisPrototype.basePrototype = basePrototype;
} else {
this.error(
DiagnosticCode._0_is_referenced_directly_or_indirectly_in_its_own_base_expression,
basePrototype.identifierNode.range,
basePrototype.identifierNode.text,
);
}
if (
basePrototype.hasDecorator(DecoratorFlags.Unmanaged) !=
thisPrototype.hasDecorator(DecoratorFlags.Unmanaged)
) {
this.error(
DiagnosticCode.Unmanaged_classes_cannot_extend_managed_classes_and_vice_versa,
Range.join(thisPrototype.identifierNode.range, extendsNode.range)
);
}
if (!thisPrototype.extends(basePrototype)) {
thisPrototype.basePrototype = basePrototype;
} else {
this.error(
DiagnosticCode.An_interface_can_only_extend_an_interface,
extendsNode.range
DiagnosticCode._0_is_referenced_directly_or_indirectly_in_its_own_base_expression,
basePrototype.identifierNode.range,
basePrototype.identifierNode.text,
);
}
} else {
this.error(
DiagnosticCode.A_class_may_only_extend_another_class,
extendsNode.range
);
}
}

Expand Down Expand Up @@ -1399,7 +1380,7 @@ export class Program extends DiagnosticEmitter {
}
}

// resolve prototypes of implemented interfaces
// resolve prototypes of implemented/extended interfaces
for (let i = 0, k = queuedImplements.length; i < k; ++i) {
let thisPrototype = queuedImplements[i];
let implementsNodes = assert(thisPrototype.implementsNodes); // must be present if in queuedImplements
Expand All @@ -1411,10 +1392,23 @@ export class Program extends DiagnosticEmitter {
let interfacePrototype = <InterfacePrototype>interfaceElement;
let interfacePrototypes = thisPrototype.interfacePrototypes;
if (!interfacePrototypes) thisPrototype.interfacePrototypes = interfacePrototypes = new Array();
interfacePrototypes.push(interfacePrototype);
if (
thisPrototype.kind == ElementKind.Interface &&
thisPrototype.implements(interfacePrototype)
) {
this.error(
DiagnosticCode._0_is_referenced_directly_or_indirectly_in_its_own_base_expression,
interfacePrototype.identifierNode.range,
interfacePrototype.identifierNode.text,
);
} else {
interfacePrototypes.push(interfacePrototype);
}
} else {
this.error(
DiagnosticCode.A_class_can_only_implement_an_interface,
thisPrototype.kind == ElementKind.InterfacePrototype
? DiagnosticCode.An_interface_can_only_extend_an_interface
: DiagnosticCode.A_class_can_only_implement_an_interface,
implementsNode.range
);
}
Expand Down Expand Up @@ -2474,7 +2468,7 @@ export class Program extends DiagnosticEmitter {
break;
}
case NodeKind.InterfaceDeclaration: {
element = this.initializeInterface(<InterfaceDeclaration>declaration, parent, queuedExtends);
element = this.initializeInterface(<InterfaceDeclaration>declaration, parent, queuedImplements);
break;
}
case NodeKind.NamespaceDeclaration: {
Expand Down Expand Up @@ -2625,7 +2619,7 @@ export class Program extends DiagnosticEmitter {
/** Parent element, usually a file or namespace. */
parent: Element,
/** So far queued `extends` clauses. */
queuedExtends: ClassPrototype[],
queuedImplements: ClassPrototype[],
): InterfacePrototype | null {
let name = declaration.name.text;
let element = new InterfacePrototype(
Expand All @@ -2638,8 +2632,8 @@ export class Program extends DiagnosticEmitter {
);
if (!parent.add(name, element)) return null;

// remember interfaces that extend another interface
if (declaration.extendsType) queuedExtends.push(element);
// remember interfaces that extend other interfaces
if (declaration.implementsTypes) queuedImplements.push(element);

let memberDeclarations = declaration.members;
for (let i = 0, k = memberDeclarations.length; i < k; ++i) {
Expand Down Expand Up @@ -2762,7 +2756,7 @@ export class Program extends DiagnosticEmitter {
break;
}
case NodeKind.InterfaceDeclaration: {
this.initializeInterface(<InterfaceDeclaration>member, original, queuedExtends);
this.initializeInterface(<InterfaceDeclaration>member, original, queuedImplements);
break;
}
case NodeKind.NamespaceDeclaration: {
Expand Down Expand Up @@ -4298,6 +4292,24 @@ export class ClassPrototype extends DeclaredElement {
return false;
}

implements(other: InterfacePrototype, seen: Set<InterfacePrototype> | null = null): bool {
if (this.interfacePrototypes) {
if (!seen) seen = new Set();
let interfacePrototypes = assert(this.interfacePrototypes);

for (let i = 0, k = interfacePrototypes.length; i < k; ++i) {
let prototype = unchecked(interfacePrototypes[i]);

if (prototype == other) return true;
if (seen.has(prototype)) continue;
seen.add(prototype);

if (prototype.implements(other, seen)) return true;
}
}
return false;
}

/** Adds an element as an instance member of this one. Returns the previous element if a duplicate. */
addInstance(name: string, element: DeclaredElement): bool {
let originalDeclaration = element.declaration;
Expand Down Expand Up @@ -4557,9 +4569,11 @@ export class Class extends TypedElement {
// Start with the interface itself, adding this class and its extenders to
// its implementers. Repeat for the interface's bases that are indirectly
// implemented by means of being extended by the interface.
let nextIface: Interface | null = iface;
// TODO: Maybe add a fast path when `iface` has no bases?
let ifaceStack = [iface];
let extenders = this.extenders;
do {
let nextIface = assert(ifaceStack.pop());
let implementers = nextIface.implementers;
if (!implementers) nextIface.implementers = implementers = new Set();
implementers.add(this);
Expand All @@ -4569,8 +4583,19 @@ export class Class extends TypedElement {
implementers.add(extender);
}
}
nextIface = <Interface | null>nextIface.base;
} while (nextIface);

let nextIfaces = nextIface.interfaces;
if (!nextIfaces) continue;

let stackIndex = ifaceStack.length;

// Calls the internal ensureCapacity() when run in the bootstrapped compiler:
ifaceStack.length = stackIndex + nextIfaces.size;

for (let _values = Set_values(nextIfaces), i = 0, k = _values.length; i < k; ++i) {
ifaceStack[stackIndex++] = unchecked(_values[i]);
}
HerrCai0907 marked this conversation as resolved.
Show resolved Hide resolved
} while (ifaceStack.length);
}

/** Adds an interface. */
Expand All @@ -4589,7 +4614,7 @@ export class Class extends TypedElement {
if (target.isInterface) {
if (this.isInterface) {
// targetInterface = thisInterface
return this == target || this.extends(target);
return this == target || this.implements(<Interface>target);
} else {
// targetInterface = thisClass
return this.implements(<Interface>target);
Expand Down Expand Up @@ -4863,7 +4888,7 @@ export class Class extends TypedElement {
return true;
}

/** Tests if this class or interface extends the given class or interface. */
/** Tests if this class extends the given class. */
extends(other: Class): bool {
return other.hasExtender(this);
}
Expand Down
Loading
Loading