use crate::engine::api::error::Error;
use crate::engine::code::bytecode::Instruction;
use crate::engine::code::bytecode::PrivateNameSource;
use crate::engine::compiler::lexer::Keyword;
use crate::engine::compiler::lexer::LexicalGoal;
use crate::engine::compiler::lexer::Span;
use crate::engine::compiler::lexer::TokenKind;
use crate::engine::compiler::model::bindings::BindingKind;
use crate::engine::compiler::model::bindings::BindingStorage;
use crate::engine::compiler::model::bindings::ResolvedBinding;
use crate::engine::compiler::model::bindings::binding_kinds_compatible;
use crate::engine::compiler::model::ir::FunctionId;
use crate::engine::compiler::model::ir::IrOp;
use crate::engine::compiler::model::ir::PrivateFieldAccess;
use crate::engine::compiler::model::ir::function::FunctionTree;
use crate::engine::compiler::model::scope::ScopeId;
use crate::engine::compiler::names::NameId;
use crate::engine::compiler::names::NameTable;
use crate::engine::compiler::parser::context::InMode;
use crate::engine::compiler::parser::context::Parser;
use crate::engine::compiler::parser::diagnostics::lex_error;
use crate::engine::compiler::parser::diagnostics::source_offset;
use crate::engine::compiler::parser::diagnostics::syntax_atom_error_without_span;
use crate::engine::compiler::resolution::capture_binding_path;
use crate::engine::compiler::resolution::ensure_string_constant;
use crate::source::SourceOffset;
pub(super) fn private_setter_binding_name(names: &mut NameTable, name: NameId) -> NameId {
let text = names.name(name);
let mut binding = String::with_capacity(text.len().saturating_add(5));
binding.push_str(text);
binding.push_str("<set>");
names.intern(&binding)
}
impl<'source> Parser<'source> {
pub(super) fn emit_private_field_get(
&mut self,
name: NameId,
span: Span,
site: SourceOffset,
) -> Result<usize, Error> {
let scope = self.current_ir().context.current_scope;
let span = self.current_ir_mut().operands.add_span(span)?;
self.emit_at(
IrOp::PrivateField {
name,
span,
scope,
access: PrivateFieldAccess::Get,
},
site,
)
}
pub(super) fn emit_private_field_operation(
&mut self,
name: NameId,
span: Span,
scope: ScopeId,
access: PrivateFieldAccess,
site: SourceOffset,
) -> Result<usize, Error> {
let span = self.current_ir_mut().operands.add_span(span)?;
self.emit_at(
IrOp::PrivateField {
name,
span,
scope,
access,
},
site,
)
}
pub(super) fn parse_private_in_head(&mut self) -> Result<bool, Error> {
if self.in_mode == InMode::Disallow {
return Ok(false);
}
if !matches!(self.current().kind, TokenKind::PrivateIdentifier(_)) {
return Ok(false);
}
let mut lexer = self.lexer.clone();
lexer.seek(self.current().span.end);
let next = self
.probe_token(&mut lexer, LexicalGoal::Div)
.map_err(lex_error)?;
if !matches!(next.kind, TokenKind::Keyword(Keyword::In)) {
return Ok(false);
}
let token = *self.current();
let TokenKind::PrivateIdentifier(identifier) = token.kind else {
unreachable!("private-in probe changed the current token")
};
let name = self.intern_private_identifier(&identifier);
let scope = self.current_ir().context.current_scope;
self.advance()?;
if !matches!(self.current().kind, TokenKind::Keyword(Keyword::In)) {
return Err(Error::internal(
"private-in lookahead disagreed with the parser",
));
}
self.advance()?;
self.parse_shift()?;
self.emit_private_field_operation(
name,
token.span,
scope,
PrivateFieldAccess::In,
source_offset(token.span)?,
)?;
self.anonymous_function_definition = None;
Ok(true)
}
}
#[derive(Clone, Copy, Debug)]
struct PrivateBindingResolution {
kind: BindingKind,
source: PrivateNameSource,
}
fn resolve_private_binding(
tree: &mut FunctionTree,
consuming_function: FunctionId,
use_scope: ScopeId,
name: NameId,
) -> Result<Option<PrivateBindingResolution>, Error> {
let mut owner = consuming_function;
let mut scope = use_scope;
loop {
loop {
let (parent, exact) = {
let function = tree
.functions
.get(owner)
.ok_or_else(|| Error::internal("private-name owner is out of bounds"))?;
let current = function
.scopes
.get(scope.0)
.ok_or_else(|| Error::internal("private-name use scope is out of bounds"))?;
let exact = current
.binding_named(name, &function.bindings)
.and_then(|binding| function.bindings.get(binding.0))
.map(|binding| ResolvedBinding {
storage: binding.storage,
kind: binding.kind,
});
(current.parent, exact)
};
if let Some(binding) = exact {
if !matches!(
binding.kind,
BindingKind::PrivateField { .. }
| BindingKind::PrivateMethod { .. }
| BindingKind::PrivateGetter { .. }
| BindingKind::PrivateSetter { .. }
| BindingKind::PrivateGetterSetter { .. }
) {
return Err(Error::internal(
"private spelling resolved to a non-private binding",
));
}
let source = if owner == consuming_function {
match binding.storage {
BindingStorage::Local(index) => PrivateNameSource::Local(index),
BindingStorage::External(index) => PrivateNameSource::Closure(index),
BindingStorage::Argument(_)
| BindingStorage::Module(_)
| BindingStorage::Global => {
return Err(Error::internal(
"private name occupied non-lexical storage",
));
}
}
} else {
let (index, kind) = capture_binding_path(
tree,
owner,
consuming_function,
binding,
name,
true,
false,
)?;
if !binding_kinds_compatible(kind, binding.kind) {
return Err(Error::internal(
"private-name closure relay changed binding kind",
));
}
PrivateNameSource::Closure(index)
};
return Ok(Some(PrivateBindingResolution {
kind: binding.kind,
source,
}));
}
let Some(parent) = parent else {
break;
};
scope = parent;
}
let Some(parent) = tree.functions[owner].parent else {
break;
};
owner = parent.function;
scope = parent.definition_scope;
}
Ok(None)
}
fn private_readonly_instruction(
tree: &mut FunctionTree,
consuming_function: FunctionId,
name: NameId,
) -> Result<Instruction, Error> {
let name = ensure_string_constant(
tree.functions
.get_mut(consuming_function)
.ok_or_else(|| Error::internal("private-name consumer is out of bounds"))?,
tree.names.js_string(name)?,
)?;
Ok(Instruction::ThrowReadOnly(name))
}
pub(super) fn resolve_private_field_operation(
tree: &mut FunctionTree,
consuming_function: FunctionId,
use_scope: ScopeId,
name: NameId,
access: PrivateFieldAccess,
) -> Result<IrOp, Error> {
let Some(primary) = resolve_private_binding(tree, consuming_function, use_scope, name)? else {
return Err(syntax_atom_error_without_span(
"undefined private field '",
tree.names.name(name),
"'",
)?);
};
let instruction = match (primary.kind, access) {
(
BindingKind::PrivateField { .. }
| BindingKind::PrivateMethod { .. }
| BindingKind::PrivateGetter { .. }
| BindingKind::PrivateGetterSetter { .. },
PrivateFieldAccess::Get,
) => Instruction::GetPrivateField(primary.source),
(
BindingKind::PrivateField { .. }
| BindingKind::PrivateMethod { .. }
| BindingKind::PrivateGetter { .. }
| BindingKind::PrivateGetterSetter { .. },
PrivateFieldAccess::GetKeepReceiver,
) => Instruction::GetPrivateField2(primary.source),
(
BindingKind::PrivateSetter { .. },
PrivateFieldAccess::Get | PrivateFieldAccess::GetKeepReceiver,
) => private_readonly_instruction(tree, consuming_function, name)?,
(BindingKind::PrivateField { .. }, PrivateFieldAccess::Put) => {
Instruction::PutPrivateField(primary.source)
}
(
BindingKind::PrivateSetter { .. } | BindingKind::PrivateGetterSetter { .. },
PrivateFieldAccess::Put,
) => {
let setter_name = private_setter_binding_name(&mut tree.names, name);
let setter = resolve_private_binding(tree, consuming_function, use_scope, setter_name)?
.ok_or_else(|| Error::internal("private setter binding is missing"))?;
if !matches!(setter.kind, BindingKind::PrivateSetter { .. }) {
return Err(Error::internal(
"private setter binding has the wrong capability kind",
));
}
Instruction::PutPrivateField(setter.source)
}
(
BindingKind::PrivateMethod { .. } | BindingKind::PrivateGetter { .. },
PrivateFieldAccess::Put,
) => private_readonly_instruction(tree, consuming_function, name)?,
(BindingKind::PrivateField { .. }, PrivateFieldAccess::Define) => {
Instruction::DefinePrivateField(primary.source)
}
(
BindingKind::PrivateMethod { .. }
| BindingKind::PrivateGetter { .. }
| BindingKind::PrivateSetter { .. }
| BindingKind::PrivateGetterSetter { .. },
PrivateFieldAccess::Define,
) => {
return Err(Error::internal(
"private callable reached data-field definition lowering",
));
}
(
BindingKind::PrivateField { .. }
| BindingKind::PrivateMethod { .. }
| BindingKind::PrivateGetter { .. }
| BindingKind::PrivateGetterSetter { .. },
PrivateFieldAccess::In,
) => Instruction::PrivateIn(primary.source),
(BindingKind::PrivateSetter { .. }, PrivateFieldAccess::In) => {
let setter_name = private_setter_binding_name(&mut tree.names, name);
let setter = resolve_private_binding(tree, consuming_function, use_scope, setter_name)?
.ok_or_else(|| Error::internal("private setter binding is missing"))?;
Instruction::PrivateIn(setter.source)
}
(
BindingKind::Normal
| BindingKind::Lexical { .. }
| BindingKind::FunctionName { .. }
| BindingKind::EvalVariableObject
| BindingKind::ArgEvalVariableObject
| BindingKind::WithObject,
_,
) => {
return Err(Error::internal(
"private spelling resolved to a non-private binding",
));
}
};
Ok(IrOp::Bytecode(instruction))
}
#[cfg(test)]
mod tests {
use crate::engine::api::error::ErrorKind;
use crate::engine::code::function::UnlinkedFunction;
use crate::engine::code::function::metadata::ClassInitializerKind;
use crate::engine::code::function::metadata::ClosureVariableKind;
use crate::engine::code::function::metadata::ConstructorKind;
use crate::engine::code::function::metadata::FunctionKind as BytecodeFunctionKind;
use crate::engine::compiler::compile_unlinked_script;
use crate::engine::compiler::model::ir::function::FunctionKind;
use crate::engine::compiler::model::scope::ScopeKind;
use crate::engine::value::JsString;
use super::*;
fn collect_functions<'a>(function: &'a UnlinkedFunction, out: &mut Vec<&'a UnlinkedFunction>) {
out.push(function);
for child in function
.constants()
.iter()
.filter_map(|constant| constant.as_child())
{
collect_functions(child, out);
}
}
#[test]
fn class_private_scope_keeps_field_staticness_and_computed_key_cells() {
let tree = Parser::parse(
"class C { [#later in {}] = 1; #instance; static #static = 2; #later; }",
JsString::from_static("<private-scope-test>"),
)
.unwrap();
let function = &tree.functions[0];
let scope = function
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
let bindings = scope
.bindings
.iter()
.map(|binding| &function.bindings[binding.0])
.collect::<Vec<_>>();
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#instance"
&& binding.kind == BindingKind::PrivateField { is_static: false }
}));
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#static"
&& binding.kind == BindingKind::PrivateField { is_static: true }
}));
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#later"
&& binding.kind == BindingKind::PrivateField { is_static: false }
}));
assert!(bindings.iter().any(|binding| {
tree.names
.name(binding.name)
.starts_with("<computed_field>")
&& binding.kind == BindingKind::Lexical { is_const: true }
}));
assert!(function.ops.iter().any(|operation| matches!(
&operation.op,
IrOp::PrivateField {
name,
access: PrivateFieldAccess::In,
..
} if tree.names.name(*name) == "#later"
)));
}
#[test]
fn private_data_fields_lower_to_authenticated_sources_and_lifecycles() {
let root = compile_unlinked_script(
r#"
class C {
#x = 1;
#fn = function(value) { return this.#x + value };
static #s = 2;
read() { return this.#x }
call() { return this.#fn(0) }
update() { return ++this.#x + this.#x++ + (this.#x += 1) }
assign(value) {
[this.#x] = [value];
({ value: this.#x } = { value });
return this.#x;
}
has(value) { const probe = () => #x in value; return probe() }
static readStatic() { return this.#s }
}
"#,
)
.unwrap();
let private_locals = root
.local_definitions()
.iter()
.enumerate()
.filter_map(|(index, definition)| {
(definition.kind == ClosureVariableKind::PrivateField)
.then_some((u16::try_from(index).unwrap(), definition))
})
.collect::<Vec<_>>();
assert_eq!(private_locals.len(), 3);
for &(index, definition) in &private_locals {
assert!(definition.is_lexical);
assert!(definition.is_const);
assert!(definition.name.as_ref().is_some_and(|name| {
matches!(name.to_utf8_lossy().as_str(), "#x" | "#fn" | "#s")
}));
assert!(
root.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::SetLocalUninitialized(actual) if *actual == index))
);
assert!(
root.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::InitializePrivateName(actual) if *actual == index))
);
assert!(
root.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::CloseLocal(actual) if *actual == index))
);
}
let mut functions = Vec::new();
collect_functions(&root, &mut functions);
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::GetPrivateField(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::GetPrivateField2(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::PutPrivateField(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::DefinePrivateField(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::PrivateIn(_)))
}));
let private_closures = functions
.iter()
.flat_map(|function| function.closure_variables())
.filter(|descriptor| descriptor.kind == ClosureVariableKind::PrivateField)
.collect::<Vec<_>>();
assert!(!private_closures.is_empty());
assert!(
private_closures
.iter()
.all(|descriptor| descriptor.is_lexical && descriptor.is_const)
);
}
#[test]
fn direct_eval_descriptors_retain_private_name_authority() {
let root =
compile_unlinked_script("class C { #x = 42; read() { return eval('this.#x') } }")
.unwrap();
let mut functions = Vec::new();
collect_functions(&root, &mut functions);
let eval_binding = functions
.iter()
.flat_map(|function| function.eval_environments())
.flat_map(|environment| environment.scopes.iter())
.flat_map(|scope| scope.bindings.iter())
.find(|binding| binding.kind == ClosureVariableKind::PrivateField)
.expect("eval-visible private binding");
assert!(eval_binding.is_lexical);
assert!(eval_binding.is_const);
assert_eq!(eval_binding.name.to_utf8_lossy(), "#x");
}
#[test]
fn private_methods_lower_to_typed_cells_home_objects_and_side_brands() {
let tree = Parser::parse(
r#"
class C {
call(value) { return value.#later() }
#later() { return 1 }
static read(value) { return #staticMethod in value }
static #staticMethod() { return 2 }
}
"#,
JsString::from_static("<private-method-scope-test>"),
)
.unwrap();
let root = &tree.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
let bindings = scope
.bindings
.iter()
.map(|binding| &root.bindings[binding.0])
.collect::<Vec<_>>();
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#later"
&& binding.kind == BindingKind::PrivateMethod { is_static: false }
}));
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#staticMethod"
&& binding.kind == BindingKind::PrivateMethod { is_static: true }
}));
let initialized = root
.ops
.iter()
.filter_map(|operation| match &operation.op {
IrOp::Bytecode(Instruction::InitializePrivateMethod(local)) => Some(*local),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(initialized.len(), 2);
assert!(initialized.iter().all(|local| {
root.bindings.iter().any(|binding| {
binding.storage == BindingStorage::Local(*local)
&& matches!(binding.kind, BindingKind::PrivateMethod { .. })
})
}));
let private_methods = tree
.functions
.iter()
.filter(|function| {
function.parent.is_some()
&& function.class_initializer_kind.is_none()
&& function.function_name.is_none()
&& function.needs_home_object
})
.collect::<Vec<_>>();
assert!(private_methods.len() >= 2);
let branded_initializers = tree
.functions
.iter()
.filter(|function| function.class_private_brand)
.collect::<Vec<_>>();
assert_eq!(branded_initializers.len(), 2);
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::InstanceFields)
}));
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::StaticElements)
}));
}
#[test]
fn private_async_methods_reuse_typed_method_cells_home_objects_and_side_brands() {
let source = r#"
class C {
async #instance(value) { return await value }
exposeInstance() { return this.#instance }
callInstance(value) { return this.#instance(value) }
hasInstance(value) { return #instance in value }
static async #static(value) { return await value }
static exposeStatic() { return this.#static }
static callStatic(value) { return this.#static(value) }
static hasStatic(value) { return #static in value }
}
"#;
let tree = Parser::parse(
source,
JsString::from_static("<private-async-method-scope-test>"),
)
.unwrap();
let root = &tree.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
let bindings = scope
.bindings
.iter()
.map(|binding| &root.bindings[binding.0])
.collect::<Vec<_>>();
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#instance"
&& binding.kind == BindingKind::PrivateMethod { is_static: false }
}));
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#static"
&& binding.kind == BindingKind::PrivateMethod { is_static: true }
}));
assert_eq!(
root.ops
.iter()
.filter(|operation| matches!(
operation.op,
IrOp::Bytecode(Instruction::InitializePrivateMethod(_))
))
.count(),
2
);
let private_async_methods = tree
.functions
.iter()
.filter(|function| {
function.kind == FunctionKind::Method
&& function.execution_kind == BytecodeFunctionKind::Async
})
.collect::<Vec<_>>();
assert_eq!(private_async_methods.len(), 2);
assert!(
private_async_methods
.iter()
.all(|function| function.needs_home_object)
);
let branded_initializers = tree
.functions
.iter()
.filter(|function| function.class_private_brand)
.collect::<Vec<_>>();
assert_eq!(branded_initializers.len(), 2);
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::InstanceFields)
}));
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::StaticElements)
}));
let published = compile_unlinked_script(source).unwrap();
let private_definitions = published
.local_definitions()
.iter()
.filter(|definition| definition.kind == ClosureVariableKind::PrivateMethod)
.collect::<Vec<_>>();
assert_eq!(private_definitions.len(), 2);
assert!(
private_definitions
.iter()
.all(|definition| definition.is_lexical && definition.is_const)
);
let mut functions = Vec::new();
collect_functions(&published, &mut functions);
let published_async_methods = functions
.iter()
.copied()
.filter(|function| function.metadata().function_kind == BytecodeFunctionKind::Async)
.collect::<Vec<_>>();
assert_eq!(published_async_methods.len(), 2);
assert!(published_async_methods.iter().all(|function| {
function.metadata().needs_home_object
&& !function.metadata().has_prototype
&& function.metadata().constructor_kind == ConstructorKind::None
}));
let mut accesses = (false, false, false);
for instruction in functions.iter().flat_map(|function| function.code()) {
accesses.0 |= matches!(instruction, Instruction::GetPrivateField(_));
accesses.1 |= matches!(instruction, Instruction::GetPrivateField2(_));
accesses.2 |= matches!(instruction, Instruction::PrivateIn(_));
}
assert_eq!(accesses, (true, true, true));
}
#[test]
fn private_async_generator_methods_reuse_typed_method_cells_and_async_driver_shape() {
let source = r#"
class C {
async *#instance(value) { yield await value }
exposeInstance() { return this.#instance }
callInstance(value) { return this.#instance(value) }
hasInstance(value) { return #instance in value }
static async *#static(value) { yield await value }
static exposeStatic() { return this.#static }
static callStatic(value) { return this.#static(value) }
static hasStatic(value) { return #static in value }
}
"#;
let tree = Parser::parse(
source,
JsString::from_static("<private-async-generator-method-scope-test>"),
)
.unwrap();
let root = &tree.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
let bindings = scope
.bindings
.iter()
.map(|binding| &root.bindings[binding.0])
.collect::<Vec<_>>();
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#instance"
&& binding.kind == BindingKind::PrivateMethod { is_static: false }
}));
assert!(bindings.iter().any(|binding| {
tree.names.name(binding.name) == "#static"
&& binding.kind == BindingKind::PrivateMethod { is_static: true }
}));
assert_eq!(
root.ops
.iter()
.filter(|operation| matches!(
operation.op,
IrOp::Bytecode(Instruction::InitializePrivateMethod(_))
))
.count(),
2
);
let private_methods = tree
.functions
.iter()
.filter(|function| {
function.kind == FunctionKind::Method
&& function.execution_kind == BytecodeFunctionKind::AsyncGenerator
})
.collect::<Vec<_>>();
assert_eq!(private_methods.len(), 2);
assert!(
private_methods
.iter()
.all(|function| function.needs_home_object)
);
let branded_initializers = tree
.functions
.iter()
.filter(|function| function.class_private_brand)
.collect::<Vec<_>>();
assert_eq!(branded_initializers.len(), 2);
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::InstanceFields)
}));
assert!(branded_initializers.iter().any(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::StaticElements)
}));
let published = compile_unlinked_script(source).unwrap();
let private_definitions = published
.local_definitions()
.iter()
.filter(|definition| definition.kind == ClosureVariableKind::PrivateMethod)
.collect::<Vec<_>>();
assert_eq!(private_definitions.len(), 2);
assert!(
private_definitions
.iter()
.all(|definition| definition.is_lexical && definition.is_const)
);
let mut functions = Vec::new();
collect_functions(&published, &mut functions);
let published_methods = functions
.iter()
.copied()
.filter(|function| {
function.metadata().function_kind == BytecodeFunctionKind::AsyncGenerator
})
.collect::<Vec<_>>();
assert_eq!(published_methods.len(), 2);
assert!(published_methods.iter().all(|function| {
function.metadata().needs_home_object
&& function.metadata().has_prototype
&& function.metadata().constructor_kind == ConstructorKind::None
&& function
.code()
.iter()
.filter(|instruction| matches!(instruction, Instruction::InitialYield))
.count()
== 1
}));
let mut accesses = (false, false, false);
for instruction in functions.iter().flat_map(|function| function.code()) {
accesses.0 |= matches!(instruction, Instruction::GetPrivateField(_));
accesses.1 |= matches!(instruction, Instruction::GetPrivateField2(_));
accesses.2 |= matches!(instruction, Instruction::PrivateIn(_));
}
assert_eq!(accesses, (true, true, true));
}
#[test]
fn private_async_method_early_errors_and_line_terminators_match_quickjs() {
for source in [
"class C { async #same(){} static async #same(){} }",
"class C { static async #same(){} async #same(){} }",
"class C { #same; async #same(){} }",
"class C { async #same(){} static #same; }",
"class C { async #same(){} get #same(){} }",
"class C { static set #same(value){} static async #same(){} }",
] {
let error = compile_unlinked_script(source).unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax, "source: {source}");
assert_eq!(
error.message(),
"private class field is already defined",
"source: {source}"
);
}
for source in [
"class C { async #constructor(){} }",
"class C { static async #constructor(){} }",
] {
let error = compile_unlinked_script(source).unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax, "source: {source}");
assert_eq!(error.message(), "invalid method name", "source: {source}");
}
let error = compile_unlinked_script("({ async #private(){} })").unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax);
let static_prototype = Parser::parse(
"class C { static async #prototype(){} }",
JsString::from_static("<private-async-static-prototype-test>"),
)
.unwrap();
let root = &static_prototype.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
assert!(scope.bindings.iter().any(|binding| {
let binding = &root.bindings[binding.0];
static_prototype.names.name(binding.name) == "#prototype"
&& binding.kind == BindingKind::PrivateMethod { is_static: true }
}));
assert_eq!(
static_prototype
.functions
.iter()
.filter(|function| {
function.kind == FunctionKind::Method
&& function.execution_kind == BytecodeFunctionKind::Async
})
.count(),
1
);
for source in [
"class C { async\n#method(){} }",
"class C { async/*\n*/#method(){} }",
] {
let tree = Parser::parse(
source,
JsString::from_static("<private-async-newline-test>"),
)
.unwrap();
let root = &tree.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
assert!(scope.bindings.iter().any(|binding| {
let binding = &root.bindings[binding.0];
tree.names.name(binding.name) == "#method"
&& binding.kind == BindingKind::PrivateMethod { is_static: false }
}));
assert_eq!(
tree.functions
.iter()
.filter(|function| {
function.kind == FunctionKind::Method
&& function.execution_kind == BytecodeFunctionKind::Async
})
.count(),
0,
"line terminator must end the contextual async modifier: {source:?}"
);
let instance_initializer = tree
.functions
.iter()
.find(|function| {
function.class_initializer_kind == Some(ClassInitializerKind::InstanceFields)
})
.expect("newline-separated `async` instance field initializer");
assert!(instance_initializer.ops.iter().any(|operation| {
matches!(operation.op, IrOp::Bytecode(Instruction::DefineField(_)))
}));
}
}
#[test]
fn private_method_closure_and_eval_descriptors_keep_callable_kind() {
let root = compile_unlinked_script(
r#"
class C {
use(value) {
const nested = () => value.#method;
eval('value.#method');
return nested();
}
assign(value) { this.#method = value }
#method() { return 42 }
}
"#,
)
.unwrap();
let method_local = root
.local_definitions()
.iter()
.enumerate()
.find(|(_, definition)| definition.kind == ClosureVariableKind::PrivateMethod)
.expect("private method local");
assert!(method_local.1.is_lexical);
assert!(method_local.1.is_const);
assert!(
method_local
.1
.name
.as_ref()
.is_some_and(|name| name.to_utf8_lossy() == "#method")
);
assert!(root.code().iter().any(|instruction| matches!(
instruction,
Instruction::InitializePrivateMethod(index)
if usize::from(*index) == method_local.0
)));
let mut functions = Vec::new();
collect_functions(&root, &mut functions);
assert!(functions.iter().any(|function| {
function
.closure_variables()
.iter()
.any(|descriptor| descriptor.kind == ClosureVariableKind::PrivateMethod)
}));
assert!(functions.iter().any(|function| {
function
.eval_environments()
.iter()
.flat_map(|environment| environment.scopes.iter())
.flat_map(|scope| scope.bindings.iter())
.any(|binding| {
binding.kind == ClosureVariableKind::PrivateMethod
&& binding.name.to_utf8_lossy() == "#method"
})
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::ThrowReadOnly(_)))
}));
}
#[test]
fn private_accessors_pair_primary_and_synthetic_setter_bindings() {
let tree = Parser::parse(
r#"
class C {
set #only(value) {}
get #pair() { return 1 }
set #pair(value) {}
static set #reverse(value) {}
static get #reverse() { return 2 }
}
"#,
JsString::from_static("<private-accessor-scope-test>"),
)
.unwrap();
let root = &tree.functions[0];
let scope = root
.scopes
.iter()
.find(|scope| scope.kind == ScopeKind::ClassPrivate)
.expect("class-private scope");
let bindings = scope
.bindings
.iter()
.map(|binding| &root.bindings[binding.0])
.collect::<Vec<_>>();
let find = |name: &str| {
bindings
.iter()
.copied()
.find(|binding| tree.names.name(binding.name) == name)
.unwrap_or_else(|| panic!("missing private binding {name}"))
};
assert_eq!(
find("#only").kind,
BindingKind::PrivateSetter { is_static: false }
);
assert_eq!(
find("#only<set>").kind,
BindingKind::PrivateSetter { is_static: false }
);
assert_eq!(
find("#pair").kind,
BindingKind::PrivateGetterSetter { is_static: false }
);
assert_eq!(
find("#pair<set>").kind,
BindingKind::PrivateSetter { is_static: false }
);
assert_eq!(
find("#reverse").kind,
BindingKind::PrivateGetterSetter { is_static: true }
);
assert_eq!(
find("#reverse<set>").kind,
BindingKind::PrivateSetter { is_static: true }
);
let initialized = root
.ops
.iter()
.filter_map(|operation| match &operation.op {
IrOp::Bytecode(Instruction::InitializePrivateAccessor(local)) => Some(*local),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(initialized.len(), 5);
let only_primary = match find("#only").storage {
BindingStorage::Local(local) => local,
_ => panic!("private setter primary did not use local storage"),
};
assert!(!initialized.contains(&only_primary));
assert!(initialized.iter().all(|local| {
root.bindings.iter().any(|binding| {
binding.storage == BindingStorage::Local(*local)
&& matches!(
binding.kind,
BindingKind::PrivateSetter { .. } | BindingKind::PrivateGetterSetter { .. }
)
})
}));
let accessor_functions = tree
.functions
.iter()
.filter(|function| {
function.parent.is_some()
&& function.class_initializer_kind.is_none()
&& function.function_name.is_none()
})
.collect::<Vec<_>>();
assert_eq!(accessor_functions.len(), 5);
assert!(
accessor_functions
.iter()
.all(|function| function.needs_home_object)
);
assert_eq!(
tree.functions
.iter()
.filter(|function| function.class_private_brand)
.count(),
2
);
}
#[test]
fn private_accessor_reads_writes_in_and_eval_keep_typed_capabilities() {
let root = compile_unlinked_script(
r#"
class C {
get #getter() { return 1 }
set #setter(value) {}
get #pair() { return 2 }
set #pair(value) {}
readGetter(value) { return value.#getter }
writeGetter(value) { value.#getter = 1 }
readSetter(value) { return value.#setter }
writeSetter(value) { value.#setter = 1 }
readPair(value) { return (() => value.#pair)() }
writePair(value) { value.#pair = 1 }
hasSetter(value) { return #setter in value }
evalPair(value) { return eval('value.#pair') }
}
"#,
)
.unwrap();
let definitions = root.local_definitions();
assert_eq!(
definitions
.iter()
.filter(|definition| definition.kind == ClosureVariableKind::PrivateGetter)
.count(),
1
);
assert_eq!(
definitions
.iter()
.filter(|definition| definition.kind == ClosureVariableKind::PrivateGetterSetter)
.count(),
1
);
assert_eq!(
definitions
.iter()
.filter(|definition| definition.kind == ClosureVariableKind::PrivateSetter)
.count(),
3
);
let setter_primary = definitions
.iter()
.enumerate()
.find(|(_, definition)| {
definition.kind == ClosureVariableKind::PrivateSetter
&& definition
.name
.as_ref()
.is_some_and(|name| name.to_utf8_lossy() == "#setter")
})
.expect("setter-only primary");
assert!(!root.code().iter().any(|instruction| matches!(
instruction,
Instruction::InitializePrivateAccessor(index)
if usize::from(*index) == setter_primary.0
)));
let mut functions = Vec::new();
collect_functions(&root, &mut functions);
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::GetPrivateField(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::PutPrivateField(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::PrivateIn(_)))
}));
assert!(functions.iter().any(|function| {
function
.code()
.iter()
.any(|instruction| matches!(instruction, Instruction::ThrowReadOnly(_)))
}));
assert!(functions.iter().any(|function| {
function
.closure_variables()
.iter()
.any(|descriptor| descriptor.kind == ClosureVariableKind::PrivateSetter)
}));
assert!(functions.iter().any(|function| {
function
.eval_environments()
.iter()
.flat_map(|environment| environment.scopes.iter())
.flat_map(|scope| scope.bindings.iter())
.any(|binding| binding.kind == ClosureVariableKind::PrivateGetterSetter)
}));
}
#[test]
fn private_data_field_early_errors_remain_fail_closed() {
for source in [
"({}).#missing",
"#missing in {}",
"class C { #x; #x; }",
"class C { #x; static #x; }",
"class C { #constructor; }",
"class C { #x; method(value) { delete value.#x; } }",
"class C extends Object { #x; method() { return super.#x; } }",
"class C { #x; method() { return #x; } }",
] {
let error = compile_unlinked_script(source).unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax, "source: {source}");
}
for source in [
"class C { get #value() {} get #value() {} }",
"class C { set #value(value) {} set #value(value) {} }",
"class C { get #value() {} static set #value(value) {} }",
"class C { #value; get #value() {} }",
"class C { #value() {} set #value(value) {} }",
] {
let error = compile_unlinked_script(source).unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax, "source: {source}");
assert_eq!(error.message(), "private class field is already defined");
}
assert_eq!(
compile_unlinked_script("class C { #constructor; }")
.unwrap_err()
.message(),
"invalid method name"
);
assert_eq!(
compile_unlinked_script("class C { #x; static #x; }")
.unwrap_err()
.message(),
"private class field is already defined"
);
assert_eq!(
compile_unlinked_script("class C { #constructor() {} }")
.unwrap_err()
.message(),
"invalid method name"
);
for source in [
"class C { #x; #x() {} }",
"class C { #x() {} #x; }",
"class C { #x() {} static #x() {} }",
] {
assert_eq!(
compile_unlinked_script(source).unwrap_err().message(),
"private class field is already defined",
"source: {source}"
);
}
assert_ne!(
compile_unlinked_script("class C { #x; #x(value =) {} }")
.unwrap_err()
.message(),
"private class field is already defined"
);
for source in [
"class C { get #x() {} get #x(value) {} }",
"class C { #x; get #x(value) {} }",
"class C { set #x(value) {} set #x(...rest) {} }",
"class C { get #x() {} get #x() { let value; let value; } }",
] {
let error = compile_unlinked_script(source).unwrap_err();
assert_eq!(error.kind(), ErrorKind::Syntax, "source: {source}");
assert_eq!(
error.message(),
"private class field is already defined",
"source: {source}"
);
}
}
}