What are rules for placing ranges for generated call sites and generated code for inlined methods so that they are correct for all major JavaScript engines?
Can two different stacks be encoded at one generated position?
Expanding on #40 (comment):
1 class Foo {
2 int value = 10;
3
4 Foo check(Foo? x) {
5 return x!;
6 }
7
8 @pragma('dart2js:never-inline')
9 void run(Foo? x, Foo? y) {
10 work(check(x), y);
11 }
12
13 void work(Foo x, Foo? y) {
14 x.speak(y);
15 }
16
17 @pragma('dart2js:never-inline')
18 void speak(Foo? z) {
19 print('Foo(${z!.value})');
20 }
21
22 void demo1() {
23 run(null, this);
24 }
25
26 void demo2() {
27 final a = Foo();
28 a.value += 90;
29 run(a, null);
30 }
31 }
32
33 @pragma('dart2js:disable-inlining')
34 main() {
35 print('---- 1');
36 try {
37 Foo().demo1();
38 } catch (e, s) {
39 print('$e\n$s');
40 }
41 print('---- 2');
42 try {
43 Foo().demo2();
44 } catch (e, s) {
45 print('$e\n$s');
46 }
47 }
The Dart VM prints the following stacks.
This is the desired translation for the JavaScript stacks.
---- 1
Null check operator used on a null value
#0 Foo.check (file:///tmp/sm.dart:5:13)
#1 Foo.run (file:///tmp/sm.dart:10:10)
#2 Foo.demo1 (file:///tmp/sm.dart:23:5)
#3 main (file:///tmp/sm.dart:37:11)
---- 2
Null check operator used on a null value
#0 Foo.speak (file:///tmp/sm.dart:19:19)
#1 Foo.work (file:///tmp/sm.dart:14:7)
#2 Foo.run (file:///tmp/sm.dart:10:5)
#3 Foo.demo2 (file:///tmp/sm.dart:29:5)
#4 main (file:///tmp/sm.dart:43:11)
The code compiled by dart2js and run on V8 prints the following:
---- 1
Null check operator used on a null value
TypeError: Cannot read properties of null (reading 'speak$1')
at Foo.run$2 (sm.js:2809:9)
at Foo.demo1$0 (sm.js:2815:12)
at main (sm.js:2401:18)
---- 2
Null check operator used on a null value
TypeError: Cannot read properties of null (reading 'value')
at Foo.speak$1 (sm.js:2812:26)
at Foo.run$2 (sm.js:2809:9)
at Foo.demo2$0 (sm.js:2820:12)
at main (sm.js:2409:18)
Foo.run$2 (sm.js:2809:9) appears in both V8 stacks.
Under ---- 1 this stack frame should be translated to an inlined call to check:
#0 Foo.check (file:///tmp/sm.dart:5:13)
#1 Foo.run (file:///tmp/sm.dart:10:10)
Under ---- 2, the identical stack frame line should be translated to an inlined call to work:
#1 Foo.work (file:///tmp/sm.dart:14:7)
#2 Foo.run (file:///tmp/sm.dart:10:5)
What is happening is that dart2js sometimes combines a null check and an subsequent property read, since in JavaScript a property read from null or undefined fails, so can be considered to be and implicit null check. Inlining check and work into run brings the null check adjacent to the method call, allowing them to be combined:
2807 A.Foo.prototype = {
2808 run$2(x, y) {
2809 x.speak$1(y);
// 12345678901234567890123456789
^
2810 },
2811 speak$1(z) {
2812 A.print("Foo(" + z.value + ")");
2813 },
2814 demo1$0() {
2815 this.run$2(null, this);
2816 },
2817 demo2$0() {
2818 var a = new A.Foo();
2819 a.value = 100;
2820 this.run$2(a, null);
2821 }
2822 };
The other JavaScript engines pick different locations:
JSC uses the positions 8 and 16 of the operators . and (.
---- 1
Null check operator used on a null value
run$2@sm.js:2809:8
demo1$0@sm.js:2815:17
main@sm.js:2401:25
---- 2
Null check operator used on a null value
speak$1@sm.js:2812:25
run$2@sm.js:2809:16
demo2$0@sm.js:2820:17
main@sm.js:2409:25
SM uses the position 7 and 9 of the identifiers x and speak$1.
---- 1
Null check operator used on a null value
run$2@sm.js:2809:7
demo1$0@sm.js:2815:12
main@sm.js:2401:18
---- 2
Null check operator used on a null value
speak$1@sm.js:2812:24
run$2@sm.js:2809:9
demo2$0@sm.js:2820:12
main@sm.js:2409:18
Two ranges, c for inlined check and w for inlined work would give a sensible result for JSC and SM
x.speak$1(y);
ccwwwwwwww--
For V8, the error in check shows as a wrong location inside work. This is confusing, since the binding for (work)x would show null for a non-nullable original source type.
What are rules for placing ranges for generated call sites and generated code for inlined methods so that they are correct for all major JavaScript engines?
Can two different stacks be encoded at one generated position?
Expanding on #40 (comment):
The Dart VM prints the following stacks.
This is the desired translation for the JavaScript stacks.
The code compiled by dart2js and run on V8 prints the following:
Foo.run$2 (sm.js:2809:9)appears in both V8 stacks.Under
---- 1this stack frame should be translated to an inlined call tocheck:Under
---- 2, the identical stack frame line should be translated to an inlined call towork:What is happening is that dart2js sometimes combines a null check and an subsequent property read, since in JavaScript a property read from
nullorundefinedfails, so can be considered to be and implicit null check. Inliningcheckandworkintorunbrings the null check adjacent to the method call, allowing them to be combined:The other JavaScript engines pick different locations:
JSC uses the positions 8 and 16 of the operators
.and(.SM uses the position 7 and 9 of the identifiers
xandspeak$1.Two ranges,
cfor inlinedcheckandwfor inlinedworkwould give a sensible result for JSC and SMFor V8, the error in
checkshows as a wrong location insidework. This is confusing, since the binding for (work)xwould shownullfor a non-nullable original source type.