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
71 changes: 71 additions & 0 deletions crates/perry-codegen/src/expr/static_field_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// inlining can't do once the class escapes its defining scope.
if !captured_args.is_empty() {
let cap_len = captured_args.len().to_string();
let mut lowered_caps: Vec<String> = Vec::with_capacity(captured_args.len());
let mut caps_arr = ctx.block().call(I64, "js_array_alloc", &[(I32, &cap_len)]);
for arg in captured_args {
let v = lower_expr(ctx, arg)?;
Expand All @@ -424,6 +425,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
"js_array_push_f64",
&[(I64, &caps_arr), (DOUBLE, &v)],
);
lowered_caps.push(v);
}
let caps_box = nanbox_pointer_inline(ctx.block(), &caps_arr);
let key_idx = ctx.strings.intern("__perry_ctor_caps");
Expand All @@ -436,6 +438,39 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
"js_object_set_field_by_name",
&[(I64, &obj), (I64, &key_raw), (DOUBLE, &caps_box)],
);
// #685: ALSO register this evaluation's captures as the
// template's CLASS_CAPTURE_VALUES snapshot. The static blocks
// invoked below run compiled static-method bodies whose
// enclosing-scope reads resolve through
// `js_param_or_class_capture_value` — i.e. the name-keyed
// snapshot — not `__perry_ctor_caps` (that array only feeds
// constructor replay). Same write-right-before-use pattern
// (and same documented per-evaluation overwrite limitation)
// as the shared-template path's `RegisterClassCaptures`.
if template_cid != 0 {
let n = lowered_caps.len();
let buf = ctx.func.alloca_entry_array(DOUBLE, n);
for (i, v) in lowered_caps.iter().enumerate() {
let slot =
ctx.block()
.gep(DOUBLE, &buf, &[(crate::types::I64, &i.to_string())]);
ctx.block().store(DOUBLE, v, &slot);
}
let ptr_reg = ctx.block().next_reg();
ctx.block().emit_raw(format!(
"{} = getelementptr [{} x double], ptr {}, i64 0, i64 0",
ptr_reg, n, buf
));
let len_str = n.to_string();
ctx.block().call_void(
"js_class_register_capture_values",
&[
(crate::types::I32, &tcid_str),
(crate::types::PTR, &ptr_reg),
(crate::types::I64, &len_str),
],
);
}
}
let obj_box = nanbox_pointer_inline(ctx.block(), &obj);
for (key, init) in symbol_statics {
Expand All @@ -447,6 +482,42 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(DOUBLE, &obj_box), (DOUBLE, &k), (DOUBLE, &v)],
);
}
// #685: run the class's `static { … }` blocks NOW — at the class
// expression's evaluation, with `this` = THIS fresh class object.
// The `ClassExprFresh` fast path previously never invoked them
// (they are also skipped by the module-init fallback when another
// evaluation site invokes them inline), so `return class { static
// { this.viaBlock = tag } }` factories produced objects whose
// blocks simply never ran. Arm the one-shot static-`this`
// override before each call so the compiled body's
// `js_static_this_resolve` prologue binds `this` to the fresh
// object (writes land as own properties of this evaluation's
// object, not the shared template). Blocks run after the named
// static fields above — the source interleaving of fields and
// blocks is not reproduced on this path (pre-existing limitation).
let block_fns: Vec<String> = ctx
.classes
.get(template)
.map(|c| {
c.static_methods
.iter()
.filter(|m| m.name.starts_with("__perry_static_init_"))
.filter_map(|m| {
ctx.methods
.get(&(
template.clone(),
crate::codegen::static_method_registry_key(&m.name),
))
.cloned()
})
.collect()
})
.unwrap_or_default();
for fn_name in block_fns {
ctx.block()
.call_void("js_static_this_arm_value", &[(DOUBLE, &obj_box)]);
ctx.block().call(DOUBLE, &fn_name, &[]);
}
Ok(obj_box)
}
// Issue #711 part 2: `<expr>.prototype = <expr>` pattern.
Expand Down
34 changes: 21 additions & 13 deletions crates/perry-codegen/src/lower_call/property_get/static_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,19 +222,27 @@ pub(crate) fn try_lower_static_dispatch(
let prev_this =
ctx.block()
.call(DOUBLE, "js_implicit_this_set", &[(DOUBLE, &recv_box)]);
// Receiver-sensitive static `this` for plain class-ref receivers:
// `D.f()` resolving to a parent's body at compile time must run
// with `this === D` (the prologue's `js_static_this_resolve`
// consumes this one-shot arm). Dynamic-value receiver shapes
// (ClassExprFresh / factory Call / LocalGet) keep their prior
// implicit-this-only behavior to avoid disturbing effect's
// per-evaluation class-object statics.
let plain_class_receiver =
matches!(object, Expr::ClassRef(_) | Expr::ExternFuncRef { .. });
if plain_class_receiver {
ctx.block()
.call_void("js_static_this_arm_value", &[(DOUBLE, &recv_box)]);
}
// Receiver-sensitive static `this`: arm the one-shot override with
// the ACTUAL receiver box so the callee prologue's
// `js_static_this_resolve` binds `this` to it (spec
// OrdinaryCallBindThis). This must cover the dynamic-value receiver
// shapes too (ClassExprFresh / factory `Call` / `LocalGet`), not
// just plain class-refs: the prologue consumes the armed override
// or falls back to the LEXICAL class-ref — it never reads implicit
// `this` — so the previous implicit-this-only treatment of these
// shapes silently bound `this` to the shared template. A class
// EXPRESSION's per-evaluation statics are OWN properties of the
// fresh heap class object (never written to the template's
// static-field globals), so `this.<field>` inside the static body
// read `undefined` (#1787 criterion 1: `make(a).viaThis()` /
// `const C = make(a); C.viaThis()`). For a local that holds a
// plain ClassRef value this arms exactly the prologue's default —
// no behavior change — and for fresh objects it restores the
// receiver, matching the runtime dispatch tower
// (`js_class_static_method_call`), which has armed its receiver
// since the static-private-brand work.
ctx.block()
.call_void("js_static_this_arm_value", &[(DOUBLE, &recv_box)]);
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|s| (DOUBLE, s.as_str())).collect();
let result = ctx.block().call(DOUBLE, &fn_name, &arg_slices);
Expand Down
57 changes: 57 additions & 0 deletions test-files/test_gap_class_expr_fresh_static_blocks_this.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// #685 / #1787: class-EXPRESSION statics on the fresh-object path.
//
// (1) `static { … }` blocks of a class expression returned from a factory
// must run at each evaluation, with `this` bound to THAT evaluation's
// fresh class object and the factory's captured locals visible.
// (2) A static METHOD called on the fresh class object (both the inline
// `make(x).m()` form and the const-bound `const C = make(x); C.m()`
// form) must bind `this` to the fresh object, so `this.<field>` reads
// the per-evaluation own static field — not the shared template's
// static-field global (which the fresh path never writes).

let log: string[] = [];
function makeExpr(tag: string) {
return class {
static viaBlock: string | null = null;
static { log.push("block:" + tag); }
static { this.viaBlock = tag; }
static ast = tag;
static viaThis() { return this.ast; }
};
}

const A = makeExpr("A");
const B = makeExpr("B");
console.log("blocks:", log.join(","));
console.log("A.viaBlock:", A.viaBlock);
console.log("B.viaBlock:", B.viaBlock);

// Static-method `this` binding — const-bound receiver.
console.log("A.viaThis():", A.viaThis());
console.log("B.viaThis():", B.viaThis());
// Inline factory-result receiver.
console.log("inline:", makeExpr("C").viaThis());

// `this === receiver` identity inside the static body.
function makeIdent() {
return class {
static self() { return this === (globalThis as any).__IDENT ? "same" : "different"; }
};
}
const I = makeIdent();
(globalThis as any).__IDENT = I;
console.log("identity:", I.self());

// Nested class DECLARATION block still runs once, in the factory (control).
log = [];
function makeDecl(tag: string) {
class D { static t: string; static { log.push("decl:" + tag); D.t = tag; } }
return D;
}
const D = makeDecl("D");
console.log("decl blocks:", log.join(","));
console.log("D.t:", D.t);

// Top-level class-expression block still runs at module init (control).
const E = class { static { console.log("toplevel block ran"); } };
void E;
Loading