Skip to content

Commit

Permalink
Add support for interfaces extending multiple interfaces
Browse files Browse the repository at this point in the history
This is a feature in TypeScript, and I didn't see much of a technical
reason to disallow it. By changing interface extension such that
implementsTypes and interfacePrototypes are used for base interfaces
instead of extendsType and basePrototype in InterfacePrototype and
Interface respectively, and by modifying the parser, existing code
doesn't seem to break, and multiple base interfaces are possible (if not
working already).

There was also a small change to the instanceof helper generation, where
arrays are now used instead of Sets, since I needed to filter for
interfaces, and Set_values was used on the constructed Set regardless.
However, the change also modified the order of instanceof checks as seen
in instanceof.debug.wat. The instanceof.release.wat file underwent more
drastic changes, but it still appears to work anyway.
  • Loading branch information
CountBleck committed Jul 24, 2023
1 parent 382aabe commit b0205ef
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 137 deletions.
23 changes: 13 additions & 10 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7683,23 +7683,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 @@ -1703,20 +1703,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 @@ -1752,14 +1772,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
145 changes: 86 additions & 59 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,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 @@ -1302,64 +1302,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, 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 @@ -1398,7 +1379,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 @@ -1410,10 +1391,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 @@ -2473,7 +2467,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 @@ -2624,7 +2618,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 @@ -2637,8 +2631,10 @@ 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
// Note: See the corresponding note in parseClassOrInterface (in parser.ts) for
// further information as to why implementsTypes is used.
if (declaration.implementsTypes) queuedImplements.push(element);

let memberDeclarations = declaration.members;
for (let i = 0, k = memberDeclarations.length; i < k; ++i) {
Expand Down Expand Up @@ -2757,7 +2753,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 @@ -4270,6 +4266,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 @@ -4529,9 +4543,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 @@ -4541,8 +4557,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]);
}
} while (ifaceStack.length);
}

/** Adds an interface. */
Expand All @@ -4561,7 +4588,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 @@ -4835,7 +4862,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

0 comments on commit b0205ef

Please sign in to comment.