diff --git a/checker/definitions/internal.ts.d.bin b/checker/definitions/internal.ts.d.bin index 720f6309..c4f51fa0 100644 Binary files a/checker/definitions/internal.ts.d.bin and b/checker/definitions/internal.ts.d.bin differ diff --git a/checker/definitions/overrides.d.ts b/checker/definitions/overrides.d.ts index 03781523..ad67900c 100644 --- a/checker/definitions/overrides.d.ts +++ b/checker/definitions/overrides.d.ts @@ -368,9 +368,21 @@ declare class Object { @Constant static freeze(on: object): object; + @Constant + static seal(on: object): object; + + @Constant + static preventExtensions(on: object): object; + @Constant static isFrozen(on: object): boolean; + @Constant + static isSealed(on: object): boolean; + + @Constant + static isExtensible(on: object): boolean; + // TODO defineProperties via body (not constant) @Constant static defineProperty(on: object, property: string, discriminator: PropertyDescriptor): boolean; diff --git a/checker/definitions/simple.d.ts b/checker/definitions/simple.d.ts index c1b03c9d..aba85ea2 100644 --- a/checker/definitions/simple.d.ts +++ b/checker/definitions/simple.d.ts @@ -371,9 +371,21 @@ declare class Object { @Constant static freeze(on: object): object; + @Constant + static seal(on: object): object; + + @Constant + static preventExtensions(on: object): object; + @Constant static isFrozen(on: object): boolean; + @Constant + static isSealed(on: object): boolean; + + @Constant + static isExtensible(on: object): boolean; + // TODO defineProperties via body (not constant) @Constant static defineProperty(on: object, property: string, discriminator: PropertyDescriptor): boolean; diff --git a/checker/specification/specification.md b/checker/specification/specification.md index f218a2b4..c571ba41 100644 --- a/checker/specification/specification.md +++ b/checker/specification/specification.md @@ -529,22 +529,6 @@ keys satisfies boolean - Expected boolean, found "nbd" -#### `Object.freeze` - -> TODO seal & preventExtensions - -```ts -const obj = {} -let result = Object.freeze(obj); -(obj === result) satisfies true; -obj.property = 2; -Object.isFrozen(obj) satisfies true; -``` - -> TODO maybe error should say that whole object is frozen - -- Cannot write to property 'property' - #### `Object.defineProperty` writable > TODO defineProperties @@ -634,7 +618,86 @@ obj satisfies string; ``` - Expected string, found { a: 1, b: 2, c: 3 } -s + +#### `Object.freeze` + +> When `Object.freeze` is called, the object's `isSealed` is inferred as `true` + +```ts +const obj = {} +let result = Object.freeze(obj); +(obj === result) satisfies true; +obj.property = 2; +Object.isSealed(obj) satisfies true; +``` + +- Cannot write to property 'property' + +#### `Object.seal` + +> When `Object.seal` is called, the object's `isFrozen` and `isSealed` are inferred as `true` + +```ts +const obj = { a: 2 } +let result = Object.seal(obj); +(obj === result) satisfies true; + +// Allowed +obj.a = 4; +// Not allowed +obj.property = 2; + +Object.isSealed(obj) satisfies true; +Object.isFrozen(obj) satisfies false; +``` + +- Cannot write to property 'property' + +#### `Object.preventExtensions` + +> When `Object.preventExtensions` is called, the object's `isFrozen` and `isSealed` are inferred as `true` + +```ts +const obj = { a: 2 } +let result = Object.preventExtensions(obj); +(obj === result) satisfies true; + +// Allowed +obj.a = 4; +// Not allowed +obj.property = 2; + +Object.isFrozen(obj) satisfies false; +Object.isSealed(obj) satisfies false; +``` + +- Cannot write to property 'property' + +#### `Object.isExtensible` + +> The object that has been applied `Object.seal`, `Object.freeze` and `Object.preventExtensions` returns `false` by `Object.isExtensible`, otherwise returns `true` + +```ts +{ + const obj = {} + Object.isExtensible(obj) satisfies true; + Object.preventExtensions(obj); + Object.isExtensible(obj) satisfies false; +} +{ + const obj = {} + Object.seal(obj); + Object.isExtensible(obj) satisfies false; +} +{ + const obj = {} + Object.freeze(obj); + Object.isExtensible(obj) satisfies 5; +} +``` + +- Expected 5, found false + ### Excess properties > The following work through the same mechanism as forward inference diff --git a/checker/src/context/information.rs b/checker/src/context/information.rs index e84835be..486587a4 100644 --- a/checker/src/context/information.rs +++ b/checker/src/context/information.rs @@ -1,5 +1,5 @@ use source_map::SpanWithSource; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use crate::{ events::{Event, RootReference}, @@ -32,8 +32,8 @@ pub struct LocalInformation { /// `ContextId` is a mini context pub(crate) closure_current_values: HashMap<(ClosureId, RootReference), TypeId>, - /// Not writeable, `TypeError: Cannot add property t, object is not extensible`. TODO conditional ? - pub(crate) frozen: HashSet, + /// Not writeable, `TypeError: Cannot add property, object is not extensible`. TODO conditional ? + pub(crate) frozen: HashMap, /// Object type (LHS), must always be RHS /// @@ -52,6 +52,13 @@ pub struct LocalInformation { pub(crate) value_of_this: ThisValue, } +#[derive(Debug, Clone, Copy, binary_serialize_derive::BinarySerializable)] +pub enum ObjectProtectionState { + Frozen, + Sealed, + NoExtensions, +} + #[derive(Debug, Default, binary_serialize_derive::BinarySerializable, Clone)] pub(crate) enum ReturnState { #[default] @@ -228,7 +235,7 @@ impl LocalInformation { .extend(other.current_properties.iter().map(|(l, r)| (*l, r.clone()))); self.closure_current_values .extend(other.closure_current_values.iter().map(|(l, r)| (l.clone(), *r))); - self.frozen.extend(other.frozen.iter().clone()); + self.frozen.extend(other.frozen.clone()); self.narrowed_values.extend(other.narrowed_values.iter().copied()); self.state = other.state.clone(); } diff --git a/checker/src/context/mod.rs b/checker/src/context/mod.rs index e8b11f28..25126536 100644 --- a/checker/src/context/mod.rs +++ b/checker/src/context/mod.rs @@ -7,6 +7,7 @@ pub mod information; pub mod invocation; mod root; +use information::ObjectProtectionState; pub(crate) use invocation::CallCheckingBehavior; pub use root::RootContext; @@ -518,7 +519,7 @@ impl Context { } /// TODO doesn't look at aliases using `get_type_fact`! - pub fn is_frozen(&self, value: TypeId) -> Option { + pub fn get_object_protection(&self, value: TypeId) -> Option { self.parents_iter().find_map(|ctx| get_on_ctx!(ctx.info.frozen.get(&value))).copied() } @@ -526,9 +527,9 @@ impl Context { // TODO should check the TypeId::is_primitive... via aliases + open_poly pub(crate) fn _is_immutable(&self, _value: TypeId) -> bool { todo!() - // let is_frozen = self.is_frozen(value); + // let get_object_protection = self.get_object_protection(value); - // if is_frozen == Some(TypeId::TRUE) { + // if get_object_protection == Some(TypeId::TRUE) { // true // } else if let Some( // Constant::Boolean(..) diff --git a/checker/src/features/constant_functions.rs b/checker/src/features/constant_functions.rs index 630f5a51..a7b18ccf 100644 --- a/checker/src/features/constant_functions.rs +++ b/checker/src/features/constant_functions.rs @@ -2,7 +2,11 @@ use iterator_endiate::EndiateIteratorExt; use source_map::SpanWithSource; use crate::{ - context::{get_on_ctx, information::InformationChain, invocation::CheckThings}, + context::{ + get_on_ctx, + information::{InformationChain, ObjectProtectionState}, + invocation::CheckThings, + }, events::printing::debug_effects, features::objects::{ObjectBuilder, Proxy}, types::{ @@ -310,7 +314,27 @@ pub(crate) fn call_constant_function( if let Some(on) = (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() { - environment.info.frozen.insert(on); + environment.info.frozen.insert(on, ObjectProtectionState::Frozen); + Ok(ConstantOutput::Value(on)) + } else { + Err(ConstantFunctionError::CannotComputeConstant) + } + } + "seal" => { + if let Some(on) = + (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() + { + environment.info.frozen.insert(on, ObjectProtectionState::Sealed); + Ok(ConstantOutput::Value(on)) + } else { + Err(ConstantFunctionError::CannotComputeConstant) + } + } + "preventExtensions" => { + if let Some(on) = + (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() + { + environment.info.frozen.insert(on, ObjectProtectionState::NoExtensions); Ok(ConstantOutput::Value(on)) } else { Err(ConstantFunctionError::CannotComputeConstant) @@ -320,9 +344,50 @@ pub(crate) fn call_constant_function( if let Some(on) = (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() { - let is_frozen = - environment.get_chain_of_info().any(|info| info.frozen.contains(&on)); - Ok(ConstantOutput::Value(if is_frozen { TypeId::TRUE } else { TypeId::FALSE })) + let object_protection = environment.get_object_protection(on); + let result = if matches!(object_protection, Some(ObjectProtectionState::Frozen)) { + TypeId::TRUE + } else { + // TODO test properties here + TypeId::FALSE + }; + Ok(ConstantOutput::Value(result)) + } else { + Err(ConstantFunctionError::CannotComputeConstant) + } + } + "isSealed" => { + if let Some(on) = + (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() + { + let object_protection = environment.get_object_protection(on); + let result = if matches!( + object_protection, + Some(ObjectProtectionState::Frozen | ObjectProtectionState::Sealed) + ) { + TypeId::TRUE + } else { + // TODO test properties here + TypeId::FALSE + }; + Ok(ConstantOutput::Value(result)) + } else { + Err(ConstantFunctionError::CannotComputeConstant) + } + } + "isExtensible" => { + if let Some(on) = + (arguments.len() == 1).then(|| arguments[0].non_spread_type().ok()).flatten() + { + // Not this method returns an inverse result + let object_protection = environment.get_object_protection(on); + let result = if object_protection.is_some() { + TypeId::FALSE + } else { + TypeId::TRUE + // TODO test properties here + }; + Ok(ConstantOutput::Value(result)) } else { Err(ConstantFunctionError::CannotComputeConstant) } diff --git a/checker/src/types/properties/assignment.rs b/checker/src/types/properties/assignment.rs index c28315a1..31538e68 100644 --- a/checker/src/types/properties/assignment.rs +++ b/checker/src/types/properties/assignment.rs @@ -1,7 +1,7 @@ use super::{get_property_unbound, Descriptor, PropertyKey, PropertyValue, Publicity}; use crate::{ - context::CallCheckingBehavior, + context::{information::ObjectProtectionState, CallCheckingBehavior}, diagnostics::{PropertyKeyRepresentation, TypeStringRepresentation}, events::Event, features::objects::Proxy, @@ -59,8 +59,11 @@ pub fn set_property( types: &mut TypeStore, ) -> SetPropertyResult { // Frozen checks + let object_protection = environment.get_object_protection(on); + { - if environment.info.frozen.contains(&on) { + if let Some(ObjectProtectionState::Frozen) = object_protection { + // FUTURE this could have a separate error? return Err(SetPropertyError::NotWriteable { property: PropertyKeyRepresentation::new(under, environment, types), position, @@ -266,6 +269,17 @@ pub fn set_property( position, }) } else { + // Sealed & no extensions check for NEW property (frozen case covered above) + { + if object_protection.is_some() { + // FUTURE this could have a separate error? + return Err(SetPropertyError::NotWriteable { + property: PropertyKeyRepresentation::new(under, environment, types), + position, + }); + } + } + crate::utilities::notify!("No property on object, assigning anyway"); let info = behavior.get_latest_info(environment); info.register_property( diff --git a/checker/src/types/subtyping.rs b/checker/src/types/subtyping.rs index eb274d06..b1ae45e0 100644 --- a/checker/src/types/subtyping.rs +++ b/checker/src/types/subtyping.rs @@ -764,7 +764,9 @@ pub(crate) fn type_is_subtype_with_generics( information, types, ) - } else if information.get_chain_of_info().any(|info| info.frozen.contains(&ty)) + } else if information + .get_chain_of_info() + .any(|info| info.frozen.contains_key(&ty)) || matches!(subtype, Type::Constant(_)) || matches!( ty,