[rust] Data structures for semantic analysis

This is two things: 

* A toy semantic analysis that handles a tiny subset of JS, including labeled 
statements, labeled break/continue, and variable 
declaration/reference/reassignment. This only exists as a way to prove out the 
API for the more important bit: 

* More importantly, this defines a data model for the semantic analysis results 
and an API for building up the semantic analysis. 

Subsequent diffs will replace the first bit (toy analysis impl), while keeping 
the second part.
This commit is contained in:
Joe Savona
2023-08-03 12:41:50 -07:00
parent 8042970ea4
commit a67fefdebd
11 changed files with 1350 additions and 87 deletions
+5
View File
@@ -566,8 +566,13 @@ dependencies = [
name = "forget_semantic_analysis"
version = "0.1.0"
dependencies = [
"forget_diagnostics",
"forget_estree",
"forget_hermes_parser",
"forget_utils",
"indexmap 2.0.0",
"insta",
"serde_json",
]
[[package]]
+1
View File
@@ -22,6 +22,7 @@ forget_fixtures = { path = "crates/forget_fixtures" }
forget_hermes_parser = { path = "crates/forget_hermes_parser" }
forget_hir = { path = "crates/forget_hir" }
forget_optimization = { path = "crates/forget_optimization" }
forget_semantic_analysis = { path = "crates/forget_semantic_analysis" }
forget_ssa = { path = "crates/forget_ssa" }
forget_swc_demo = { path = "crates/forget_swc_demo" }
forget_utils = { path = "crates/forget_utils" }
@@ -2,10 +2,10 @@ use crate::{
AssignmentPropertyOrRestElement, AssignmentTarget, Class, ClassItem, ClassPrivateProperty,
ClassProperty, Declaration, DeclarationOrExpression, ExportAllDeclaration,
ExportDefaultDeclaration, ExportNamedDeclaration, Expression, ExpressionOrPrivateIdentifier,
ExpressionOrSpread, ExpressionOrSuper, ForInInit, ForInit, Function, FunctionBody, Identifier,
ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportDeclaration, Literal,
MethodDefinition, ModuleItem, Pattern, PrivateIdentifier, PrivateName, Program, Statement,
StaticBlock, Super, SwitchCase, VariableDeclarator, _Literal,
ExpressionOrSpread, ExpressionOrSuper, ForInInit, ForInit, Function, FunctionBody,
FunctionDeclaration, Identifier, ImportDeclaration, ImportDeclarationSpecifier,
ImportOrExportDeclaration, Literal, MethodDefinition, ModuleItem, Pattern, PrivateIdentifier,
PrivateName, Program, Statement, StaticBlock, Super, SwitchCase, VariableDeclarator, _Literal,
};
/// Trait for visiting an estree
@@ -17,7 +17,18 @@ pub trait Visitor<'ast> {
f(self);
}
fn visit_rvalue<F>(&mut self, f: F)
where
F: FnOnce(&mut Self) -> (),
{
f(self);
}
fn visit_program(&mut self, program: &'ast Program) {
self.default_visit_program(program)
}
fn default_visit_program(&mut self, program: &'ast Program) {
for item in &program.body {
self.visit_module_item(item);
}
@@ -73,7 +84,7 @@ pub trait Visitor<'ast> {
fn visit_import_declaration(&mut self, declaration: &'ast ImportDeclaration) {
self.visit_lvalue(|visitor| {
for specifier in &declaration.specifiers {
visitor.visit_import_declaration_specifier(specifier, &declaration.source)
visitor.visit_import_declaration_specifier(specifier);
}
});
self.visit_import_source(&declaration.source);
@@ -101,11 +112,7 @@ pub trait Visitor<'ast> {
}
}
fn visit_import_declaration_specifier(
&mut self,
specifier: &'ast ImportDeclarationSpecifier,
_source: &'ast _Literal,
) {
fn visit_import_declaration_specifier(&mut self, specifier: &'ast ImportDeclarationSpecifier) {
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
self.visit_identifier(&specifier.local);
@@ -129,7 +136,7 @@ pub trait Visitor<'ast> {
self.visit_class(&declaration.class);
}
Declaration::FunctionDeclaration(declaration) => {
self.visit_function(&declaration.function);
self.visit_function_declaration(declaration);
}
Declaration::VariableDeclaration(declaration) => {
for declarator in &declaration.declarations {
@@ -142,6 +149,10 @@ pub trait Visitor<'ast> {
}
}
fn visit_function_declaration(&mut self, declaration: &'ast FunctionDeclaration) {
self.visit_function(&declaration.function);
}
fn visit_statement(&mut self, stmt: &'ast Statement) {
self.default_visit_statement(stmt);
}
@@ -198,7 +209,7 @@ pub trait Visitor<'ast> {
self.visit_statement(&stmt.body);
}
Statement::FunctionDeclaration(stmt) => {
self.visit_function(&stmt.function);
self.visit_function_declaration(stmt);
}
Statement::IfStatement(stmt) => {
self.visit_expression(&stmt.test);
@@ -270,32 +281,28 @@ pub trait Visitor<'ast> {
}
for item in &class.body.body {
match item {
ClassItem::MethodDefinition(item) => self.visit_method_definition(class, item),
ClassItem::MethodDefinition(item) => self.visit_method_definition(item),
ClassItem::ClassProperty(item) => {
self.visit_class_property(class, item);
self.visit_class_property(item);
}
ClassItem::ClassPrivateProperty(item) => {
self.visit_class_private_property(class, item);
self.visit_class_private_property(item);
}
ClassItem::StaticBlock(item) => {
self.visit_static_block(class, item);
self.visit_static_block(item);
}
}
}
}
fn visit_class_property(&mut self, _class: &'ast Class, property: &'ast ClassProperty) {
fn visit_class_property(&mut self, property: &'ast ClassProperty) {
self.visit_expression(&property.key);
if let Some(value) = &property.value {
self.visit_expression(value)
}
}
fn visit_class_private_property(
&mut self,
_class: &'ast Class,
property: &'ast ClassPrivateProperty,
) {
fn visit_class_private_property(&mut self, property: &'ast ClassPrivateProperty) {
match &property.key {
ExpressionOrPrivateIdentifier::Expression(key) => self.visit_expression(key),
ExpressionOrPrivateIdentifier::PrivateIdentifier(key) => {
@@ -308,21 +315,17 @@ pub trait Visitor<'ast> {
}
}
fn visit_static_block(&mut self, _class: &'ast Class, property: &'ast StaticBlock) {
fn visit_static_block(&mut self, property: &'ast StaticBlock) {
for stmt in &property.body {
self.visit_statement(stmt)
}
}
fn visit_method_definition(&mut self, class: &'ast Class, method: &'ast MethodDefinition) {
self.default_visit_method_definition(class, method);
fn visit_method_definition(&mut self, method: &'ast MethodDefinition) {
self.default_visit_method_definition(method);
}
fn default_visit_method_definition(
&mut self,
_class: &'ast Class,
method: &'ast MethodDefinition,
) {
fn default_visit_method_definition(&mut self, method: &'ast MethodDefinition) {
self.visit_expression(&method.key);
self.visit_function(&method.value.function);
}
@@ -386,8 +389,10 @@ pub trait Visitor<'ast> {
}
Pattern::RestElement(pattern) => self.visit_pattern(&pattern.argument),
Pattern::AssignmentPattern(pattern) => {
self.visit_expression(&pattern.right);
self.visit_pattern(&pattern.left);
self.visit_rvalue(|visitor| {
visitor.visit_expression(&pattern.right);
});
}
}
}
@@ -460,8 +465,31 @@ pub trait Visitor<'ast> {
}
}
}
Expression::CallExpression(expr) => {
match &expr.callee {
ExpressionOrSuper::Expression(callee) => self.visit_expression(callee),
ExpressionOrSuper::Super(callee) => self.visit_super(callee),
}
for arg in &expr.arguments {
match arg {
ExpressionOrSpread::Expression(arg) => self.visit_expression(arg),
ExpressionOrSpread::SpreadElement(arg) => {
self.visit_expression(&arg.argument)
}
}
}
}
Expression::UpdateExpression(expr) => {
self.visit_expression(&expr.argument);
}
Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::StringLiteral(_)
| Expression::NumericLiteral(_) => {
// no-op
}
_ => {
todo!("more expression types")
todo!("{:#?}", expr)
}
}
}
@@ -11,8 +11,8 @@ use swc_core::ecma::ast::{
AssignOp, BinaryOp, BlockStmt, BlockStmtOrExpr, Callee, Decl, EsVersion, Expr, ExprOrSpread,
Function, Ident, JSXAttr, JSXAttrName, JSXAttrOrSpread, JSXAttrValue, JSXElement,
JSXElementChild, JSXElementName, JSXExpr, JSXMemberExpr, JSXObject, Lit, MemberExpr,
MemberProp, ModuleItem, OptChainBase, Pat, PatOrExpr, Program, Stmt, UnaryOp, VarDecl,
VarDeclKind, VarDeclOrExpr,
MemberProp, ModuleItem, OptChainBase, Pat, PatOrExpr, Program, Stmt, UnaryOp, UpdateOp,
VarDecl, VarDeclKind, VarDeclOrExpr,
};
use swc_core::ecma::parser::{Syntax, TsConfig};
use swc_core::ecma::transforms::base::resolver;
@@ -271,6 +271,14 @@ fn convert_statement(cx: &Context, stmt: &Stmt) -> forget_estree::Statement {
range: convert_span(&item.span),
}))
}
Stmt::Labeled(item) => {
forget_estree::Statement::LabeledStatement(Box::new(forget_estree::LabeledStatement {
label: convert_identifier(cx, &item.label),
body: convert_statement(cx, &item.body),
loc: None,
range: convert_span(&item.span),
}))
}
_ => todo!("translate statement {:#?}", stmt),
}
}
@@ -461,6 +469,15 @@ fn convert_expression(cx: &Context, expr: &Expr) -> forget_estree::Expression {
forget_estree::Expression::JSXElement(Box::new(convert_jsx_element(cx, expr)))
}
Expr::Paren(expr) => convert_expression(cx, &expr.expr),
Expr::Update(expr) => {
forget_estree::Expression::UpdateExpression(Box::new(forget_estree::UpdateExpression {
operator: convert_update_operator(expr.op),
argument: convert_expression(cx, &expr.arg),
prefix: expr.prefix,
loc: None,
range: convert_span(&expr.span),
}))
}
_ => todo!("translate expression {:#?}", expr),
}
}
@@ -885,6 +902,13 @@ fn convert_binary_operator(op: BinaryOp) -> Operator {
}
}
fn convert_update_operator(op: UpdateOp) -> forget_estree::UpdateOperator {
match op {
UpdateOp::MinusMinus => forget_estree::UpdateOperator::Decrement,
UpdateOp::PlusPlus => forget_estree::UpdateOperator::Increment,
}
}
fn convert_pattern(cx: &Context, pat: &Pat) -> forget_estree::Pattern {
match pat {
Pat::Ident(pat) => {
@@ -12,7 +12,7 @@ repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
insta = { workspace = true }
forget_estree = { workspace = true }
forget_estree_swc = { workspace = true }
@@ -12,5 +12,12 @@ repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
forget_diagnostics = { workspace = true }
forget_estree = { workspace = true }
forget_utils = { workspace = true }
forget_utils = { workspace = true }
indexmap = { workspace = true }
[dev-dependencies]
forget_hermes_parser = { workspace = true }
insta = { workspace = true }
serde_json = { workspace = true }
@@ -0,0 +1,500 @@
use forget_diagnostics::Diagnostic;
use forget_estree::{
BreakStatement, ContinueStatement, ESTreeNode, Identifier, LabeledStatement, Program,
Statement, Visitor,
};
use forget_utils::PointerAddress;
use indexmap::IndexMap;
pub fn analyze(ast: &Program) -> SemanticAnalysis {
let mut analyzer = Analyzer::new();
analyzer.visit_program(ast);
analyzer.results
}
pub struct SemanticAnalysis {
root: ScopeId,
// Storage of the semantic information
scopes: Vec<Scope>,
labels: Vec<Label>,
declarations: Vec<Declaration>,
references: Vec<Reference>,
// Mapping of AST nodes (by pointer address) to semantic information
// Not all nodes will have all types of information available
node_scopes: IndexMap<AstNode, ScopeId>,
node_labels: IndexMap<AstNode, LabelId>,
node_declarations: IndexMap<AstNode, DeclarationId>,
node_references: IndexMap<AstNode, ReferenceId>,
diagnostics: Vec<Diagnostic>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct SemanticAnalysisDebug<'a> {
root: ScopeId,
// Storage of the semantic information
scopes: &'a Vec<Scope>,
labels: &'a Vec<Label>,
declarations: &'a Vec<Declaration>,
references: &'a Vec<Reference>,
}
impl SemanticAnalysis {
fn new() -> Self {
let root_id = ScopeId(0);
Self {
root: root_id,
scopes: vec![Scope {
id: root_id,
kind: ScopeKind::Global,
parent: None,
labels: Default::default(),
declarations: Default::default(),
references: Default::default(),
children: Default::default(),
}],
labels: Default::default(),
declarations: Default::default(),
references: Default::default(),
node_scopes: Default::default(),
node_labels: Default::default(),
node_declarations: Default::default(),
node_references: Default::default(),
diagnostics: Default::default(),
}
}
pub fn debug(&self) -> SemanticAnalysisDebug<'_> {
SemanticAnalysisDebug {
root: self.root,
scopes: &self.scopes,
labels: &self.labels,
declarations: &self.declarations,
references: &self.references,
}
}
pub fn root(&self) -> &Scope {
&self.scopes[self.root.0]
}
pub fn scope(&self, id: ScopeId) -> &Scope {
&self.scopes[id.0]
}
pub fn label(&self, id: LabelId) -> &Label {
&self.labels[id.0]
}
pub fn declaration(&self, id: DeclarationId) -> &Declaration {
&self.declarations[id.0]
}
pub fn reference(&self, id: ScopeId) -> &Reference {
&self.references[id.0]
}
pub fn node_scope<T: ESTreeNode>(&self, node: &T) -> Option<&Scope> {
self.node_scopes
.get(&AstNode::from(node))
.map(|id| &self.scopes[id.0])
}
pub fn node_label(&self, node: &LabeledStatement) -> Option<&Label> {
self.node_labels
.get(&AstNode::from(node))
.map(|id| &self.labels[id.0])
}
pub fn break_label(&self, node: &BreakStatement) -> Option<&Label> {
self.node_labels
.get(&AstNode::from(node))
.map(|id| &self.labels[id.0])
}
pub fn continue_label(&self, node: &ContinueStatement) -> Option<&Label> {
self.node_labels
.get(&AstNode::from(node))
.map(|id| &self.labels[id.0])
}
pub fn node_declaration(&self, node: &Identifier) -> Option<&Declaration> {
self.node_declarations
.get(&AstNode::from(node))
.map(|id| &self.declarations[id.0])
}
pub fn node_reference(&self, node: &Identifier) -> Option<&Reference> {
self.node_references
.get(&AstNode::from(node))
.map(|id| &self.references[id.0])
}
pub fn lookup_label(&self, scope: ScopeId, name: &str) -> Option<&Label> {
let mut current = &self.scopes[scope.0];
loop {
if let Some(id) = current.labels.get(name) {
return Some(&self.labels[id.0]);
}
if let Some(parent) = current.parent {
current = &self.scopes[parent.0];
} else {
return None;
}
}
}
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(crate) fn root_id(&self) -> ScopeId {
self.root
}
pub(crate) fn add_scope(&mut self, parent: ScopeId, kind: ScopeKind) -> ScopeId {
let id = ScopeId(self.scopes.len());
self.scopes.push(Scope {
id,
kind,
parent: Some(parent),
labels: Default::default(),
declarations: Default::default(),
references: Default::default(),
children: Default::default(),
});
self.scopes[parent.0].children.push(id);
id
}
pub(crate) fn add_label(&mut self, scope: ScopeId, kind: LabelKind, name: String) -> LabelId {
let id = LabelId(self.labels.len());
self.labels.push(Label { id, kind, scope });
self.scopes[scope.0].labels.insert(name, id);
id
}
pub(crate) fn add_declaration(
&mut self,
scope: ScopeId,
name: String,
kind: DeclarationKind,
) -> DeclarationId {
let id = DeclarationId(self.declarations.len());
self.declarations.push(Declaration { id, kind, scope });
self.scopes[scope.0].declarations.insert(name, id);
id
}
pub(crate) fn add_reference(
&mut self,
scope: ScopeId,
kind: ReferenceKind,
declaration: DeclarationId,
) -> ReferenceId {
let id = ReferenceId(self.references.len());
self.references.push(Reference {
id,
kind,
declaration,
scope,
});
self.scopes[scope.0].references.push(id);
id
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct ScopeId(usize);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct DeclarationId(usize);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct ReferenceId(usize);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct LabelId(usize);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum ScopeKind {
Global,
Function,
Class,
Block,
}
#[derive(Debug, Clone)]
pub struct Scope {
pub id: ScopeId,
pub kind: ScopeKind,
pub parent: Option<ScopeId>,
pub labels: IndexMap<String, LabelId>,
pub declarations: IndexMap<String, DeclarationId>,
pub references: Vec<ReferenceId>,
pub children: Vec<ScopeId>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum LabelKind {
Loop,
Other,
}
#[derive(Debug, Clone)]
pub struct Label {
pub id: LabelId,
pub kind: LabelKind,
pub scope: ScopeId,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum DeclarationKind {
Const,
Var,
Let,
}
#[derive(Debug, Clone)]
pub struct Declaration {
pub id: DeclarationId,
pub kind: DeclarationKind,
pub scope: ScopeId,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub enum ReferenceKind {
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone)]
pub struct Reference {
pub id: ReferenceId,
pub kind: ReferenceKind,
pub declaration: DeclarationId,
pub scope: ScopeId,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
struct AstNode(PointerAddress);
impl AstNode {
fn new<T: ESTreeNode>(node: &T) -> Self {
Self(PointerAddress::new(node))
}
}
impl<T> From<&T> for AstNode
where
T: ESTreeNode,
{
fn from(value: &T) -> Self {
Self::new(value)
}
}
impl<T> From<&mut T> for AstNode
where
T: ESTreeNode,
{
fn from(value: &mut T) -> Self {
Self::new(value)
}
}
struct Analyzer {
results: SemanticAnalysis,
current: ScopeId,
is_lvalue: bool,
}
impl Analyzer {
fn new() -> Self {
let results = SemanticAnalysis::new();
let current = results.root_id();
Self {
results,
current,
is_lvalue: false,
}
}
fn enter<F>(&mut self, kind: ScopeKind, mut f: F) -> ScopeId
where
F: FnMut(&mut Self) -> (),
{
let scope = self.results.add_scope(self.current, kind);
let previous = std::mem::replace(&mut self.current, scope);
f(self);
let scope = std::mem::replace(&mut self.current, previous);
scope
}
}
impl<'ast> Visitor<'ast> for Analyzer {
fn visit_function_declaration(
&mut self,
declaration: &'ast forget_estree::FunctionDeclaration,
) {
let scope = self.enter(ScopeKind::Function, |visitor| {
visitor.visit_function(&declaration.function);
});
self.results
.node_scopes
.insert(AstNode::from(declaration), scope);
}
fn visit_statement(&mut self, stmt: &'ast forget_estree::Statement) {
match stmt {
Statement::LabeledStatement(stmt) => {
let inner = &stmt.body;
let kind = match inner {
Statement::ForStatement(_)
| Statement::ForInStatement(_)
| Statement::ForOfStatement(_)
| Statement::WhileStatement(_)
| Statement::DoWhileStatement(_) => LabelKind::Loop,
_ => LabelKind::Other,
};
let id = self
.results
.add_label(self.current, kind, stmt.label.name.clone());
self.results
.node_labels
.insert(AstNode::from(stmt.as_ref()), id);
self.visit_statement(&stmt.body);
}
Statement::BreakStatement(stmt) => {
if let Some(label) = &stmt.label {
if let Some(label) = self.results.lookup_label(self.current, &label.name) {
self.results
.node_labels
.insert(AstNode::from(stmt.as_ref()), label.id);
} else {
self.results.diagnostics.push(Diagnostic::invalid_syntax(
"Undefined break label",
label.range,
));
}
}
}
Statement::ContinueStatement(stmt) => {
if let Some(label_node) = &stmt.label {
if let Some(label) = self.results.lookup_label(self.current, &label_node.name) {
if label.kind == LabelKind::Loop {
self.results
.node_labels
.insert(AstNode::from(stmt.as_ref()), label.id);
} else {
self.results.diagnostics.push(Diagnostic::invalid_syntax(
"Invalid continue statement, can only continue to a label associated with a loop statement (for, for..in, for..of, etc)",
label_node.range,
));
}
} else {
self.results.diagnostics.push(Diagnostic::invalid_syntax(
"Undefined continue label",
label_node.range,
));
}
}
}
Statement::BlockStatement(stmt) => {
let scope = self.enter(ScopeKind::Block, |visitor| {
for item in &stmt.body {
visitor.visit_statement(item);
}
});
self.results
.node_scopes
.insert(AstNode::from(stmt.as_ref()), scope);
}
_ => {
self.default_visit_statement(stmt);
}
}
}
fn visit_identifier(&mut self, identifier: &'ast Identifier) {
if self.is_lvalue {
let declaration = self
.results
.lookup_declaration(self.current, &identifier.name);
if let Some(declaration) = declaration {
let id = self.results.add_reference(
self.current,
ReferenceKind::ReadWrite,
declaration.id,
);
self.results
.node_references
.insert(AstNode::from(identifier), id);
} else {
let id = self.results.add_declaration(
self.current,
identifier.name.clone(),
DeclarationKind::Let,
); // TODO: determine the correct kind!
self.results
.node_declarations
.insert(AstNode::from(identifier), id);
}
} else {
let declaration = self
.results
.lookup_declaration(self.current, &identifier.name);
if let Some(declaration) = declaration {
let declaration_id = declaration.id;
let id =
self.results
.add_reference(self.current, ReferenceKind::Read, declaration.id);
self.results
.node_references
.insert(AstNode::from(identifier), id);
} else {
// Oops, undefined variable
self.results.diagnostics.push(Diagnostic::invalid_syntax(
"Undefined variable",
identifier.range,
));
}
}
}
fn visit_literal(&mut self, _literal: &'ast forget_estree::Literal) {}
fn visit_lvalue<F>(&mut self, f: F)
where
F: FnOnce(&mut Self) -> (),
{
let prev = self.is_lvalue;
self.is_lvalue = true;
f(self);
self.is_lvalue = prev;
}
fn visit_rvalue<F>(&mut self, f: F)
where
F: FnOnce(&mut Self) -> (),
{
let prev = self.is_lvalue;
self.is_lvalue = false;
f(self);
self.is_lvalue = prev;
}
}
@@ -1,52 +1,3 @@
use std::collections::HashMap;
mod analyze;
use forget_estree::{ESTreeNode, Identifier, Program};
use forget_utils::PointerAddress;
pub fn analyze<'ast>(ast: &'ast Program) -> SemanticAnalysis {
todo!("Actually analyze code")
}
#[derive(Default, Debug)]
pub struct SemanticAnalysis {
scopes: HashMap<AstNode, Scope>,
references: HashMap<AstNode, Reference>,
}
impl SemanticAnalysis {
pub(crate) fn new() -> Self {
Default::default()
}
fn scope<T: ESTreeNode>(&self, node: &T) -> Option<&Scope> {
self.scopes.get(&node.into())
}
fn reference(&self, identifier: &Identifier) -> Option<&Reference> {
self.references.get(&identifier.into())
}
}
#[derive(Debug)]
pub struct Scope {}
#[derive(Debug)]
pub struct Reference {}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
struct AstNode(PointerAddress);
impl AstNode {
fn new<T: ESTreeNode>(node: &T) -> Self {
Self(PointerAddress::new(node))
}
}
impl<T> From<&T> for AstNode
where
T: ESTreeNode,
{
fn from(value: &T) -> Self {
Self::new(value)
}
}
pub use analyze::analyze;
@@ -0,0 +1,19 @@
use forget_hermes_parser::parse;
use forget_semantic_analysis::analyze;
use insta::{assert_snapshot, glob};
#[test]
fn fixtures() {
glob!("fixtures/**.js", |path| {
println!("fixture {}", path.to_str().unwrap());
let input = std::fs::read_to_string(path).unwrap();
let ast = parse(&input, path.to_str().unwrap()).unwrap();
let analysis = analyze(&ast);
let ast_output = serde_json::to_string_pretty(&ast).unwrap();
let analysis_output = format!("{:#?}", analysis.debug());
assert_snapshot!(format!(
"Input:\n{input}\n\nAST:\n{ast_output}\n\nAnalysis:\n{analysis_output}"
));
});
}
@@ -0,0 +1,13 @@
function Component(props) {
let y = 0;
foo: for (let x = 0; x < 10; x++) {
if (x == 7) {
break foo;
}
y = x + y;
continue foo;
}
bar: if (props) {
break bar;
}
}
@@ -0,0 +1,715 @@
---
source: crates/forget_semantic_analysis/tests/analysis_test.rs
expression: "format!(\"Input:\\n{input}\\n\\nAST:\\n{ast_output}\\n\\nAnalysis:\\n{analysis_output}\")"
input_file: crates/forget_semantic_analysis/tests/fixtures/labels.js
---
Input:
function Component(props) {
let y = 0;
foo: for (let x = 0; x < 10; x++) {
if (x == 7) {
break foo;
}
y = x + y;
continue foo;
}
bar: if (props) {
break bar;
}
}
AST:
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "Component",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"params": [
{
"type": "Identifier",
"name": "props",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "VariableDeclaration",
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "y",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"init": {
"type": "NumericLiteral",
"value": 0,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
{
"type": "LabeledStatement",
"label": {
"type": "Identifier",
"name": "foo",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"body": {
"type": "ForStatement",
"init": {
"type": "VariableDeclaration",
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "x",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"init": {
"type": "NumericLiteral",
"value": 0,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"test": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "x",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"operator": "<",
"right": {
"type": "NumericLiteral",
"value": 4621819117588971520,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"update": {
"type": "UpdateExpression",
"operator": "++",
"argument": {
"type": "Identifier",
"name": "x",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"prefix": false,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"body": {
"type": "BlockStatement",
"body": [
{
"type": "IfStatement",
"test": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "x",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"operator": "==",
"right": {
"type": "NumericLiteral",
"value": 4619567317775286272,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "BreakStatement",
"label": {
"type": "Identifier",
"name": "foo",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"alternate": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "y",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"right": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "x",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"operator": "+",
"right": {
"type": "Identifier",
"name": "y",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"directive": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
{
"type": "ContinueStatement",
"label": {
"type": "Identifier",
"name": "foo",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
{
"type": "LabeledStatement",
"label": {
"type": "Identifier",
"name": "bar",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"body": {
"type": "IfStatement",
"test": {
"type": "Identifier",
"name": "props",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "BreakStatement",
"label": {
"type": "Identifier",
"name": "bar",
"typeAnnotation": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"alternate": null,
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"loc": null,
"range": {
"start": 0,
"end": 1
}
},
"generator": false,
"async": false,
"loc": null,
"range": {
"start": 0,
"end": 1
},
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
],
"sourceType": "script",
"loc": null,
"range": {
"start": 0,
"end": 1
}
}
Analysis:
SemanticAnalysisDebug {
root: ScopeId(
0,
),
scopes: [
Scope {
id: ScopeId(
0,
),
kind: Global,
parent: None,
labels: {},
declarations: {},
references: [],
children: [
ScopeId(
1,
),
],
},
Scope {
id: ScopeId(
1,
),
kind: Function,
parent: Some(
ScopeId(
0,
),
),
labels: {
"foo": LabelId(
0,
),
"bar": LabelId(
1,
),
},
declarations: {
"props": DeclarationId(
0,
),
"y": DeclarationId(
1,
),
"x": DeclarationId(
2,
),
},
references: [
ReferenceId(
0,
),
ReferenceId(
1,
),
ReferenceId(
6,
),
],
children: [
ScopeId(
2,
),
ScopeId(
4,
),
],
},
Scope {
id: ScopeId(
2,
),
kind: Block,
parent: Some(
ScopeId(
1,
),
),
labels: {},
declarations: {},
references: [
ReferenceId(
2,
),
ReferenceId(
3,
),
ReferenceId(
4,
),
ReferenceId(
5,
),
],
children: [
ScopeId(
3,
),
],
},
Scope {
id: ScopeId(
3,
),
kind: Block,
parent: Some(
ScopeId(
2,
),
),
labels: {},
declarations: {},
references: [],
children: [],
},
Scope {
id: ScopeId(
4,
),
kind: Block,
parent: Some(
ScopeId(
1,
),
),
labels: {},
declarations: {},
references: [],
children: [],
},
],
labels: [
Label {
id: LabelId(
0,
),
kind: Loop,
scope: ScopeId(
1,
),
},
Label {
id: LabelId(
1,
),
kind: Other,
scope: ScopeId(
1,
),
},
],
declarations: [
Declaration {
id: DeclarationId(
0,
),
kind: Let,
scope: ScopeId(
1,
),
},
Declaration {
id: DeclarationId(
1,
),
kind: Let,
scope: ScopeId(
1,
),
},
Declaration {
id: DeclarationId(
2,
),
kind: Let,
scope: ScopeId(
1,
),
},
],
references: [
Reference {
id: ReferenceId(
0,
),
kind: Read,
declaration: DeclarationId(
2,
),
scope: ScopeId(
1,
),
},
Reference {
id: ReferenceId(
1,
),
kind: Read,
declaration: DeclarationId(
2,
),
scope: ScopeId(
1,
),
},
Reference {
id: ReferenceId(
2,
),
kind: Read,
declaration: DeclarationId(
2,
),
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
3,
),
kind: ReadWrite,
declaration: DeclarationId(
1,
),
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
4,
),
kind: Read,
declaration: DeclarationId(
2,
),
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
5,
),
kind: Read,
declaration: DeclarationId(
1,
),
scope: ScopeId(
2,
),
},
Reference {
id: ReferenceId(
6,
),
kind: Read,
declaration: DeclarationId(
0,
),
scope: ScopeId(
1,
),
},
],
}