From 72ddeb6ee5d704a092471f58fc1a382c9c46fc5d Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Thu, 8 Oct 2026 13:32:58 +0900 Subject: [PATCH 1/2] A method's boxed parameter iterated with a mutating block is left to its callers' walk The boxed-receiver demand added for an Array read out of a Hash value also caught a method's own poly parameter (`def f(x) = x.each { |e| e << y }`). Demanding its stores walks back through every caller of the method, each caller's parameter on through its callers, scanning every call at each step; on a large program with many such parameters (the activesupport sample, 33k lines) the analysis did not finish in 20 minutes where master takes under 7. That receiver was never what the change was for -- a block parameter bound to a Hash's value, an index read, or a local bound from one -- so a method parameter is left as it was before. The sample analyzes in the time it did and writes the same C. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/analyze.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/analyze.c b/src/analyze.c index 9e5c0ea7be..ad95a944cc 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -19078,7 +19078,13 @@ static int promote_shared_stored_strings(Compiler *c) { if (nt_kind(nt, recv4) == NK_LocalVariableReadNode) { const char *pn4 = nt_str(nt, recv4, "name"); Scope *ps4 = pn4 ? comp_scope_of(c, recv4) : NULL; - if (ps4) changed |= strbuf_demand_container_stores(c, pn4, ps4); + LocalVar *pv4 = ps4 ? scope_local(ps4, pn4) : NULL; + /* a method's boxed parameter stays as it was: its walk goes back + through every caller, and on a large program's poly parameters + (each mutating block over one) that walk is what the + analysis spent its time on */ + if (!pv4 || (pv4->is_param && !pv4->is_block_param)) continue; + changed |= strbuf_demand_container_stores(c, pn4, ps4); } else changed |= strbuf_container_source_walk(c, recv4, 0, SB_DEMAND); continue; From 02f3d131c3c0b2412dec2ff0918fb4b0fdf036b3 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Thu, 8 Oct 2026 13:32:58 +0900 Subject: [PATCH 2/2] The walk back to a parameter's callers follows each parameter once per walk strbuf_demand_param_container_stores follows a container parameter to the arguments its callers pass, and a caller passing its own parameter on to the next level. A parameter reached along several paths was walked again on each, so a chain of methods called from several places cost the product of their call counts, every step scanning every call. Within one outermost walk a parameter (and walk mode) already followed at no greater depth is not followed again: the demand it makes is the same, already made. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/analyze.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/analyze.c b/src/analyze.c index ad95a944cc..cfea9e6146 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -15641,8 +15641,43 @@ static int strbuf_demand_container_stores_here(Compiler *c, const char *contn, S static int an_call_targets_scope(Compiler *c, int u, int mi2, Scope *m2); static int an_class_dynamic_new_risk(Compiler *c, int cid); static int strbuf_demand_local_container(Compiler *c, const char *vn, Scope *vs, int depth, int mode); +/* The parameters one outermost walk has already followed back to their + callers, each with the shallowest depth it was walked at. A parameter + handed on through a chain of methods, each called from several places, + was walked once per path to it -- exponential in the chain's length, and + each walk scans every call. Walking it again within the same walk, no + shallower than before, demands nothing new. */ +typedef struct { int mi, pj, mode, depth; } SbParamSeen; +static SbParamSeen *sb_param_seen; +static int sb_param_seen_n, sb_param_seen_cap, sb_param_walk_nest; +static int sb_param_seen_check(int mi, int pj, int mode, int depth) { + for (int i = 0; i < sb_param_seen_n; i++) { + SbParamSeen *s = &sb_param_seen[i]; + if (s->mi != mi || s->pj != pj || s->mode != mode) continue; + if (s->depth <= depth) return 1; + s->depth = depth; + return 0; + } + if (sb_param_seen_n == sb_param_seen_cap) { + sb_param_seen_cap = sb_param_seen_cap ? sb_param_seen_cap * 2 : 64; + sb_param_seen = realloc(sb_param_seen, sizeof *sb_param_seen * (size_t)sb_param_seen_cap); + if (!sb_param_seen) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } + } + sb_param_seen[sb_param_seen_n++] = (SbParamSeen){ mi, pj, mode, depth }; + return 0; +} +static int strbuf_demand_param_container_stores_walk(Compiler *c, const char *pn, Scope *ps, + int depth, int mode); static int strbuf_demand_param_container_stores(Compiler *c, const char *pn, Scope *ps, int depth, int mode) { + if (sb_param_walk_nest == 0) sb_param_seen_n = 0; + sb_param_walk_nest++; + int r = strbuf_demand_param_container_stores_walk(c, pn, ps, depth, mode); + sb_param_walk_nest--; + return r; +} +static int strbuf_demand_param_container_stores_walk(Compiler *c, const char *pn, Scope *ps, + int depth, int mode) { const NodeTable *nt = c->nt; int changed = 0; if (depth > 8 || !ps || !pn) return 0; @@ -15651,6 +15686,7 @@ static int strbuf_demand_param_container_stores(Compiler *c, const char *pn, Sco int pj = an_param_idx(ps, pn); if (pj < 0) return 0; int mi = (int)(ps - c->scopes); + if (sb_param_seen_check(mi, pj, mode, depth)) return 0; for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode) continue; if (!an_call_targets_scope(c, u, mi, ps)) continue;