Skip to content

Commit

Permalink
feat: support Object.seal and Object.preventExtensions (#213)
Browse files Browse the repository at this point in the history
* chore: add `Object.seal` and `Object.preventExtensions`'s type
* chore: update the checker definition
* feat: extends frozen of LocalInformation for `Object.seal` and `Object.preventExtensions`
* test: add staging cases
* chore: update the checker definition
* feat: support `Object.isExtensible`
* Update function names and slight fixes to better mirror JS behaviour

---------

Co-authored-by: Ben <[email protected]>
  • Loading branch information
sor4chi and kaleidawave authored Nov 4, 2024
1 parent 8ce921e commit d67cd1b
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 32 deletions.
Binary file modified checker/definitions/internal.ts.d.bin
Binary file not shown.
12 changes: 12 additions & 0 deletions checker/definitions/overrides.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions checker/definitions/simple.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 80 additions & 17 deletions checker/specification/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions checker/src/context/information.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use source_map::SpanWithSource;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;

use crate::{
events::{Event, RootReference},
Expand Down Expand Up @@ -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<TypeId>,
/// Not writeable, `TypeError: Cannot add property, object is not extensible`. TODO conditional ?
pub(crate) frozen: HashMap<TypeId, ObjectProtectionState>,

/// Object type (LHS), must always be RHS
///
Expand All @@ -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]
Expand Down Expand Up @@ -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();
}
Expand Down
7 changes: 4 additions & 3 deletions checker/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod information;
pub mod invocation;
mod root;

use information::ObjectProtectionState;
pub(crate) use invocation::CallCheckingBehavior;
pub use root::RootContext;

Expand Down Expand Up @@ -518,17 +519,17 @@ impl<T: ContextType> Context<T> {
}

/// TODO doesn't look at aliases using `get_type_fact`!
pub fn is_frozen(&self, value: TypeId) -> Option<TypeId> {
pub fn get_object_protection(&self, value: TypeId) -> Option<ObjectProtectionState> {
self.parents_iter().find_map(|ctx| get_on_ctx!(ctx.info.frozen.get(&value))).copied()
}

// TODO temp declaration
// 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(..)
Expand Down
75 changes: 70 additions & 5 deletions checker/src/features/constant_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
18 changes: 16 additions & 2 deletions checker/src/types/properties/assignment.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -59,8 +59,11 @@ pub fn set_property<B: CallCheckingBehavior>(
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,
Expand Down Expand Up @@ -266,6 +269,17 @@ pub fn set_property<B: CallCheckingBehavior>(
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(
Expand Down
4 changes: 3 additions & 1 deletion checker/src/types/subtyping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit d67cd1b

Please sign in to comment.