diff --git a/tools/mujs/lib/compiler/transform.ts b/tools/mujs/lib/compiler/transform.ts index 2821b32f8..b84864ca7 100644 --- a/tools/mujs/lib/compiler/transform.ts +++ b/tools/mujs/lib/compiler/transform.ts @@ -298,8 +298,7 @@ export class Transformer { case ts.SyntaxKind.ExportDeclaration: return [ this.transformExportDeclaration(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(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[] = 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, { 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 = >element; + contract.assert(!vardecl.initializer, "Interface properties do not have initializers"); + decl = vardecl.variable; + } + else { + decl = element; + } + + members[decl.name.ident] = decl; + } + } + + return this.withLocation(node, { + 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(node); + return this.transformFunctionLikeDeclaration(node); case ts.SyntaxKind.MethodDeclaration: - return this.transformClassElementFunctionLike(node); + return this.transformFunctionLikeDeclaration(node); case ts.SyntaxKind.GetAccessor: - return this.transformClassElementFunctionLike(node); + return this.transformFunctionLikeDeclaration(node); case ts.SyntaxKind.SetAccessor: - return this.transformClassElementFunctionLike(node); + return this.transformFunctionLikeDeclaration(node); // Properties are not function-like, so we translate them differently. case ts.SyntaxKind.PropertyDeclaration: - return this.transformClassElementProperty(node); + return this.transformPropertyDeclarationOrSignature(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(node); + case ts.SyntaxKind.MethodSignature: + return this.transformMethodSignature(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, { kind: ast.classMethodKind, name: decl.name, @@ -850,7 +898,8 @@ export class Transformer { }); } - private transformClassElementProperty(node: ts.PropertyDeclaration): VariableDeclaration { + private transformPropertyDeclarationOrSignature( + node: ts.PropertyDeclaration | ts.PropertySignature): VariableDeclaration { 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, { + 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 { diff --git a/tools/mujs/tests/output/index.ts b/tools/mujs/tests/output/index.ts index 17e863d57..2369473a8 100644 --- a/tools/mujs/tests/output/index.ts +++ b/tools/mujs/tests/output/index.ts @@ -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`); } diff --git a/tools/mujs/tests/output/modules/iface_1/Mu.json b/tools/mujs/tests/output/modules/iface_1/Mu.json new file mode 100644 index 000000000..1172541da --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_1/Mu.json @@ -0,0 +1,4 @@ +{ + "name": "iface_1" +} + diff --git a/tools/mujs/tests/output/modules/iface_1/Mu.out.json b/tools/mujs/tests/output/modules/iface_1/Mu.out.json new file mode 100644 index 000000000..a16a70b98 --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_1/Mu.out.json @@ -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 + } + } + } + } +} diff --git a/tools/mujs/tests/output/modules/iface_1/index.ts b/tools/mujs/tests/output/modules/iface_1/index.ts new file mode 100644 index 000000000..696f06998 --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_1/index.ts @@ -0,0 +1,3 @@ +interface I { +} + diff --git a/tools/mujs/tests/output/modules/iface_1/tsconfig.json b/tools/mujs/tests/output/modules/iface_1/tsconfig.json new file mode 100644 index 000000000..c74ac439c --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_1/tsconfig.json @@ -0,0 +1,6 @@ +{ + "files": [ + "index.ts" + ] +} + diff --git a/tools/mujs/tests/output/modules/iface_exp_1/Mu.json b/tools/mujs/tests/output/modules/iface_exp_1/Mu.json new file mode 100644 index 000000000..bd5ac453a --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_exp_1/Mu.json @@ -0,0 +1,4 @@ +{ + "name": "iface_exp_1" +} + diff --git a/tools/mujs/tests/output/modules/iface_exp_1/Mu.out.json b/tools/mujs/tests/output/modules/iface_exp_1/Mu.out.json new file mode 100644 index 000000000..8a3aeb8f4 --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_exp_1/Mu.out.json @@ -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 + } + } + } + } +} diff --git a/tools/mujs/tests/output/modules/iface_exp_1/index.ts b/tools/mujs/tests/output/modules/iface_exp_1/index.ts new file mode 100644 index 000000000..907c383fd --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_exp_1/index.ts @@ -0,0 +1,3 @@ +export interface I { +} + diff --git a/tools/mujs/tests/output/modules/iface_exp_1/tsconfig.json b/tools/mujs/tests/output/modules/iface_exp_1/tsconfig.json new file mode 100644 index 000000000..c74ac439c --- /dev/null +++ b/tools/mujs/tests/output/modules/iface_exp_1/tsconfig.json @@ -0,0 +1,6 @@ +{ + "files": [ + "index.ts" + ] +} +