feat(semantic): implement scopes (#135)

closes #119
This commit is contained in:
Boshen 2023-03-05 07:43:32 -08:00 committed by GitHub
parent cb886d8a36
commit 683778dfe2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 410 additions and 20 deletions

1
Cargo.lock generated
View file

@ -900,6 +900,7 @@ dependencies = [
name = "oxc_semantic"
version = "0.0.0"
dependencies = [
"bitflags",
"indextree",
"oxc_ast",
]

View file

@ -124,6 +124,7 @@ This project partially copies code from the following projects:
| [microsoft/TypeScript](https://github.com/microsoft/TypeScript) | [Apache 2.0](https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt) |
| [rome/tools](https://github.com/rome/tools) | [MIT](https://github.com/rome/tools/blob/main/LICENSE) |
| [mozilla-spidermonkey/jsparagus](https://github.com/mozilla-spidermonkey/jsparagus) | [MIT](https://github.com/mozilla-spidermonkey/jsparagus/blob/master/LICENSE-MIT) [Apache 2.0](https://github.com/mozilla-spidermonkey/jsparagus/blob/master/LICENSE-APACHE-2.0) |
| [acorn](https://github.com/acornjs/acorn) | [MIT](https://github.com/acornjs/acorn/blob/master/acorn/LICENSE) |
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg?color=brightgreen

View file

@ -146,3 +146,29 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
acorn
MIT License
Copyright (C) 2012-2022 by various contributors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -135,7 +135,7 @@ impl Cli {
let program = allocator.alloc(ret.program);
let trivias = Rc::new(ret.trivias);
let semantic = SemanticBuilder::new().build(program, trivias);
let semantic = SemanticBuilder::new(source_type).build(program, trivias);
let result = Linter::new().run(&Rc::new(semantic), &source_text, fix);
if result.is_empty() {

View file

@ -63,7 +63,7 @@ impl Tester {
assert!(ret.errors.is_empty(), "{:?}", &ret.errors);
let program = allocator.alloc(ret.program);
let trivias = Rc::new(ret.trivias);
let semantic = SemanticBuilder::new().build(program, trivias);
let semantic = SemanticBuilder::new(source_type).build(program, trivias);
let semantic = Rc::new(semantic);
let rule = RULES
.iter()

View file

@ -13,3 +13,4 @@ version.workspace = true
oxc_ast = { path = "../oxc_ast" }
indextree = { workspace = true }
bitflags = { workspace = true }

View file

@ -4,28 +4,33 @@
use std::rc::Rc;
use oxc_ast::{ast::Program, visit::Visit, AstKind, Trivias};
use oxc_ast::{ast::Program, visit::Visit, AstKind, SourceType, Trivias};
use crate::{
node::{AstNodeId, AstNodes, SemanticNode},
node::{AstNodeId, AstNodes, NodeFlags, SemanticNode},
scope::ScopeBuilder,
Semantic,
};
#[derive(Debug)]
pub struct SemanticBuilder<'a> {
nodes: AstNodes<'a>,
// states
current_node_id: AstNodeId,
current_node_flags: NodeFlags,
// builders
nodes: AstNodes<'a>,
scope: ScopeBuilder,
}
impl<'a> SemanticBuilder<'a> {
#[must_use]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
pub fn new(source_type: SourceType) -> Self {
let scope = ScopeBuilder::new(source_type);
let mut nodes = AstNodes::default();
let semantic_node = SemanticNode::new(AstKind::Root);
let semantic_node =
SemanticNode::new(AstKind::Root, scope.current_scope_id, NodeFlags::empty());
let current_node_id = nodes.new_node(semantic_node).into();
Self { nodes, current_node_id }
Self { current_node_id, nodes, scope, current_node_flags: NodeFlags::empty() }
}
#[must_use]
@ -36,7 +41,8 @@ impl<'a> SemanticBuilder<'a> {
}
fn create_ast_node(&mut self, kind: AstKind<'a>) {
let ast_node = SemanticNode::new(kind);
let ast_node =
SemanticNode::new(kind, self.scope.current_scope_id, self.current_node_flags);
let node_id = self.nodes.new_node(ast_node);
self.current_node_id.append(node_id, &mut self.nodes);
self.current_node_id = node_id.into();
@ -46,14 +52,35 @@ impl<'a> SemanticBuilder<'a> {
self.current_node_id =
self.nodes[self.current_node_id.indextree_id()].parent().unwrap().into();
}
fn try_enter_scope(&mut self, kind: AstKind<'a>) {
if let Some(flags) = ScopeBuilder::scope_flags_from_ast_kind(kind) {
self.scope.enter(flags);
}
}
fn try_leave_scope(&mut self, kind: AstKind<'a>) {
if ScopeBuilder::scope_flags_from_ast_kind(kind).is_some()
|| matches!(kind, AstKind::Program(_))
{
self.scope.leave();
}
}
}
impl<'a> Visit<'a> for SemanticBuilder<'a> {
// Setup all the context for the binder,
// the order is important here.
fn enter_node(&mut self, kind: AstKind<'a>) {
// create new self.scope.current_scope_id
self.try_enter_scope(kind);
// create new self.current_node_id
self.create_ast_node(kind);
}
fn leave_node(&mut self, _kind: AstKind<'a>) {
fn leave_node(&mut self, kind: AstKind<'a>) {
self.pop_ast_node();
self.try_leave_scope(kind);
}
}

View file

@ -1,11 +1,13 @@
mod builder;
mod node;
mod scope;
use std::rc::Rc;
pub use builder::SemanticBuilder;
pub use node::{AstNode, AstNodes};
use oxc_ast::Trivias;
pub use scope::{Scope, ScopeTree};
pub struct Semantic<'a> {
nodes: AstNodes<'a>,

View file

@ -1,9 +1,13 @@
#![allow(non_upper_case_globals)] // for bitflags
mod id;
mod tree;
pub use id::AstNodeId;
use bitflags::bitflags;
use oxc_ast::AstKind;
pub use tree::AstNodes;
pub use self::{id::AstNodeId, tree::AstNodes};
use crate::scope::{Scope, ScopeId};
/// Indextree node containing a semantic node
pub type AstNode<'a> = indextree::Node<SemanticNode<'a>>;
@ -13,15 +17,42 @@ pub type AstNode<'a> = indextree::Node<SemanticNode<'a>>;
pub struct SemanticNode<'a> {
/// A pointer to the ast node, which resides in the `bumpalo` memory arena.
kind: AstKind<'a>,
/// Associated Scope (initialized by binding)
scope_id: ScopeId,
flags: NodeFlags,
}
bitflags! {
#[derive(Default)]
pub struct NodeFlags: u8 {
const Class = 1 << 0; // If Node is inside a class
}
}
impl<'a> SemanticNode<'a> {
#[must_use]
pub const fn new(kind: AstKind<'a>) -> Self {
Self { kind }
pub const fn new(kind: AstKind<'a>, scope_id: ScopeId, flags: NodeFlags) -> Self {
Self { kind, scope_id, flags }
}
pub const fn kind(&self) -> AstKind<'a> {
self.kind
}
pub const fn scope_id(&self) -> ScopeId {
self.scope_id
}
#[must_use]
pub const fn strict_mode(&self, scope: &Scope) -> bool {
// All parts of a ClassDeclaration or a ClassExpression are strict mode code.
scope.strict_mode() || self.in_class()
}
#[must_use]
pub const fn in_class(self) -> bool {
self.flags.contains(NodeFlags::Class)
}
}

View file

@ -0,0 +1,71 @@
use oxc_ast::{AstKind, SourceType};
use super::{Scope, ScopeFlags, ScopeId, ScopeTree};
#[derive(Debug)]
pub struct ScopeBuilder {
pub scopes: ScopeTree,
pub current_scope_id: ScopeId,
}
impl ScopeBuilder {
#[must_use]
pub fn new(source_type: SourceType) -> Self {
// Module code is always strict mode code.
let strict_mode = source_type.is_module();
let scopes = ScopeTree::new(strict_mode);
let current_scope_id = scopes.root_scope_id();
Self { scopes, current_scope_id }
}
pub fn enter(&mut self, flags: ScopeFlags) {
// Inherit strict mode for functions
// https://tc39.es/ecma262/#sec-strict-mode-code
let mut strict_mode = self.scopes[self.scopes.root_scope_id()].strict_mode;
let parent_scope = self.current_scope();
if !strict_mode && parent_scope.is_function() && parent_scope.strict_mode {
strict_mode = true;
}
// inherit flags for non-function scopes
let flags = if flags.contains(ScopeFlags::Function) {
flags
} else {
flags | (parent_scope.flags & ScopeFlags::MODIFIERS)
};
let scope = Scope::new(flags, strict_mode);
let new_scope_id = self.scopes.new_node(scope);
self.current_scope_id.append(new_scope_id, &mut self.scopes);
self.current_scope_id = new_scope_id.into();
}
pub fn leave(&mut self) {
if let Some(parent_id) = self.scopes[self.current_scope_id.indextree_id()].parent() {
self.current_scope_id = parent_id.into();
}
}
#[must_use]
pub fn current_scope(&self) -> &Scope {
&self.scopes[self.current_scope_id]
}
#[must_use]
pub fn scope_flags_from_ast_kind(kind: AstKind) -> Option<ScopeFlags> {
match kind {
AstKind::Function(_) => Some(ScopeFlags::Function),
AstKind::ArrowExpression(_) => Some(ScopeFlags::Function | ScopeFlags::Arrow),
AstKind::StaticBlock(_) => Some(ScopeFlags::ClassStaticBlock),
AstKind::TSModuleBlock(_) => Some(ScopeFlags::TsModuleBlock),
AstKind::BlockStatement(_)
| AstKind::CatchClause(_)
| AstKind::ForStatement(_)
| AstKind::ForInStatement(_)
| AstKind::ForOfStatement(_)
| AstKind::SwitchStatement(_) => Some(ScopeFlags::empty()),
_ => None,
}
}
}

View file

@ -0,0 +1,32 @@
use std::ops::Deref;
use indextree::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScopeId(NodeId);
impl ScopeId {
#[must_use]
pub const fn new(node_id: NodeId) -> Self {
Self(node_id)
}
#[must_use]
pub const fn indextree_id(&self) -> NodeId {
self.0
}
}
impl From<NodeId> for ScopeId {
fn from(node_id: NodeId) -> Self {
Self(node_id)
}
}
impl Deref for ScopeId {
type Target = NodeId;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -0,0 +1,86 @@
//! ECMAScript Scope Tree
//! See [Scope Analysis](https://tc39.es/ecma262/#sec-syntax-directed-operations-scope-analysis)
//! Code Adapted from [acorn](https://github.com/acornjs/acorn/blob/master/acorn/src/scope.js)
#![allow(non_upper_case_globals)]
mod builder;
mod id;
mod tree;
use bitflags::bitflags;
pub use builder::*;
pub use tree::ScopeTree;
pub use self::id::ScopeId;
#[derive(Debug, Clone)]
pub struct Scope {
/// [Strict Mode Code](https://tc39.es/ecma262/#sec-strict-mode-code)
/// [Use Strict Directive Prologue](https://tc39.es/ecma262/#sec-directive-prologues-and-the-use-strict-directive)
pub(crate) strict_mode: bool,
pub flags: ScopeFlags,
}
bitflags! {
#[derive(Default)]
pub struct ScopeFlags: u16 {
const Top = 1 << 0;
const Function = 1 << 1;
const Arrow = 1 << 2;
const ClassStaticBlock = 1 << 4;
const TsModuleBlock = 1 << 5; // `declare namespace`
const Constructor = 1 << 6;
const GetAccessor = 1 << 7;
const SetAccessor = 1 << 8;
const VAR = Self::Top.bits | Self::Function.bits | Self::ClassStaticBlock.bits | Self::TsModuleBlock.bits;
const MODIFIERS = Self::Constructor.bits | Self::GetAccessor.bits | Self::SetAccessor.bits;
}
}
impl Scope {
#[must_use]
pub const fn new(flags: ScopeFlags, strict_mode: bool) -> Self {
Self { strict_mode, flags }
}
#[must_use]
pub const fn strict_mode(&self) -> bool {
self.strict_mode
}
#[must_use]
pub const fn is_top(&self) -> bool {
self.flags.intersects(ScopeFlags::Top)
}
#[must_use]
pub const fn is_ts_module(&self) -> bool {
self.flags.intersects(ScopeFlags::TsModuleBlock)
}
#[must_use]
pub const fn is_function(&self) -> bool {
self.flags.intersects(ScopeFlags::Function)
}
#[must_use]
pub const fn is_static_block(&self) -> bool {
self.flags.intersects(ScopeFlags::ClassStaticBlock)
}
#[must_use]
pub const fn is_constructor(&self) -> bool {
self.flags.intersects(ScopeFlags::Constructor)
}
#[must_use]
pub const fn is_get_accessor(&self) -> bool {
self.flags.intersects(ScopeFlags::GetAccessor)
}
#[must_use]
pub const fn is_set_accessor(&self) -> bool {
self.flags.intersects(ScopeFlags::SetAccessor)
}
}

View file

@ -0,0 +1,111 @@
use std::ops::{Deref, DerefMut, Index, IndexMut};
use indextree::{Ancestors, Arena, Node, NodeId};
use super::{Scope, ScopeFlags, ScopeId};
use crate::node::AstNode;
#[derive(Debug)]
pub struct ScopeTree {
scopes: Arena<Scope>,
root_scope_id: ScopeId,
}
impl ScopeTree {
#[must_use]
pub fn new(root_strict_mode: bool) -> Self {
let mut scopes = Arena::new();
let root_scope = Scope::new(ScopeFlags::Top, root_strict_mode);
let root_scope_id = scopes.new_node(root_scope).into();
Self { scopes, root_scope_id }
}
#[must_use]
pub const fn root_scope_id(&self) -> ScopeId {
self.root_scope_id
}
#[must_use]
pub fn ancestors(&self, scope_id: ScopeId) -> Ancestors<'_, Scope> {
scope_id.ancestors(&self.scopes)
}
#[must_use]
pub fn node_scope(&self, node: &AstNode) -> &Scope {
self.scopes[node.get().scope_id().indextree_id()].get()
}
#[must_use]
pub fn node_scope_ancestors(&self, node: &AstNode) -> Ancestors<'_, Scope> {
self.ancestors(node.get().scope_id())
}
/// # Panics
/// When parent scope cannot be found, but this will not happen because
/// scopes are never removed.
#[must_use]
pub fn parent_node_id(&self, scope_id: ScopeId) -> NodeId {
self.scopes[*scope_id].parent().unwrap()
}
#[must_use]
pub fn parent_scope(&self, scope_id: ScopeId) -> &Scope {
let parent_id = self.parent_node_id(scope_id);
self.scopes[parent_id].get()
}
#[must_use]
pub fn parent_scope_mut(&mut self, scope_id: ScopeId) -> &mut Scope {
let parent_id = self.parent_node_id(scope_id);
self.scopes[parent_id].get_mut()
}
#[must_use]
pub fn strict_mode(&self, node: &AstNode) -> bool {
let scope = self.node_scope(node);
node.get().strict_mode(scope)
}
}
impl Index<NodeId> for ScopeTree {
type Output = Node<Scope>;
fn index(&self, id: NodeId) -> &Self::Output {
&self.scopes[id]
}
}
impl IndexMut<NodeId> for ScopeTree {
fn index_mut(&mut self, id: NodeId) -> &mut Node<Scope> {
&mut self.scopes[id]
}
}
impl Index<ScopeId> for ScopeTree {
type Output = Scope;
fn index(&self, id: ScopeId) -> &Self::Output {
self.scopes[id.indextree_id()].get()
}
}
impl IndexMut<ScopeId> for ScopeTree {
fn index_mut(&mut self, id: ScopeId) -> &mut Scope {
self.scopes[id.indextree_id()].get_mut()
}
}
impl Deref for ScopeTree {
type Target = Arena<Scope>;
fn deref(&self) -> &Self::Target {
&self.scopes
}
}
impl DerefMut for ScopeTree {
fn deref_mut(&mut self) -> &mut Arena<Scope> {
&mut self.scopes
}
}

View file

@ -91,12 +91,13 @@ fn bench_semantic(criterion: &mut Criterion, codes: &[Code]) {
&code.source_text,
|b, source_text| {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, SourceType::default()).parse();
let source_type = SourceType::from_path(&code.file_name).unwrap();
let ret = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);
let trivias = Rc::new(ret.trivias);
b.iter(|| {
let _semantic =
SemanticBuilder::new().build(black_box(program), trivias.clone());
let _semantic = SemanticBuilder::new(source_type)
.build(black_box(program), trivias.clone());
});
},
);