Map MuJS interface types to MuIL

This change maps MuJS interface types to MuIL, including property and
method declarations.  The mapping is fairly straightforward, and similar
to the existing class lowering, except for some minor AST differences in
the TypeScript representations.  We emit interface types as classes with
the interface bit set, and all method declarations being abstract.
This commit is contained in:
joeduffy 2017-01-12 12:12:29 -08:00
parent 069ab369e9
commit c3027f6822
10 changed files with 244 additions and 41 deletions

View file

@ -298,8 +298,7 @@ export class Transformer {
case ts.SyntaxKind.ExportDeclaration:
return [ this.transformExportDeclaration(<ts.ExportDeclaration>node) ];
case ts.SyntaxKind.ImportDeclaration:
// TODO: register the import name so we can "mangle" any references to it accordingly later on.
return contract.fail("NYI");
return [ this.transformImportDeclaration(<ts.ImportDeclaration>node) ];
// Handle declarations; each of these results in a definition.
case ts.SyntaxKind.ClassDeclaration:
@ -561,33 +560,12 @@ export class Transformer {
}
}
// A common routine for transforming FunctionLikeDeclarations. The return is specialized per callsite, since it
// will differ slightly between module methods, class methods, lambdas, and so on.
private transformFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): FunctionLikeDeclaration {
// Ensure we are dealing with the supported subset of functions.
if (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Async) {
this.diagnostics.push(this.dctx.newAsyncNotSupportedError(node));
}
private transformFunctionLikeCommon(node: ts.FunctionLikeDeclaration): FunctionLikeDeclaration {
if (!!node.asteriskToken) {
this.diagnostics.push(this.dctx.newGeneratorsNotSupportedError(node.asteriskToken));
}
// First transform the name into an identifier. In the absence of a name, we will proceed under the assumption
// that it is the default export. This should be verified later on.
let name: ast.Identifier;
if (node.name) {
name = this.transformPropertyName(node.name);
}
else if (node.kind === ts.SyntaxKind.Constructor) {
// Constructors have a special name.
name = ident(symbols.specialFunctionConstructor);
}
else {
// All others are assumed to be default exports.
name = ident(defaultExport);
}
// Now visit the body; it can either be a block or a free-standing expression.
// First, visit the body; it can either be a block or a free-standing expression.
let body: ast.Block | undefined;
if (node.body) {
switch (node.body.kind) {
@ -611,6 +589,32 @@ export class Transformer {
break;
}
}
return this.transformFunctionLikeOrSignatureCommon(node, body);
}
// A common routine for transforming FunctionLikeDeclarations and MethodSignatures. The return is specialized per
// callsite, since differs slightly between module methods, class and interface methods, lambdas, and so on.
private transformFunctionLikeOrSignatureCommon(
node: ts.FunctionLikeDeclaration | ts.MethodSignature, body?: ast.Block): FunctionLikeDeclaration {
// Ensure we are dealing with the supported subset of functions.
if (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Async) {
this.diagnostics.push(this.dctx.newAsyncNotSupportedError(node));
}
// First transform the name into an identifier. In the absence of a name, we will proceed under the assumption
// that it is the default export. This should be verified later on.
let name: ast.Identifier;
if (node.name) {
name = this.transformPropertyName(node.name);
}
else if (node.kind === ts.SyntaxKind.Constructor) {
// Constructors have a special name.
name = ident(symbols.specialFunctionConstructor);
}
else {
// All others are assumed to be default exports.
name = ident(defaultExport);
}
// Next transform the parameter variables into locals.
let parameters: VariableDeclaration<ast.LocalVariable>[] = node.parameters.map(
@ -634,7 +638,7 @@ export class Transformer {
private transformModuleFunctionDeclaration(
node: ts.FunctionDeclaration, access: symbols.Accessibility): ast.ModuleMethod {
let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node);
let decl: FunctionLikeDeclaration = this.transformFunctionLikeCommon(node);
return this.withLocation(node, <ast.ModuleMethod>{
kind: ast.moduleMethodKind,
name: decl.name,
@ -645,8 +649,38 @@ export class Transformer {
});
}
// transformInterfaceDeclaration turns a TypeScript interface into a MuIL interface class.
private transformInterfaceDeclaration(node: ts.InterfaceDeclaration, access: symbols.Accessibility): ast.Class {
return contract.fail("NYI");
// TODO(joe): generics.
// TODO(joe): decorators.
// TODO(joe): extends/implements.
// Transform all valid members for this declaration into ClassMembers.
let members: ast.ClassMembers = {};
for (let member of node.members) {
if (member.kind !== ts.SyntaxKind.MissingDeclaration) {
let decl: ast.ClassMember;
let element: ClassElement = this.transformTypeElement(member);
if (isVariableDeclaration(element)) {
let vardecl = <VariableDeclaration<ast.ClassProperty>>element;
contract.assert(!vardecl.initializer, "Interface properties do not have initializers");
decl = vardecl.variable;
}
else {
decl = <ast.ClassMember>element;
}
members[decl.name.ident] = decl;
}
}
return this.withLocation(node, <ast.Class>{
kind: ast.classKind,
name: this.transformIdentifier(node.name),
access: access,
members: members,
interface: true,
});
}
private transformModuleDeclaration(node: ts.ModuleDeclaration, access: symbols.Accessibility): ast.Module {
@ -791,27 +825,41 @@ export class Transformer {
return node.declarations.map((decl: ts.VariableDeclaration) => this.transformVariableDeclaration(decl));
}
/** Classes **/
/** Class/type elements **/
private transformClassElement(node: ts.ClassElement): ClassElement {
switch (node.kind) {
// All the function-like members go here:
case ts.SyntaxKind.Constructor:
return this.transformClassElementFunctionLike(<ts.ConstructorDeclaration>node);
return this.transformFunctionLikeDeclaration(<ts.ConstructorDeclaration>node);
case ts.SyntaxKind.MethodDeclaration:
return this.transformClassElementFunctionLike(<ts.MethodDeclaration>node);
return this.transformFunctionLikeDeclaration(<ts.MethodDeclaration>node);
case ts.SyntaxKind.GetAccessor:
return this.transformClassElementFunctionLike(<ts.GetAccessorDeclaration>node);
return this.transformFunctionLikeDeclaration(<ts.GetAccessorDeclaration>node);
case ts.SyntaxKind.SetAccessor:
return this.transformClassElementFunctionLike(<ts.SetAccessorDeclaration>node);
return this.transformFunctionLikeDeclaration(<ts.SetAccessorDeclaration>node);
// Properties are not function-like, so we translate them differently.
case ts.SyntaxKind.PropertyDeclaration:
return this.transformClassElementProperty(<ts.PropertyDeclaration>node);
return this.transformPropertyDeclarationOrSignature(<ts.PropertyDeclaration>node);
// Unrecognized cases:
case ts.SyntaxKind.SemicolonClassElement:
return contract.fail("Expected all SemiColonClassElements to be filtered out of AST tree");
default:
return contract.fail(`Unrecognized ClassElement node kind: ${ts.SyntaxKind[node.kind]}`);
}
}
// transformTypeElement turns a TypeScript type element, typically an interface member, into a MuIL class member.
private transformTypeElement(node: ts.TypeElement): ClassElement {
switch (node.kind) {
// Property and method signatures are like their class counterparts, but have no bodies:
case ts.SyntaxKind.PropertySignature:
return this.transformPropertyDeclarationOrSignature(<ts.PropertySignature>node);
case ts.SyntaxKind.MethodSignature:
return this.transformMethodSignature(<ts.MethodSignature>node);
default:
return contract.fail(`Unrecognized TypeElement node kind: ${ts.SyntaxKind[node.kind]}`);
}
@ -831,13 +879,13 @@ export class Transformer {
}
}
private transformClassElementFunctionLike(node: ts.FunctionLikeDeclaration): ast.ClassMethod {
private transformFunctionLikeDeclaration(node: ts.FunctionLikeDeclaration): ast.ClassMethod {
// Get/Set accessors aren't yet supported.
contract.assert(node.kind !== ts.SyntaxKind.GetAccessor, "GetAccessor NYI");
contract.assert(node.kind !== ts.SyntaxKind.SetAccessor, "SetAccessor NYI");
let mods: ts.ModifierFlags = ts.getCombinedModifierFlags(node);
let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node);
let decl: FunctionLikeDeclaration = this.transformFunctionLikeCommon(node);
return this.withLocation(node, <ast.ClassMethod>{
kind: ast.classMethodKind,
name: decl.name,
@ -850,7 +898,8 @@ export class Transformer {
});
}
private transformClassElementProperty(node: ts.PropertyDeclaration): VariableDeclaration<ast.ClassProperty> {
private transformPropertyDeclarationOrSignature(
node: ts.PropertyDeclaration | ts.PropertySignature): VariableDeclaration<ast.ClassProperty> {
let initializer: ast.Expression | undefined;
if (node.initializer) {
initializer = this.transformExpression(node.initializer);
@ -873,6 +922,18 @@ export class Transformer {
);
}
private transformMethodSignature(node: ts.MethodSignature): ast.ClassMethod {
let decl: FunctionLikeDeclaration = this.transformFunctionLikeOrSignatureCommon(node);
return this.withLocation(node, <ast.ClassMethod>{
kind: ast.classMethodKind,
name: decl.name,
access: this.getClassAccessibility(node),
parameters: decl.parameters,
returnType: decl.returnType,
abstract: true,
});
}
/** Control flow statements **/
private transformBreakStatement(node: ts.BreakStatement): ast.BreakStatement {

View file

@ -20,6 +20,8 @@ let testCases: string[] = [
"modules/func_exp_def_1",
"modules/class_1",
"modules/class_exp_1",
"modules/iface_1",
"modules/iface_exp_1",
// These are not quite real-world-code, but they are more complex "integration" style tests.
"scenarios/point",
@ -55,7 +57,7 @@ describe("outputs", () => {
for (let i = 0; i < output.diagnostics.length; i++) {
actualMessages.push(output.formatDiagnostic(i));
}
compareLines(actualMessages, expectedMessages);
compareLines(actualMessages, expectedMessages, "messages");
// Next, see if there is an expected program tree (possibly none in the case of fatal errors).
let expectedOutputTree: string | undefined;
@ -76,7 +78,7 @@ describe("outputs", () => {
// Do a line-by-line comparison to make debugging failures nicer.
let actualLines: string[] = mupackTreeText.split("\n");
let expectLines: string[] = expectedOutputTree.split("\n");
compareLines(actualLines, expectLines);
compareLines(actualLines, expectLines, "AST");
}
else {
assert(false, "Expected an empty program tree, but one was returned");
@ -89,7 +91,7 @@ describe("outputs", () => {
}
});
function compareLines(actuals: string[], expects: string[]): void {
function compareLines(actuals: string[], expects: string[], label: string): void {
let mismatches: { num: number, actual: string, expect: string }[] = [];
for (let i = 0; i < actuals.length && i < expects.length; i++) {
if (actuals[i] !== expects[i]) {
@ -108,8 +110,8 @@ function compareLines(actuals: string[], expects: string[]): void {
actual += `${mismatch.num}: ${mismatch.actual}${os.EOL}`;
expect += `${mismatch.num}: ${mismatch.expect}${os.EOL}`;
}
assert.strictEqual(actual, expect, `Expected messages to match; ${mismatches.length} did not`);
assert.strictEqual(actual, expect, `Expected ${label} to match; ${mismatches.length} did not`);
}
assert.strictEqual(actuals.length, expects.length, "Expected message count to match");
assert.strictEqual(actuals.length, expects.length, `Expected ${label} line count to match`);
}

View file

@ -0,0 +1,4 @@
{
"name": "iface_1"
}

View file

@ -0,0 +1,57 @@
{
"name": "iface_1",
"modules": {
"index": {
"kind": "Module",
"name": {
"kind": "Identifier",
"ident": "index"
},
"members": {
"I": {
"kind": "Class",
"name": {
"kind": "Identifier",
"ident": "I",
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 10
},
"end": {
"line": 1,
"column": 11
}
}
},
"access": "private",
"members": {},
"interface": true,
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 2,
"column": 1
}
}
}
},
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 4,
"column": 0
}
}
}
}
}

View file

@ -0,0 +1,3 @@
interface I {
}

View file

@ -0,0 +1,6 @@
{
"files": [
"index.ts"
]
}

View file

@ -0,0 +1,4 @@
{
"name": "iface_exp_1"
}

View file

@ -0,0 +1,57 @@
{
"name": "iface_exp_1",
"modules": {
"index": {
"kind": "Module",
"name": {
"kind": "Identifier",
"ident": "index"
},
"members": {
"I": {
"kind": "Class",
"name": {
"kind": "Identifier",
"ident": "I",
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 18
}
}
},
"access": "public",
"members": {},
"interface": true,
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 2,
"column": 1
}
}
}
},
"loc": {
"file": "index.ts",
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 4,
"column": 0
}
}
}
}
}

View file

@ -0,0 +1,3 @@
export interface I {
}

View file

@ -0,0 +1,6 @@
{
"files": [
"index.ts"
]
}