open
https://gitlab.synchro.net/main/sbbs/-/issues/1143
## Summary
`MsgBase.get_all_msg_headers()` header objects return `undefined` on the **first** (cold) access of a `LAZY_STRING_TRUNCSP_NULL` field (`to_ext`, `from_ext`, `replyto`, `to_list`, `cc_list`, `summary`, `tags`, `from_org`, …) via **dot/literal** property access — even when the field is populated. Touching any other (non-NULL) field first "primes" the object and the field then reads correctly.
This issue documents the **proven root cause** and leaves the fix direction open for discussion. Two prior commits are involved:
- `ca448cb8b` — eager `JS_DefineProperty("number")` per header. **Ineffective** on real bases (see below).
- `666ff71ce` — eager full-resolve per header. **Works**, but abandons the lazy design, and its commit message overstated the mechanism as proven (it was written before the decisive evidence below existed). The message should be corrected regardless of the final fix.
## Reproduced (read-only, live ~7.2k-msg mail base)
With the pre-fix binary, cold `hdr.to_ext` → **7138 `undefined`** of **7026** actually-populated `to_ext`; priming with `.to` first → all correct. Strictly read-only (no `save_msg`/`remove_msg`). Repro scripts attached.
## Root cause (evidence-based)
SpiderMonkey 1.8.5's **`JSOP_GETPROP` per-(shape, pc) property cache**, interacting with `js_msghdr_class`'s **old-style resolve hook** (`js_get_msg_header_resolve`) that **conditionally** defines `*_NULL` fields — the field becomes an own property iff `p->msg.<field>` is non-NULL.
All bulk headers are created identically (`JS_NewObject(&js_msghdr_class, proto, …)`) and the hook defines fields in a fixed order, so they **share one shape lineage** but have **divergent real property sets** (a field is present on some headers, absent on others). The per-`(shape, pc)` GETPROP cache can't represent "sometimes present", so once an entry for `(shared-shape, .to_ext)` is established it mis-serves `undefined` to same-shape headers.
## Three discriminators that nailed it
| Test | Result | Conclusion |
|------|--------|------------|
| Siblings (`attr`/`number`/`to`) after a cold `to_ext` miss | **alive on all 7140** | `p->enumerated` is **not** set; resolve hook runs fine |
| `hdr["to_ext"]` (computed, **JSOP_GETELEM**) | undef = 213 = true NULL count → **correct** | not the resolve hook / SMB layer / data |
| `hdr.to_ext` (literal, **JSOP_GETPROP**) | undef = 7138 → **the bug** | it's specifically the literal-name property cache |
| cold-defined-but-*wrong* | **0** | purely spurious `undefined`, never bad data |
SM source confirms the shape of it: `js_GetPropertyHelperWithShapeInline` fills the cache **only on the found path** (`jsobj.cpp:5438`) and explicitly **not** on a miss (`nofills++`, `jsobj.cpp:5366`) — i.e. found-then-mispredict across shared shapes, not negative caching.
### Why the prior fixes behaved as they did
- `ca448cb8b` ("define number") can't work: it just moves every header off the empty shape onto **one shared** `{number,…}` shape; the conditional-property divergence is untouched.
- `666ff71ce` (eager full-resolve) **does** address the root cause — every populated field becomes a real own property at construction, so GETPROP finds them by normal scope lookup with consistent shapes, no conditional resolve-add divergence. The cost is the loss of laziness.
## Notes on hypotheses raised in discussion
- **`p->enumerated` spuriously set** — ruled out (siblings alive on the same object after a cold miss).
- **`js_get_msg_header_resolve(…, JSID_VOID)` early-returning through macros** — doesn't happen; in the `name==NULL` path every `LAZY_*` macro only `return`s when `name` is non-NULL, so it falls through and defines all available fields, and that path never sets `enumerated`.
## Candidate fixes (undecided)
1. **`JSCLASS_NEW_RESOLVE`** conversion — the SM-sanctioned way to keep laziness; hook sets `*objp` only when it actually defines a field. Must be built and proven against the live base (not guaranteed to fix it).
2. **Keep eager-resolve** (`666ff71ce`) and just correct the commit message; optionally benchmark eager vs. lazy on the live base to quantify perf.
3. **Lazy-define-as-null variants** that don't change the public contract (e.g. only in the bulk path, or non-enumerable null) so same-shape headers stop diverging.
## Reproducers (attached)
Both run via `jsexec -c /sbbs/ctrl <script>` against the live `mail` base, strictly read-only:
- [probe_to_ext.js](/uploads/e84b9f7807c1bc07057efb35633a6e62/probe_to_ext.js) — cold vs. primed `to_ext` counts, transition point, mismatch detection.
- [probe_enum.js](/uploads/b261bac444a39ba2730c7b0fdaed12dd/probe_enum.js) — the discriminators: sibling-after-miss, and GETELEM-vs-GETPROP per-field cold reads.
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)