[rust][sema] Handle var/let/const redeclaration

JavaScript has ~sane~ fun rules around where variables can be redeclared or not, 
and which kinds of variables this applies to. Actually the rules are pretty 
straightforward: 

* `var` can be redeclared any number of times. 

* In strict mode, other declarations cannot be redeclared within the same scope. 
This implies that a `var` declaration cannot conflict with these other forms, 
which must take into account hoisting. So you can't have a `var a; let a` in the 
same scope, but you also can't have a `let a` at a scope and then a `var a` 
which will hoist to that same scope.
This commit is contained in:
Joe Savona
2023-08-16 12:26:04 -07:00
parent 796080f4d1
commit bfb7d2bfbd
16 changed files with 823 additions and 71 deletions
@@ -1,6 +1,8 @@
import { useMemo } from "react";
function Component(x) {
const x = useMemo(() => {
return y;
const y = useMemo(() => {
return x;
});
return x;
return y;
}
@@ -21,7 +21,17 @@ fn fixtures() {
let mut output = String::new();
let analysis = analyze(&ast);
let mut analysis = analyze(&ast);
let diagnostics = analysis.diagnostics();
if !diagnostics.is_empty() {
for diagnostic in diagnostics {
eprintln!(
"{:?}",
Report::new(diagnostic)
.with_source_code(NamedSource::new(path.to_string_lossy(), input.clone(),))
);
}
}
let environment = Environment::new(
Features {
validate_frozen_lambdas: true,
@@ -4,11 +4,13 @@ expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")"
input_file: crates/forget_fixtures/tests/fixtures/use-memo.js
---
Input:
import { useMemo } from "react";
function Component(x) {
const x = useMemo(() => {
return y;
const y = useMemo(() => {
return x;
});
return x;
return y;
}
@@ -23,12 +25,12 @@ bb0 (block)
[2] Label block=bb1 fallthrough=bb6
bb1 (block)
predecessors: bb0
[3] unknown $13 = LoadGlobal y
[3] unknown $13 = LoadLocal unknown x$10
[4] unknown $19 = StoreLocal Reassign unknown t$18 = unknown $13
[5] Goto bb6
bb6 (block)
predecessors: bb1
[6] unknown $14 = LoadLocal unknown t$18
[7] unknown $16 = StoreLocal Const unknown x$15 = unknown $14
[8] unknown $17 = LoadLocal unknown x$15
[7] unknown $16 = StoreLocal Const unknown y$15 = unknown $14
[8] unknown $17 = LoadLocal unknown y$15
[9] Return unknown $17
@@ -157,7 +157,7 @@ impl Analyzer {
Analyzer::visit_declaration_pattern(
visitor,
param,
Some(DeclarationKind::FunctionDeclaration),
Some(DeclarationKind::Function),
);
}
@@ -204,22 +204,9 @@ impl Analyzer {
decl_kind: Option<DeclarationKind>,
) {
if let Some(decl_kind) = decl_kind {
// Declaring a "new" variable, report an error if this is a duplicate
// definition. In either case, we create a new declaration. Ie we
// act as if shadowing is allowed in the language
let previous_declaration = self.manager.lookup_declaration(self.current, &ast.name);
if let Some(previous_declaration) = previous_declaration {
if previous_declaration.scope == self.current {
// duplicate definition in the same scope
self.manager.diagnostics.push(Diagnostic::invalid_syntax(
"Duplicate declaration",
ast.range,
));
}
}
let id = self
.manager
.add_declaration(self.current, ast.name.clone(), decl_kind);
let id =
self.manager
.add_declaration(self.current, ast.name.clone(), decl_kind, ast.range);
self.manager
.node_declarations
.insert(AstNode::from(ast), id);
@@ -350,7 +337,8 @@ impl Visitor for Analyzer {
let declaration = self.manager.add_declaration(
self.current,
id.name.clone(),
DeclarationKind::FunctionDeclaration,
DeclarationKind::Function,
id.range,
);
self.manager
.node_declarations
@@ -366,7 +354,8 @@ impl Visitor for Analyzer {
let declaration = self.manager.add_declaration(
self.current,
id.name.clone(),
DeclarationKind::FunctionDeclaration,
DeclarationKind::Function,
id.range,
);
self.manager
.node_declarations
@@ -1,6 +1,6 @@
use forget_diagnostics::Diagnostic;
use forget_estree::{
BreakStatement, ContinueStatement, ESTreeNode, LabeledStatement, SourceType,
BreakStatement, ContinueStatement, ESTreeNode, LabeledStatement, SourceRange, SourceType,
VariableDeclarationKind,
};
use forget_utils::PointerAddress;
@@ -174,20 +174,6 @@ impl ScopeManager {
})
}
pub fn lookup_declaration(&self, scope: ScopeId, name: &str) -> Option<&Declaration> {
let mut current = &self.scopes[scope.0];
loop {
if let Some(id) = current.declarations.get(name) {
return Some(&self.declarations[id.0]);
}
if let Some(parent) = current.parent {
current = &self.scopes[parent.0];
} else {
return None;
}
}
}
pub fn lookup_reference(
&self,
scope: ScopeId,
@@ -271,20 +257,86 @@ impl ScopeManager {
pub(crate) fn add_declaration(
&mut self,
scope: ScopeId,
scope_id: ScopeId,
name: String,
kind: DeclarationKind,
range: Option<SourceRange>,
) -> DeclarationId {
let hoisted_scope = self.get_scope_for_declaration(scope, kind);
let scope = self.scope(scope_id);
// Determine the scope to which this declaration should be hoisted. This mainly applies to var declarations
let hoisted_scope_id = self.get_scope_for_declaration(scope_id, kind);
// Check for redeclaration. The rules are roughly:
// * `var` can be redeclared any number of times in a given scope. These redeclarations have no effect,
// subsequent declarations are equivalent to just reassigning a value to the original declaration.
// ie `var a = 1; var a = 2;` is equivalent to `var a; a = 1; a = 2`.
// * Other forms (in strict mode) may not be redeclared in a given scope.
// * This implies that `var` cannot conflict with other types of declarations, either in the scope
// at which they are declared or the scope to which the var will hoist:
// * `function() { {let a; var a;} }` conflicts at the declaration scope, even though the var will hoise above.
// * `function() { let a; { var a; } }` conflicts bc the var hoists to the scope w a conflicting let.
match kind {
DeclarationKind::Var => {
if let Some(declaration) = scope.declarations.get(&name) {
let declaration = self.declaration(*declaration);
if is_block_scoped_declaration(declaration.kind) {
// Var cannot be declared in the same scope as let/const/class/import/etc
self.diagnostics
.push(Diagnostic::invalid_syntax("Duplicate declaration", range));
}
} else if hoisted_scope_id != scope_id {
if let Some(declaration) = self.scope(hoisted_scope_id).declarations.get(&name)
{
let declaration = self.declaration(*declaration);
if is_block_scoped_declaration(declaration.kind) {
// Var cannot *hoist* to the same scope as let/const/class/import/etc
self.diagnostics
.push(Diagnostic::invalid_syntax("Duplicate declaration", range));
}
}
}
// Redeclaration of `var` in a given scope has no effect, subsequent declarations
// are equivalent to re-declarations
// ie `var a = 1; var a = 2;` is equivalent to `var a; a = 1; a = 2`.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var#redeclarations
if let Some(declaration) = self.scope(hoisted_scope_id).declarations.get(&name) {
let declaration = self.declaration(*declaration);
if declaration.kind == DeclarationKind::Var {
return declaration.id;
}
}
}
DeclarationKind::CatchClause
| DeclarationKind::Let
| DeclarationKind::Const
| DeclarationKind::Import
| DeclarationKind::Class
| DeclarationKind::Function => {
// When duplicate declarations occur we report an error and then resolve references to the
// first declaration. It doesn't really matter which declaration we refer to, because
// semantic results are invalid if there are errors. The main consideration is that we do
// not want to report a "cannot find declaration for `x`" reference error just because there
// were duplicate declarations of `x`.
if let Some(_declaration) = scope.declarations.get(&name) {
self.diagnostics
.push(Diagnostic::invalid_syntax("Duplicate declaration", range));
}
}
}
// Always create a new declaration and id...
let id = DeclarationId(self.declarations.len());
self.declarations.push(Declaration {
id,
kind,
name: name.clone(),
scope: hoisted_scope,
scope: hoisted_scope_id,
});
self.scopes[hoisted_scope.0].declarations.insert(name, id);
// ...but only save the first declaration for a given name in each scope
self.scopes[hoisted_scope_id.0]
.declarations
.entry(name)
.or_insert(id);
id
}
@@ -294,8 +346,8 @@ impl ScopeManager {
| DeclarationKind::Import
| DeclarationKind::Const
| DeclarationKind::CatchClause
| DeclarationKind::For => scope,
DeclarationKind::Var | DeclarationKind::FunctionDeclaration => {
| DeclarationKind::Class => scope,
DeclarationKind::Var | DeclarationKind::Function => {
let mut current = scope;
loop {
let scope = self.scope(current);
@@ -342,6 +394,18 @@ impl ScopeManager {
}
}
fn is_block_scoped_declaration(kind: DeclarationKind) -> bool {
match kind {
DeclarationKind::Let
| DeclarationKind::Const
| DeclarationKind::Import
| DeclarationKind::Class
| DeclarationKind::Function
| DeclarationKind::CatchClause => true,
DeclarationKind::Var => false,
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct ScopeId(usize);
@@ -393,11 +457,11 @@ pub struct Label {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum DeclarationKind {
Class,
Const,
Var,
Let,
FunctionDeclaration,
For,
Function,
CatchClause,
Import,
}
@@ -0,0 +1,25 @@
function Component() {
let a = 1;
let a = 2; // error
const b = 3;
const b = 4; // error
function foo() {}
function foo() {} // error
try {
} catch (c) {
let c = true; // error
const c = true; // error
function c() {} // error
// class c {} // error
}
}
function Component() {
// error
}
const x = true;
const x = false; // error
@@ -0,0 +1,29 @@
function Component() {
let a;
{
var a; // error, conflicts when hoisted
}
const b = 1;
{
var b; // error, conflicts
}
{
let c;
var c; // error, conflicts
}
{
const d = 2;
var d; // error, conflicts
}
// there should be one instance of `e`:
var e = 3;
console.log(e); // 3
var e = 4;
console.log(e); // 4
var e;
console.log(e); // 4
}
@@ -0,0 +1,308 @@
---
source: crates/forget_semantic_analysis/tests/analysis_test.rs
expression: "format!(\"Input:\\n{input}\\n\\nAnalysis:\\n{output}\")"
input_file: crates/forget_semantic_analysis/tests/fixtures/block-item-duplication.js
---
Input:
function Component() {
let a = 1;
let a = 2; // error
const b = 3;
const b = 4; // error
function foo() {}
function foo() {} // error
try {
} catch (c) {
let c = true; // error
const c = true; // error
function c() {} // error
// class c {} // error
}
}
function Component() {
// error
}
const x = true;
const x = false; // error
Analysis:
Scope {
id: ScopeId(
0,
),
kind: Module,
declarations: {
"Component": Declaration {
id: DeclarationId(
0,
),
kind: Function,
scope: ScopeId(
0,
),
},
"x": Declaration {
id: DeclarationId(
12,
),
kind: Const,
scope: ScopeId(
0,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
1,
),
kind: Function,
declarations: {
"a": Declaration {
id: DeclarationId(
1,
),
kind: Let,
scope: ScopeId(
1,
),
},
"b": Declaration {
id: DeclarationId(
3,
),
kind: Const,
scope: ScopeId(
1,
),
},
"foo": Declaration {
id: DeclarationId(
5,
),
kind: Function,
scope: ScopeId(
1,
),
},
"c": Declaration {
id: DeclarationId(
10,
),
kind: Function,
scope: ScopeId(
1,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
2,
),
kind: Function,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
3,
),
kind: Function,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
4,
),
kind: Block,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
5,
),
kind: CatchClause,
declarations: {
"c": Declaration {
id: DeclarationId(
7,
),
kind: CatchClause,
scope: ScopeId(
5,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
6,
),
kind: Block,
declarations: {
"c": Declaration {
id: DeclarationId(
8,
),
kind: Let,
scope: ScopeId(
6,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
7,
),
kind: Function,
declarations: {},
references: [],
children: [],
},
],
},
],
},
],
},
Scope {
id: ScopeId(
8,
),
kind: Function,
declarations: {},
references: [],
children: [],
},
],
}
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
42,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
82,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
130,
),
length: 3,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
210,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
242,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
301,
),
length: 9,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
351,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
@@ -39,7 +39,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -57,7 +57,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -66,7 +66,7 @@ Scope {
id: DeclarationId(
2,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -191,7 +191,7 @@ Scope {
id: DeclarationId(
4,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
9,
),
@@ -61,7 +61,7 @@ Scope {
id: DeclarationId(
3,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -79,7 +79,7 @@ Scope {
id: DeclarationId(
4,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -30,7 +30,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -48,7 +48,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -25,7 +25,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -43,7 +43,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -26,7 +26,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -44,7 +44,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -94,7 +94,7 @@ Scope {
id: DeclarationId(
4,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
2,
),
@@ -112,7 +112,7 @@ Scope {
id: DeclarationId(
5,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
3,
),
@@ -31,7 +31,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -49,7 +49,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -0,0 +1,323 @@
---
source: crates/forget_semantic_analysis/tests/analysis_test.rs
expression: "format!(\"Input:\\n{input}\\n\\nAnalysis:\\n{output}\")"
input_file: crates/forget_semantic_analysis/tests/fixtures/var-duplication.js
---
Input:
function Component() {
let a;
{
var a; // error, conflicts when hoisted
}
const b = 1;
{
var b; // error, conflicts
}
{
let c;
var c; // error, conflicts
}
{
const d = 2;
var d; // error, conflicts
}
// there should be one instance of `e`:
var e = 3;
console.log(e); // 3
var e = 4;
console.log(e); // 4
var e;
console.log(e); // 4
}
Analysis:
Scope {
id: ScopeId(
0,
),
kind: Module,
declarations: {
"Component": Declaration {
id: DeclarationId(
0,
),
kind: Function,
scope: ScopeId(
0,
),
},
},
references: [],
children: [
Scope {
id: ScopeId(
1,
),
kind: Function,
declarations: {
"a": Declaration {
id: DeclarationId(
1,
),
kind: Let,
scope: ScopeId(
1,
),
},
"b": Declaration {
id: DeclarationId(
3,
),
kind: Const,
scope: ScopeId(
1,
),
},
"c": Declaration {
id: DeclarationId(
6,
),
kind: Var,
scope: ScopeId(
1,
),
},
"d": Declaration {
id: DeclarationId(
8,
),
kind: Var,
scope: ScopeId(
1,
),
},
"e": Declaration {
id: DeclarationId(
9,
),
kind: Var,
scope: ScopeId(
1,
),
},
},
references: [
Reference {
id: ReferenceId(
0,
),
kind: Read,
declaration: DeclarationId(
9,
),
declaration (name): "e",
scope: ScopeId(
1,
),
},
Reference {
id: ReferenceId(
1,
),
kind: Read,
declaration: DeclarationId(
9,
),
declaration (name): "e",
scope: ScopeId(
1,
),
},
Reference {
id: ReferenceId(
2,
),
kind: Read,
declaration: DeclarationId(
9,
),
declaration (name): "e",
scope: ScopeId(
1,
),
},
],
children: [
Scope {
id: ScopeId(
2,
),
kind: Block,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
3,
),
kind: Block,
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
4,
),
kind: Block,
declarations: {
"c": Declaration {
id: DeclarationId(
5,
),
kind: Let,
scope: ScopeId(
4,
),
},
},
references: [],
children: [],
},
Scope {
id: ScopeId(
5,
),
kind: Block,
declarations: {
"d": Declaration {
id: DeclarationId(
7,
),
kind: Const,
scope: ScopeId(
5,
),
},
},
references: [],
children: [],
},
],
},
],
}
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
44,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
112,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
163,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Duplicate declaration",
span: Some(
SourceSpan {
offset: SourceOffset(
220,
),
length: 1,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
305,
),
length: 7,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
341,
),
length: 7,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
Diagnostic(
DiagnosticData {
message: "Undefined variable",
span: Some(
SourceSpan {
offset: SourceOffset(
373,
),
length: 7,
},
),
related_information: [],
severity: InvalidSyntax,
data: [],
},
)
@@ -36,7 +36,7 @@ Scope {
id: DeclarationId(
0,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
0,
),
@@ -63,7 +63,7 @@ Scope {
id: DeclarationId(
1,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),
@@ -72,7 +72,7 @@ Scope {
id: DeclarationId(
2,
),
kind: FunctionDeclaration,
kind: Function,
scope: ScopeId(
1,
),