Skip to content

Commit 2616eb2

Browse files
natebiggsCommit Queue
authored andcommitted
[ddc] Use TemporaryId when emitting all kernel VariableDeclaration references.
Some CFE lowerings (e.g. pattern lowerings) result in nested scopes containing VariableDeclarations with the same 'name'. The current DDC transform translates these to the exact same name in JS leading to incorrect semantics. The `TemporaryId` mechanism automatically renames any variables with the same name that would shadow each other. So we re-use that here to ensure the variables all have a unique name if the CFE hasn't already given them one. If the name is already okay (i.e. not shadowing something else), the name in JS will appear unchanged. Side note: In a future change perhaps we should rename `TemporaryId`. The general mechanism it implements is more useful than its original intended use. Fixes: #59613 Change-Id: I708c72528d5df19af48dde01163d375a5588baae Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/398504 Commit-Queue: Nate Biggs <[email protected]> Reviewed-by: Sigmund Cherem <[email protected]> Reviewed-by: Nicholas Shahan <[email protected]>
1 parent cd7b134 commit 2616eb2

File tree

8 files changed

+342
-40
lines changed

8 files changed

+342
-40
lines changed

pkg/dev_compiler/lib/src/compiler/js_names.dart

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,31 @@ abstract class FixedNames {
2424
}
2525

2626
/// Unique instance for temporary variables. Will be renamed consistently
27-
/// across the entire file. Different instances will be named differently
28-
/// even if they have the same name, this makes it safe to use in code
29-
/// generation without needing global knowledge. See [TemporaryNamer].
27+
/// across the entire file. Instances with different IDs will be named
28+
/// differently even if they have the same name, this makes it safe to use in
29+
/// code generation without needing global knowledge. See [TemporaryNamer].
3030
// TODO(jmesserly): move into js_ast? add a boolean to Identifier?
3131
class TemporaryId extends Identifier {
32-
// TODO(jmesserly): by design, temporary identifier nodes are shared
33-
// throughout the AST, so any source information we attach in one location
34-
// be incorrect for another location (and overwrites previous data).
35-
//
36-
// If we want to track source information for temporary variables, we'll
37-
// need to separate the identity of the variable from its Identifier.
38-
//
39-
// In practice that makes temporaries more difficult to use: they're no longer
40-
// JS AST nodes, so `toIdentifier()` is required to put them in the JS AST.
41-
// And anywhere we currently use type `Identifier` to hold Identifier or
42-
// TemporaryId, those types would need to change to `Identifier Function()`.
43-
//
44-
// However we may need to fix this if we want hover to work well for things
45-
// like library prefixes and field-initializing formals.
32+
final int _id;
33+
4634
@override
47-
dynamic get sourceInformation => null;
35+
TemporaryId withSourceInformation(dynamic sourceInformation) =>
36+
TemporaryId.from(this)..sourceInformation = sourceInformation;
37+
38+
static int _idCounter = 0;
39+
40+
TemporaryId(super.name) : _id = _idCounter++;
41+
TemporaryId.from(TemporaryId other)
42+
: _id = other._id,
43+
super(other.name);
44+
4845
@override
49-
set sourceInformation(Object? obj) {}
46+
int get hashCode => _id;
5047

51-
TemporaryId(super.name);
48+
@override
49+
bool operator ==(Object other) {
50+
return other is TemporaryId && other._id == _id;
51+
}
5252
}
5353

5454
/// Creates a qualified identifier, without determining for sure if it needs to

pkg/dev_compiler/lib/src/kernel/compiler.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
9797
@override
9898
final variableIdentifiers = <VariableDeclaration, js_ast.Identifier>{};
9999

100+
/// Identifiers for kernel variables with an analgous identifier in JS.
101+
///
102+
/// [VariableDeclaration.name] is not necessarily a safe identifier for JS
103+
/// transpiled code. The same name can be used in shadowing contexts. We map
104+
/// each kernel variable to a [js_ast.TemporaryId] so that at code emission
105+
/// time, declarations that would be shadowed are given a unique name. If
106+
/// there is no risk of shadowing, the original name will be used.
107+
final Map<VariableDeclaration, js_ast.TemporaryId> _variableTempIds = {};
108+
100109
/// Maps a library URI import, that is not in [_libraries], to the
101110
/// corresponding Kernel summary module we imported it with.
102111
///
@@ -4930,7 +4939,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
49304939
var v = node.variable;
49314940
var id = _emitVariableRef(v);
49324941
if (id.name == v.name) {
4933-
id.sourceInformation = _variableSpan(node.fileOffset, v.name!.length);
4942+
id = id.withSourceInformation(
4943+
_variableSpan(node.fileOffset, v.name!.length));
49344944
}
49354945
return id;
49364946
}
@@ -4964,7 +4974,7 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
49644974
return null;
49654975
}
49664976

4967-
js_ast.Identifier _emitVariableRef(VariableDeclaration v) {
4977+
js_ast.TemporaryId _emitVariableRef(VariableDeclaration v) {
49684978
if (_isTemporaryVariable(v)) {
49694979
var name = _debuggerFriendlyTemporaryVariableName(v);
49704980
name ??= 't\$${_tempVariables.length}';
@@ -4978,7 +4988,8 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
49784988
// See https://github.com/dart-lang/sdk/issues/55918
49794989
name = extractLocalNameFromLateLoweredLocal(name);
49804990
}
4981-
return _emitIdentifier(name);
4991+
return js_ast.TemporaryId.from(
4992+
_variableTempIds[v] ??= _emitTemporaryId(name));
49824993
}
49834994

49844995
/// Emits the declaration of a variable.

pkg/dev_compiler/lib/src/kernel/compiler_new.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
248248
Map<VariableDeclaration, js_ast.Identifier> get variableIdentifiers =>
249249
_symbolData.variableIdentifiers;
250250

251+
/// Identifiers for kernel variables with an analgous identifier in JS.
252+
///
253+
/// [VariableDeclaration.name] is not necessarily a safe identifier for JS
254+
/// transpiled code. The same name can be used in shadowing contexts. We map
255+
/// each kernel variable to a [js_ast.TemporaryId] so that at code emission
256+
/// time, references that would be shadowed are given a unique name. If there
257+
/// is no risk of shadowing, the original name will be used.
258+
final Map<VariableDeclaration, js_ast.TemporaryId> _variableTempIds = {};
259+
251260
/// Maps a library URI import, that is not in [_libraries], to the
252261
/// corresponding Kernel summary module we imported it with.
253262
///
@@ -5250,7 +5259,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
52505259
var v = node.variable;
52515260
var id = _emitVariableRef(v);
52525261
if (id.name == v.name) {
5253-
id.sourceInformation = _variableSpan(node.fileOffset, v.name!.length);
5262+
id = id.withSourceInformation(
5263+
_variableSpan(node.fileOffset, v.name!.length));
52545264
}
52555265
return id;
52565266
}
@@ -5284,7 +5294,7 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
52845294
return null;
52855295
}
52865296

5287-
js_ast.Identifier _emitVariableRef(VariableDeclaration v) {
5297+
js_ast.TemporaryId _emitVariableRef(VariableDeclaration v) {
52885298
if (_isTemporaryVariable(v)) {
52895299
var name = _debuggerFriendlyTemporaryVariableName(v);
52905300
name ??= 't\$${_tempVariables.length}';
@@ -5298,7 +5308,8 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
52985308
// See https://github.com/dart-lang/sdk/issues/55918
52995309
name = extractLocalNameFromLateLoweredLocal(name);
53005310
}
5301-
return _emitIdentifier(name);
5311+
return js_ast.TemporaryId.from(
5312+
_variableTempIds[v] ??= _emitTemporaryId(name));
53025313
}
53035314

53045315
/// Emits the declaration of a variable.

pkg/dev_compiler/lib/src/kernel/expression_compiler.dart

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,80 @@ class ExpressionCompiler {
115115
dartScope.definitions[extractLocalName(localName)] =
116116
dartScope.definitions.remove(localName)!;
117117
}
118+
119+
// Create a mapping from Dart variable names in scope to the corresponding
120+
// JS variable names. The Dart variable may have had a suffix of the
121+
// form '$N' added to it where N is either the empty string or an
122+
// integer >= 0.
123+
final dartNameToJsName = <String, String>{};
124+
125+
int nameCompare(String a, String b) {
126+
final lengthCmp = b.length.compareTo(a.length);
127+
if (lengthCmp != 0) return lengthCmp;
128+
return b.compareTo(a);
129+
}
130+
131+
// Sort Dart names in case a user-defined name includes a '$'. The
132+
// resulting normalized JS name might seem like a suffixed version of a
133+
// shorter Dart name. Since longer Dart names can't incorrectly match a
134+
// shorter JS name (JS names are always at least as long as the Dart
135+
// name), we process them from longest to shortest.
136+
final dartNames = [...dartScope.definitions.keys]..sort(nameCompare);
137+
138+
// Sort JS names so that the highest suffix value gets assigned to the
139+
// corresponding Dart name first. Since names are suffixed in ascending
140+
// order as inner scopes are visited, the highest suffix value will be
141+
// the one that matches the visible Dart name in a given scope.
142+
final jsNames = [...jsScope.keys]..sort(nameCompare);
143+
144+
const removedSentinel = '';
145+
const thisJsName = r'$this';
146+
147+
for (final dartName in dartNames) {
148+
if (isExtensionThisName(dartName)) {
149+
if (jsScope.containsKey(thisJsName)) {
150+
dartNameToJsName[dartName] = thisJsName;
151+
}
152+
continue;
153+
}
154+
// Any name containing a '$' symbol will have that symbol expanded to
155+
// '$36' in JS. We do a similar expansion here to normalize the names.
156+
final jsNamePrefix =
157+
js_ast.toJSIdentifier(dartName).replaceAll('\$', '\\\$');
158+
print(jsNamePrefix);
159+
final regexp = RegExp(r'^' + jsNamePrefix + r'(\$[0-9]*)?$');
160+
for (var i = 0; i < jsNames.length; i++) {
161+
final jsName = jsNames[i];
162+
if (jsName == removedSentinel) continue;
163+
if (jsName.length < dartName.length) break;
164+
if (regexp.hasMatch(jsName)) {
165+
dartNameToJsName[dartName] = jsName;
166+
jsNames[i] = removedSentinel;
167+
168+
// Remove any additional JS names that match this name as these will
169+
// correspond to shadowed Dart variables that are not visible in the
170+
// current scope.
171+
//
172+
// Note: In some extreme cases this can match the wrong variable.
173+
// This would require a combination of 36 nested variables with the
174+
// same name and a similarly named variable with a $ in its name.
175+
for (var j = i; j < jsNames.length; j++) {
176+
final jsName = jsNames[j];
177+
if (jsName == removedSentinel) continue;
178+
if (jsName.length < dartName.length) break;
179+
if (regexp.hasMatch(jsNames[j])) {
180+
jsNames[j] = removedSentinel;
181+
}
182+
}
183+
break;
184+
}
185+
}
186+
}
187+
118188
// remove undefined js variables (this allows us to get a reference error
119189
// from chrome on evaluation)
120-
dartScope.definitions.removeWhere((variable, type) =>
121-
!jsScope.containsKey(_dartNameToJsName(variable)));
190+
dartScope.definitions.removeWhere(
191+
(variable, type) => !dartNameToJsName.containsKey(variable));
122192

123193
dartScope.typeParameters
124194
.removeWhere((parameter) => !jsScope.containsKey(parameter.name));
@@ -128,7 +198,7 @@ class ExpressionCompiler {
128198
var localJsScope = [
129199
...dartScope.typeParameters.map((parameter) => jsScope[parameter.name]),
130200
...dartScope.definitions.keys
131-
.map((variable) => jsScope[_dartNameToJsName(variable)])
201+
.map((variable) => jsScope[dartNameToJsName[variable]])
132202
];
133203

134204
_log('Performed scope substitutions for expression');
@@ -177,12 +247,6 @@ class ExpressionCompiler {
177247
}
178248
}
179249

180-
String? _dartNameToJsName(String? dartName) {
181-
if (dartName == null) return dartName;
182-
if (isExtensionThisName(dartName)) return r'$this';
183-
return dartName;
184-
}
185-
186250
DartScope? _findScopeAt(
187251
Uri libraryUri, Uri? scriptFileUri, int line, int column) {
188252
if (line < 0) {

0 commit comments

Comments
 (0)