use std::collections::HashMap;
use hermes_ast::context::GCLock;
use hermes_ast::SemaId;
use hermes_atom_table::INVALID_ATOM_BYTES;
use crate::ids::{DeclId, FunctionInfoId, ScopeId};
use crate::sem_context::{Atom, DeclKind, DeclSpecial, SemContext};
struct Numbering<Id> {
next_number: usize,
numbers: HashMap<Id, usize>,
}
impl<Id: Copy + Eq + std::hash::Hash> Numbering<Id> {
fn new() -> Self {
Numbering {
next_number: 1,
numbers: HashMap::new(),
}
}
fn get_number(&mut self, id: Id) -> usize {
if let Some(&n) = self.numbers.get(&id) {
return n;
}
let n = self.next_number;
self.next_number += 1;
self.numbers.insert(id, n);
n
}
}
#[derive(Clone, Copy)]
struct Env<'e, 'ast, 'ctx> {
gc: &'e GCLock<'ast, 'ctx>,
sc: &'e SemContext,
}
pub type AnnotateDeclFunc = Box<dyn Fn(&mut Vec<u8>, DeclId)>;
pub struct SemContextDumper {
annotate_decl: Option<AnnotateDeclFunc>,
decl_numbers: Numbering<DeclId>,
scope_numbers: Numbering<ScopeId>,
}
impl Default for SemContextDumper {
fn default() -> Self {
Self::new()
}
}
impl SemContextDumper {
pub fn new() -> Self {
SemContextDumper {
annotate_decl: None,
decl_numbers: Numbering::new(),
scope_numbers: Numbering::new(),
}
}
pub fn new_annotated(f: AnnotateDeclFunc) -> Self {
SemContextDumper {
annotate_decl: Some(f),
decl_numbers: Numbering::new(),
scope_numbers: Numbering::new(),
}
}
pub fn print_sem_context(
&mut self,
out: &mut Vec<u8>,
gc: &GCLock,
ctx: &SemContext,
root_func: Option<FunctionInfoId>,
) {
let root_func = root_func
.unwrap_or_else(|| FunctionInfoId::from_sema_id(SemaId(0)));
push_str(out, "SemContext\n");
let mut children: HashMap<Option<FunctionInfoId>, Vec<FunctionInfoId>> =
HashMap::new();
for i in 0..ctx.functions_len() {
let fid = FunctionInfoId::from_sema_id(SemaId(i as u32));
if fid == root_func {
continue;
}
let parent = ctx.function(fid).parent_function;
children.entry(parent).or_default().push(fid);
}
let env = Env { gc, sc: ctx };
self.dump_function(out, env, &children, root_func, 0);
}
fn dump_function(
&mut self,
out: &mut Vec<u8>,
env: Env<'_, '_, '_>,
children: &HashMap<Option<FunctionInfoId>, Vec<FunctionInfoId>>,
f: FunctionInfoId,
level: u32,
) {
self.print_function(out, env, f, level);
if let Some(kids) = children.get(&Some(f)) {
for &child in kids {
self.dump_function(out, env, children, child, level + 1);
}
}
}
fn print_function(
&mut self,
out: &mut Vec<u8>,
env: Env<'_, '_, '_>,
f: FunctionInfoId,
level: u32,
) {
let info = env.sc.function(f);
push_indent(out, level);
push_str(
out,
if info.is_static_block {
"StaticBlock "
} else {
"Func "
},
);
push_str(out, if info.strict { "strict" } else { "loose" });
push_str(
out,
if info.may_reach_implicit_return {
" mayReachImplicitReturn"
} else {
" noImplicitReturn"
},
);
out.push(b'\n');
let scopes = info.get_scopes();
debug_assert!(
!scopes.is_empty(),
"every FunctionInfo has at least one scope"
);
let first = scopes[0];
let mut children: HashMap<Option<ScopeId>, Vec<ScopeId>> =
HashMap::new();
for &sc_id in &scopes[1..] {
let parent = env.sc.scope(sc_id).parent_scope;
children.entry(parent).or_default().push(sc_id);
}
let processed = self.dump_scope(out, env, &children, first, level + 1);
debug_assert_eq!(
processed,
scopes.len(),
"not all scopes were visited"
);
}
fn dump_scope(
&mut self,
out: &mut Vec<u8>,
env: Env<'_, '_, '_>,
children: &HashMap<Option<ScopeId>, Vec<ScopeId>>,
s: ScopeId,
level: u32,
) -> usize {
self.print_scope(out, env, s, level);
let mut processed = 1;
if let Some(kids) = children.get(&Some(s)) {
for &child in kids {
processed +=
self.dump_scope(out, env, children, child, level + 1);
}
}
processed
}
fn print_scope(
&mut self,
out: &mut Vec<u8>,
env: Env<'_, '_, '_>,
s: ScopeId,
level: u32,
) {
push_indent(out, level);
push_str(out, "Scope %s.");
let n = self.scope_numbers.get_number(s);
push_str(out, &n.to_string());
out.push(b'\n');
let scope = env.sc.scope(s);
for &d in &scope.decls {
push_indent(out, level + 1);
self.print_decl(out, env, d);
out.push(b'\n');
}
for fd in &scope.hoisted_functions {
push_indent(out, level + 1);
push_str(out, "hoistedFunction ");
let node = fd.node(env.gc);
let func_decl = node.as_function_declaration().expect(
"SemContext::hoistedFunctions entries are always \
FunctionDeclaration nodes",
);
match func_decl.id {
Some(id_node) => {
let ident = id_node
.as_identifier()
.expect("FunctionDeclaration.id is an Identifier");
push_atom(out, env.gc, ident.name.get());
}
None => push_str(out, "*default*"),
}
out.push(b'\n');
}
}
pub fn print_scope_ref(&mut self, out: &mut Vec<u8>, s: ScopeId) {
push_str(out, "Scope %s.");
push_str(out, &self.scope_numbers.get_number(s).to_string());
}
fn print_decl(
&mut self,
out: &mut Vec<u8>,
env: Env<'_, '_, '_>,
d: DeclId,
) {
push_str(out, "Decl %d.");
let n = self.decl_numbers.get_number(d);
push_str(out, &n.to_string());
push_str(out, " '");
let decl = env.sc.decl(d);
push_atom(out, env.gc, decl.name);
push_str(out, "' ");
push_str(out, decl_kind_str(decl.kind));
if decl.special != DeclSpecial::NotSpecial {
out.push(b' ');
push_str(out, decl_special_str(decl.special));
}
if let Some(annotate) = &self.annotate_decl {
annotate(out, d);
}
}
pub fn print_decl_ref(
&mut self,
out: &mut Vec<u8>,
gc: &GCLock,
ctx: &SemContext,
d: DeclId,
print_name: bool,
) {
push_str(out, "%d.");
push_str(out, &self.decl_numbers.get_number(d).to_string());
if print_name {
let decl = ctx.decl(d);
if decl.name != INVALID_ATOM_BYTES {
push_str(out, " '");
push_atom(out, gc, decl.name);
out.push(b'\'');
}
}
}
}
pub(crate) fn push_indent(out: &mut Vec<u8>, level: u32) {
out.resize(out.len() + (level * 4) as usize, b' ');
}
pub(crate) fn push_str(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(s.as_bytes());
}
pub(crate) fn push_atom(out: &mut Vec<u8>, gc: &GCLock, atom: Atom) {
out.extend_from_slice(gc.bytes(atom));
}
fn decl_kind_str(kind: DeclKind) -> &'static str {
match kind {
DeclKind::Let => "Let",
DeclKind::Const => "Const",
DeclKind::Class => "Class",
DeclKind::Catch => "Catch",
DeclKind::Import => "Import",
DeclKind::ES5Catch => "ES5Catch",
DeclKind::FunctionExprName => "FunctionExprName",
DeclKind::ClassExprName => "ClassExprName",
DeclKind::TypedBuiltin => "TypedBuiltin",
DeclKind::ScopedFunction => "ScopedFunction",
DeclKind::Var => "Var",
DeclKind::Parameter => "Parameter",
DeclKind::GlobalProperty => "GlobalProperty",
DeclKind::UndeclaredGlobalProperty => "UndeclaredGlobalProperty",
DeclKind::PrivateField => "PrivateField",
DeclKind::PrivateMethod => "PrivateMethod",
DeclKind::PrivateGetter => "PrivateGetter",
DeclKind::PrivateSetter => "PrivateSetter",
DeclKind::PrivateGetterSetter => "PrivateGetterSetter",
}
}
fn decl_special_str(special: DeclSpecial) -> &'static str {
match special {
DeclSpecial::NotSpecial => {
debug_assert!(false, "callers must filter out NotSpecial");
"NotSpecial"
}
DeclSpecial::Arguments => "Arguments",
DeclSpecial::Eval => "Eval",
DeclSpecial::PrivateStatic => "PrivateStatic",
}
}
#[cfg(test)]
mod tests {
use super::*;
use hermes_ast::context::Context;
use crate::keywords::Keywords;
use crate::sem_context::{ConstructorKind, FuncIsArrow};
#[test]
fn new_annotated_hook_runs_after_the_decl_line() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
Default::default(),
);
let s = sc.new_scope(f, None);
sc.new_decl_in_scope(
gc.atom_bytes("x"),
DeclKind::Let,
s,
DeclSpecial::NotSpecial,
);
let annotate: AnnotateDeclFunc =
Box::new(|out: &mut Vec<u8>, d: DeclId| {
push_str(out, " /* annotated ");
push_str(out, &d.index().to_string());
push_str(out, " */");
});
let mut dumper = SemContextDumper::new_annotated(annotate);
let mut out = Vec::new();
dumper.print_sem_context(&mut out, &gc, &sc, None);
let expected = "\
SemContext
Func loose mayReachImplicitReturn
Scope %s.1
Decl %d.1 'x' Let /* annotated 0 */
";
assert_eq!(String::from_utf8(out).unwrap(), expected);
}
#[test]
fn print_decl_ref_without_name_omits_the_quoted_name() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
Default::default(),
);
let s = sc.new_scope(f, None);
let d = sc.new_decl_in_scope(
gc.atom_bytes("x"),
DeclKind::Let,
s,
DeclSpecial::NotSpecial,
);
let mut dumper = SemContextDumper::new();
let mut out = Vec::new();
dumper.print_decl_ref(&mut out, &gc, &sc, d, false);
assert_eq!(out, b"%d.1");
let mut out2 = Vec::new();
dumper.print_decl_ref(&mut out2, &gc, &sc, d, true);
assert_eq!(out2, b"%d.1 'x'");
}
#[test]
fn print_scope_ref_has_no_name() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
Default::default(),
);
let s = sc.new_scope(f, None);
let mut dumper = SemContextDumper::new();
let mut out = Vec::new();
dumper.print_scope_ref(&mut out, s);
assert_eq!(out, b"Scope %s.1");
}
#[test]
fn decl_with_special_prints_the_special_suffix() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
Default::default(),
);
let s = sc.new_scope(f, None);
sc.new_decl_in_scope(
gc.atom_bytes("arguments"),
DeclKind::Var,
s,
DeclSpecial::Arguments,
);
let mut dumper = SemContextDumper::new();
let mut out = Vec::new();
dumper.print_sem_context(&mut out, &gc, &sc, None);
let expected = "\
SemContext
Func loose mayReachImplicitReturn
Scope %s.1
Decl %d.1 'arguments' Var Arguments
";
assert_eq!(String::from_utf8(out).unwrap(), expected);
}
#[test]
fn static_block_function_prints_static_block_label() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
true,
Default::default(),
);
sc.function_mut(f).is_static_block = true;
sc.new_scope(f, None);
let mut dumper = SemContextDumper::new();
let mut out = Vec::new();
dumper.print_sem_context(&mut out, &gc, &sc, None);
assert_eq!(out, b"SemContext\nStaticBlock strict mayReachImplicitReturn\n Scope %s.1\n");
}
#[test]
fn decl_name_with_wtf8_lone_surrogate_passes_through_unmodified() {
let mut ctx = Context::new();
let gc = GCLock::new(&mut ctx);
let mut sc = SemContext::new(Keywords::new(&gc));
let f = sc.new_function(
FuncIsArrow::No,
ConstructorKind::None,
None,
None,
false,
Default::default(),
);
let s = sc.new_scope(f, None);
let lone_surrogate_name: Vec<u8> = vec![0xED, 0xA0, 0x80];
let name = gc.atom_bytes(lone_surrogate_name.clone());
let d = sc.new_decl_in_scope(
name,
DeclKind::Let,
s,
DeclSpecial::NotSpecial,
);
let mut dumper = SemContextDumper::new();
let mut out = Vec::new();
dumper.print_decl_ref(&mut out, &gc, &sc, d, true);
let mut expected = b"%d.1 '".to_vec();
expected.extend_from_slice(&lone_surrogate_name);
expected.push(b'\'');
assert_eq!(out, expected);
let mut out2 = Vec::new();
dumper.print_sem_context(&mut out2, &gc, &sc, None);
let mut expected2 =
b"SemContext\nFunc loose mayReachImplicitReturn\n \
Scope %s.1\n Decl %d.1 '"
.to_vec();
expected2.extend_from_slice(&lone_surrogate_name);
expected2.extend_from_slice(b"' Let\n");
assert_eq!(out2, expected2);
}
}