Skip to content

codegen: inline obj.prop.method(arg) (property access chained into a method call) miscompiles — returns wrong result; extracting the property to a local fixes it #5195

@proggeramlug

Description

@proggeramlug

Perry version: 0.5.1161

Summary

Calling a method inline on a property access of an array/object element — expr.prop.method(arg) — miscompiles: the method runs against the wrong receiver and returns a wrong result. Extracting the property into a local variable first makes it correct.

This silently breaks common code like arr[i].id.indexOf(x) and Array.prototype.filter(p => p.id.indexOf(x) >= 0) (the filter predicate is the same shape internally), with no error — the call just returns the wrong value.

Minimal repro

interface P { id: string }
const arr: P[] = [{ id: "GSC_PRO_MONTHLY" }, { id: "GSC_AGENCY_MONTHLY" }, { id: "GSC_PRO_ANNUAL" }]

// A — inline property-access chained into a method call
let a = 0
for (let i = 0; i < arr.length; i++) {
  if (arr[i].id.indexOf("PRO") >= 0) a++
}
console.log("A inline =", a)            // prints 0  ❌  (expected 2)

// B — extract the property to a local first
let b = 0
for (let i = 0; i < arr.length; i++) {
  const pid: string = arr[i].id
  if (pid.indexOf("PRO") >= 0) b++
}
console.log("B extracted =", b)         // prints 2  ✅

// Also affected — filter with the same predicate shape:
console.log("filter =", arr.filter(function (p: P) { return p.id.indexOf("PRO") >= 0 }).length) // 0 ❌ (expected 2)

// NOT affected — these work:
console.log("map =", arr.map(function (p: P) { return p.id }).join(","))   // correct ids
console.log("filter-true =", arr.filter(function (p: P) { return true }).length) // 3 ✅
console.log("num =", [1,2,3,4].filter(function (n: number) { return n > 2 }).length) // 2 ✅

Run on the native host target (perry run repro.ts): A prints 0, B prints 2.

Observations

  • The bug is the chaining (<objectExpr>.prop.method(...)), not indexOf itself or .filter itself:
    • localString.indexOf("PRO") works.
    • const pid = arr[i].id; pid.indexOf("PRO") works.
    • arr[i].id.indexOf("PRO") / p.id.indexOf("PRO") (inline) returns wrong.
  • .map(p => p.id) returns the correct property values, so reading p.id alone is fine — it's specifically reading a property and immediately invoking a method on it that breaks. Looks like the method receiver is bound to the object (arr[i] / p) rather than the property value (.id), or the property load is dropped before the call.
  • Reproduces on the native host (macOS) target, so it's codegen/runtime, not platform-specific.

Impact

Silent wrong results in extremely common code shapes (item.name.startsWith(...), row.url.indexOf(...), obj.field.toLowerCase(), and any arr.filter(x => x.prop.method())). Downstream (GSC Master) this made a StoreKit product lookup (products.filter(p => p.id.indexOf("PRO") >= 0)) silently return empty, so the in-app purchase button reported the plan as unavailable. Worked around by extracting the property to a local before the method call.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions