diff --git a/tools/mujs/lib/compiler/transform.ts b/tools/mujs/lib/compiler/transform.ts index 5f6e28c27..70a269c26 100644 --- a/tools/mujs/lib/compiler/transform.ts +++ b/tools/mujs/lib/compiler/transform.ts @@ -133,6 +133,15 @@ function isComputed(name: ts.Node | undefined): boolean { return false; } +// Translates a TypeScript position into a MuIL position (0 to 1 based lines). +function mapPosition(s: ts.SourceFile, p: number): ast.Position { + let lc = s.getLineAndCharacterOfPosition(p); + return { + line: lc.line + 1, // transform to 1-based line number + column: lc.character, + }; +} + // A transpiler is responsible for transforming TypeScript program artifacts into MuPack/MuIL AST forms. export class Transpiler { private meta: pack.Metadata; // the package's metadata. @@ -175,36 +184,63 @@ export class Transpiler { /** Helpers **/ // This annotates a given MuPack/MuIL node with another TypeScript node's source position information. - private copyLocation(src: ts.Node, dst: T): T { - let pos = (s: ts.SourceFile, p: number) => { - // Translate a TypeScript position into a MuIL position (0 to 1 based lines). - let lc = s.getLineAndCharacterOfPosition(p); - return { - line: lc.line + 1, // transform to 1-based line number - column: lc.character, - }; - }; - + private addLocation(src: ts.Node, dst: T): T { // Turn the source file name into one relative to the current root path. let s: ts.SourceFile = src.getSourceFile(); let relativePath: string = fspath.relative(this.comp.root, s.fileName); - dst.loc = { file: relativePath, - start: pos(s, src.getStart()), - end: pos(s, src.getEnd()), + start: mapPosition(s, src.getStart()), + end: mapPosition(s, src.getEnd()), }; // Despite mutating in place, we return the node to facilitate a more fluent style. return dst; } + // This annotates a given MuPack/MuIL node with a range of TypeScript node source positions. + private addLocationRange(start: ts.Node, end: ts.Node, dst: T): T { + contract.assert(start.getSourceFile() === end.getSourceFile()); + // Turn the source file name into one relative to the current root path. + let s: ts.SourceFile = start.getSourceFile(); + let relativePath: string = fspath.relative(this.comp.root, s.fileName); + dst.loc = { + file: relativePath, + start: mapPosition(s, start.getStart()), + end: mapPosition(s, end.getEnd()), + }; + + // Despite mutating in place, we return the node to facilitate a more fluent style. + return dst; + } + + // This just carries forward an existing location from another node. + private copyLocation(src: ast.Node, dst: T): T { + dst.loc = src.loc; + return dst; + } + + // This just carries forward an existing location range from another node. + private copyLocationRange(start: ast.Node, end: ast.Node, dst: T): T { + let sloc: ast.Location | undefined = start.loc || end.loc; + let eloc: ast.Location | undefined = end.loc || start.loc; + if (sloc !== undefined && eloc !== undefined) { + contract.assert(sloc.file === eloc.file); + dst.loc = { + file: sloc.file, + start: sloc.start, + end: eloc.end, + }; + } + return dst; + } + /** Transformations **/ /** Symbols **/ private transformIdentifier(node: ts.Identifier): ast.Identifier { - return this.copyLocation(node, ident(node.text)); + return this.addLocation(node, ident(node.text)); } /** Modules **/ @@ -268,7 +304,7 @@ export class Transpiler { moduleName = moduleName.substring(0, moduleExtIndex); } - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.moduleKind, name: ident(moduleName), members: members, @@ -440,7 +476,7 @@ export class Transpiler { if (statement.kind === ast.blockKind) { return statement; } - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.blockKind, statements: [ statement ], }); @@ -518,7 +554,7 @@ export class Transpiler { } let mods: ts.ModifierFlags = ts.getCombinedModifierFlags(node); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.classKind, name: name, access: access, @@ -626,7 +662,7 @@ export class Transpiler { private transformModuleFunctionDeclaration( node: ts.FunctionDeclaration, access: symbols.Accessibility): ast.ModuleMethod { let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.moduleMethodKind, name: decl.name, access: access, @@ -675,7 +711,7 @@ export class Transpiler { private makeVariableInitializer(decl: VariableDeclaration): ast.Statement { contract.requires(!!decl.initializer, "decl", "Expected variable declaration to have an initializer"); - return this.copyLocation(decl.node, { + return this.addLocation(decl.node, { kind: ast.binaryOperatorExpressionKind, left: { kind: ast.loadLocationExpressionKind, @@ -727,14 +763,21 @@ export class Transpiler { statements.push(this.makeVariableInitializer(vdecl)); } } - if (statements.length === 1) { - return statements[0]; + + contract.assert(statements.length > 0); + + if (statements.length > 1) { + return this.copyLocationRange( + statements[0], + statements[statements.length-1], + { + kind: ast.multiStatementKind, + statements: statements, + }, + ); } else { - return { - kind: ast.multiStatementKind, - statements: statements, - }; + return statements[0]; } } @@ -823,7 +866,7 @@ export class Transpiler { let mods: ts.ModifierFlags = ts.getCombinedModifierFlags(node); let decl: FunctionLikeDeclaration = this.transformFunctionLikeDeclaration(node); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.classMethodKind, name: decl.name, access: this.getClassAccessibility(node), @@ -861,7 +904,7 @@ export class Transpiler { /** Control flow statements **/ private transformBreakStatement(node: ts.BreakStatement): ast.BreakStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.breakStatementKind, label: object.maybeUndefined(node.label, (id: ts.Identifier) => this.transformIdentifier(id)), }); @@ -876,7 +919,7 @@ export class Transpiler { } private transformContinueStatement(node: ts.ContinueStatement): ast.ContinueStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.continueStatementKind, label: object.maybeUndefined(node.label, (id: ts.Identifier) => this.transformIdentifier(id)), }); @@ -885,7 +928,7 @@ export class Transpiler { // This transforms a higher-level TypeScript `do`/`while` statement by expanding into an ordinary `while` statement. private transformDoStatement(node: ts.DoStatement): ast.WhileStatement { // Now create a new block that simply concatenates the existing one with a test/`break`. - let body: ast.Block = this.copyLocation(node.statement, { + let body: ast.Block = this.addLocation(node.statement, { kind: ast.blockKind, statements: [ this.transformStatement(node.statement), @@ -903,7 +946,7 @@ export class Transpiler { ], }); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.whileStatementKind, test: { kind: ast.boolLiteralKind, @@ -930,7 +973,7 @@ export class Transpiler { } private transformIfStatement(node: ts.IfStatement): ast.IfStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.ifStatementKind, condition: this.transformExpression(node.expression), consequent: this.transformStatement(node.thenStatement), @@ -940,7 +983,7 @@ export class Transpiler { } private transformReturnStatement(node: ts.ReturnStatement): ast.ReturnStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.returnStatementKind, expression: object.maybeUndefined( node.expression, (expr: ts.Expression) => this.transformExpression(expr)), @@ -952,7 +995,7 @@ export class Transpiler { } private transformThrowStatement(node: ts.ThrowStatement): ast.ThrowStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.throwStatementKind, expression: this.transformExpression(node.expression), }); @@ -963,7 +1006,7 @@ export class Transpiler { } private transformWhileStatement(node: ts.WhileStatement): ast.WhileStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.whileStatementKind, test: this.transformExpression(node.expression), body: this.transformStatementAsBlock(node.statement), @@ -974,7 +1017,7 @@ export class Transpiler { private transformBlock(node: ts.Block): ast.Block { // TODO(joe): map directives. - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.blockKind, statements: node.statements.map((stmt: ts.Statement) => this.transformStatement(stmt)), }); @@ -983,26 +1026,26 @@ export class Transpiler { private transformDebuggerStatement(node: ts.DebuggerStatement): ast.Statement { // The debugger statement in ECMAScript can be used to trip a breakpoint. We don't have the equivalent in Mu at // the moment, so we simply produce an empty statement in its place. - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.emptyStatementKind, }); } private transformEmptyStatement(node: ts.EmptyStatement): ast.EmptyStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.emptyStatementKind, }); } private transformExpressionStatement(node: ts.ExpressionStatement): ast.ExpressionStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.expressionStatementKind, expression: this.transformExpression(node.expression), }); } private transformLabeledStatement(node: ts.LabeledStatement): ast.LabeledStatement { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.labeledStatementKind, label: this.transformIdentifier(node.label), statement: this.transformStatement(node.statement), @@ -1120,7 +1163,7 @@ export class Transpiler { // TODO: finish binary operator mapping; for any left that are unsupported, introduce a real error message. return contract.fail(`Unsupported binary operator: ${ts.SyntaxKind[node.operatorToken.kind]}`); } - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.binaryOperatorExpressionKind, operator: operator, left: this.transformExpression(node.left), @@ -1130,6 +1173,8 @@ export class Transpiler { private transformBinarySequenceExpression(node: ts.BinaryExpression): ast.SequenceExpression { contract.assert(node.operatorToken.kind === ts.SyntaxKind.CommaToken); + + // Pile up the expressions in the right order. let curr: ts.Expression = node; let binary: ts.BinaryExpression = node; let expressions: ast.Expression[] = []; @@ -1139,14 +1184,20 @@ export class Transpiler { curr = binary.left; } expressions.unshift(this.transformExpression(curr)); - return { - kind: ast.sequenceExpressionKind, - expressions: expressions, - }; + + contract.assert(expressions.length > 0); + return this.copyLocationRange( + expressions[0], + expressions[expressions.length-1], + { + kind: ast.sequenceExpressionKind, + expressions: expressions, + }, + ); } private transformCallExpression(node: ts.CallExpression): ast.InvokeFunctionExpression { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.invokeFunctionExpressionKind, function: this.transformExpression(node.expression), arguments: node.arguments.map((expr: ts.Expression) => this.transformExpression(expr)), @@ -1158,7 +1209,7 @@ export class Transpiler { } private transformConditionalExpression(node: ts.ConditionalExpression): ast.ConditionalExpression { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.conditionalExpressionKind, condition: this.transformExpression(node.condition), consequent: this.transformExpression(node.whenTrue), @@ -1175,13 +1226,13 @@ export class Transpiler { if (node.argumentExpression) { switch (node.argumentExpression.kind) { case ts.SyntaxKind.Identifier: - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.loadLocationExpressionKind, object: object, name: this.transformIdentifier(node.argumentExpression), }); default: - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.loadDynamicExpressionKind, object: object, name: this.transformExpression(node.argumentExpression), @@ -1218,7 +1269,7 @@ export class Transpiler { private transformPostfixUnaryExpression(node: ts.PostfixUnaryExpression): ast.UnaryOperatorExpression { let operator: ast.UnaryOperator | undefined = postfixUnaryOperators.get(node.operator); contract.assert(!!(operator = operator!)); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.unaryOperatorExpressionKind, postfix: true, operator: operator, @@ -1229,7 +1280,7 @@ export class Transpiler { private transformPrefixUnaryExpression(node: ts.PrefixUnaryExpression): ast.UnaryOperatorExpression { let operator: ast.UnaryOperator | undefined = prefixUnaryOperators.get(node.operator); contract.assert(!!(operator = operator!)); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.unaryOperatorExpressionKind, postfix: false, operator: operator, @@ -1238,7 +1289,7 @@ export class Transpiler { } private transformPropertyAccessExpression(node: ts.PropertyAccessExpression): ast.LoadLocationExpression { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.loadLocationExpressionKind, object: this.transformExpression(node.expression), name: this.transformIdentifier(node.name), @@ -1255,11 +1306,11 @@ export class Transpiler { return contract.fail("New T(..) expression must have an identifier T"); } - return { + return this.addLocation(node, { kind: ast.newExpressionKind, type: ty, arguments: node.arguments.map((expr: ts.Expression) => this.transformExpression(expr)), - }; + }); } private transformOmittedExpression(node: ts.OmittedExpression): ast.Expression { @@ -1275,7 +1326,7 @@ export class Transpiler { } private transformSuperExpression(node: ts.SuperExpression): ast.LoadLocationExpression { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.loadLocationExpressionKind, name: ident(symbols.specialVariableSuper), }); @@ -1290,7 +1341,7 @@ export class Transpiler { } private transformThisExpression(node: ts.ThisExpression): ast.LoadLocationExpression { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.loadLocationExpressionKind, name: ident(symbols.specialVariableThis), }); @@ -1312,7 +1363,7 @@ export class Transpiler { private transformBooleanLiteral(node: ts.BooleanLiteral): ast.BoolLiteral { contract.assert(node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.TrueKeyword); - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.boolLiteralKind, raw: node.getText(), value: (node.kind === ts.SyntaxKind.TrueKeyword), @@ -1324,14 +1375,14 @@ export class Transpiler { } private transformNullLiteral(node: ts.NullLiteral): ast.NullLiteral { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.nullLiteralKind, raw: node.getText(), }); } private transformNumericLiteral(node: ts.NumericLiteral): ast.NumberLiteral { - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.numberLiteralKind, raw: node.text, value: Number(node.text), @@ -1346,7 +1397,7 @@ export class Transpiler { // TODO: we need to dynamically populate the resulting object with ECMAScript-style string functions. It's not // yet clear how to do this in a way that facilitates inter-language interoperability. This is especially // challenging because most use of such functions will be entirely dynamic. - return this.copyLocation(node, { + return this.addLocation(node, { kind: ast.stringLiteralKind, raw: node.text, value: node.text, @@ -1382,7 +1433,7 @@ export class Transpiler { } private transformIdentifierExpression(node: ts.Identifier): ast.Identifier { - return this.copyLocation(node, ident(node.text)); + return this.addLocation(node, ident(node.text)); } private transformObjectBindingPattern(node: ts.ObjectBindingPattern): ast.Expression { diff --git a/tools/mujs/tests/output/scenarios/point/Mu.out.json b/tools/mujs/tests/output/scenarios/point/Mu.out.json index 6a0db9c1b..b2b2ce149 100644 --- a/tools/mujs/tests/output/scenarios/point/Mu.out.json +++ b/tools/mujs/tests/output/scenarios/point/Mu.out.json @@ -561,7 +561,18 @@ } } } - ] + ], + "loc": { + "file": "index.ts", + "start": { + "line": 14, + "column": 15 + }, + "end": { + "line": 14, + "column": 60 + } + } }, "loc": { "file": "index.ts",