Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Object.seal object Object.preventExtensions #213

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
Loading