Symptom
A declared-number class field that actually holds a string (Perry does not enforce annotations) diverges from Node when read in an arithmetic +:
class C { x: number; constructor(x: number) { this.x = x; } }
function g(o: C): void { (o as any).x = "s"; }
function f(): string {
const o = new C(1);
g(o);
return `${o.x + 1}`;
}
console.log(f());
Node (26.5.1, strip-types): s1 (string concat — the runtime value decides).
Perry: NaN (the Add was classified numeric from the DECLARED field type, so the class-field get lowers in number context and its fallback arm hard-coerces via js_number_coerce).
Second shape — the add EVAPORATES through an any local
const v: any = o.x;
return `${v + 1}`;
Node: s1. Perry: s — the + 1 is dropped entirely, not even coerced (smells like the #7590 discard_expr_value family or an any-add lowering hole).
Attribution
Both reproduce with PERRY_PTR_SHAPE_LOCALS=0, no arrays, no element groups, on a build of main + #7770 (the #7770 collector change is not involved; its A/B arms behave identically here). The escape (g(o)) is required — a non-escaping o gets scalar-replaced and prints s1 correctly, which is why the trivial form never showed this.
Where to look
expr/property_get/helpers.rs::lower_raw_f64_class_field_get_for_number_context is only entered when the surrounding context was already classified numeric; the misclassification is upstream in the Add lowering (type_analysis::is_numeric_expr trusting the declared field type for an aliased receiver). The guarded diamond's fallback arm then coerces unconditionally.
Found while validating #7770 (its gap test deliberately avoids + on poisoned fields and points here).
Symptom
A declared-
numberclass field that actually holds a string (Perry does not enforce annotations) diverges from Node when read in an arithmetic+:Node (26.5.1, strip-types):
s1(string concat — the runtime value decides).Perry:
NaN(the Add was classified numeric from the DECLARED field type, so the class-field get lowers in number context and its fallback arm hard-coerces viajs_number_coerce).Second shape — the add EVAPORATES through an
anylocalNode:
s1. Perry:s— the+ 1is dropped entirely, not even coerced (smells like the #7590discard_expr_valuefamily or an any-add lowering hole).Attribution
Both reproduce with
PERRY_PTR_SHAPE_LOCALS=0, no arrays, no element groups, on a build of main + #7770 (the #7770 collector change is not involved; its A/B arms behave identically here). The escape (g(o)) is required — a non-escapingogets scalar-replaced and printss1correctly, which is why the trivial form never showed this.Where to look
expr/property_get/helpers.rs::lower_raw_f64_class_field_get_for_number_contextis only entered when the surrounding context was already classified numeric; the misclassification is upstream in the Add lowering (type_analysis::is_numeric_exprtrusting the declared field type for an aliased receiver). The guarded diamond's fallback arm then coerces unconditionally.Found while validating #7770 (its gap test deliberately avoids
+on poisoned fields and points here).