[JS IR] Generate debug info for JS injections

Also, fix the JS parser to collect the "correct" debug info
(see the note in `translateJsCodeIntoStatementList`)

#KT-53361 Fixed
This commit is contained in:
Sergej Jaskiewicz
2022-08-02 13:32:26 +02:00
committed by Space
parent 746c1b5903
commit d57ddc5f83
12 changed files with 182 additions and 47 deletions
@@ -23,7 +23,7 @@ import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.backend.js.lower.JsInnerClassesSupport import org.jetbrains.kotlin.ir.backend.js.lower.JsInnerClassesSupport
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsPolyfills import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsPolyfills
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.parseJsCode import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.translateJsCodeIntoStatementList
import org.jetbrains.kotlin.ir.backend.js.utils.* import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.builders.declarations.addFunction import org.jetbrains.kotlin.ir.builders.declarations.addFunction
import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.*
@@ -391,9 +391,10 @@ class JsIrBackendContext(
val jsFunction = outlinedJsCodeFunctions[symbol] val jsFunction = outlinedJsCodeFunctions[symbol]
if (jsFunction != null) return jsFunction if (jsFunction != null) return jsFunction
val jsFunAnnotation = symbol.owner.getAnnotation(JsAnnotations.jsFunFqn) ?: return null val jsFunAnnotation = symbol.owner.getAnnotation(JsAnnotations.jsFunFqn) ?: return null
val jsCode = jsFunAnnotation.getSingleConstStringArgument() val jsCode = jsFunAnnotation.getValueArgument(0)
// FIXME: Provide debug info ?: compilationException("@JsFun annotation must contain an argument", jsFunAnnotation)
val statements = parseJsCode(jsCode) ?: compilationException("Could not parse JS code", jsFunAnnotation) val statements = translateJsCodeIntoStatementList(jsCode, this, symbol.owner)
?: compilationException("Could not parse JS code", jsFunAnnotation)
val parsedJsFunction = statements.singleOrNull() val parsedJsFunction = statements.singleOrNull()
?.safeAs<JsExpressionStatement>() ?.safeAs<JsExpressionStatement>()
?.expression ?.expression
@@ -134,7 +134,7 @@ private class JsCodeOutlineTransformer(
return null return null
val jsCodeArg = expression.getValueArgument(0) ?: compilationException("Expected js code string", expression) val jsCodeArg = expression.getValueArgument(0) ?: compilationException("Expected js code string", expression)
val jsStatements = translateJsCodeIntoStatementList(jsCodeArg, backendContext) ?: return null val jsStatements = translateJsCodeIntoStatementList(jsCodeArg, backendContext, container) ?: return null
// Collect used Kotlin local variables and parameters. // Collect used Kotlin local variables and parameters.
val scope = JsScopesCollector().apply { acceptList(jsStatements) } val scope = JsScopesCollector().apply { acceptList(jsStatements) }
@@ -69,7 +69,8 @@ class JsCallTransformer(private val jsOrJsFuncCall: IrCall, private val context:
context.checkIfJsCode(jsOrJsFuncCall.symbol) -> { context.checkIfJsCode(jsOrJsFuncCall.symbol) -> {
translateJsCodeIntoStatementList( translateJsCodeIntoStatementList(
jsOrJsFuncCall.getValueArgument(0) ?: compilationException("JsCode is expected", jsOrJsFuncCall), jsOrJsFuncCall.getValueArgument(0) ?: compilationException("JsCode is expected", jsOrJsFuncCall),
context.staticContext.backendContext context.staticContext.backendContext,
context.currentFile.fileEntry
) )
?: compilationException("Cannot compute js code", jsOrJsFuncCall) ?: compilationException("Cannot compute js code", jsOrJsFuncCall)
} }
@@ -5,9 +5,11 @@
package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.ir.backend.js.utils.getJsPolyfill import org.jetbrains.kotlin.ir.backend.js.utils.JsAnnotations
import org.jetbrains.kotlin.ir.backend.js.utils.hasJsPolyfill import org.jetbrains.kotlin.ir.backend.js.utils.hasJsPolyfill
import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.js.backend.ast.JsStatement import org.jetbrains.kotlin.js.backend.ast.JsStatement
class JsPolyfills { class JsPolyfills {
@@ -31,9 +33,11 @@ class JsPolyfills {
private fun Iterable<IrDeclaration>.asImplementationList() = private fun Iterable<IrDeclaration>.asImplementationList() =
asSequence().asImplementationList() asSequence().asImplementationList()
private fun Sequence<IrDeclaration>.asImplementationList(): List<JsStatement> = @Suppress("UNCHECKED_CAST")
map { it.getJsPolyfill()!! } private fun Sequence<IrDeclaration>.asImplementationList(): List<JsStatement> {
.distinct() return map { it to it.getAnnotation(JsAnnotations.JsPolyfillFqn)!!.getValueArgument(0)!! }
.flatMap { parseJsCode(it).orEmpty() } // FIXME!!! .distinctBy { (it.second as IrConst<String>).value }
.flatMap { (container, polyfill) -> translateJsCodeIntoStatementList(polyfill, null, container).orEmpty() }
.toList() .toList()
}
} }
@@ -7,10 +7,13 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.backend.common.compilationException import org.jetbrains.kotlin.backend.common.compilationException
import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrFileEntry
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsStatementOrigins import org.jetbrains.kotlin.ir.backend.js.JsStatementOrigins
import org.jetbrains.kotlin.ir.backend.js.utils.* import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.*
@@ -491,15 +494,22 @@ internal fun <T : JsNode> T.withSource(node: IrElement, context: JsGenerationCon
private inline fun <T : JsNode> T.addSourceInfoIfNeed(node: IrElement, context: JsGenerationContext) { private inline fun <T : JsNode> T.addSourceInfoIfNeed(node: IrElement, context: JsGenerationContext) {
if (!context.staticContext.genSourcemaps) return if (!context.staticContext.genSourcemaps) return
if (node.startOffset == UNDEFINED_OFFSET || node.endOffset == UNDEFINED_OFFSET) return val sourceInfo = node.getSourceInfo(context.currentFile.fileEntry) ?: return
val fileEntry = context.currentFile.fileEntry
val path = fileEntry.name
val startLine = fileEntry.getLineNumber(node.startOffset)
val startColumn = fileEntry.getColumnNumber(node.startOffset)
// TODO maybe it's better to fix in JsExpressionStatement // TODO maybe it's better to fix in JsExpressionStatement
val locationTarget = if (this is JsExpressionStatement) this.expression else this val locationTarget = if (this is JsExpressionStatement) this.expression else this
locationTarget.source = JsLocation(path, startLine, startColumn) locationTarget.source = sourceInfo
}
fun IrElement.getSourceInfo(container: IrDeclaration): JsLocation? {
val fileEntry = container.fileOrNull?.fileEntry ?: return null
return getSourceInfo(fileEntry)
}
fun IrElement.getSourceInfo(fileEntry: IrFileEntry): JsLocation? {
if (startOffset == UNDEFINED_OFFSET || endOffset == UNDEFINED_OFFSET) return null
val path = fileEntry.name
val startLine = fileEntry.getLineNumber(startOffset)
val startColumn = fileEntry.getColumnNumber(startOffset)
return JsLocation(path, startLine, startColumn)
} }
@@ -8,27 +8,44 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import com.google.gwt.dev.js.ThrowExceptionOnErrorReporter import com.google.gwt.dev.js.ThrowExceptionOnErrorReporter
import com.google.gwt.dev.js.rhino.CodePosition import com.google.gwt.dev.js.rhino.CodePosition
import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrFileEntry
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.lower.PropertyLazyInitLowering import org.jetbrains.kotlin.ir.backend.js.lower.PropertyLazyInitLowering
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrVariable import org.jetbrains.kotlin.ir.declarations.IrVariable
import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.util.fileOrNull
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.js.backend.ast.JsFunctionScope import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.JsProgram
import org.jetbrains.kotlin.js.backend.ast.JsRootScope
import org.jetbrains.kotlin.js.backend.ast.JsStatement
import org.jetbrains.kotlin.js.parser.parseExpressionOrStatement import org.jetbrains.kotlin.js.parser.parseExpressionOrStatement
// Returns null if constant expression could not be parsed /**
fun translateJsCodeIntoStatementList(code: IrExpression, context: JsIrBackendContext): List<JsStatement>? { * Returns null if constant expression could not be parsed.
// TODO: support proper symbol linkage and label clash resolution */
// FIXME: Provide debug info fun translateJsCodeIntoStatementList(code: IrExpression, context: JsIrBackendContext?, container: IrDeclaration) =
return parseJsCode(foldString(code, context) ?: return null) translateJsCodeIntoStatementList(
} code,
context,
code.getSourceInfo(container) ?: container.fileOrNull?.fileEntry?.let { JsLocation(it.name, 0, 0) }
)
/**
* Returns null if constant expression could not be parsed.
*/
fun translateJsCodeIntoStatementList(code: IrExpression, context: JsIrBackendContext?, fileEntry: IrFileEntry) =
translateJsCodeIntoStatementList(code, context, code.getSourceInfo(fileEntry) ?: JsLocation(fileEntry.name, 0, 0))
private fun translateJsCodeIntoStatementList(
code: IrExpression,
context: JsIrBackendContext?,
sourceInfo: JsLocation?
): List<JsStatement>? {
// TODO: support proper symbol linkage and label clash resolution
val (fileName, startLine, offset) = sourceInfo ?: JsLocation("<js-code>", 0, 0)
val jsCode = foldString(code, context) ?: return null
fun parseJsCode(jsCode: String): List<JsStatement>? {
// Parser can change local or global scope. // Parser can change local or global scope.
// In case of js we want to keep new local names, // In case of js we want to keep new local names,
// but no new global ones. // but no new global ones.
@@ -36,12 +53,17 @@ fun parseJsCode(jsCode: String): List<JsStatement>? {
val temporaryRootScope = JsRootScope(JsProgram()) val temporaryRootScope = JsRootScope(JsProgram())
val currentScope = JsFunctionScope(temporaryRootScope, "js") val currentScope = JsFunctionScope(temporaryRootScope, "js")
// TODO: write debug info, see how it's done in CallExpressionTranslator.parseJsCode // NOTE: emitting the correct debug info for JS injections is a non-trivial task, because the injection may consist of
// constant-evaluated strings, whose origin is hard to track in the JS parser. Also, the presence of explicit \n characters in
return parseExpressionOrStatement(jsCode, ThrowExceptionOnErrorReporter, currentScope, CodePosition(0, 0), "<js-code>") // the JS string literal breaks the debug info, because at this level we are unable to distinguish multiline string literals and
// single-line string literals with explicit \n in it.
//
// So we try to generate the debug info on the best-effort basis. It should work correctly with plain string literals without
// concatenations, interpolations or backslash replacements like \n.
return parseExpressionOrStatement(jsCode, ThrowExceptionOnErrorReporter, currentScope, CodePosition(startLine, offset), fileName)
} }
fun foldString(expression: IrExpression, context: JsIrBackendContext): String? { fun foldString(expression: IrExpression, context: JsIrBackendContext?): String? {
val builder = StringBuilder() val builder = StringBuilder()
var foldingFailed = false var foldingFailed = false
expression.acceptVoid(object : IrElementVisitorVoid { expression.acceptVoid(object : IrElementVisitorVoid {
@@ -65,7 +87,7 @@ fun foldString(expression: IrExpression, context: JsIrBackendContext): String? {
override fun visitGetField(expression: IrGetField) { override fun visitGetField(expression: IrGetField) {
val owner = expression.symbol.owner val owner = expression.symbol.owner
owner.initializer?.expression?.acceptVoid(this) owner.initializer?.expression?.acceptVoid(this)
?: context.fieldToInitializer[owner]?.acceptVoid(this) ?: context?.fieldToInitializer?.get(owner)?.acceptVoid(this)
} }
override fun visitCall(expression: IrCall) { override fun visitCall(expression: IrCall) {
@@ -101,12 +123,12 @@ fun foldString(expression: IrExpression, context: JsIrBackendContext): String? {
return builder.toString() return builder.toString()
} }
private class InitFunVisitor(private val context: JsIrBackendContext) : IrElementVisitorVoid { private class InitFunVisitor(private val context: JsIrBackendContext?) : IrElementVisitorVoid {
override fun visitElement(element: IrElement) { override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this) element.acceptChildrenVoid(this)
} }
override fun visitSetField(expression: IrSetField) { override fun visitSetField(expression: IrSetField) {
context.fieldToInitializer[expression.symbol.owner] = expression.value context?.fieldToInitializer?.set(expression.symbol.owner, expression.value)
} }
} }
@@ -51,9 +51,6 @@ fun IrAnnotationContainer.getJsQualifier(): String? =
fun IrAnnotationContainer.getJsName(): String? = fun IrAnnotationContainer.getJsName(): String? =
getAnnotation(JsAnnotations.jsNameFqn)?.getSingleConstStringArgument() getAnnotation(JsAnnotations.jsNameFqn)?.getSingleConstStringArgument()
fun IrAnnotationContainer.getJsPolyfill(): String? =
getAnnotation(JsAnnotations.JsPolyfillFqn)?.getSingleConstStringArgument()
fun IrAnnotationContainer.hasJsPolyfill(): Boolean = fun IrAnnotationContainer.hasJsPolyfill(): Boolean =
hasAnnotation(JsAnnotations.JsPolyfillFqn) hasAnnotation(JsAnnotations.JsPolyfillFqn)
+80
View File
@@ -0,0 +1,80 @@
// TARGET_BACKEND: JS_IR
// FILE: a.kt
@JsExport
fun exclamate(a: String) =
"$a!"
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@JsFun("""
function (a, b) {
return _.exclamate(a) +
_.exclamate(b);
}
""")
external fun exclamateAndConcat(a: String, b: String): String
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@JsPolyfill("""
if (typeof String.prototype.myAwesomePolyfill === "undefined") {
Object.defineProperty(String.prototype, "myAwesomePolyfill", {
value: function () {
return this + "!";
}
});
}
""")
internal inline fun String.myAwesomePolyfill(): Boolean =
asDynamic()
.myAwesomePolyfill()
val walter1VarDecl = "var walter1 = _.exclamate('Walter');"
val jesse1VarDecl = "var jesse1 = _.exclamate(jesse);"
// FILE: test.kt
fun noop() {}
fun box() {
noop()
exclamateAndConcat(
"hello",
"world")
val jesse = "Jesse"
js(
"_.exclamate(jesse);") // Local variable is captured
js(
"_.exclamate('Walter');") // No local variables captured
js(
walter1VarDecl +
"_.exclamate(walter1);")
js(
jesse1VarDecl +
"_.exclamate(jesse1);")
"foo".myAwesomePolyfill()
}
// EXPECTATIONS
// test.kt:39 box
// test.kt:41 box
// test.kt:42 box
// a.kt:11 box
// a.kt:6 exclamate
// a.kt:12 box
// a.kt:6 exclamate
// test.kt:43 box
// test.kt:45 box
// a.kt:6 exclamate
// test.kt:47 box
// a.kt:6 exclamate
// test.kt:49 box
// a.kt:6 exclamate
// test.kt:49 box
// a.kt:6 exclamate
// test.kt:52 box
// a.kt:6 exclamate
// test.kt:52 box
// a.kt:6 exclamate
// a.kt:29 box
// a.kt:22 value
@@ -67,6 +67,7 @@ final class LineBuffer {
LineBuffer(Reader in, CodePosition position) { LineBuffer(Reader in, CodePosition position) {
this.in = in; this.in = in;
this.lineno = position.getLine(); this.lineno = position.getLine();
this.columnno = position.getOffset();
this.lineStart = -position.getOffset(); this.lineStart = -position.getOffset();
} }
@@ -77,14 +78,16 @@ final class LineBuffer {
int c = buffer[offset]; int c = buffer[offset];
++offset; ++offset;
prevColumnno = columnno++;
if ((c & EOL_HINT_MASK) == 0) { if ((c & EOL_HINT_MASK) == 0) {
switch (c) { switch (c) {
case '\r': case '\r':
// if the next character is a newline, skip past it. // if the next character is a newline, skip past it.
if (offset != end) { if (offset != end) {
if (buffer[offset] == '\n') if (buffer[offset] == '\n') {
++offset; ++offset;
}
} else { } else {
// set a flag for fill(), in case the first char // set a flag for fill(), in case the first char
// of the next fill is a newline. // of the next fill is a newline.
@@ -95,6 +98,8 @@ final class LineBuffer {
prevStart = lineStart; prevStart = lineStart;
lineStart = offset; lineStart = offset;
lineno++; lineno++;
prevColumnno = columnno;
columnno = 0;
return '\n'; return '\n';
} }
} }
@@ -120,10 +125,12 @@ final class LineBuffer {
if (offset == 0) // Same as if (hitEOF) if (offset == 0) // Same as if (hitEOF)
return; return;
offset--; offset--;
columnno--;
int c = buffer[offset]; int c = buffer[offset];
if ((c & EOL_HINT_MASK) == 0 && isEndOfLine(c)) { if ((c & EOL_HINT_MASK) == 0 && isEndOfLine(c)) {
lineStart = prevStart; lineStart = prevStart;
lineno--; lineno--;
columnno = prevColumnno;
} }
} }
@@ -147,6 +154,7 @@ final class LineBuffer {
} }
++offset; ++offset;
prevColumnno = columnno++;
} }
int peek() throws IOException { int peek() throws IOException {
@@ -182,6 +190,7 @@ final class LineBuffer {
int c = buffer[offset]; int c = buffer[offset];
if (test == c) { if (test == c) {
++offset; ++offset;
prevColumnno = columnno++;
return true; return true;
} }
if (c < 128 || !formatChar(c)) { if (c < 128 || !formatChar(c)) {
@@ -292,7 +301,8 @@ final class LineBuffer {
// likely get to EOF for real) by doing yet another fill(). // likely get to EOF for real) by doing yet another fill().
if (lastWasCR) { if (lastWasCR) {
if (buffer[0] == '\n') { if (buffer[0] == '\n') {
offset++; ++offset;
prevColumnno = columnno++;
if (end == 1) if (end == 1)
return fill(); return fill();
} }
@@ -303,6 +313,9 @@ final class LineBuffer {
} }
int getLineno() { return lineno; } int getLineno() { return lineno; }
int getColumnno() { return columnno; };
boolean eof() { return hitEOF; } boolean eof() { return hitEOF; }
private static boolean formatChar(int c) { private static boolean formatChar(int c) {
@@ -322,6 +335,8 @@ final class LineBuffer {
private int end = 0; private int end = 0;
private int otherEnd; private int otherEnd;
private int lineno; private int lineno;
private int columnno;
private int prevColumnno = 0;
private int lineStart = 0; private int lineStart = 0;
private int otherStart = 0; private int otherStart = 0;
@@ -503,6 +503,7 @@ public class TokenStream {
flags = 0; flags = 0;
secondToLastPosition = position; secondToLastPosition = position;
lastPosition = position; lastPosition = position;
tokenPosition = position;
lastTokenPosition = position; lastTokenPosition = position;
} }
@@ -624,7 +625,7 @@ public class TokenStream {
} }
} while (isJSSpace(c) || c == '\n'); } while (isJSSpace(c) || c == '\n');
tokenPosition = new CodePosition(in.getLineno(), Math.max(in.getOffset() - 1, 0)); tokenPosition = new CodePosition(in.getLineno(), in.getColumnno() - 1);
if (c == EOF_CHAR) if (c == EOF_CHAR)
return EOF; return EOF;
if (c != '-' && c != '\n') if (c != '-' && c != '\n')
@@ -247,6 +247,12 @@ public class IrJsSteppingTestGenerated extends AbstractIrJsSteppingTest {
runTest("compiler/testData/debug/stepping/inlineSimpleCall.kt"); runTest("compiler/testData/debug/stepping/inlineSimpleCall.kt");
} }
@Test
@TestMetadata("jsCode.kt")
public void testJsCode() throws Exception {
runTest("compiler/testData/debug/stepping/jsCode.kt");
}
@Test @Test
@TestMetadata("kt15259.kt") @TestMetadata("kt15259.kt")
public void testKt15259() throws Exception { public void testKt15259() throws Exception {
+2 -4
View File
@@ -5,7 +5,5 @@ fun foo() {
println("after: $x") println("after: $x")
} }
// LINES(JS): 1 6 2 2 3 3 4 4 5 5 // LINES(JS): 1 6 2 2 3 3 4 4 5 5
// LINES(JS_IR): 2 2 3 3 2 2 5 5 // LINES(JS_IR): 2 2 3 3 4 4 5 5
// FIXME: ^^^^^^^
// js function call debug info is incorrect