Skip to content

Reading .apply (or any Function.prototype method) off a leaf mutates its record: function leaf becomes a namespace with a phantom child #304

Description

@Shinrai

Summary

Merely reading a standard function property such as .apply/.call/.bind off a materialized function leaf permanently rewrites that leaf's loader record: the leaf flips from kind: "function" to kind: "namespace", and a phantom child leaf (<path>.apply, kind: "function") appears. The property access is treated as an api-tree traversal that materializes a child, rather than returning the function's own Function.prototype.apply. A later leaves(".", { details: true }) — or a second composition/serve of the same instance — then reports the corrupted shape.

Reproduce

Verified against @cldmv/slothlet@3.14.0 (repo v3.13.3-9-g0dd03b7), eager mode. api dir with one file tool.mjs exporting export function ping() { return "pong"; }:

const api = await slothlet({ base: dirUrl, silent: true, mode: "eager" });

await api.slothlet.api.leaves(".", { details: true });
// → [ { path: "tool", kind: "namespace" }, { path: "tool.ping", kind: "function" } ]

const ap = api.tool.ping.apply;   // just READ it — no call
typeof ap;                         // "function"
api.tool.ping();                   // still "pong"

await api.slothlet.api.leaves(".", { details: true });
// → [ { path: "tool", kind: "namespace" },
//     { path: "tool.ping", kind: "namespace" },          ← was "function"
//     { path: "tool.ping.apply", kind: "function" } ]    ← phantom leaf

// Reflect.apply(api.tool.ping, thisArg, args) does NOT trigger this.

Expected

Accessing Function.prototype members (apply/call/bind/name/length/…) on a callable leaf should return the function's own properties and leave the record untouched — the leaf is a function; those are its methods, not child endpoints. Only genuine child names should materialize a namespace.

Impact

Any code that invokes a leaf as leaf.apply(thisArg, args) (a common idiom for forwarding this + a spread of args) silently corrupts the api's own record tree, and re-enumerating or re-composing the instance then surfaces Function.prototype.apply bound to the real leaf in place of the leaf. Found while building @cldmv/slothlet-vine: answering a forwarded call with leaf.apply(parent, args) corrupted the served surface; the workaround is Reflect.apply(leaf, parent, args), which routes around the leaf proxy's get trap entirely.

Severity

Correctness — silent record corruption from a read-only, idiomatic access. Likely the lazy/eager leaf proxy's get trap treating every property (including Function.prototype names) as a child traversal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreTouches core library / runtime source codepriority: highNeeds attention soon — not blocking, but don't let it sitsemver: patchThis release contains only backwards-compatible bug fixesstatus: confirmedThe issue has been verified and reproduced by a maintainertype: bugSomething is broken or not behaving as expected

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions