Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/perry-codegen/src/expr/static_field_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let obj =
ctx.block()
.call(I64, "js_object_alloc", &[(I32, &tcid_str), (I32, &nfields)]);
// #1789: mark it as a class object (object_type = OBJECT_TYPE_CLASS)
// so `typeof` reports "function" and `new`/`instanceof` read the
// class_id from this object rather than treating it as an instance.
ctx.block()
.call_void("js_object_mark_class", &[(I64, &obj)]);
for (name, init) in named_statics {
let key_idx = ctx.strings.intern(name);
let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global);
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ use super::*;
/// `js_object_alloc(0, N)` is the fallback for dynamic cases.
pub fn declare_phase_b_objects(module: &mut LlModule) {
module.declare_function("js_object_alloc", I64, &[I32, I32]);
// #1789: stamp a class-expression's heap object as a class object
// (object_type = OBJECT_TYPE_CLASS) so typeof → "function" and
// new/instanceof read class_id from it.
module.declare_function("js_object_mark_class", VOID, &[I64]);
// Shape-cache-aware variant: pre-populates keys_array via SHAPE_INLINE_CACHE,
// so subsequent field stores can use index-based set_field (skipping the
// per-call linear key-search done by js_object_set_field_by_name).
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/builtins/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ pub extern "C" fn js_value_typeof(value: f64) -> *mut StringHeader {
let type_tag = unsafe { *(ptr.add(12) as *const u32) };
if type_tag == crate::closure::CLOSURE_MAGIC {
get_cached(&TYPEOF_FUNCTION, "function")
} else if crate::object::is_class_object_ptr(ptr) {
// #1789: a class-expression VALUE is a heap object stamped
// with OBJECT_TYPE_CLASS — `typeof aClassObject ===
// "function"` (classes are callable in JS), matching the
// INT32 ClassRef case below.
get_cached(&TYPEOF_FUNCTION, "function")
} else {
get_cached(&TYPEOF_OBJECT, "object")
}
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-runtime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use crate::string::{js_string_from_bytes, StringHeader};
/// Object type tag for runtime type discrimination
pub const OBJECT_TYPE_REGULAR: u32 = 1;
pub const OBJECT_TYPE_ERROR: u32 = 2;
/// #1789: a heap "class object" — the value a class EXPRESSION evaluates to
/// (a regular object stamped with the compile-time template's `class_id`,
/// carrying per-evaluation static fields as own properties). Marks the value
/// as the CLASS itself (vs an instance) so `typeof` is "function", and
/// `new`/`instanceof` read `class_id` from the object. Own-field get/set
/// treat it like OBJECT_TYPE_REGULAR (the get/set paths are gated on
/// `gc_type`/`class_id`, not on this tag).
pub const OBJECT_TYPE_CLASS: u32 = 3;

/// Error subclass discriminator (stored in `error_kind`).
/// Used by `instanceof TypeError` etc. to check kind without name string compare.
Expand Down
54 changes: 54 additions & 0 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,24 @@ pub unsafe extern "C" fn js_new_function_construct(
_ => {}
}
}
// #1789: `new (classObjectValue)(args)` — the callee is a heap class
// object (the value a class EXPRESSION evaluates to, e.g.
// `const C = make(x); new C()`). Read its class_id (the compile-time
// template) and allocate an instance of it, so instance methods
// dispatch and `x instanceof C` matches. The template's constructor
// body / field initializers are emitted on the static `new ClassName()`
// path, not this dynamic helper, so a dynamically-constructed instance
// starts with no own props (constructor-run on dynamic new is a
// refinement); fields written afterward and prototype methods work.
if is_class_object_value(func_value) {
let obj =
crate::value::JSValue::from_bits(func_value.to_bits()).as_pointer::<ObjectHeader>();
let class_cid = js_object_get_class_id(obj);
if class_cid != 0 {
let inst = js_object_alloc(class_cid, 0);
return crate::value::js_nanbox_pointer(inst as i64);
}
}
let cid = synthetic_class_id_for_function(func_value);
// Allocate the instance with the synthetic class id (or 0 if the
// value isn't callable). The object starts with no own props; the
Expand Down Expand Up @@ -1429,6 +1447,42 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, parent_value:
}
}

/// #1789: stamp a freshly-allocated object as a heap "class object" (the
/// value a class EXPRESSION evaluates to). Sets `object_type =
/// OBJECT_TYPE_CLASS` so `typeof` reports "function" and `new`/`instanceof`
/// read `class_id` from it. Called by codegen right after `js_object_alloc`
/// in the `ClassExprFresh` lowering.
#[no_mangle]
pub extern "C" fn js_object_mark_class(obj: i64) {
if obj != 0 {
unsafe {
(*(obj as *mut ObjectHeader)).object_type = crate::error::OBJECT_TYPE_CLASS;
}
}
}

/// #1789: is `ptr` a heap "class object" (`object_type == OBJECT_TYPE_CLASS`)?
/// Validates the GcHeader is a `GC_TYPE_OBJECT` before reading `object_type`,
/// so raw Map/Set/Buffer pointers (no GcHeader) are never misread. Used by
/// `typeof`, `new`, and `instanceof` to recognize a class value.
pub fn is_class_object_ptr(ptr: *const u8) -> bool {
if ptr.is_null() || (ptr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 {
return false;
}
unsafe {
let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
(*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT
&& (*(ptr as *const ObjectHeader)).object_type == crate::error::OBJECT_TYPE_CLASS
}
}

/// #1789: f64-value form of [`is_class_object_ptr`] — true only for a
/// POINTER-tagged value that is a class object.
pub fn is_class_object_value(value: f64) -> bool {
let jsval = crate::value::JSValue::from_bits(value.to_bits());
jsval.is_pointer() && is_class_object_ptr(jsval.as_pointer::<u8>())
}

/// Look up parent class ID from the registry
pub(crate) fn get_parent_class_id(class_id: u32) -> Option<u32> {
let registry = CLASS_REGISTRY.read().unwrap();
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/object/instanceof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 {
return js_instanceof(value, class_id);
}
}
// #1789: `x instanceof C` where C is a heap class object (the value a
// class EXPRESSION evaluates to, e.g. `const C = make(x); c instanceof
// C`). Read its class_id (the compile-time template) and walk the
// candidate's class chain against it.
if is_class_object_value(type_ref) {
let obj = crate::JSValue::from_bits(bits).as_pointer::<ObjectHeader>();
let class_id = js_object_get_class_id(obj);
if class_id != 0 {
return js_instanceof(value, class_id);
}
}
f64::from_bits(TAG_FALSE)
}

Expand Down
33 changes: 33 additions & 0 deletions test-files/test_gap_class_expr_new_instanceof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Issue #1789: `new` / `instanceof` / `typeof` on a class-object VALUE — a
// class expression (with static fields, so it lowers to a heap class object
// stamped OBJECT_TYPE_CLASS) bound to a value. Per JS, a class is callable
// (`typeof === "function"`), `new C()` constructs an instance whose methods
// dispatch, and `c instanceof C` is true.
//
// Scope note: per-evaluation class objects share the compile-time template's
// class_id, so cross-evaluation `instanceof` (a `make()` result tested
// against a *different* `make()` result) can't be distinguished by the
// class_id walk — that's inherent to the shared-class_id model and not
// covered here. Constructor-body/field-initializer execution via dynamic
// `new` on a class-object value is a tracked refinement.
//
// Expected output:
// typeof: function
// C.kind: K
// instanceof: true
// method: 42

function make() {
return class {
static kind = "K";
foo() {
return 42;
}
};
}
const C = make();
console.log("typeof:", typeof C);
console.log("C.kind:", (C as any).kind);
const c = new C();
console.log("instanceof:", c instanceof C);
console.log("method:", (c as any).foo());