#include "cext_bridge.h" #include #include #include #include "analyze_internal.h" #include "implicit_calls.h" #include "builtin_ops.h" #include "repr.h" #include "decide.h" #include "call_plan.h" #include "share.h" #include "timing.h" #include "value_pos.h" #include "builtin_names.h" static int narrow_int_table_ivars(Compiler *c, int in_round); /* declared early: the fixpoint calls it */ int callee_param_is_declared_kwarg(Compiler *c, Scope *m, const char *name); /* --int-overflow=promote flag; see analyze.h. Default off. */ int g_promote_mode = 0; int g_wrap_mode = 0; /* --exceptions=abort (src/main.c): the constructs that need a frame to catch an exception are refused */ int g_exc_abort = 0; int g_gc_none = 0; int g_plan_check = 0; int g_arm_check = 0; /* Post-convergence bind pass: lets an empty array-literal argument fill a parameter that stayed UNKNOWN through the fixpoint (see bind_call_params); applied only after convergence so any concrete kind wins first. */ int g_final_bind_pass = 0; /* Set for the one pass that decides, on the settled types, the container seeds a box reached while the fixpoint ran: a parameter's, a return's and an ivar's (LocalVar.rbs_box_held, Scope.ret_rbs_box_held, ClassInfo.ivar_box_held; an_settle_seed_boxes). */ int g_seed_box_settled = 0; /* Set while the type fixpoint iterates: a pass that would otherwise GUESS a type from ambiguous or not-yet-arrived evidence must instead leave the slot at UNKNOWN (bottom) and wait. "Not yet known" is not "could be anything", but the monotonic passes cannot tell the difference after the fact -- parameter and return types only ever widen, so a guess made on the first iteration, when nothing has a type yet, is permanent. It then cascades: the caller's slot unifies int-array with the guess and lands on the scalar poly, the callee's element reads go poly, and every arithmetic helper downstream boxes. Cleared once the fixpoint converges and iteration continues, so a slot whose evidence really never arrives still takes the pessimistic type. */ int g_infer_optimistic = 0; int g_infer_write_round = 0; LocalVar *g_infer_self_write = NULL; int g_fixpoint_rounds = 0; static int g_fixpoint_capped = 0; /* the round cap stopped the fixpoint short of settling */ /* Defined in codegen.c (linked into the same binary). Used to specialize a `rescue => e` binding to the exception subclass's object type. */ int class_is_exc_subclass(Compiler *c, int ci); /* True when the class body defines an instance method of its own (an exception subclass with behaviour, not just a name). */ static int class_defines_own_method(Compiler *c, int ci) { for (int s = 1; s < c->nscopes; s++) if (c->scopes[s].class_id == ci && !c->scopes[s].is_cmethod && c->scopes[s].name) return 1; return 0; } /* The class index a rescue arm specializes its bound variable to: exactly one named user exception subclass that carries state or behaviour of its own -- ivars (#1415) or methods (#3707) -- else -1. */ static int rescue_arm_spec_cid(Compiler *c, int rescue_id) { int nexc = 0; const int *exc = nt_arr(c->nt, rescue_id, "exceptions", &nexc); if (nexc != 1) return -1; NodeKind en_k = nt_kind(c->nt, exc[0]); if (en_k != NK_ConstantReadNode && en_k != NK_ConstantPathNode) return -1; const char *enm = nt_str(c->nt, exc[0], "name"); int xc = enm ? comp_class_index(c, enm) : -1; if (xc >= 0 && class_is_exc_subclass(c, xc) && (c->classes[xc].nivars > 0 || class_defines_own_method(c, xc))) return xc; return -1; } /* Collect the single user-exception class raised in a subtree (not crossing DefNode). *cid accumulates; *bad is set when a raise doesn't name one ivar-carrying user exception class, or two raises disagree. */ static void scan_raise_classes(Compiler *c, int id, int *cid, int *bad) { if (id < 0 || *bad) return; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) return; if (ty_k == NK_DefNode) return; if (ty_k == NK_CallNode) { const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, "raise") && nt_ref(nt, id, "receiver") < 0) { int a = nt_ref(nt, id, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; const char *cn = NULL; if (an >= 1) { NodeKind ak = nt_kind(nt, av[0]); if (ak == NK_ConstantReadNode) cn = nt_str(nt, av[0], "name"); else if (ak == NK_CallNode && nt_str(nt, av[0], "name") && sp_streq(nt_str(nt, av[0], "name"), "new")) { int rr = nt_ref(nt, av[0], "receiver"); if (rr >= 0 && nt_kind(nt, rr) == NK_ConstantReadNode) cn = nt_str(nt, rr, "name"); } } int rcid = cn ? comp_class_index(c, cn) : -1; if (rcid < 0 || !class_is_exc_subclass(c, rcid) || c->classes[rcid].nivars <= 0) { *bad = 1; return; } if (*cid >= 0 && *cid != rcid) { *bad = 1; return; } *cid = rcid; return; } } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) scan_raise_classes(c, nt_ref_at(nt, id, i), cid, bad); int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, id, i, &n); for (int k = 0; k < n; k++) scan_raise_classes(c, ids[k], cid, bad); } } /* Bare `rescue => e`: when every raise in the guarded begin body constructs the same ivar-carrying user exception class, specialize the binding to that class like a typed arm would. The emitted binder guards the carried-object cast with a class match, so a foreign StandardError arriving at the bare arm still binds a safe zero-ivar struct. */ static int bare_rescue_spec_cid(Compiler *c, int rescue_id) { const NodeTable *nt = c->nt; int nexc = 0; nt_arr(nt, rescue_id, "exceptions", &nexc); if (nexc != 0) return -1; NT_FOREACH_KIND(nt, NK_BeginNode, bid) { int arm = nt_ref(nt, bid, "rescue_clause"); int hit = 0; for (int r = arm; r >= 0; r = nt_ref(nt, r, "subsequent")) if (r == rescue_id) { hit = 1; break; } if (!hit) continue; int cid = -1, bad = 0; scan_raise_classes(c, nt_ref(nt, bid, "statements"), &cid, &bad); return (!bad && cid >= 0) ? cid : -1; } return -1; } /* Names the emitted program can reach with no CallNode of its own: the runtime protocols. `5 + money` calls Money#coerce, `puts obj` calls to_s, a `for` loop calls each. Reachability keeps such a method alive; a pass reasoning about how a method is entered or what its C signature may be must likewise treat it as having a caller it cannot see. */ int method_name_implicitly_invoked(const char *nm) { static const char *const implicit[] = { "to_s", "inspect", "==", "<=>", "eql?", "hash", "each", "coerce", "__noarg to_s", "__noarg inspect", /* desugar_noarg_display_wrappers */ "to_str", "to_ary", "to_a", "to_i", "to_int", "to_h", "to_hash", "to_proc", "call", "to_path", /* File, Dir and IO's path slots ask it first (rb_get_path) */ "to_io", /* IO.select waits on whatever answers it; the only caller is the generated dispatch behind sp_user_to_io_hook, so nothing in the program names it (#4105-adjacent, the TLS socket shape) */ "initialize_copy", /* the materializer synthesized for a class that includes Enumerable: the generated sp_obj_to_a dispatch calls it for an instance the poly machinery meets, and nothing in the program names it (#3761) */ "__enum_to_a", NULL }; if (!nm) return 0; return str_in(nm, implicit); } /* Can this fold seed only ever be a BUILTIN? A literal, or an expression whose value a literal decides, can never reach a user class's `+`, so a seeded `sum` written with one must not keep every `+` in the program alive. Asked of the Prism node because compute_reachable runs before inference. */ static int an_seed_is_builtin(const NodeTable *nt, int id) { if (id < 0) return 0; switch (nt_kind(nt, id)) { case NK_IntegerNode: case NK_FloatNode: case NK_StringNode: case NK_InterpolatedStringNode: case NK_SymbolNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: case NK_ArrayNode: case NK_HashNode: case NK_RangeNode: case NK_RationalNode: case NK_ImaginaryNode: return 1; case NK_CallNode: { const char *nm = nt_str(nt, id, "name"); int rcv = nt_ref(nt, id, "receiver"); if (!nm) return 0; /* the Kernel constructors, which name a builtin class and answer one */ if (rcv < 0 && (sp_streq(nm, "Rational") || sp_streq(nm, "Complex") || sp_streq(nm, "Float") || sp_streq(nm, "Integer") || sp_streq(nm, "String") || sp_streq(nm, "Array"))) return 1; /* arithmetic ON a literal is still a literal's class: `10**30`, `-1`. An operator name is the one that does not start like an identifier. */ if (rcv >= 0 && nm[0] && !((nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z') || nm[0] == '_')) return an_seed_is_builtin(nt, rcv); return 0; } default: return 0; } } /* A hashed name set (ANameHash). Passes that asked "is this name among those" with a linear scan, once per node or per mark, paid (asks x names) every round (rubys/roundhouse#72); compute_reachable's called-name set is the first user. */ int anh_find(const ANameHash *st, const char *nm) { if (!st->nb) return -1; for (int i = st->head[sp_strhash(nm) % (unsigned)st->nb]; i >= 0; i = st->next[i]) if (sp_streq(st->key[i], nm)) return i; /* the most recent add of it */ return -1; } int anh_has(const ANameHash *st, const char *nm) { return anh_find(st, nm) >= 0; } void anh_add(ANameHash *st, const char *nm) { if (!st->nb) { st->nb = 4096; st->head = malloc(sizeof(int) * (size_t)st->nb); for (int b = 0; b < st->nb; b++) st->head[b] = -1; } if (st->n == st->cap) { st->cap = st->cap ? st->cap * 2 : 64; st->key = realloc(st->key, sizeof(char *) * (size_t)st->cap); st->next = realloc(st->next, sizeof(int) * (size_t)st->cap); } /* the buckets grow with the names: a fixed count let a program's every method name (tens of thousands) chain behind each bucket, and every ask walked the chain. Rehashed in add order, so each chain still answers its most recent add first. */ if (st->n >= st->nb * 2) { st->nb *= 4; st->head = realloc(st->head, sizeof(int) * (size_t)st->nb); for (int b = 0; b < st->nb; b++) st->head[b] = -1; for (int i = 0; i < st->n; i++) { unsigned b = sp_strhash(st->key[i]) % (unsigned)st->nb; st->next[i] = st->head[b]; st->head[b] = i; } } unsigned b = sp_strhash(nm) % (unsigned)st->nb; st->key[st->n] = nm; st->next[st->n] = st->head[b]; st->head[b] = st->n; st->n++; } void anh_free(ANameHash *st) { free(st->key); free(st->next); free(st->head); } /* The program as written, read ahead of every desugar (analyze_program's first step). It notes each name the program gives a method by a def or may give one by a Symbol (an attr, a Struct member, a name handed to a call), and whether it holds a site where a method is named, made or loaded by something the text does not spell: (1) a call that takes a method's name (an_by_name below) where the name is no Symbol literal, or where it calls, reads or renames another such word by its Symbol; an alias or an undef likewise; (2) a block-pass of anything but a Symbol literal that is no such word (the parser spells most of those as a block and marks it), a literal lambda, or the block parameter of its own def, which that def only reads; (3) text evaluated: an eval with an argument or with no literal block, an exec, a trap or a trace_var with no literal block, `binding`, ERB; (4) a file not read: a require left standing in the table (the parser splices away each one it resolves), or g_require_unread, which stays clear only where the program's text proves every file it loads was read (spinel_parse.c); (5) a module mixed in: an include, a prepend or an extend with an argument that is no literal (`s.prepend("x")` is String's), a hook beneath them called by hand, or one of those six words as a def or as a Symbol. A module's methods run ahead of a class's own (prepend), of its parent's (include) or of an object's class's (extend), and a core module's are no text of the program's; that a constant names a module of the program's own is not proved here. The desugars rewrite such calls away, so the table cannot be asked later. A key is a copy: a node's strings do not outlive its rewrite. */ enum { AN_CALLS = 1, AN_LAST, AN_RECV, AN_GIVES, AN_GIVES_ALL, AN_RENAMES, AN_VIS, AN_EVAL, AN_EXEC, AN_SITE, AN_WORD, AN_MIX, AN_MIX_HOOK }; static const struct { const char *nm; int how; } an_by_name[] = { /* its first argument names what it calls or reads */ { "send", AN_CALLS }, { "__send__", AN_CALLS }, { "public_send", AN_CALLS }, { "method", AN_CALLS }, { "public_method", AN_CALLS }, { "singleton_method", AN_CALLS }, { "instance_method", AN_CALLS }, { "public_instance_method", AN_CALLS }, { "enum_for", AN_CALLS }, { "to_enum", AN_CALLS }, { "const_get", AN_CALLS }, /* its last argument does, or its receiver */ { "inject", AN_LAST }, { "reduce", AN_LAST }, { "to_proc", AN_RECV }, /* its arguments name what it gives, takes away or hides */ { "define_method", AN_GIVES }, { "define_singleton_method", AN_GIVES }, { "undef_method", AN_GIVES_ALL }, { "remove_method", AN_GIVES_ALL }, { "attr", AN_GIVES_ALL }, { "attr_reader", AN_GIVES_ALL }, { "attr_writer", AN_GIVES_ALL }, { "attr_accessor", AN_GIVES_ALL }, { "private", AN_VIS }, { "protected", AN_VIS }, { "public", AN_VIS }, { "module_function", AN_VIS }, { "private_class_method", AN_VIS }, { "public_class_method", AN_VIS }, /* it gives one method another's name: a word under a new name is a word no more */ { "alias_method", AN_RENAMES }, { "delegate", AN_RENAMES }, { "instance_delegate", AN_RENAMES }, { "single_delegate", AN_RENAMES }, { "def_delegator", AN_RENAMES }, { "def_delegators", AN_RENAMES }, { "def_instance_delegator", AN_RENAMES }, { "def_instance_delegators", AN_RENAMES }, { "def_single_delegator", AN_RENAMES }, { "def_single_delegators", AN_RENAMES }, /* text evaluated, and a file */ { "eval", AN_EVAL }, { "instance_eval", AN_EVAL }, { "class_eval", AN_EVAL }, { "module_eval", AN_EVAL }, { "instance_exec", AN_EXEC }, { "class_exec", AN_EXEC }, { "module_exec", AN_EXEC }, { "trap", AN_EXEC }, { "trace_var", AN_EXEC }, { "binding", AN_SITE }, { "require", AN_SITE }, { "require_relative", AN_SITE }, /* it mixes a module in */ { "include", AN_MIX }, { "prepend", AN_MIX }, { "extend", AN_MIX }, { "append_features", AN_MIX_HOOK }, { "prepend_features", AN_MIX_HOOK }, { "extend_object", AN_MIX_HOOK }, { "load", AN_WORD }, { "autoload", AN_WORD }, { "ERB", AN_WORD }, { NULL, 0 } }; static ANameHash g_written, g_written_twice, g_by_name_words, g_symbol_names; static int g_written_by_value, g_meta_by_value; static void an_written_add(ANameHash *st, const char *nm) { if (nm && !anh_has(st, nm)) anh_add(st, strdup(nm)); } static int an_by_name_how(const char *nm) { if (!nm || !anh_has(&g_by_name_words, nm)) return 0; for (int k = 0; an_by_name[k].nm; k++) if (sp_streq(nm, an_by_name[k].nm)) return an_by_name[k].how; return 0; } /* Is `n` a name the text spells: a Symbol literal that is none of the words? */ static int an_name_spelled(const NodeTable *nt, int n) { return nt_kind(nt, n) == NK_SymbolNode && !an_by_name_how(nt_str(nt, n, "value")); } static int an_mixes(const char *nm) { int how = an_by_name_how(nm); return how == AN_MIX || how == AN_MIX_HOOK; } /* A literal, which is no module. */ static int an_mix_literal(const NodeTable *nt, int n) { switch (nt_kind(nt, n)) { case NK_StringNode: case NK_InterpolatedStringNode: case NK_SymbolNode: case NK_IntegerNode: case NK_FloatNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: return 1; default: return 0; } } /* The block-passes of the block parameter `bp` under `n`, a def's body; -1 where the body does more with that name than read it (a write, a parameter of a block inside). A def inside is another scope. */ static int an_own_block_passes(const NodeTable *nt, int n, const char *bp) { if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode) return 0; if (k == NK_BlockArgumentNode) { int e = nt_ref(nt, n, "expression"); const char *en = nt_kind(nt, e) == NK_LocalVariableReadNode ? nt_str(nt, e, "name") : NULL; if (en && sp_streq(en, bp)) return 1; } else if (k != NK_LocalVariableReadNode && k != NK_CallNode) { const char *nm = nt_str(nt, n, "name"); if (nm && sp_streq(nm, bp)) return -1; } int cnt = 0, nr = nt_num_refs(nt, n), na = nt_num_arrs(nt, n); for (int i = 0; i < nr; i++) { int r = an_own_block_passes(nt, nt_ref_at(nt, n, i), bp); if (r < 0) return -1; cnt += r; } for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) { int r = an_own_block_passes(nt, ids[j], bp); if (r < 0) return -1; cnt += r; } } return cnt; } static void an_note_written_names(Compiler *c) { const NodeTable *nt = c->nt; int by_value = 0, passes = 0, own_passes = 0, erb = 0, mixes = 0, loads = g_require_unread; for (int k = 0; an_by_name[k].nm; k++) if (!anh_has(&g_by_name_words, an_by_name[k].nm)) anh_add(&g_by_name_words, an_by_name[k].nm); for (int id = 0; id < nt->count; id++) { switch (nt_kind(nt, id)) { case NK_AliasMethodNode: if (nt_kind(nt, nt_ref(nt, id, "new_name")) != NK_SymbolNode || !an_name_spelled(nt, nt_ref(nt, id, "old_name"))) by_value = 1; break; case NK_UndefNode: { int n = 0; const int *v = nt_arr(nt, id, "names", &n); for (int i = 0; i < n; i++) if (nt_kind(nt, v[i]) != NK_SymbolNode) by_value = 1; break; } case NK_SymbolNode: { const char *nm = nt_str(nt, id, "value"); an_written_add(&g_symbol_names, nm); an_written_add(&g_written, nm); an_written_add(&g_written_twice, nm); if (an_mixes(nm)) mixes = 1; break; } case NK_DefNode: { const char *nm = nt_str(nt, id, "name"); if (nm && anh_has(&g_written, nm)) an_written_add(&g_written_twice, nm); an_written_add(&g_written, nm); if (an_mixes(nm)) mixes = 1; const char *bp = nt_str(nt, nt_ref(nt, nt_ref(nt, id, "parameters"), "block"), "name"); int own = bp ? an_own_block_passes(nt, nt_ref(nt, id, "body"), bp) : 0; if (own > 0) own_passes += own; break; } case NK_BlockArgumentNode: { int e = nt_ref(nt, id, "expression"); NodeKind ek = nt_kind(nt, e); if (ek == NK_LocalVariableReadNode) passes++; else if (e >= 0 && ek != NK_LambdaNode && !an_name_spelled(nt, e)) by_value = 1; break; } case NK_BlockNode: { /* the parser spells `m(&:sym)` as `m { |_spx| _spx.sym }` and marks the block */ if (!nt_int(nt, id, "sym_proc_block", 0)) break; int n = 0; const int *v = nt_arr(nt, nt_ref(nt, id, "body"), "body", &n); if (n != 1 || nt_kind(nt, v[0]) != NK_CallNode || an_by_name_how(nt_str(nt, v[0], "name"))) by_value = 1; break; } case NK_ConstantReadNode: case NK_ConstantPathNode: if (sp_streq(nt_str(nt, id, "name") ? nt_str(nt, id, "name") : "", "ERB")) erb++; break; case NK_ClassNode: case NK_ModuleNode: { const char *cn = nt_str(nt, nt_ref(nt, id, "constant_path"), "name"); if (cn && sp_streq(cn, "ERB")) erb--; break; } case NK_CallNode: { int how = an_by_name_how(nt_str(nt, id, "name")); if (!how || how == AN_WORD) break; int an = 0, blk = nt_ref(nt, id, "block"); const int *av = nt_arr(nt, nt_ref(nt, id, "arguments"), "arguments", &an); int lit_blk = nt_kind(nt, blk) == NK_BlockNode; int by_value_before = by_value; by_value = 0; switch (how) { case AN_SITE: case AN_MIX_HOOK: by_value = 1; break; case AN_MIX: for (int i = 0; i < an; i++) if (!an_mix_literal(nt, av[i])) by_value = 1; break; case AN_EVAL: if (an > 0 || !lit_blk) by_value = 1; break; case AN_EXEC: if (!lit_blk) by_value = 1; break; case AN_RECV: if (!an_name_spelled(nt, nt_ref(nt, id, "receiver"))) by_value = 1; break; case AN_CALLS: if (an > 0 && !an_name_spelled(nt, av[0])) by_value = 1; break; case AN_GIVES: if (an > 0 && nt_kind(nt, av[0]) != NK_SymbolNode) by_value = 1; break; case AN_LAST: /* one operand beside a literal block is the first value, not a name; beside a block-pass it is the name whenever no block was given */ if (an > 0 && !(an == 1 && lit_blk) && !an_name_spelled(nt, av[an - 1])) by_value = 1; break; default: for (int i = 0; i < an; i++) { NodeKind ak = nt_kind(nt, av[i]); /* `private def m` and `private attr_reader :m` name what they wrap */ if (how == AN_VIS && (ak == NK_DefNode || (ak == NK_CallNode && an_by_name_how(nt_str(nt, av[i], "name")) == AN_GIVES_ALL))) continue; if (ak != NK_SymbolNode || (how == AN_RENAMES && i > 0 && !an_name_spelled(nt, av[i]))) by_value = 1; } break; } if (by_value && (how == AN_GIVES || how == AN_GIVES_ALL || how == AN_RENAMES || how == AN_VIS)) g_meta_by_value = 1; if (by_value && (how == AN_MIX || how == AN_MIX_HOOK)) { mixes = 1; by_value = 0; } if (by_value && is_file_load_name(nt_str(nt, id, "name"))) { loads = 1; by_value = 0; } by_value |= by_value_before; break; } default: break; } } c->names_by_value = by_value || passes > own_passes || erb > 0; if (c->names_by_value || mixes || loads) g_written_by_value = 1; } /* Does the program, as written, give no class or module a method named `nm`, beyond `defs` (0 or 1) defs of that name? Proved by what it does not hold and by nothing cleverer: no other def of that name, no Symbol of that name, and nowhere a site that names, makes or loads a method by something the text does not spell. */ int an_prog_never_gives(const char *nm, int defs) { return !g_written_by_value && !anh_has(defs ? &g_written_twice : &g_written, nm); } /* Does the program name a method `nm` by a Symbol (an alias, an undef, a visibility or define_method call, a send), or define, rename, remove or hide a method by a name it computes? */ int an_prog_names_method(const char *nm) { return g_meta_by_value || anh_has(&g_symbol_names, nm); } /* Does the program define, rename, remove or hide a method by a name it computes? */ int an_prog_meta_by_value(void) { return g_meta_by_value; } static int cr_class_is_ancestor(Compiler *c, int sup, int cls) { for (int k = cls, hops = 0; k >= 0 && k < c->nclasses && hops < c->nclasses; k = c->classes[k].parent, hops++) if (k == sup) return 1; return 0; } static int cr_has_super(const NodeTable *nt, int id) { if (id < 0) return 0; NodeKind k = nt_kind(nt, id); if (k == NK_SuperNode || k == NK_ForwardingSuperNode) return 1; if (k == NK_DefNode) return 0; int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) if (cr_has_super(nt, nt_ref_at(nt, id, i))) return 1; int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int nn = 0; const int *ids = nt_arr_at(nt, id, i, &nn); for (int j = 0; j < nn; j++) if (cr_has_super(nt, ids[j])) return 1; } return 0; } static int cr_scope_has_super(Compiler *c, int s) { const Scope *sc = &c->scopes[s]; return cr_has_super(c->nt, sc->body) || (sc->def_node >= 0 && cr_has_super(c->nt, nt_ref(c->nt, sc->def_node, "parameters"))); } /* Does scope s call `super` (cr_scope_has_super) -- the exception text dispatch asks it of a #message / #to_s override */ int scope_calls_super(Compiler *c, int s) { return cr_scope_has_super(c, s); } /* Is `nm` called by bare name, or on an instance, with an argument count the call site shows ("\x04" is added with each "\x04 ", compute_reachable)? An alias or a prepend chain of it is live whatever the count: they are not scopes the count can rule out. */ static int cn_counted(const ANameHash *cn_set, const char *nm, int on_instance) { char ab[300]; snprintf(ab, sizeof ab, "\x04%s%s", on_instance ? "\x03" : "", nm); return anh_has(cn_set, ab); } /* Does a call by name reach scope t: not a replaced version, and for a call on an instance not a class method (a module function is one an instance can run) */ static int cr_name_reaches(Compiler *c, int t, int on_instance) { const Scope *sc = &c->scopes[t]; return !sc->replaced && (!on_instance || !sc->is_cmethod || sc->is_module_function); } /* Is `nm` called: by bare name, or on a known instance (`K.new.nm`, marked "\x03nm" by compute_reachable)? */ static int cn_live(const ANameHash *cn_set, const char *nm) { char ib[300]; snprintf(ib, sizeof ib, "\x03%s", nm); return anh_has(cn_set, nm) || anh_has(cn_set, ib) || cn_counted(cn_set, nm, 0) || cn_counted(cn_set, nm, 1); } /* The settled element kind already includes a literal block's next values. Before inference, retain every possible coerce target as before. */ static int an_sum_needs_coerce(Compiler *c, int id, int typed) { if (!typed) return 1; int blk = nt_ref(c->nt, id, "block"), recv = nt_ref(c->nt, id, "receiver"); TyKind rt = comp_ntype(c, recv), et; if (blk >= 0) { if (nt_kind(c->nt, blk) != NK_BlockNode) return 1; et = infer_sum_block_ty(c, blk); /* a block answering a Rational --int-overflow=promote holds boxed is a number of the tower, no object's coerce */ if (et == TY_POLY) { int body = nt_ref(c->nt, blk, "body"), bn = 0; const int *bb = body >= 0 && nt_kind(c->nt, body) == NK_StatementsNode ? nt_arr(c->nt, body, "body", &bn) : NULL; if (bn > 0 && an_rat_value(c, bb[bn - 1])) et = TY_RATIONAL; } } else et = rt == TY_RANGE ? TY_INT : ty_array_elem(rt); return et == TY_UNKNOWN || et == TY_POLY || ty_is_object(et); } extern const ANameHash *g_cr_typed, *g_cr_untyped; void cr_literal_locals_build(Compiler *c); void cr_class_only_names(Compiler *c, ANameHash *out); /* Can the receiver of the `%` call `id` be a format String? A String literal can; once types are known (typed), a receiver typed String can. A boxed one is not counted: the desugared Integer#fdiv takes a boxed `%` of its own, which would turn the bridge on with no format at all. */ static int format_recv_maybe_string(Compiler *c, int id, int typed) { int r = nt_ref(c->nt, id, "receiver"); if (r < 0) return 0; NodeKind k = nt_kind(c->nt, r); if (k == NK_StringNode || k == NK_InterpolatedStringNode) return 1; if (k == NK_IntegerNode || k == NK_FloatNode) return 0; if (!typed) return 1; TyKind t = comp_ntype(c, r); return t == TY_STRING || t == TY_STRBUF; } /* Class liveness (#10592). A class the program opens with `class` keeps its methods back until something can run them. Its singleton methods run for a call on the class: on its constant (`Date.civil`, not as the namespace of a longer path like `Date::ITALY`, nor as a class definition's name or superclass), on self in one of its singleton methods or a subclass's, on `self.class` in one of its instance methods, or with a receiver that is none of these (`klass.civil`) once something names the class. Its instance methods and initialize run once one of it can exist: reached code reads its constant as a value (`k = Date`, `x.is_a?(Date)`), calls a class method it does not define on it (`Date.allocate`), or a singleton method of it, or of an ancestor it inherits one from, builds one (`new`, `allocate`) or hands `self` out. A method a reached call names waits for its class to be opened so; reached, it brings its ancestors along. A program that finds classes by name or by enumeration -- const_get, eval, Marshal, ObjectSpace, subclasses, a C extension, or an included / extended / prepended / const_missing hook -- keeps every class open, and an inherited hook its class's descendants. Before this, `require "date"` compiled most of the date package into a program that never named Date: `<=>`, `new` and `jd` are called somewhere, and every initialize was a root. */ enum { CR_VALUE, CR_CONST_CALL, CR_SELF_CALL, CR_DYN_CALL }; typedef struct { const char *name, *call; int cls; int kind; int persist; } CrRead; typedef struct { Compiler *c; int *queue, *qtail; unsigned char *held_c, *held_i; /* per class: its class side, its instance side not open */ unsigned char *builds; /* per class: a reached singleton method of it builds one */ int *pend_c, *pend_i; /* per class: the first scope held on a side */ int *pend_next; /* per scope: the next scope held on the same side */ unsigned char *pend_mark; /* per scope: 1 held, 2 held and bound to its call */ ANameHash seg; /* a gated class's last name segment */ int *seg_first, *seg_next; /* segment -> its classes, class -> the next */ ANameHash *dyn; /* names called on a receiver no constant names */ ANameHash *adm; /* ":": called on that class */ CrRead **reads; int *nreads, *creads; /* per scope: the classes it names, the calls it makes */ int *open_next; /* per class: the next class a side opened with */ int *cls_first, *cls_next; /* class -> its scopes, scope -> the next */ unsigned char *weak_done; /* per scope: its reads counted unreached */ int all; int typed; /* the node types are settled */ } CrLive; static int cr_live_gated(Compiler *c, int k) { const ClassInfo *ci = &c->classes[k]; int dn = ci->def_node; return k != c->toplevel_class && dn >= 0 && dn < c->nt->count && nt_kind(c->nt, dn) == NK_ClassNode && !ci->is_builtin_const && !ci->is_native_class && !ci->is_struct && !ci->is_data && !ci->is_anon_struct && !ci->cext_layout && !ci->cext_extension; } /* is_descendant without its reading of a class an earlier pass left unreferenced */ static int cr_live_descends(Compiler *c, int k, int anc) { for (int h = 0; k >= 0 && k < c->nclasses && h <= c->nclasses; k = c->classes[k].parent, h++) if (k == anc) return 1; return 0; } static const char *cr_live_segment(const char *nm) { const char *p = strrchr(nm, ':'); return p ? p + 1 : nm; } static void cr_live_add(CrLive *lv, int s, int kind, const char *nm, const char *call, int cls, int persist) { if ((kind == CR_VALUE || kind == CR_CONST_CALL) && !nm) return; if (kind != CR_VALUE && !call) call = ""; CrRead *r = lv->reads[s]; for (int i = 0; i < lv->nreads[s]; i++) if (r[i].kind == kind && r[i].cls == cls && (r[i].name == nm || (r[i].name && nm && sp_streq(r[i].name, nm))) && (r[i].call == call || (r[i].call && call && sp_streq(r[i].call, call)))) { r[i].persist |= persist; return; } if (lv->nreads[s] >= lv->creads[s]) { lv->creads[s] = lv->creads[s] ? lv->creads[s] * 2 : 8; lv->reads[s] = realloc(lv->reads[s], sizeof(CrRead) * (size_t)lv->creads[s]); } lv->reads[s][lv->nreads[s]++] = (CrRead){ nm, call, cls, kind, persist }; } /* A send's Symbol names the method it calls, and `method(:m)`'s the one it takes */ static const char *cr_live_call_name(const NodeTable *nt, int id) { const char *call = nt_str(nt, id, "name"); if (call && (sp_streq(call, "send") || sp_streq(call, "__send__") || sp_streq(call, "public_send") || sp_streq(call, "method") || sp_streq(call, "public_method") || sp_streq(call, "singleton_method"))) { int an = 0; const int *av = nt_arr(nt, nt_ref(nt, id, "arguments"), "arguments", &an); return an >= 1 && nt_kind(nt, av[0]) == NK_SymbolNode ? nt_str(nt, av[0], "value") : NULL; } return call; } /* Does a node write a slot that outlives the method: an instance, class or global variable, or a constant? */ static int cr_live_slot_write(const NodeTable *nt, int id) { const char *ty = nt_type(nt, id); if (!ty) return 0; size_t tl = strlen(ty); if (tl < 9 || strcmp(ty + tl - 9, "WriteNode") != 0) return 0; return strncmp(ty, "InstanceVariable", 16) == 0 || strncmp(ty, "ClassVariable", 13) == 0 || strncmp(ty, "GlobalVariable", 14) == 0 || strncmp(ty, "Constant", 8) == 0; } /* Is the receiver at `r` a slot that outlives the method: an instance, class or global variable, or a constant that holds a value, not a class (`CACHE << x`)? */ static int cr_live_slot_read(CrLive *lv, int r) { const NodeTable *nt = lv->c->nt; NodeKind k = nt_kind(nt, r); if (k == NK_InstanceVariableReadNode || k == NK_ClassVariableReadNode || k == NK_GlobalVariableReadNode) return 1; if (k != NK_ConstantReadNode && k != NK_ConstantPathNode) return 0; const char *nm = nt_str(nt, r, "name"); return nm && anh_find(&lv->seg, nm) < 0 && comp_class_index(lv->c, nm) < 0; } /* The classes the code at `id` names in scope s, and the calls it makes that can reach a singleton method, not entering a nested def. `cls` is the class whose body the code is in, where self is that class, or -1. `persist`: the code's value goes into a slot that outlives the method (a write of one, or an argument of a call on one: `@list << K.new`). */ static void cr_live_collect(CrLive *lv, int s, int id, int cls, int persist) { Compiler *c = lv->c; const NodeTable *nt = c->nt; if (id < 0) return; NodeKind k = nt_kind(nt, id); if (k == NK_DefNode) return; if (k == NK_ConstantReadNode || k == NK_ConstantPathNode) { cr_live_add(lv, s, CR_VALUE, nt_str(nt, id, "name"), NULL, -1, persist); int p = k == NK_ConstantPathNode ? nt_ref(nt, id, "parent") : -1; if (p >= 0 && nt_kind(nt, p) != NK_ConstantReadNode && nt_kind(nt, p) != NK_ConstantPathNode) cr_live_collect(lv, s, p, cls, persist); return; } if (k == NK_ClassNode) { int cp = nt_ref(nt, id, "constant_path"); cr_live_collect(lv, s, nt_ref(nt, id, "body"), cp >= 0 ? const_node_class_index(c, cp) : -1, 0); return; } if (k == NK_ModuleNode) { cr_live_collect(lv, s, nt_ref(nt, id, "body"), -1, 0); return; } int skip = -1; if (cr_live_slot_write(nt, id)) persist = 1; if (k == NK_CallNode) { int r = nt_ref(nt, id, "receiver"); NodeKind rk = r >= 0 ? nt_kind(nt, r) : NK_ProgramNode; const Scope *sc = &c->scopes[s]; const char *call = cr_live_call_name(nt, id); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { cr_live_add(lv, s, CR_CONST_CALL, nt_str(nt, r, "name"), call, -1, persist); int p = rk == NK_ConstantPathNode ? nt_ref(nt, r, "parent") : -1; if (p >= 0 && nt_kind(nt, p) != NK_ConstantReadNode && nt_kind(nt, p) != NK_ConstantPathNode) cr_live_collect(lv, s, p, cls, persist); skip = r; } else if ((r < 0 || rk == NK_SelfNode) && cls >= 0) cr_live_add(lv, s, CR_SELF_CALL, NULL, call, cls, 0); else if ((r < 0 || rk == NK_SelfNode) && sc->is_cmethod && sc->class_id >= 0) cr_live_add(lv, s, CR_SELF_CALL, NULL, call, sc->class_id, 0); else if (rk == NK_CallNode && !sc->is_cmethod && sc->class_id >= 0 && sp_streq(nt_str(nt, r, "name"), "class") && nt_ref(nt, r, "receiver") >= 0 && nt_kind(nt, nt_ref(nt, r, "receiver")) == NK_SelfNode) cr_live_add(lv, s, CR_SELF_CALL, NULL, call, sc->class_id, 0); else if (r >= 0 || s == 0) cr_live_add(lv, s, CR_DYN_CALL, NULL, call, -1, 0); } /* a call on a slot hands its arguments to it: their reads persist, the receiver's as it was */ int recv = k == NK_CallNode ? nt_ref(nt, id, "receiver") : -1; int arg_persist = persist || (recv >= 0 && cr_live_slot_read(lv, recv)); int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, id, i); if (ch != skip) cr_live_collect(lv, s, ch, cls, ch == recv ? persist : arg_persist); } int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int nn = 0; const int *ids = nt_arr_at(nt, id, i, &nn); for (int j = 0; j < nn; j++) cr_live_collect(lv, s, ids[j], cls, arg_persist); } } /* Does the code at `id`, run with self a class, build an instance of it or hand `self` out? `new` or `allocate` on self, `super` in a `new` or `allocate` of its own, or self as a value. Not entering a nested def or class. */ static int cr_live_builds(Compiler *c, int id, const char *mname) { const NodeTable *nt = c->nt; if (id < 0) return 0; NodeKind nk = nt_kind(nt, id); if (nk == NK_DefNode || nk == NK_ClassNode || nk == NK_ModuleNode) return 0; if (nk == NK_SelfNode) return 1; if ((nk == NK_SuperNode || nk == NK_ForwardingSuperNode) && mname && (sp_streq(mname, "new") || sp_streq(mname, "allocate"))) return 1; int skip = -1; if (nk == NK_SingletonClassNode) skip = nt_ref(nt, id, "expression"); if (nk == NK_CallNode) { int r = nt_ref(nt, id, "receiver"); const char *nm = nt_str(nt, id, "name"); if (r < 0 || nt_kind(nt, r) == NK_SelfNode) { if (nm && (sp_streq(nm, "new") || sp_streq(nm, "allocate"))) return 1; skip = r; } } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, id, i); if (ch != skip && cr_live_builds(c, ch, mname)) return 1; } int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int nn = 0; const int *ids = nt_arr_at(nt, id, i, &nn); for (int j = 0; j < nn; j++) if (cr_live_builds(c, ids[j], mname)) return 1; } return 0; } /* Does class k's body, as the program loads, hand `self` out? Not entering a def or a nested class; `class << self` and a call on self keep it. */ static int cr_live_body_escapes(Compiler *c, int id) { const NodeTable *nt = c->nt; if (id < 0) return 0; NodeKind nk = nt_kind(nt, id); if (nk == NK_DefNode || nk == NK_ClassNode || nk == NK_ModuleNode) return 0; if (nk == NK_SelfNode) return 1; int skip = -1; if (nk == NK_SingletonClassNode) skip = nt_ref(nt, id, "expression"); if (nk == NK_CallNode) { int r = nt_ref(nt, id, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_SelfNode) skip = r; } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, id, i); if (ch != skip && cr_live_body_escapes(c, ch)) return 1; } int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int nn = 0; const int *ids = nt_arr_at(nt, id, i, &nn); for (int j = 0; j < nn; j++) if (cr_live_body_escapes(c, ids[j])) return 1; } return 0; } static int cr_live_admitted(CrLive *lv, int k, const char *nm) { char key[512]; snprintf(key, sizeof key, "%d:%s", k, nm); return anh_has(lv->adm, key) || (!lv->held_c[k] && anh_has(lv->dyn, nm)); } /* Reach scope t, or hold it for its class: a singleton method until a call that can run it (bound: a call resolved to it, once its class is named), an instance method until an instance can exist */ static void cr_live_reach_as(CrLive *lv, int t, int bound) { Compiler *c = lv->c; Scope *sc = &c->scopes[t]; if (sc->reachable) return; int k = sc->class_id; if (k >= 0 && k < c->nclasses && lv->pend_c[k] != -2) { /* a copy the compiler made of a singleton method (an inherited or included copy `__inc 0 m`, a proc form `m#pf`) runs for the calls the compiler routes to it: it waits only for its class to be named */ const char *nm = sc->name ? sc->name : ""; if (sc->is_cmethod && (strchr(nm, ' ') || strchr(nm, '#'))) bound = 1; int hold = sc->is_cmethod ? (bound ? lv->held_c[k] : !cr_live_admitted(lv, k, nm)) : lv->held_i[k]; if (hold) { if (!lv->pend_mark[t]) { int *head = sc->is_cmethod ? &lv->pend_c[k] : &lv->pend_i[k]; lv->pend_next[t] = *head; *head = t; } if (lv->pend_mark[t] < 1 + bound) lv->pend_mark[t] = (unsigned char)(1 + bound); return; } } sc->reachable = 1; comp_touch_scope(sc->idx); lv->queue[(*lv->qtail)++] = t; } static void cr_live_reach(CrLive *lv, int t) { cr_live_reach_as(lv, t, 0); } /* Retry each scope held on a side; the ones still held stay listed */ static void cr_live_retry(CrLive *lv, int *head) { int t = *head; *head = -1; for (int nx; t >= 0; t = nx) { nx = lv->pend_next[t]; int bound = lv->pend_mark[t] == 2; lv->pend_mark[t] = 0; cr_live_reach_as(lv, t, bound); } } static void cr_live_instance(CrLive *lv, int k); /* The type fixpoint reads a method whether or not it is reached: a slot an unreached method writes (`@enums ||= Enums.new` in a method nothing calls) carries its class to the code that reads the slot, which then calls the class's methods. So a method of an open owner -- top-level, a module, a class not gated, or a gated one once the side it sits on opens -- opens the instance side of each class it builds or reads as a value into such a slot, reached or not. Class k's methods on one side (cmethod 1, instance 0), or the ungated owners' (k -1). */ static void cr_live_weak(CrLive *lv, int k, int cmethod) { Compiler *c = lv->c; for (int s = k >= 0 ? lv->cls_first[k] : 0; s >= 0 && s < c->nscopes; s = k >= 0 ? lv->cls_next[s] : s + 1) { const Scope *sc = &c->scopes[s]; if (lv->weak_done[s]) continue; if (k >= 0 ? (sc->is_cmethod != cmethod) : (sc->class_id >= 0 && lv->pend_c[sc->class_id] != -2)) continue; lv->weak_done[s] = 1; for (int i = 0; i < lv->nreads[s] && !lv->all; i++) { const CrRead *r = &lv->reads[s][i]; if ((r->kind != CR_VALUE && r->kind != CR_CONST_CALL) || !r->persist) continue; int j = anh_find(&lv->seg, r->name); for (int d = j >= 0 ? lv->seg_first[j] : -1; d >= 0; d = lv->seg_next[d]) if (r->kind == CR_VALUE || !r->call[0] || comp_cmethod_in_chain(c, d, r->call, NULL) < 0) if (lv->held_i[d]) cr_live_instance(lv, d); } } } /* Class k is named: its class side opens, with its ancestors' */ static void cr_live_class(CrLive *lv, int k) { Compiler *c = lv->c; int k0 = k, builds = 0; for (int h = 0; k >= 0 && k < c->nclasses && h <= c->nclasses; k = c->classes[k].parent, h++) { builds |= lv->builds[k]; if (!lv->held_c[k]) continue; lv->held_c[k] = 0; cr_live_retry(lv, &lv->pend_c[k]); cr_live_weak(lv, k, 1); } /* an ancestor's singleton method a subclass receives builds the subclass */ if (builds && lv->held_i[k0]) cr_live_instance(lv, k0); } /* A call of nm on class k: k is named, and k's singleton method nm, or the one it inherits, can run */ static void cr_live_call(CrLive *lv, int k, const char *nm) { Compiler *c = lv->c; cr_live_class(lv, k); for (int h = 0; k >= 0 && k < c->nclasses && h <= c->nclasses; k = c->classes[k].parent, h++) { char key[512]; snprintf(key, sizeof key, "%d:%s", k, nm); if (anh_has(lv->adm, key)) continue; anh_add(lv->adm, strdup(key)); /* the table keeps the pointer */ cr_live_retry(lv, &lv->pend_c[k]); } } /* An instance of class k can exist: its instance side opens, with its ancestors' and its class side */ static void cr_live_instance(CrLive *lv, int k) { Compiler *c = lv->c; int k0 = k, top = -1; for (int h = 0; k >= 0 && k < c->nclasses && h <= c->nclasses; k = c->classes[k].parent, h++) if (lv->held_i[k]) { lv->held_i[k] = 0; lv->open_next[k] = top; top = k; } cr_live_class(lv, k0); for (k = top; k >= 0; k = lv->open_next[k]) { cr_live_retry(lv, &lv->pend_i[k]); cr_live_weak(lv, k, 0); } } static void cr_live_all(CrLive *lv) { if (lv->all) return; lv->all = 1; for (int k = 0; k < lv->c->nclasses; k++) { cr_live_instance(lv, k); lv->pend_c[k] = -2; /* every singleton method is reached as before */ } for (int t = 0; t < lv->c->nscopes; t++) if (lv->pend_mark[t]) { lv->pend_mark[t] = 0; cr_live_reach_as(lv, t, 1); } } static void cr_live_event(CrLive *lv, const CrRead *r) { Compiler *c = lv->c; if (lv->all) return; if (r->kind == CR_SELF_CALL) { /* self is the class or a subclass of it the program names */ for (int d = 0; d < c->nclasses; d++) if (d == r->cls || (!lv->held_c[d] && cr_live_descends(c, d, r->cls))) cr_live_call(lv, d, r->call); return; } if (r->kind == CR_DYN_CALL) { if (!r->call[0] || anh_has(lv->dyn, r->call)) return; anh_add(lv->dyn, strdup(r->call)); for (int k = 0; k < c->nclasses; k++) if (!lv->held_c[k] && lv->pend_c[k] >= 0) cr_live_retry(lv, &lv->pend_c[k]); return; } if (sp_streq(r->name, "Marshal") || sp_streq(r->name, "ObjectSpace")) { cr_live_all(lv); return; } int i = anh_find(&lv->seg, r->name); if (i < 0) return; for (int k = lv->seg_first[i]; k >= 0; k = lv->seg_next[k]) { /* a singleton method of its own answers the call */ if (r->kind == CR_CONST_CALL && r->call[0] && comp_cmethod_in_chain(c, k, r->call, NULL) >= 0) cr_live_call(lv, k, r->call); else cr_live_instance(lv, k); } } /* The settled types read every method, reached or not: a parameter a call in an unreached method binds (`Frag.matches?(Sel.new)` in a method nothing calls) carries its class into reached code, which then calls the class's methods directly. So reached code typed with an instance of a class opens its instance side. Not entering a nested def. */ static void cr_live_typed(CrLive *lv, int id) { Compiler *c = lv->c; const NodeTable *nt = c->nt; if (id < 0 || lv->all || nt_kind(nt, id) == NK_DefNode) return; TyKind t = comp_ntype(c, id); int k = ty_is_object(t) ? ty_object_class(t) : ty_is_obj_array(t) ? ty_obj_array_class(t) : -1; if (k >= 0 && k < c->nclasses && lv->held_i[k]) cr_live_instance(lv, k); int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) cr_live_typed(lv, nt_ref_at(nt, id, i)); int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int nn = 0; const int *ids = nt_arr_at(nt, id, i, &nn); for (int j = 0; j < nn; j++) cr_live_typed(lv, ids[j]); } } /* A reached scope: the classes it names, the calls it makes, whether it builds its class, and its calls that find classes by name */ static void cr_live_scope(CrLive *lv, int s, char **calls, int ncalls) { Compiler *c = lv->c; if (lv->all) return; for (int i = 0; i < lv->nreads[s]; i++) cr_live_event(lv, &lv->reads[s][i]); if (lv->typed && s > 0) { cr_live_typed(lv, c->scopes[s].body); if (c->scopes[s].def_node >= 0) cr_live_typed(lv, nt_ref(c->nt, c->scopes[s].def_node, "parameters")); } /* a send whose name is known only at run time can reach any of them */ for (int i = 0; i < ncalls && !lv->all; i++) if (calls[i] && calls[i][0] == '\x02') cr_live_event(lv, &(CrRead){ NULL, calls[i] + 1, -1, CR_DYN_CALL, 0 }); const Scope *sc = &c->scopes[s]; int k = sc->class_id; if (sc->is_cmethod && k >= 0 && k < c->nclasses && !lv->builds[k] && (cr_live_builds(c, sc->body, sc->name) || (sc->def_node >= 0 && cr_live_builds(c, nt_ref(c->nt, sc->def_node, "parameters"), sc->name)))) { lv->builds[k] = 1; /* it runs for k, or for a subclass the program names */ for (int d = 0; d < c->nclasses; d++) if ((d == k || !lv->held_c[d]) && cr_live_descends(c, d, k)) cr_live_instance(lv, d); } static const char *const finders[] = { "const_get", "eval", "class_eval", "module_eval", "instance_eval", "constants", "instance_exec", "class_exec", "module_exec", "subclasses", "descendants", "each_object", "const_source_location", NULL }; for (int i = 0; i < ncalls && !lv->all; i++) { const char *nm = calls[i]; if (!nm || nm[0] == '\x01') continue; if (nm[0] == '\x02' || nm[0] == '\x03') nm++; else if (nm[0] == '\x04') { nm = strchr(nm, ' ') + 1; nm += nm[0] == '\x03'; } for (int f = 0; finders[f]; f++) if (sp_streq(nm, finders[f])) { cr_live_all(lv); break; } } } /* What an earlier pass opened stays open (ClassInfo.live_sides, and these two): the passes after the first see the settled types and can name fewer calls, and a class whose initialize the type fixpoint saw must not lose it to a later pass. */ static Compiler *g_cr_live_owner; static ANameHash g_cr_live_dyn, g_cr_live_adm; static void cr_live_begin(CrLive *lv, Compiler *c, int *queue, int *qtail) { memset(lv, 0, sizeof *lv); lv->c = c; lv->queue = queue; lv->qtail = qtail; if (g_cr_live_owner != c) { for (int i = 0; i < g_cr_live_dyn.n; i++) free((char *)g_cr_live_dyn.key[i]); for (int i = 0; i < g_cr_live_adm.n; i++) free((char *)g_cr_live_adm.key[i]); anh_free(&g_cr_live_dyn); anh_free(&g_cr_live_adm); memset(&g_cr_live_dyn, 0, sizeof g_cr_live_dyn); memset(&g_cr_live_adm, 0, sizeof g_cr_live_adm); g_cr_live_owner = c; } lv->dyn = &g_cr_live_dyn; lv->adm = &g_cr_live_adm; int nc = c->nclasses ? c->nclasses : 1, ns = c->nscopes ? c->nscopes : 1; lv->held_c = calloc((size_t)nc, 1); lv->held_i = calloc((size_t)nc, 1); lv->builds = calloc((size_t)nc, 1); lv->pend_c = malloc(sizeof(int) * (size_t)nc); lv->pend_i = malloc(sizeof(int) * (size_t)nc); lv->seg_next = malloc(sizeof(int) * (size_t)nc); lv->pend_next = malloc(sizeof(int) * (size_t)ns); lv->pend_mark = calloc((size_t)ns, 1); lv->reads = calloc((size_t)ns, sizeof(CrRead *)); lv->nreads = calloc((size_t)ns, sizeof(int)); lv->creads = calloc((size_t)ns, sizeof(int)); lv->open_next = malloc(sizeof(int) * (size_t)nc); lv->cls_first = malloc(sizeof(int) * (size_t)nc); lv->cls_next = malloc(sizeof(int) * (size_t)ns); lv->weak_done = calloc((size_t)ns, 1); for (int k = 0; k < c->nclasses; k++) lv->pend_c[k] = lv->pend_i[k] = lv->seg_next[k] = lv->cls_first[k] = -1; for (int s = c->nscopes - 1; s >= 0; s--) { int k = c->scopes[s].class_id; lv->cls_next[s] = -1; if (k >= 0 && k < c->nclasses) { lv->cls_next[s] = lv->cls_first[k]; lv->cls_first[k] = s; } } int all = 0; /* a C extension makes classes by name (rb_path2class, rb_const_get) */ if (c->n_cext_extensions) all = 1; for (int s = 0; s < c->nscopes && !all; s++) { const char *nm = c->scopes[s].name; if (!nm || c->scopes[s].def_node < 0 || nt_int(c->nt, c->scopes[s].def_node, "node_bi", 0)) continue; if (sp_streq(nm, "included") || sp_streq(nm, "extended") || sp_streq(nm, "prepended") || sp_streq(nm, "const_missing")) all = 1; } for (int k = 0; k < c->nclasses; k++) { /* an ungated class reaches each method by name, as before */ if (all || !cr_live_gated(c, k)) { lv->pend_c[k] = -2; continue; } lv->held_c[k] = !(c->classes[k].live_sides & 1); lv->held_i[k] = !(c->classes[k].live_sides & 2); const char *sg = cr_live_segment(c->classes[k].name); int i = anh_find(&lv->seg, sg); if (i < 0) { anh_add(&lv->seg, sg); i = lv->seg.n - 1; lv->seg_first = realloc(lv->seg_first, sizeof(int) * (size_t)lv->seg.cap); lv->seg_first[i] = -1; } lv->seg_next[k] = lv->seg_first[i]; lv->seg_first[i] = k; } if (all) { lv->all = 1; return; } /* an inherited hook is handed each subclass as it is defined, a body that hands self out does so as the program loads, and an --ext-entry is called from C */ for (int s = 0; s < c->nscopes; s++) { const Scope *sc = &c->scopes[s]; if (sc->name && sc->is_cmethod && sp_streq(sc->name, "inherited") && sc->class_id >= 0) for (int k = 0; k < c->nclasses; k++) if (k != sc->class_id && cr_live_descends(c, k, sc->class_id) && lv->pend_c[k] != -2) { lv->held_c[k] = lv->held_i[k] = 0; lv->pend_c[k] = -2; } if (sc->is_ext_entry && sc->class_id >= 0 && lv->pend_c[sc->class_id] != -2) { lv->held_c[sc->class_id] = lv->held_i[sc->class_id] = 0; lv->pend_c[sc->class_id] = -2; } } for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_ClassNode) continue; int cp = nt_ref(c->nt, id, "constant_path"); const char *cn = cp >= 0 ? nt_str(c->nt, cp, "name") : NULL; int i = cn ? anh_find(&lv->seg, cn) : -1; if (i < 0 || !cr_live_body_escapes(c, nt_ref(c->nt, id, "body"))) continue; for (int k = lv->seg_first[i]; k >= 0; k = lv->seg_next[k]) { lv->held_c[k] = lv->held_i[k] = 0; lv->pend_c[k] = -2; } } /* a class open from the start opens its ancestors */ for (int k = 0; k < c->nclasses; k++) { if (lv->pend_c[k] != -2) continue; for (int p = c->classes[k].parent, h = 0; p >= 0 && h <= c->nclasses; p = c->classes[p].parent, h++) { lv->held_c[p] = lv->held_i[p] = 0; lv->pend_c[p] = -2; } } } /* Once every scope's reads are collected: the open owners' unreached methods (cr_live_weak), and the classes an earlier pass or the start left open */ static void cr_live_start(CrLive *lv) { Compiler *c = lv->c; if (lv->all) return; cr_live_weak(lv, -1, 0); for (int k = 0; k < c->nclasses && !lv->all; k++) { if (lv->pend_c[k] == -2) continue; if (!lv->held_c[k]) cr_live_weak(lv, k, 1); if (!lv->held_i[k]) cr_live_weak(lv, k, 0); } } /* Record what stayed held, and free the state */ static void cr_live_end(CrLive *lv) { Compiler *c = lv->c; for (int k = 0; k < c->nclasses; k++) { int u = !lv->all && lv->held_c[k] && lv->held_i[k]; int ni = !lv->all && lv->held_i[k]; int sides = c->classes[k].live_sides | (lv->all || !lv->held_c[k] ? 1 : 0) | (lv->all || !lv->held_i[k] ? 2 : 0); if (c->classes[k].unreferenced != u || c->classes[k].no_instance != ni || c->classes[k].live_sides != sides) { c->classes[k].unreferenced = u; c->classes[k].no_instance = ni; c->classes[k].live_sides = sides; comp_touch_class(c->classes[k].idx); } } for (int s = 0; s < c->nscopes; s++) free(lv->reads[s]); free(lv->reads); free(lv->nreads); free(lv->creads); free(lv->held_c); free(lv->held_i); free(lv->builds); free(lv->pend_c); free(lv->pend_i); free(lv->pend_next); free(lv->pend_mark); free(lv->open_next); free(lv->cls_first); free(lv->cls_next); free(lv->weak_done); free(lv->seg_first); free(lv->seg_next); anh_free(&lv->seg); } /* The scopes of a name, from `first` along `link`, that can take `argc` positional arguments: a call by name with a count the call shows ("\x04 ", compute_reachable) */ static void cr_reach_counted(CrLive *lv, int first, const int *link, int argc, int on_instance) { for (int t = first; t >= 0; t = link[t]) if (cr_name_reaches(lv->c, t, on_instance) && !lv->c->scopes[t].reachable && cplan_def_takes_argc(lv->c, t, argc)) cr_live_reach(lv, t); } void compute_reachable(Compiler *c, int typed) { /* Build per-scope call sets (CallNode names, not entering nested DefNodes). */ char ***scope_calls = calloc((size_t)c->nscopes, sizeof(char **)); int *sc_n = calloc((size_t)c->nscopes, sizeof(int)); int *sc_cap = calloc((size_t)c->nscopes, sizeof(int)); /* BFS queue (scope indices). */ int *queue = malloc((size_t)(c->nscopes ? c->nscopes : 1) * sizeof(int)); int qhead = 0, qtail = 0; CrLive lv; cr_live_begin(&lv, c, queue, &qtail); lv.typed = typed == 2; ic_begin(c, typed); if (typed == 2) { g_cr_typed = &c->ic->class_only; cplan_targets_drop(); } else { g_cr_untyped = &c->ic->class_only; cr_literal_locals_build(c); } for (int s = 0; s < c->nscopes; s++) { const Scope *sc = &c->scopes[s]; int instance_self = sc->name && !sc->is_cmethod && sc->class_id >= 0 && !comp_class_is_module(c, &c->classes[sc->class_id]); ic_scope(c, s); if (c->scopes[s].body >= 0) cr_collect_calls(c, c->nt, c->scopes[s].body, &scope_calls[s], &sc_n[s], &sc_cap[s], instance_self); if (c->scopes[s].body >= 0) cr_live_collect(&lv, s, c->scopes[s].body, -1, 0); /* Also scan parameter defaults (e.g. def foo(opt = bar)) -- these emit calls within the method scope but live in the DefNode parameters subtree. */ if (c->scopes[s].def_node >= 0) { int pn = nt_ref(c->nt, c->scopes[s].def_node, "parameters"); if (pn >= 0) cr_collect_calls(c, c->nt, pn, &scope_calls[s], &sc_n[s], &sc_cap[s], instance_self); if (pn >= 0) cr_live_collect(&lv, s, pn, -1, 0); } } ic_scope(c, 0); cr_collect_program_end(c, &scope_calls[0], &sc_n[0], &sc_cap[0]); ic_scope(c, -1); cr_live_start(&lv); g_cr_typed = NULL; g_cr_untyped = NULL; /* --ext-entry designations join the graph as roots: an entry has no compiled call site, so without this it would be unreachable and DCE'd out of the very library it is the point of (ext-design.md, Layer 1). */ if (g_ext_entries && *g_ext_entries) { char list[2048]; snprintf(list, sizeof list, "%s", g_ext_entries); for (char *tok = strtok(list, ","); tok; tok = strtok(NULL, ",")) { char *dot = strrchr(tok, '.'); if (!dot) { fprintf(stderr, "spinel: --ext-entry `%s`: spell it Module.method " "(module singleton methods export in v1)\n", tok); exit(1); } *dot = '\0'; int ci2 = comp_class_index(c, tok); int mi2 = ci2 >= 0 ? comp_cmethod_in_chain(c, ci2, dot + 1, NULL) : -1; if (mi2 < 0) { fprintf(stderr, "spinel: --ext-entry `%s.%s` does not name a " "`def self.%s` in module %s\n", tok, dot + 1, dot + 1, tok); exit(1); } c->scopes[mi2].is_ext_entry = 1; } } for (int s = 0; s < c->nscopes; s++) { c->scopes[s].reachable = 0; comp_touch_scope(c->scopes[s].idx); } for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; /* The roots: the top level, a def with no name, initialize, and an --ext-entry. A method CRuby calls back is no root of its own: a node that makes the call keeps it (ic_node_calls, cr_collect_calls), the end of the program the message of an exception class. (A replaced version, or a version entry, is reached only by the calls bound to it.) */ int bound = sc->replaced || sc->entry != ENTRY_NONE; int is_root = (s == 0 || !sc->name || (!bound && is_initialize_name(sc->name)) || sc->is_ext_entry); if (is_root) cr_live_reach(&lv, s); } /* "called_names" tracks every method name reached from any reachable scope. Used by alias and prep_to propagation (aliases have no scope of their own). */ /* Both name lookups below were linear scans, one per mark: (calls x names) for the called set, (calls x scopes) for a name's scopes. */ char **called_names = NULL; int cn_n = 0, cn_cap = 0; ANameHash cn_set; memset(&cn_set, 0, sizeof cn_set); /* name -> its scopes, ascending: scope names are fixed for this pass */ ANameHash sn_set; memset(&sn_set, 0, sizeof sn_set); int *sn_first = NULL, *sn_link = malloc(sizeof(int) * (size_t)(c->nscopes + 1)); for (int t = 0; t < c->nscopes; t++) { sn_link[t] = -1; const char *tn = c->scopes[t].name; /* entries are reached by their bound calls or table routes; a named entry is the method by name */ if (!tn || (c->scopes[t].entry != ENTRY_NONE && c->scopes[t].entry != ENTRY_NAMED)) continue; int k = -1; if (sn_set.nb) for (int i = sn_set.head[sp_strhash(tn) % (unsigned)sn_set.nb]; i >= 0; i = sn_set.next[i]) if (sp_streq(sn_set.key[i], tn)) { k = i; break; } if (k < 0) { anh_add(&sn_set, tn); k = sn_set.n - 1; sn_first = realloc(sn_first, sizeof(int) * (size_t)sn_set.cap); sn_first[k] = t; } else { int last = sn_first[k]; while (sn_link[last] >= 0) last = sn_link[last]; sn_link[last] = t; } } #define SN_FIRST(NM) ({ const char *_q = (NM); int _r = -1; \ if (sn_set.nb) for (int _i = sn_set.head[sp_strhash(_q) % (unsigned)sn_set.nb]; _i >= 0; _i = sn_set.next[_i]) \ if (sp_streq(sn_set.key[_i], _q)) { _r = sn_first[_i]; break; } _r; }) /* A name already called is fully marked: reachability never clears, so a second MARK_NAME of it found nothing to do. */ #define CN_ADD(NM) do { const char *_n=(NM); if(_n && !anh_has(&cn_set,_n)){ \ if(cn_n>=cn_cap){cn_cap=cn_cap?cn_cap*2:32;called_names=realloc(called_names,sizeof(char*)*cn_cap);} \ called_names[cn_n++]=strdup(_n); anh_add(&cn_set,called_names[cn_n-1]);} } while(0) /* Helper: mark a name reachable -- all scopes with that name join the BFS. "\x01" marks one resolved scope. "\x02" is a runtime-name send's arm: the method of that name in every class's chain, and a top-level def of it, a private method of Object every receiver has. "\x03" is a call on a known instance (`K.new.m`, or a receiverless or `self.` call in a class's instance method): every scope of the name but the class methods, which no instance can reach. "\x04 " is a plain call, or "\x04 \x03" a call on a known instance, whose positional argument count is known: the scopes of the name that can take that many (cr_reach_counted); the targets the call plan lists, and those of a super, are marked "\x01". None of these reaches a replaced method version, which only the calls bound to it reach ("\x01", cr_collect_versions). */ #define MARK_NAME(NM) do { const char *_mn=(NM); if(_mn && _mn[0]=='\x01'){ int _t=atoi(_mn+1); \ if(_t>=0 && _tnscopes && !c->scopes[_t].reachable){ cr_live_reach_as(&lv, _t, 1); \ if(cr_scope_has_super(c, _t)) for(int _u=SN_FIRST(c->scopes[_t].name);_u>=0;_u=sn_link[_u]) \ if(!c->scopes[_u].reachable && c->scopes[_u].is_cmethod && cr_class_is_ancestor(c, c->scopes[_u].class_id, c->scopes[_t].class_id)) \ cr_live_reach_as(&lv, _u, 1); } } \ else if(_mn && _mn[0]=='\x02'){ if(!anh_has(&cn_set,_mn)){ CN_ADD(_mn); \ for(int _k=0;_knclasses;_k++){ if(comp_class_is_module(c,&c->classes[_k])) continue; \ for(int _p=_k,_h=0;_p>=0&&_pnclasses&&_h<64;_p=c->classes[_p].parent,_h++){ \ int _t=comp_method_in_chain(c,_p,_mn+1,NULL); \ if(_t>=0 && !c->scopes[_t].reachable)cr_live_reach(&lv, _t); } } \ for(int _t=SN_FIRST(_mn+1);_t>=0;_t=sn_link[_t]) \ if(c->scopes[_t].class_id<0 && !c->scopes[_t].is_cmethod && !c->scopes[_t].replaced && !c->scopes[_t].reachable) \ cr_live_reach(&lv, _t); } } \ else if(_mn && _mn[0]=='\x03'){ if(!anh_has(&cn_set,_mn) && !anh_has(&cn_set,_mn+1)){ CN_ADD(_mn); \ for(int _t=SN_FIRST(_mn+1);_t>=0;_t=sn_link[_t]) \ if((!c->scopes[_t].is_cmethod || c->scopes[_t].is_module_function) && !c->scopes[_t].replaced && !c->scopes[_t].reachable) \ cr_live_reach(&lv, _t); } } \ else if(_mn && _mn[0]=='\x04'){ const char *_nm=strchr(_mn,' ')+1; int _in=_nm[0]=='\x03'; \ if(!anh_has(&cn_set,_mn) && !anh_has(&cn_set,_nm) && !anh_has(&cn_set,_nm+_in)){ \ char _sk[300]; snprintf(_sk,sizeof _sk,"\x04%s",_nm); CN_ADD(_mn); CN_ADD(_sk); \ cr_reach_counted(&lv, SN_FIRST(_nm+_in), sn_link, atoi(_mn+1), _in); } } \ else if(_mn && !anh_has(&cn_set,_mn)){ CN_ADD(_mn); \ for(int _t=SN_FIRST(_mn);_t>=0;_t=sn_link[_t]) \ if(!c->scopes[_t].replaced && !c->scopes[_t].reachable) \ cr_live_reach(&lv, _t); } } while(0) while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } /* The synthesized compiler_state dump method calls ir_emit_int/str/sa/ia, but it has no AST so the BFS above can't see those calls. Mark them reachable when any class declares compiler_state fields. */ { int any_cs = 0; for (int ci = 0; ci < c->nclasses; ci++) if (c->classes[ci].ncs > 0) { any_cs = 1; break; } if (any_cs) { MARK_NAME("ir_emit_int"); MARK_NAME("ir_emit_str"); MARK_NAME("ir_emit_sa"); MARK_NAME("ir_emit_ia"); while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } } /* `obj.attr ||= v` calls the reader and, conditionally, the writer -- and it is a CallOrWriteNode, not a CallNode, so the walk above never saw either name. It did not matter while the emitter reached past both to the ivar; it does now that a hand-written pair is called (#4148). */ { int any_cw = 0; for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_CallOrWriteNode && ty_k != NK_CallAndWriteNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm) continue; char wnm[300]; snprintf(wnm, sizeof wnm, "%s=", nm); MARK_NAME(nm); MARK_NAME(wnm); any_cw = 1; } if (any_cw) while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } /* A `case obj when other` arm calls the pattern's #=== (or #==, which Ruby's default #=== is), and that call has no CallNode of its own -- the arm is a WhenNode. Mark them reachable when any case has a when arm (#3820). */ { int has_when = 0; for (int id = 0; id < c->nt->count && !has_when; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k == NK_WhenNode) has_when = 1; } if (has_when) { MARK_NAME("==="); MARK_NAME("=="); while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } } /* `reduce(:sym)` / `inject(seed, :sym)` names the method by Symbol, which is not a CallNode, so the BFS above never saw the name and the method was pruned -- the fold then fell through to the unresolved-call raise, which reads as "undefined method '^' for an instance of F" for a method the program plainly defines (#4069). Only a method with no OTHER call site was affected, which is what made it look like an operator problem rather than a reachability one. */ { for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm) continue; if (!sp_streq(nm, "reduce") && !sp_streq(nm, "inject")) continue; int args = nt_ref(c->nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(c->nt, args, "arguments", &an) : NULL; for (int k = 0; k < an; k++) if (nt_kind(c->nt, av[k]) == NK_SymbolNode && nt_str(c->nt, av[k], "value")) MARK_NAME(nt_str(c->nt, av[k], "value")); /* `reduce(seed, &:sym)` spells the same operator in the block, where the argument walk above cannot see it. */ { int blk = nt_call_block_arg(c->nt, id); if (blk >= 0) { int ex = nt_ref(c->nt, blk, "expression"); if (ex >= 0 && nt_kind(c->nt, ex) == NK_SymbolNode && nt_str(c->nt, ex, "value")) MARK_NAME(nt_str(c->nt, ex, "value")); } } } /* `sum(seed)` names no operator at all: the fold applies the SEED's `+` once per element. A user class used as the seed therefore needs its `+` kept, exactly as `reduce(seed, :+)` does -- without it the dispatch arm was emitted empty and the fold raised NoMethodError for a method the program defines. MARK_NAME is by name and global, so ask first whether the seed could BE a user object: a literal, or an expression that can only build a builtin, never reaches a user `+`, and marking for one resurrects every `+` in the program -- including bodies the emitter cannot compile, which turned a program that built into one that does not. A local, an ivar, a constant or a call such as `Money.new(0)` marks, which is the same reach `reduce(seed, :+)` already has. */ for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_CallNode) continue; { const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, "sum")) continue; } /* `"abc".sum(16)` is String's checksum width, not a fold seed */ { int rcv = nt_ref(c->nt, id, "receiver"); if (rcv >= 0 && nt_kind(c->nt, rcv) == NK_StringNode) continue; } { int args = nt_ref(c->nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(c->nt, args, "arguments", &an) : NULL; /* A numeric seed can reach a user + through an element's coerce, including a block's value. Reuse the pass's indexed method names. */ if (SN_FIRST("coerce") >= 0 && an_sum_needs_coerce(c, id, typed)) { MARK_NAME("coerce"); MARK_NAME("+"); break; } if (an < 1 || !av) continue; if (an_seed_is_builtin(c->nt, av[0])) continue; MARK_NAME("+"); break; } } while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } /* case/in array and hash patterns may call a user object's #deconstruct / #deconstruct_keys, which have no explicit call site in the AST. If any such pattern exists, mark those methods reachable (dead ones are stripped). */ { int has_arr_pat = 0, has_hash_pat = 0; for (int id = 0; id < c->nt->count; id++) { const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) continue; if (ty_k == NK_ArrayPatternNode || ty_k == NK_FindPatternNode) has_arr_pat = 1; else if (ty_k == NK_HashPatternNode) has_hash_pat = 1; } if (has_arr_pat) MARK_NAME("deconstruct"); if (has_hash_pat) MARK_NAME("deconstruct_keys"); if (has_arr_pat || has_hash_pat) while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } /* Kernel#Integer converts its argument through its #to_int, then #to_str, then #to_i, and Kernel#Float through its #to_f -- calls the conversion site names nowhere in the AST. The receiver forms (obj.send(:Integer, x) desugared, Kernel.Float(x)) convert the same way, so any call by the name counts, and the program is marked as one that converts at all. Float formatting directives also call a user object's #to_f (a def or a define_method of it in the program) implicitly; format, sprintf, printf and a String's % keep that conversion and its bridge. */ { int has_kint = 0, has_kflt = 0, has_hmerge = 0; for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm) continue; if (sp_streq(nm, "Integer")) has_kint = 1; else if (sp_streq(nm, "Float") || ((is_format_alias(nm) || sp_streq(nm, "printf") || (sp_streq(nm, "%") && format_recv_maybe_string(c, id, typed))) && SN_FIRST("to_f") >= 0)) has_kflt = 1; else if (sp_streq(nm, "merge") || sp_streq(nm, "merge!") || sp_streq(nm, "update") || sp_streq(nm, "Hash")) has_hmerge = 1; } /* a Numeric of the program's own converts through its #to_f wherever a Float argument is taken (Math.sqrt(big_decimal)): the same bridge */ int has_unum = 0; NT_FOREACH_KIND(c->nt, NK_ClassNode, cid) { int sup = nt_ref(c->nt, cid, "superclass"); const char *sn = sup >= 0 ? nt_str(c->nt, sup, "name") : NULL; if (sn && sp_streq(sn, "Numeric")) has_unum = 1; } c->uses_kconv = has_kint || has_kflt || has_unum; /* a `**` operand converts through its #to_hash, which a boxed one reaches through the same bridge (sp_kw_splat_conv), named nowhere in the AST either */ int has_kwsplat = 0, has_to_hash = 0; NT_FOREACH_KIND(c->nt, NK_AssocSplatNode, sid) { if (nt_ref(c->nt, sid, "value") >= 0) has_kwsplat = 1; } NT_FOREACH_KIND(c->nt, NK_DefNode, did) { const char *dn = nt_str(c->nt, did, "name"); if (dn && sp_streq(dn, "to_hash")) has_to_hash = 1; } NT_FOREACH_KIND(c->nt, NK_AliasMethodNode, aid) { int nn = nt_ref(c->nt, aid, "new_name"); if (nn >= 0 && nt_str(c->nt, nn, "value") && sp_streq(nt_str(c->nt, nn, "value"), "to_hash")) has_to_hash = 1; } c->uses_kw_to_hash = (has_kwsplat || has_hmerge) && has_to_hash; if (has_kint) { MARK_NAME("to_int"); MARK_NAME("to_str"); MARK_NAME("to_i"); } if (has_kflt || has_unum) MARK_NAME("to_f"); if (c->uses_kw_to_hash) MARK_NAME("to_hash"); if (has_kint || has_kflt || has_unum || c->uses_kw_to_hash) while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } /* JSON.generate serializes a nested user object through its own #to_json, which likewise has no call site of its own in the AST. */ if (sp_feature_required("json")) { MARK_NAME("to_json"); while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } /* Alias/prep_to propagation: when alias_new (or alias_old) is in called_names, make the counterpart reachable too (aliases have no scope of their own). */ int changed = 1; while (changed) { changed = 0; /* a name the compiler synthesized for a protocol method (`__enum_to_a` for each, to_a and deconstruct, `__noarg to_s` for to_s) is as live as one of the protocol names it answers for is */ for (int t = 0; t < c->nscopes; t++) { const char *proto = NULL; const Scope *st = &c->scopes[t]; int live = 0; if (st->reachable || st->entry || !st->name || st->replaced) continue; for (int i = 0; !live && synth_stands_for(st->name, i, &proto); i++) live = cn_live(&cn_set, proto); if (!live) continue; /* a change only when it reached something: a method held for its class (cr_live_reach) stays unreached */ int prev_qtail = qtail; MARK_NAME(st->name); if (qtail > prev_qtail) changed = 1; while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cls = &c->classes[ci]; for (int i = 0; i < cls->naliases; i++) { const char *an = cls->alias_new[i], *ao = cls->alias_old[i]; int an_live = (an && cn_live(&cn_set, an)), ao_live = (ao && cn_live(&cn_set, ao)); /* also check reachable scope names (covers scope-backed aliases) */ if (an) for (int t = SN_FIRST(an); t >= 0 && !an_live; t = sn_link[t]) if (c->scopes[t].reachable) an_live = 1; if (ao) for (int t = SN_FIRST(ao); t >= 0 && !ao_live; t = sn_link[t]) if (c->scopes[t].reachable) ao_live = 1; /* Marked whenever the counterpart is live, not only when no scope of the other name is yet: a module both included and extended carries a copy of the target to each side, and a class-side copy made live by its own call must not stop the alias from reaching the instance side (cgi's escape_html on CGI::Escape). The marking is by name and idempotent, so a live counterpart costs nothing. */ /* A name newly called counts as a change even with no scope of its own: the old name of an alias of an alias is itself an alias, and the entry that carries it on may come earlier in the table. */ if (an_live) { int prev_qtail = qtail, prev_cn = cn_n; MARK_NAME(ao); if (qtail > prev_qtail || cn_n > prev_cn) changed = 1; /* drain newly enqueued scopes */ while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } if (ao_live) { int prev_qtail = qtail, prev_cn = cn_n; MARK_NAME(an); if (qtail > prev_qtail || cn_n > prev_cn) changed = 1; while (qhead < qtail) { int s = queue[qhead++]; for (int ni = 0; ni < sc_n[s]; ni++) MARK_NAME(scope_calls[s][ni]); cr_live_scope(&lv, s, scope_calls[s], sc_n[s]); } } } for (int i = 0; i < cls->nprep_chain; i++) { const char *pf = cls->prep_from[i]; /* user-facing name, e.g. "hi" */ const char *pt = cls->prep_to[i]; /* shadow name, e.g. "__prep 0 hi" */ if (!pf || !pt) continue; int cside = strncmp(pf, "self.", 5) == 0; /* a class method's chain */ if (cside) pf += 5; /* When the user-facing name is called, the codegen wrapper calls the shadow implementation directly -- so mark the shadow reachable too. A class method's chain is live through a call that can reach a class method: not an instance-only "\x03" mark (`K.new.m`), nor an instance method of the name being reachable. */ int pf_in_called = cside ? (anh_has(&cn_set, pf) || cn_counted(&cn_set, pf, 0)) : cn_live(&cn_set, pf); if (!pf_in_called) for (int t = SN_FIRST(pf); t >= 0; t = sn_link[t]) if (c->scopes[t].reachable && (!cside || c->scopes[t].is_cmethod)) { pf_in_called = 1; break; } if (pf_in_called) { int prev_qtail = qtail; MARK_NAME(pt); if (qtail > prev_qtail) { changed = 1; while (qhead < qtail) { int s=queue[qhead++]; for(int ni=0;ninscopes; s++) { for (int i = 0; i < sc_n[s]; i++) free(scope_calls[s][i]); free(scope_calls[s]); refuse_native_singleton_reopen(c, &c->scopes[s]); } free(scope_calls); free(sc_n); free(sc_cap); free(queue); } /* Mark each user class whose exact cls_id can appear at runtime. A poly value carries class C's cls_id only if a C instance was minted: `C.new`, `C.allocate`, `raise C` (exception construct), or a `Struct.new`-defined C. `dup`/`clone` copy an existing C value (propagate, never originate) and a subclass D mints cls_id D (not C), so neither marks C. `Marshal.load` / `Marshal.restore` can mint *any* user class, as can `.new` on a dynamic Class value -- either disables the gating (every class kept) so we never drop a live arm. Conservative by construction: this is a whole-program scan (it does not require the origination site to be reachable), so it only over-marks, never under-marks. The poly-dispatch switch reads `instantiated` to skip the `case` arm of a class no value can be (codegen_call.c). */ /* Is the Marshal module read as a value anywhere: a constant read that is not the receiver of Marshal's own dump or load? Any other call on it may answer the module (`Marshal.freeze`, `itself`, `then`, `tap`). Only then can a receiver the types cannot name hold it. */ static int marshal_read_as_value(Compiler *c) { const NodeTable *nt = c->nt; int *par = an_parent_map(nt), ans = 0; for (int id = 0; id < nt->count && !ans; id++) { if (nt_kind(nt, id) != NK_ConstantReadNode) continue; const char *nm = nt_str(nt, id, "name"); if (!is_marshal_const(nm)) continue; int p = par ? par[id] : -1; const char *meth = p >= 0 && nt_kind(nt, p) == NK_CallNode && nt_ref(nt, p, "receiver") == id ? nt_str(nt, p, "name") : NULL; if (!meth || !(is_marshal_dump(nm, meth) || is_object_loader(nm, meth, 1))) ans = 1; } free(par); return ans; } /* Twice: `early`, before the type fixpoint, so that the return type a poly receiver's call unifies over its candidates (analyze_infer.c) leaves out a class no reachable code constructs -- a dead FFI wrapper's `Vector2.new` put Vector2's float `x` into every `x` read in the program, and a Struct field read beside it went poly (#4460); and again after it, when a `.new` on a receiver the types can name as a Class value can be told from one that cannot. The early pass has no types, so it takes every `.new` on a non-constant receiver as the dynamic case and keeps every class: it can only over-mark relative to the late pass, and the late pass can only un-mark, so an arm codegen drops was never in the inferred union. */ void compute_instantiated(Compiler *c, int early) { const NodeTable *nt = c->nt; int disable = 0, marshal_val = -1; /* marshal_val: Marshal is read as a value (found on first need) */ /* The early pass writes ctor_reachable and leaves `instantiated` as the fixpoint has always seen it (unset); the late pass writes both. */ int *keep = NULL; if (early) { keep = (int *)malloc(sizeof(int) * (size_t)(c->nclasses ? c->nclasses : 1)); for (int k = 0; k < c->nclasses; k++) keep[k] = c->classes[k].instantiated; } for (int k = 0; k < c->nclasses; k++) c->classes[k].instantiated = 0; /* Struct classes: conservatively live (their instances flow as poly). A native (C-backed) class too: its instances can be minted on the C side and handed back boxed through a :any binding, with no `.new` in the program for this census to see, and a class it left uninstantiated had its arms dropped from every poly dispatch (#4504). */ /* (forced after the scan, which records what it saw in ctor_site first) */ for (int id = 0; id < nt->count && !disable; id++) { /* super in a reachable `self.new` constructs the receiving class: this one, or a descendant inheriting the method */ if (comp_super_is_class_new(c, id)) { Scope *encl = comp_scope_of(c, id); if (encl && encl->reachable) for (int k = 0; k < c->nclasses; k++) if (k == encl->class_id || is_descendant(c, k, encl->class_id)) c->classes[k].instantiated = 1; continue; } if (nt_kind(nt, id) != NK_CallNode) continue; const char *name = nt_str(nt, id, "name"); if (!name) continue; int recv = nt_ref(nt, id, "receiver"); /* Marshal.load / Marshal.restore -> any class can be minted. Only from a method something reaches: date's `Date._load` loads with Marshal, and a program that required date but never called Marshal kept every class, with each one's methods and every poly arm (#10592). */ if (recv >= 0 && (sp_streq(name, "load") || sp_streq(name, "restore"))) { NodeKind rk = nt_kind(nt, recv); const char *rn = nt_str(nt, recv, "name"); Scope *encl = comp_scope_of(c, id); if (rk == NK_ConstantReadNode && rn && sp_streq(rn, "Marshal") && (!encl || encl->reachable)) { disable = 1; break; } /* the same on a receiver that may hold the Marshal module (`m.load(s)` with Marshal handed in as an argument), when the program reads Marshal as a value anywhere */ if (rk != NK_ConstantReadNode && (!encl || encl->reachable) && (comp_ntype(c, recv) == TY_CLASS || comp_ntype(c, recv) == TY_POLY || comp_ntype(c, recv) == TY_UNKNOWN)) { if (marshal_val < 0) marshal_val = marshal_read_as_value(c); if (marshal_val) { disable = 1; break; } } } /* allocate on the method's own class, or `self.class.new(...)` in one of its instance methods: that class, and each subclass the method can run for. date's Infinity#abs builds `self.class.new`; reachable through any `abs`, it read as a Class known only at run time and kept every class of the program (#10592). */ { int oc = allocate_on_own_class(c, id); if (oc < 0 && sp_streq(name, "new")) oc = self_class_base_ci(c, recv); if (oc >= 0) { Scope *encl = comp_scope_of(c, id); if (!encl || encl->reachable) for (int k = 0; k < c->nclasses; k++) if (k == oc || is_descendant(c, k, oc)) c->classes[k].instantiated = 1; continue; } } /* C.new / C.allocate */ if (recv >= 0 && (sp_streq(name, "new") || sp_streq(name, "allocate"))) { NodeKind rk = nt_kind(nt, recv); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { const char *cn = nt_str(nt, recv, "name"); int ci = cn ? comp_class_index(c, cn) : -1; /* A construction inside a method nothing reaches mints nothing: compute_reachable has run, and its answer is conservative (every method whose name is mentioned anywhere is reachable), so a site it leaves dead is dead. A dead FFI wrapper's `Vector2.new` used to make Vector2 an arm of every `x` / `y` dispatch in the program, and the float it returns widened an unrelated hot loop's Struct field reads to poly (#4460). */ Scope *encl = comp_scope_of(c, id); if (ci >= 0 && (!encl || encl->reachable)) c->classes[ci].instantiated = 1; /* a constant holding a class value (`K = make`) is a Class value known only at run time, as a variable's below */ else if (ci < 0 && class_recv_is_dynamic(c, recv)) { disable = 1; break; } /* an unknown constant is a builtin (Array.new, ...) -- not a user arm */ } else if (early || comp_ntype(c, recv) == TY_CLASS || comp_ntype(c, recv) == TY_POLY || (comp_ntype(c, recv) == TY_UNKNOWN && (!comp_scope_of(c, id) || comp_scope_of(c, id)->reachable))) { /* `klass.new` on a dynamic Class value -- typed TY_CLASS, or a poly value that is a Class at run time (`REG["c"].new`, #2888): the class is unresolvable at compile time, so keep every class instantiated (its constructor and methods must exist for the runtime dispatch). A reachable receiver whose type is not known yet may be one too: a parameter seeded `untyped` that its call sites hand a Class (#9208). */ disable = 1; break; } } /* bare `new(...)` (no receiver) inside a class/singleton method (`def self.foo; ...; new(...); end`) is implicit `self.new(...)`, i.e. the enclosing class -- e.g. doom's `Texture.parse_texture` builds each Texture via a bare `new(name, ...)`. Missing this meant such a class's `instantiated` flag never got set unless some other call site also did `Texture.new` explicitly -- so a poly-array element member-read dispatch silently dropped its case arm, reading back nil/0 for every field of an otherwise fully-constructed object. Class methods only: inside an *instance* method bare `new` is not Class#new (Ruby raises NameError unless a method `new` is in scope), so it must not mark the class instantiated. */ if (recv < 0 && sp_streq(name, "new")) { Scope *encl = comp_scope_of(c, id); /* the singleton method is inherited: it builds the receiving class, this one or a descendant (cmethod_super_receiving_class) */ if (encl && encl->class_id >= 0 && encl->is_cmethod && encl->reachable) for (int k = 0; k < c->nclasses; k++) if (k == encl->class_id || is_descendant(c, k, encl->class_id)) c->classes[k].instantiated = 1; } /* raise Cls / raise Cls, msg : constructs an instance of Cls */ if (recv < 0 && sp_streq(name, "raise")) { int rargs = nt_ref(nt, id, "arguments"); int ran = 0; const int *rav = rargs >= 0 ? nt_arr(nt, rargs, "arguments", &ran) : NULL; if (ran >= 1 && rav) { NodeKind ak = nt_kind(nt, rav[0]); if (ak == NK_ConstantReadNode || ak == NK_ConstantPathNode) { const char *cn = nt_str(nt, rav[0], "name"); int ci = cn ? comp_class_index(c, cn) : -1; if (ci >= 0) c->classes[ci].instantiated = 1; } /* `raise k, msg` on a Class value (`k = WidgetError`): the runtime builds whichever exception class k holds, so each one of the program's may be an instance */ else if (rav[0] >= 0 && (early || comp_ntype(c, rav[0]) == TY_CLASS || comp_ntype(c, rav[0]) == TY_POLY || comp_ntype(c, rav[0]) == TY_UNKNOWN)) { Scope *encl = comp_scope_of(c, id); if (!encl || encl->reachable) for (int k = 0; k < c->nclasses; k++) if (class_is_exc_subclass(c, k)) c->classes[k].instantiated = 1; } } } } /* The object a `rescue ... => e` binds is built at the clause when the exception was raised with none (Thread#raise, Fiber#raise, Timeout.timeout(n, Cls) and the runtime's own raises take a class, not an object): a binding typed as a program exception class is handed an instance of the class the clause names, or a subclass of it, which no `new` of the program writes (emit_rescue). */ if (!early && !disable) for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_RescueNode) continue; int ref = nt_ref(nt, id, "reference"); if (ref < 0 || nt_kind(nt, ref) != NK_LocalVariableTargetNode) continue; Scope *encl = comp_scope_of(c, ref); if (!encl || !encl->reachable) continue; LocalVar *lv = scope_local(encl, nt_str(nt, ref, "name")); int xc = lv && ty_is_object(lv->type) ? ty_object_class(lv->type) : -1; if (xc < 0 || !class_is_exc_subclass(c, xc)) continue; for (int k = 0; k < c->nclasses; k++) if (k == xc || is_descendant(c, k, xc)) c->classes[k].instantiated = 1; } if (disable) for (int k = 0; k < c->nclasses; k++) { /* main's ivars live on the Toplevel pseudo-class, which is never built */ if (k == c->toplevel_class) continue; /* a module has no instances, whatever `klass.new` can reach */ { int dn = c->classes[k].def_node; if (dn >= 0 && dn < c->nt->count && nt_kind(c->nt, dn) == NK_ModuleNode) continue; } c->classes[k].instantiated = 1; } for (int k = 0; k < c->nclasses; k++) { /* no reached code can make one (compute_reachable) */ if (c->classes[k].no_instance) c->classes[k].instantiated = 0; c->classes[k].ctor_site = c->classes[k].instantiated; /* a TypedData class (or a Ruby subclass of one) has its objects made by C, with TypedData_Make_Struct, and no `new` of the program names it */ if (c->classes[k].is_struct || c->classes[k].is_native_class || c->classes[k].cext_extension) c->classes[k].instantiated = 1; /* a C extension makes any exception class of the program's by name (rb_exc_new2 through sp_cext_exception_new_fn), with no `.new` here */ if (c->n_cext_extensions && class_is_exc_subclass(c, k)) c->classes[k].instantiated = 1; } /* A singleton class's constructor stamps its objects with the parent's id until the singleton's first method runs (ctor_cls_id), and each `extend` moves the object up one link of the chain (sg_chain_link, whose parent is the previous link), so an object whose exact class is the original class, or any link below the last, exists wherever the last link is built. */ for (int k = 0; k < c->nclasses; k++) { if (!c->classes[k].instantiated || !c->classes[k].is_singleton_of) continue; for (int p = c->classes[k].parent; p >= 0 && p < c->nclasses && c->classes[p].is_singleton_of; p = c->classes[p].parent) c->classes[p].instantiated = 1; c->classes[c->classes[k].is_singleton_of - 1].instantiated = 1; } for (int k = 0; k < c->nclasses; k++) c->classes[k].inst_below = 0; for (int k = 0; k < c->nclasses; k++) { if (!c->classes[k].instantiated) continue; int hops = 0; for (int p = c->classes[k].parent; p >= 0 && p < c->nclasses && hops <= c->nclasses; p = c->classes[p].parent, hops++) c->classes[p].inst_below = 1; } for (int k = 0; k < c->nclasses; k++) { c->classes[k].ctor_reachable = c->classes[k].instantiated; comp_touch_class(c->classes[k].idx); } if (early) { for (int k = 0; k < c->nclasses; k++) c->classes[k].instantiated = keep[k]; free(keep); } comp_touch_global(); /* instantiated and ctor_reachable moved: inference reads them (respond_to? on a box, the plans) */ } /* ---- proc capture detection (closures) ---- A local read inside a proc body that isn't bound by the proc (param or a local the body itself writes) is a captured/free variable; its enclosing local must live in a heap cell so the closure and the enclosing scope share mutable storage. Mark those enclosing locals is_cell. */ /* ANameSet: moved to analyze_internal.h */ int aname_has(ANameSet *s, const char *nm) { if (!nm) return 1; for (int i = 0; i < s->n; i++) if (sp_streq(s->v[i], nm)) return 1; return 0; } void aname_add(ANameSet *s, const char *nm) { if (aname_has(s, nm)) return; if (s->n >= s->cap) { s->cap = s->cap ? s->cap * 2 : 8; s->v = realloc(s->v, sizeof(char *) * (size_t)s->cap); } s->v[s->n++] = nm; } int a_is_local_node(const char *ty) { return ty && (sp_streq(ty, "LocalVariableReadNode") || sp_streq(ty, "LocalVariableWriteNode") || sp_streq(ty, "LocalVariableTargetNode") || sp_streq(ty, "LocalVariableOperatorWriteNode") || sp_streq(ty, "LocalVariableOrWriteNode") || sp_streq(ty, "LocalVariableAndWriteNode")); } int a_is_write_node(const char *ty) { return ty && (sp_streq(ty, "LocalVariableWriteNode") || sp_streq(ty, "LocalVariableTargetNode") || sp_streq(ty, "LocalVariableOperatorWriteNode") || sp_streq(ty, "LocalVariableOrWriteNode") || sp_streq(ty, "LocalVariableAndWriteNode")); } /* Mark every node id in the subtree (crossing nested blocks: a node inside an inner block is still "inside a proc"). */ void a_mark_subtree(Compiler *c, int id, char *inproc) { if (id < 0) return; inproc[id] = 1; int nr = nt_num_refs(c->nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(c->nt, id, i); if (ch >= 0) a_mark_subtree(c, ch, inproc); } int na = nt_num_arrs(c->nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, id, i, &n); for (int k = 0; k < n; k++) if (ids[k] >= 0) a_mark_subtree(c, ids[k], inproc); } } /* Count this subtree's nodes into `cnt` (one pass per proc body gives every node the number of proc bodies covering it, i.e. its nesting depth). */ static void a_count_subtree(Compiler *c, int id, int *cnt, int depth) { if (id < 0 || depth > 400) return; cnt[id]++; int nr = nt_num_refs(c->nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(c->nt, id, i); if (ch >= 0) a_count_subtree(c, ch, cnt, depth + 1); } int na = nt_num_arrs(c->nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, id, i, &n); for (int k = 0; k < n; k++) if (ids[k] >= 0) a_count_subtree(c, ids[k], cnt, depth + 1); } } /* Stamp every node in the subtree with `owner` (a proc-create node id). */ static void a_stamp_subtree(Compiler *c, int id, int *owner_of, int owner, int depth) { if (id < 0 || depth > 400) return; owner_of[id] = owner; int nr = nt_num_refs(c->nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(c->nt, id, i); if (ch >= 0) a_stamp_subtree(c, ch, owner_of, owner, depth + 1); } int na = nt_num_arrs(c->nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, id, i, &n); for (int k = 0; k < n; k++) if (ids[k] >= 0) a_stamp_subtree(c, ids[k], owner_of, owner, depth + 1); } } /* Names used (read or written) anywhere in the proc/fiber body, INCLUDING nested blocks. A nested block (`Fiber.new { 3.times { |i| acc += i } }`) is inlined into the same flat C function as the body, so a use of an enclosing local there must still be seen as a capture. A nested block's own params / locals are collected too, but the caller's `owned` test (which requires a non-proc write in the enclosing scope) classifies them as block-local, not captures, so they are harmless. */ void a_collect_used(Compiler *c, int id, ANameSet *out) { if (id < 0) return; const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) return; if (a_is_local_node(ty)) aname_add(out, nt_str(c->nt, id, "name")); /* Yield and block_given? read the forwarded block without a local read, so a lifted body needs an explicit capture of that parameter. */ if (ty_k == NK_YieldNode || (nt_kind(c->nt, id) == NK_CallNode && nt_str(c->nt, id, "name") && sp_streq(nt_str(c->nt, id, "name"), "block_given?") && (nt_ref(c->nt, id, "receiver") < 0 || nt_kind(c->nt, nt_ref(c->nt, id, "receiver")) == NK_SelfNode))) { Scope *ys = comp_scope_of(c, id); if (ys && (ys->is_lowered_yield || ys->is_proc_form) && ys->blk_param && ys->blk_param[0]) aname_add(out, ys->blk_param); } int zs = ty_k == NK_ForwardingSuperNode; Scope *fs = zs || ty_k == NK_SuperNode ? comp_scope_of(c, id) : NULL; for (int i = 0; zs && fs && i < fs->nparams; i++) aname_add(out, fs->pnames[i]); if (fs && fs->blk_param && fs->blk_param[0]) aname_add(out, fs->blk_param); int nr = nt_num_refs(c->nt, id); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(c->nt, id, i); if (ch >= 0) a_collect_used(c, ch, out); } int na = nt_num_arrs(c->nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, id, i, &n); for (int k = 0; k < n; k++) if (ids[k] >= 0) a_collect_used(c, ids[k], out); } } /* The names a closure's body reads through a lazy chain held in a local it reads (lazy_alias_chain): a force there fuses the chain into the body, which then reads what the chain reads, its blocks' own parameters aside (those the fused walk binds in the closure's frame). So the scan that cells captured locals counts them as read in the body. */ static void a_collect_block_params(Compiler *c, int id, ANameSet *out) { const NodeTable *nt = c->nt; if (id < 0) return; if (nt_kind(nt, id) == NK_BlockNode) { int bp = nt_ref(nt, id, "parameters"), pn = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int rn = 0; const int *reqs = pn >= 0 ? nt_arr(nt, pn, "requireds", &rn) : NULL; for (int k = 0; k < rn; k++) aname_add(out, nt_str(nt, reqs[k], "name")); } for (int i = 0; i < nt_num_refs(nt, id); i++) a_collect_block_params(c, nt_ref_at(nt, id, i), out); for (int i = 0; i < nt_num_arrs(nt, id); i++) { int n = 0; const int *ids = nt_arr_at(nt, id, i, &n); for (int k = 0; k < n; k++) a_collect_block_params(c, ids[k], out); } } void a_collect_alias_used(Compiler *c, int id, ANameSet *out, int depth) { const NodeTable *nt = c->nt; if (id < 0 || depth > 8) return; if (nt_kind(nt, id) == NK_LocalVariableReadNode) { int chain = lazy_alias_chain(c, id); if (chain >= 0) { ANameSet all = {0}, own = {0}; a_collect_used(c, chain, &all); a_collect_block_params(c, chain, &own); for (int k = 0; k < all.n; k++) if (!aname_has(&own, all.v[k])) aname_add(out, all.v[k]); free(all.v); free(own.v); a_collect_alias_used(c, chain, out, depth + 1); } } for (int i = 0; i < nt_num_refs(nt, id); i++) a_collect_alias_used(c, nt_ref_at(nt, id, i), out, depth); for (int i = 0; i < nt_num_arrs(nt, id); i++) { int n = 0; const int *ids = nt_arr_at(nt, id, i, &n); for (int k = 0; k < n; k++) a_collect_alias_used(c, ids[k], out, depth); } } int a_proc_params_node(Compiler *c, int create) { NodeKind k = nt_kind(c->nt, create); if (k == NK_LambdaNode) return nt_ref(c->nt, create, "parameters"); int block = nt_ref(c->nt, create, "block"); if (block < 0) return -1; int bp = nt_ref(c->nt, block, "parameters"); return bp < 0 ? -1 : nt_ref(c->nt, bp, "parameters"); } int a_proc_body(Compiler *c, int create) { NodeKind k = nt_kind(c->nt, create); if (k == NK_LambdaNode) return nt_ref(c->nt, create, "body"); int block = nt_ref(c->nt, create, "block"); return block >= 0 ? nt_ref(c->nt, block, "body") : -1; } static int a_call_forwards_blk_param(const NodeTable *nt, int nid, const Scope *m) { int blk = nt_call_block_arg(nt, nid); if (blk < 0) return 0; int fwd = nt_ref(nt, blk, "expression"); if (fwd >= 0) { /* named `&blk`: only THIS method's block param counts */ NodeKind fk = nt_kind(nt, fwd); const char *fn = fk == NK_LocalVariableReadNode ? nt_str(nt, fwd, "name") : NULL; if (!fn || !m->blk_param[0] || !sp_streq(fn, m->blk_param)) return 0; } return 1; } /* A name used inside a proc is captured iff it belongs to the enclosing scope: it is an enclosing parameter, or it is assigned somewhere in the enclosing scope OUTSIDE any proc body. (A name assigned only inside the proc is a proc-local, not a capture -- Ruby's block-local rule.) Captured enclosing locals get a heap cell. */ /* A plain block `m(args) { ... }` passed to a method that keeps a real &block parameter (not yield-inlined) is lifted to a standalone proc function, so it captures enclosing variables exactly like a proc literal. */ /* Does this method hand its own block on to a POLY receiver? The dispatch there materializes whatever block reaches it as a real proc, so even though the method itself is yield-inlined (its `&blk` calls are spliced at the call site), the block passed to it still escapes into that proc and its captures need cells. */ static int a_scope_forwards_block_to_poly(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (!m->blk_param) return 0; /* (the scope's calls off the calls-by-scope chain: this and the two walks below read every node of the program for each method a block is handed to, 2.3 million per ask on a Rails application, half of a sample inside the post-fixpoint phase) */ for (int nid = comp_scall_first(c, mi); nid >= 0; nid = comp_scall_next(c, nid)) { if (c->nscope[nid] != mi) continue; NodeKind k = nt_kind(nt, nid); if (k != NK_CallNode) continue; if (!a_call_forwards_blk_param(nt, nid, m)) continue; int recv = nt_ref(nt, nid, "receiver"); if (recv < 0) continue; if (infer_type(c, recv) == TY_POLY) return 1; } return 0; } /* `recv.new` with recv naming a class: 1, with the initialize it runs in *init (-1 for none). 0 for any other receiver. */ static int an_const_new_init(Compiler *c, int recv, int *init) { int rk = recv >= 0 ? nt_kind(c->nt, recv) : -1; if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) return 0; int ci = const_node_class_index(c, recv); if (ci < 0) return 0; *init = comp_method_in_chain(c, ci, "initialize", NULL); return 1; } /* A block forwarded (`&blk`, `&`) to a `new` on a cycle of constructors reaches the clone as a proc (ctor_site_on_cycle), and so does the literal block it was given, which has to be lifted for that */ static int ctor_new_forwards_to_cycle(Compiler *c, int nid, const char *tn) { int init = -1; return sp_streq(tn, "new") && an_const_new_init(c, nt_ref(c->nt, nid, "receiver"), &init) && ctor_site_on_cycle(c, nid, init); } /* Does initialize `init` forward its block to such a `new`? */ static int ctor_init_forwards_to_cycle(Compiler *c, int init) { Scope *m = &c->scopes[init]; if (!m->blk_param || !m->blk_param[0]) return 0; for (int nid = an_calls_named_first(c, "new"); nid >= 0; nid = an_calls_named_next(nid)) if (c->nscope[nid] == init && nt_kind(c->nt, nid) == NK_CallNode && a_call_forwards_blk_param(c->nt, nid, m) && ctor_new_forwards_to_cycle(c, nid, "new")) return 1; return 0; } /* Does scope `mi` hand its block param on to a method that takes a REAL &block -- one lowered out of yield-inlining because it recurses (or yields from inside a lifted body)? Such a target cannot have the block spliced into it, so the block has to be materialized as a proc, and a forwarder in front of it cannot splice either. The forwarder is still marked `yields` (forwarding is what earns that mark), which is exactly what made a_block_is_lifted call the literal block unlifted and leave its captures without cells (#4145). */ static int a_scope_forwards_block_to_lowered_1(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (!m->blk_param) return 0; for (int nid = 0; nid < nt->count; nid++) { if (c->nscope[nid] != mi) continue; NodeKind k = nt_kind(nt, nid); if (k != NK_CallNode) continue; if (!nt_call_has_block_arg(nt, nid)) continue; const char *tn = nt_str(nt, nid, "name"); if (!tn || nt_ref(nt, nid, "receiver") >= 0) continue; int tmi = comp_self_call_mi(c, nid, tn); if (tmi >= 0 && tmi != mi && c->scopes[tmi].is_lowered_yield) return 1; } return 0; } static int a_scope_forwards_block_to_lowered(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (!m->blk_param) return 0; for (int nid = comp_scall_first(c, mi); nid >= 0; nid = comp_scall_next(c, nid)) { if (c->nscope[nid] != mi) continue; NodeKind ty_k = nt_kind(nt, nid); if (ty_k != NK_CallNode) continue; if (!a_call_forwards_blk_param(nt, nid, m)) continue; const char *tn = nt_str(nt, nid, "name"); if (!tn) continue; if (ctor_new_forwards_to_cycle(c, nid, tn)) return 1; int recv = nt_ref(nt, nid, "receiver"); int tmi = -1; /* a `new` of a class whose initialize forwards the block on into a cycle of constructors, one link as below */ if (sp_streq(tn, "new") && an_const_new_init(c, recv, &tmi)) { if (tmi >= 0 && tmi != mi && ctor_init_forwards_to_cycle(c, tmi)) return 1; continue; } if (recv < 0) tmi = comp_self_call_mi(c, nid, tn); else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) tmi = call_target(c, nid, ty_object_class(rt), tn, 0, NULL); /* a boxed receiver is dispatched on its class at run time, and the dispatch hands the block on as a proc (`k.walk(&blk)` over a mixed Array): it is a block that does not stop here whichever class owns the method */ else if (rt == TY_POLY && an_user_defines_method(c, tn)) return 1; } if (tmi < 0 || tmi == mi) continue; if (c->scopes[tmi].is_lowered_yield) return 1; /* the target is itself a forwarder: follow one link, which is as deep as the shape goes in practice and keeps this terminating */ if (c->scopes[tmi].blk_param && c->scopes[tmi].blk_param[0] && a_scope_forwards_block_to_lowered_1(c, tmi)) return 1; } return 0; } static int super_reach(Compiler *c, Scope *s); int a_block_is_lifted(Compiler *c, int id) { const NodeTable *nt = c->nt; NodeKind ty_k = nt_kind(nt, id); /* `super(...) { }` into a method that keeps its block as a real &blk parameter -- among them a proc-form clone's super, which reaches the parent's clone (a constructor cycle's subclass, mark_ctor_cycles) -- hands it the block as a proc (emit_super_block_arg) */ if (nt_kind(nt, id) == NK_SuperNode || nt_kind(nt, id) == NK_ForwardingSuperNode) { int blk = nt_ref(nt, id, "block"), s = c->nscope[id]; if (nt_kind(nt, blk) != NK_BlockNode || s < 0 || s >= c->nscopes || c->scopes[s].class_id < 0) return 0; int t = super_reach(c, &c->scopes[s]); return t >= 0 && c->scopes[t].blk_param && c->scopes[t].blk_param[0] && !c->scopes[t].yields; } if (ty_k != NK_CallNode) return 0; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; const char *name = nt_str(nt, id, "name"); if (!name) return 0; int recv = nt_ref(nt, id, "receiver"); /* a literal block on a first-class proc's .call is lifted onto the _sp_proc_blk side-channel as a real proc (#2648), so its captures need cells exactly like any other escaping block */ if (recv >= 0 && is_method_invoke(name)) { TyKind rt = infer_type(c, recv); if (rt == TY_PROC && is_call_alias(name)) return 1; /* Method invocation also materializes its literal block, including when the Method is held in a local. The capture pass revisits this query after inference settles, before storage is consumed by codegen. */ if (rt == TY_METHOD) return 1; /* and so does a Method or a Proc read out of a slot that holds other values (sp_poly_callable_call_blk) */ if (rt == TY_POLY && is_call_alias(name)) return 1; } /* so is Fiber.blocking { }'s, which sp_Fiber_blocking_proc calls */ if (sp_streq(name, "blocking") && recv >= 0 && nt_kind(nt, recv) == NK_ConstantReadNode && nt_str(nt, recv, "name") && sp_streq(nt_str(nt, recv, "name"), "Fiber")) return 1; int mi = -1; if (recv < 0) { mi = call_target(c, id, -1, name, CT_TOP, NULL); if (mi < 0) { Scope *self = comp_scope_of(c, id); if (self && self->class_id >= 0) { mi = call_target(c, id, self->class_id, name, 0, NULL); /* an implicit-self call inside a class method resolves to a CLASS method; its literal block is lifted all the same (#2444) */ if (mi < 0) mi = call_target(c, id, self->class_id, name, CT_CMETHOD, NULL); /* and a bare `new` there runs the class's initialize */ if (mi < 0 && self->is_cmethod && sp_streq(name, "new")) mi = comp_method_in_chain(c, self->class_id, "initialize", NULL); } } /* self may be an instance of a subclass whose override of the method takes the block (a Phlex component's view_template): the call then dispatches on the runtime class and the block becomes a real proc, which needs cells for what it captures even when the method resolved here ignores its block (#5381). A spurious cell is only a missed optimization. */ Scope *sself = comp_scope_of(c, id); if (sself && sself->class_id >= 0 && !sself->is_cmethod) { for (int k = 0; k < c->nclasses; k++) { if (k == sself->class_id || !is_descendant(c, k, sself->class_id)) continue; int mk = call_target(c, id, k, name, 0, NULL); if (mk < 0 || mk == mi) continue; if (c->scopes[mk].yields || (c->scopes[mk].blk_param && c->scopes[mk].blk_param[0])) return 1; } } } else { NodeKind rk = nt_kind(nt, recv); /* `Klass.cmeth { }` / `Mod::Sub.cmeth { }`: a class/module method keeps a real &block the same way an instance method does, so its block is lifted and captures enclosing locals. (Was omitted -- only ty_is_object was handled -- so a block passed to a module method never celled its captures, silently dropping writes to them.) */ int const_recv = (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode); int const_is_class = 0; if (const_recv) { int ci = const_node_class_index(c, recv); if (ci >= 0) { const_is_class = 1; mi = call_target(c, id, ci, name, CT_CMETHOD, NULL); /* `Klass.new { }` with no `def self.new` hands the block to initialize, which lifts it like any method keeping a real &block */ if (mi < 0 && sp_streq(name, "new")) mi = comp_method_in_chain(c, ci, "initialize", NULL); } } /* `self.new { }` in a class method, as a bare `new { }` there */ Scope *cs = comp_scope_of(c, id); if (rk == NK_SelfNode && sp_streq(name, "new") && cs && cs->is_cmethod && cs->class_id >= 0 && comp_cmethod_in_chain(c, cs->class_id, "new", NULL) < 0) { const_is_class = 1; mi = comp_method_in_chain(c, cs->class_id, "initialize", NULL); } /* A constant that names no class is an ordinary VALUE (`CONFIG.each { }`), so it is typed like any other receiver -- including poly, whose dispatch lifts the block. Reading it as a class name and stopping there left such a block unlifted and its captures without storage. */ if (!const_is_class) { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) mi = call_target(c, id, ty_object_class(rt), name, 0, NULL); /* A Class value known only at run time dispatches on it to the class methods, which take the block as a real proc the same way */ else if (rt == TY_CLASS && class_recv_is_dynamic(c, recv)) { for (int k = 0; k < c->nclasses; k++) { int ck = call_target(c, id, k, name, CT_CMETHOD, NULL); if (ck >= 0 && (c->scopes[ck].yields || (c->scopes[ck].blk_param && c->scopes[ck].blk_param[0]))) return 1; } } /* --share-strings: a String reopening's yielding method runs its block through the method's proc-form clone (emit_reopen_block_call, and a builtin's name with a literal block, reopen_override_block_ok): the block is a real proc, and its captures need cells. A spurious cell, where the method is spliced instead, is a missed optimization. */ else if ((rt == TY_STRING || rt == TY_STRBUF) && comp_core_class(c, CORE_STRING) >= 0 && call_target(c, id, comp_core_class(c, CORE_STRING), name, 0, NULL) >= 0 && c->scopes[call_target(c, id, comp_core_class(c, CORE_STRING), name, 0, NULL)].yields) return 1; /* A program Array#concat owns its block too. The reopening's proc form receives it, so captured writes need cells. */ else if ((ty_is_array(rt) || ty_is_obj_array(rt)) && sp_streq(name, "concat")) { int cm = an_reopen_owner(c, id, CORE_ARRAY); if (cm >= 0 && (c->scopes[cm].yields || (c->scopes[cm].blk_param && c->scopes[cm].blk_param[0]))) return 1; } /* A poly receiver dispatches on the runtime class, and the dispatch materializes the block as a real proc once, ahead of the switch, for any candidate that takes a real &block or is served by a proc-form clone. That proc is as escaping as any other, so its captures need cells -- which nothing here decided, because no single `mi` names the target. Codegen then met a capture with no storage and refused the whole call. Reachable only since a yielding method became poly-dispatchable at all (#3408). */ else if (rt == TY_POLY) { /* not filtered by `instantiated`: that is decided after this runs, and a spurious cell is only a missed optimization */ for (int k = 0; k < c->nclasses; k++) { /* a Class in the union reaches its class method the same way */ int ck = call_target(c, id, k, name, CT_CMETHOD, NULL); if (ck >= 0 && (c->scopes[ck].yields || (c->scopes[ck].blk_param && c->scopes[ck].blk_param[0]))) return 1; int ci2 = call_target(c, id, k, name, 0, NULL); if (ci2 < 0) continue; Scope *cm2 = &c->scopes[ci2]; if (cm2->yields || (cm2->blk_param && cm2->blk_param[0])) return 1; /* The candidate ignores the block, but a builtin Array or Hash in the same union does not: the dispatch's builtin arm materializes the block as a proc and drives it over the elements. That proc escapes like any other, and without cells the block's writes to enclosing locals went to a copy -- `a.map { |x| n = n + 1 }` on a real Array ran the block and left n at 0 (#3459). */ { int bargs = nt_ref(nt, id, "arguments"); int bn2 = 0; if (bargs >= 0) nt_arr(nt, bargs, "arguments", &bn2); if (bn2 == 0 && poly_enum_op_for(name)) return 1; } } } } } if (mi < 0) return 0; Scope *m = &c->scopes[mi]; /* `Y.new { }` on a cycle of constructors (`Y.new` in Y's own yielding initialize, or in its clone) hands its block to the clone as a proc (ctor_site_on_cycle, emit_ctor_new_with_proc), the body not being spliced there */ if (m->yields && sp_streq(name, "new") && m->name && sp_streq(comp_scope_user_name(m), "initialize") && ctor_site_on_cycle(c, id, mi)) return 1; /* A lowered yielding method also receives its block as a real proc, so a block passed to it is lifted and captures enclosing locals like any other. */ if (!m->blk_param || !m->blk_param[0]) return 0; /* A yielding method has its block SPLICED into it, so nothing escapes and the block needs no cells -- unless the block does not stop here. A forwarder is marked `yields` too, and if what it forwards to is a lowered yielder taking a real &block, the block is materialized as a proc after all. Its captures need storage like any other proc's (#4145). */ if (m->yields && !a_scope_forwards_block_to_lowered(c, mi)) return 0; /* instance_eval/exec trampolines splice their block at the call site rather than lifting it to a proc, so they are not lifted-block captures. */ if (m->class_id >= 0 && !m->is_cmethod && m->name && sp_streq(m->name, name) && comp_trampoline_kind(c, m->class_id, m->name, NULL)) return 0; return 1; } /* `Fiber.new { }` / `Enumerator.new { }` / `Thread.new { }` run their block on a fiber stack, so -- like an escaping proc -- an enclosing local they mutate must live in a shared heap cell rather than be captured by value. */ int a_is_fiber_or_gen_create(Compiler *c, int id) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) return 0; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "new") || nt_ref(nt, id, "block") < 0) return 0; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) return 0; NodeKind rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) return 0; const char *rn = nt_str(nt, recv, "name"); return rn && (sp_streq(rn, "Fiber") || sp_streq(rn, "Enumerator") || sp_streq(rn, "Thread")); } /* every arm counts: the dispatch makes one proc for all of them, so the resolved method ignoring the block proves nothing */ static int a_dispatch_arm_takes_block(Compiler *c, int cls, const char *name) { if (dispatch_impl_count(c, -1, cls, name) < 2) return 0; for (int k = 0; k < c->nclasses; k++) { if (!is_descendant(c, k, cls)) continue; int mk = comp_method_in_chain(c, k, name, NULL); if (mk < 0 || c->scopes[mk].is_cmethod) continue; if (scope_proc_form_of(c, mk) >= 0 || (c->scopes[mk].blk_param && c->scopes[mk].blk_param[0])) return 1; } return 0; } /* Does yield-inlined scope `mi` hand its block on to a method that KEEPS it -- one taking a real named &block that it is not spliced into? An anonymous `&` is always inlined (it has no name to escape through), so a literal block at its call site is spliced onto the forward and lands on the keeper's call, which materializes it as a proc that outlives the call. A named `&blk` forward straight into a keeper is never inlined in the first place (#3772); one into an anonymous forwarder is, so inlined forwarders are followed a few links deep. */ static int a_scope_forwards_block_to_keeper(Compiler *c, int mi, int depth) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (!m->blk_param || !m->yields || depth > 4) return 0; for (int nid = comp_scall_first(c, mi); nid >= 0; nid = comp_scall_next(c, nid)) { if (c->nscope[nid] != mi) continue; if (nt_kind(nt, nid) != NK_CallNode) continue; if (!a_call_forwards_blk_param(nt, nid, m)) continue; const char *tn = nt_str(nt, nid, "name"); if (!tn) continue; int recv = nt_ref(nt, nid, "receiver"); int tmi = -1, cls = -1; if (recv < 0) { tmi = comp_self_call_mi(c, nid, tn); if (!m->is_cmethod) cls = m->class_id; } else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) tmi = call_target(c, nid, cls = ty_object_class(rt), tn, 0, NULL); } if (tmi < 0 || tmi == mi) continue; Scope *t = &c->scopes[tmi]; if (cls >= 0 && !t->is_cmethod && a_dispatch_arm_takes_block(c, cls, tn)) return 1; if (!t->blk_param) continue; if (!t->yields && t->blk_param[0]) return 1; if (a_scope_forwards_block_to_keeper(c, tmi, depth + 1)) return 1; } return 0; } /* Does the program name Dir (a constant read or path ending in Dir)? Only then can a boxed value be a Dir, and a boxed one-argument `seek` answer the Dir rather than IO#seek's Integer. */ int an_program_makes_dir(Compiler *c) { static const NodeTable *memo_nt = NULL; static int memo_n = -1, memo = 0; const NodeTable *nt = c->nt; if (memo_nt == nt && memo_n == nt->count) return memo; memo = 0; for (int id = 0; id < nt->count && !memo; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_ConstantReadNode && k != NK_ConstantPathNode) continue; const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, "Dir")) memo = 1; } memo_nt = nt; memo_n = nt->count; return memo; } /* Can the program make a bsearch Enumerator (Array#bsearch without a block, or a bsearch it names by Symbol)? Only then does `e.each { }` over an Enumerator hand its block on as a proc, to the walk a bsearch one takes (codegen's sp_enum_bsearch_each_proc arm). */ int an_program_makes_bsearch_enum(Compiler *c) { static const NodeTable *memo_nt = NULL; static int memo_n = -1, memo = 0; const NodeTable *nt = c->nt; if (memo_nt == nt && memo_n == nt->count) return memo; memo = 0; for (int id = 0; id < nt->count && !memo; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_CallNode) { const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, "bsearch") && nt_ref(nt, id, "block") < 0) memo = 1; } else if (k == NK_SymbolNode) { const char *v = nt_str(nt, id, "value"); if (v && sp_streq(v, "bsearch")) memo = 1; } } memo_nt = nt; memo_n = nt->count; return memo; } /* Can a boxed value in the program be an IO? A conservative syntactic test: the program names an IO class or stream constant (File, IO, the STD streams, ARGF, Pathname, Tempfile, a *Socket or *Server class), reads $stdin / $stdout / $stderr, or calls Kernel#open. Without one, a boxed iterator keeps its String-only forms. */ static int an_io_const_name(const char *nm) { static const char *const names[] = { "File", "IO", "STDIN", "STDOUT", "STDERR", "ARGF", "Pathname", "Tempfile" }; for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) if (sp_streq(nm, names[i])) return 1; size_t n = strlen(nm); return (n >= 6 && sp_streq(nm + n - 6, "Socket")) || (n >= 6 && sp_streq(nm + n - 6, "Server")); } int an_program_makes_io(Compiler *c) { static const NodeTable *memo_nt = NULL; static int memo_n = -1, memo = 0; const NodeTable *nt = c->nt; if (memo_nt == nt && memo_n == nt->count) return memo; memo = 0; for (int id = 0; id < nt->count && !memo; id++) { NodeKind k = nt_kind(nt, id); const char *nm = nt_str(nt, id, "name"); if (!nm) continue; if (k == NK_ConstantReadNode || k == NK_ConstantPathNode) memo = an_io_const_name(nm); else if (k == NK_GlobalVariableReadNode) memo = sp_streq(nm, "$stdin") || sp_streq(nm, "$stdout") || sp_streq(nm, "$stderr"); else if (k == NK_CallNode) memo = sp_streq(nm, "open") && nt_ref(nt, id, "receiver") < 0; } memo_nt = nt; memo_n = nt->count; return memo; } /* The block of a call whose target hands the block on to a poly receiver, or to a method that keeps it. The target is yield-inlined, so the block is spliced into its body -- and lands on the dispatch or the keeper's call there, which materializes it as a real proc. Only the capture marking needs this: the target itself keeps no &block, so the inlining decision (which reads a_proc_create_or_lifted) must not see it. */ static int a_block_forwarded_to_proc(Compiler *c, int id) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, id); /* `super(x) { }` in a proc-form clone reaches the yielding parent's clone, which takes the literal block as a real proc */ if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { int sblk = nt_ref(nt, id, "block"); if (sblk < 0 || nt_kind(nt, sblk) != NK_BlockNode) return 0; Scope *s = comp_scope_of(c, id); if (!s || !s->is_proc_form || s->class_id < 0 || !s->name) return 0; int p = comp_super_parent(c, s->class_id, s->is_cmethod); if (p < 0) return 0; const char *u = comp_super_name(c, p, comp_scope_def_name(c, s), s->is_cmethod); int tmi = s->is_cmethod ? comp_cmethod_in_chain(c, p, u, NULL) : comp_method_in_chain(c, p, u, NULL); return tmi >= 0 && c->scopes[tmi].is_proc_form; } if (k != NK_CallNode) return 0; int blk = nt_ref(nt, id, "block"); NodeKind bk = nt_kind(nt, blk); if (bk != NK_BlockNode) return 0; const char *name = nt_str(nt, id, "name"); if (!name) return 0; int recv = nt_ref(nt, id, "receiver"); /* `e.each { }` over an Enumerator (the walk behind the to_a hop) in a program that can make a bsearch one, whose walk takes the block as a proc */ if (sp_streq(name, "each") && recv >= 0 && nt_kind(nt, recv) == NK_CallNode && nt_str(nt, recv, "enum_each_wrap") && nt_ref(nt, recv, "receiver") >= 0 && infer_type(c, nt_ref(nt, recv, "receiver")) == TY_ENUMERATOR && an_program_makes_bsearch_enum(c)) return 1; int mi = -1, cls = -1; if (recv < 0) { Scope *self = comp_scope_of(c, id); if (self && self->class_id >= 0) { mi = call_target(c, id, self->class_id, name, 0, NULL); if (!self->is_cmethod) cls = self->class_id; } /* the class chain misses a top-level def, which a bare call still reaches */ if (mi < 0) mi = call_target(c, id, -1, name, CT_TOP, NULL); } else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) mi = call_target(c, id, cls = ty_object_class(rt), name, 0, NULL); } if (mi < 0) return 0; /* a yielding method a subclass overrides is not spliced: the cls_id switch calls its proc-form clone with the block as a real proc */ if (cls >= 0 && !c->scopes[mi].is_cmethod && a_dispatch_arm_takes_block(c, cls, name)) return 1; return a_scope_forwards_block_to_poly(c, mi) || a_scope_forwards_block_to_keeper(c, mi, 0); } int a_proc_create_or_lifted(Compiler *c, int id) { return is_proc_create(c, id) || a_block_is_lifted(c, id) || a_is_fiber_or_gen_create(c, id) || is_handler_proc_block(c, id); } /* Does the subtree rooted at `root` contain node `id`? Bounded recursion over every ref/arr field. */ static int a_subtree_contains(const NodeTable *nt, int root, int id, int depth) { if (root < 0 || root >= nt->count || depth > 300) return 0; if (root == id) return 1; const SpNode *nd = &nt->nodes[root]; for (int i = 0; i < nd->nr; i++) if (a_subtree_contains(nt, nd->r[i].ref, id, depth + 1)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (a_subtree_contains(nt, nd->a[i].ids[j], id, depth + 1)) return 1; return 0; } /* Does this method's RETURN VALUE come out of a `yield`? For a lowered recursive yielder that matters to the C type: the emitted tail reads the block's answer out of the poly side-channel as a raw slot (`_sp_proc_poly_ret.v.i`), so the function has to be typed to receive it. A yield whose value is discarded -- a bare `yield` statement -- says nothing about the return, and typing on it made `walk` answer Integer where it answers nil, and a body ending in a String emit `return char *` from a function typed sp_int (#4145). */ static int a_subtree_has_yield(const NodeTable *nt, int id, int depth) { if (id < 0 || id >= nt->count || depth > 200) return 0; NodeKind k = nt_kind(nt, id); if (k == NK_YieldNode) return 1; /* a nested block or lambda has its own yield target */ if (k == NK_BlockNode || k == NK_LambdaNode) return 0; const SpNode *nd = &nt->nodes[id]; for (int i = 0; i < nd->nr; i++) if (a_subtree_has_yield(nt, nd->r[i].ref, depth + 1)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (a_subtree_has_yield(nt, nd->a[i].ids[j], depth + 1)) return 1; return 0; } static int a_scope_returns_a_yield(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; /* an explicit `return yield ...` anywhere in the body */ for (int id = 0; id < nt->count; id++) { if (c->nscope[id] != mi) continue; NodeKind k = nt_kind(nt, id); if (k != NK_ReturnNode) continue; if (a_subtree_has_yield(nt, nt_ref(nt, id, "arguments"), 0)) return 1; } /* the body's tail, which is the value a method without `return` answers */ if (m->body >= 0) { int bn = 0; const int *bb = nt_arr(nt, m->body, "body", &bn); if (bb && bn > 0 && a_subtree_has_yield(nt, bb[bn - 1], 0)) return 1; } return 0; } /* (key, scope) -> ascending node list, for mark_proc_captures' per-name lookups (rubys/roundhouse#72): the writes of a name in the enclosing scope, and the procs and block calls that bind a name as a parameter. Each was a walk of every write, or every proc and block call, per captured name per proc. */ typedef struct MpEnt { const char *key; int sc; int *ids; int n, cap; struct MpEnt *next; } MpEnt; typedef struct { MpEnt **tab; int nb; } MpIx; static unsigned mp_hash(const char *k, int sc) { unsigned h = 2166136261u ^ (unsigned)sc * 2654435761u; for (; *k; k++) { h ^= (unsigned char)*k; h *= 16777619u; } return h; } static void mp_add(MpIx *ix, const char *k, int sc, int id) { if (!ix->tab) { ix->nb = 4096; ix->tab = calloc((size_t)ix->nb, sizeof(MpEnt *)); } unsigned b = mp_hash(k, sc) % (unsigned)ix->nb; MpEnt *e = ix->tab[b]; while (e && !(e->sc == sc && sp_streq(e->key, k))) e = e->next; if (!e) { e = calloc(1, sizeof *e); e->key = k; e->sc = sc; e->next = ix->tab[b]; ix->tab[b] = e; } if (e->n && e->ids[e->n - 1] == id) return; /* a proc naming a param twice */ if (e->n == e->cap) { e->cap = e->cap ? e->cap * 2 : 4; e->ids = realloc(e->ids, sizeof(int) * (size_t)e->cap); } e->ids[e->n++] = id; } static const int *mp_get(const MpIx *ix, const char *k, int sc, int *n) { *n = 0; if (!ix->tab) return NULL; for (MpEnt *e = ix->tab[mp_hash(k, sc) % (unsigned)ix->nb]; e; e = e->next) if (e->sc == sc && sp_streq(e->key, k)) { *n = e->n; return e->ids; } return NULL; } static void mp_free(MpIx *ix) { if (!ix->tab) return; for (int b = 0; b < ix->nb; b++) for (MpEnt *e = ix->tab[b]; e; ) { MpEnt *nx = e->next; free(e->ids); free(e); e = nx; } free(ix->tab); } void mark_proc_captures(Compiler *c) { const NodeTable *nt = c->nt; char *inproc = (char *)calloc((size_t)nt->count, 1); if (!inproc) return; for (int id = 0; id < nt->count; id++) if (a_proc_create_or_lifted(c, id) || a_block_forwarded_to_proc(c, id)) { int body = a_proc_body(c, id); if (body >= 0) a_mark_subtree(c, body, inproc); } /* Which proc frame each node belongs to: the INNERMOST proc whose body contains it, or -1 for the enclosing scope's own code. A local written in the body of the proc that encloses this one lives in that frame and is capturable from here, so asking only "is it inside some proc" refused a lambda nested in another proc -- including the wrapper that desugar_block_capture_wrap builds around an iteration block, which is why a lambda capturing a block local alongside the block parameter was rejected (#3912). Stamping outer procs first lets each inner one overwrite its own region. */ int *procof = (int *)malloc(sizeof(int) * (size_t)nt->count); int *pdepth = (int *)calloc((size_t)nt->count, sizeof(int)); if (procof && pdepth) { for (int id = 0; id < nt->count; id++) procof[id] = -1; /* nesting depth of each proc = how many proc bodies cover it, counted by stamping every proc body once (O(nodes) per proc, not O(nodes) per pair) */ for (int id = 0; id < nt->count; id++) { if (!a_proc_create_or_lifted(c, id) && !a_block_forwarded_to_proc(c, id)) continue; int body = a_proc_body(c, id); if (body >= 0) a_count_subtree(c, body, pdepth, 0); } int maxd = 0; for (int id = 0; id < nt->count; id++) { if (!a_proc_create_or_lifted(c, id) && !a_block_forwarded_to_proc(c, id)) continue; pdepth[id] += 1; /* 0 means "not a proc" */ if (pdepth[id] > maxd) maxd = pdepth[id]; } for (int d = 1; d <= maxd; d++) for (int id = 0; id < nt->count; id++) { if (pdepth[id] != d) continue; int body = a_proc_body(c, id); if (body >= 0) a_stamp_subtree(c, body, procof, id, 0); } } free(pdepth); /* The two walks per captured name below read only two kinds of node: the writes, and the procs and block calls that may bind the name. Listed once, in node order, they were whole-table walks per name per proc. */ int *wl = malloc(sizeof(int) * (size_t)(nt->count + 1)); int *ql = malloc(sizeof(int) * (size_t)(nt->count + 1)); char *qproc = calloc((size_t)nt->count + 1, 1); int nwl = 0, nql = 0; if (wl && ql && qproc) for (int q = 0; q < nt->count; q++) { if (a_is_write_node(nt_type(nt, q))) wl[nwl++] = q; int qp = is_proc_create(c, q); int qb = 0; if (!qp && nt_kind(nt, q) == NK_CallNode) { int qblk = nt_ref(nt, q, "block"); qb = qblk >= 0 && nt_kind(nt, qblk) == NK_BlockNode; } if (qp || qb) { qproc[q] = (char)qp; ql[nql++] = q; } } MpIx wix = {0}, qix = {0}; for (int wi = 0; wi < nwl; wi++) { const char *wn = nt_str(nt, wl[wi], "name"); if (wn) mp_add(&wix, wn, c->nscope[wl[wi]], wl[wi]); } for (int qi = 0; qi < nql; qi++) { int q = ql[qi]; int qpn = a_proc_params_node(c, q); int qrn = 0; const int *qreqs = qpn >= 0 ? nt_arr(nt, qpn, "requireds", &qrn) : NULL; for (int k = 0; k < qrn; k++) { const char *qn = nt_str(nt, qreqs[k], "name"); if (qn) mp_add(&qix, qn, -1, q); } } for (int id = 0; id < nt->count; id++) { if (!a_proc_create_or_lifted(c, id) && !a_block_forwarded_to_proc(c, id)) continue; /* A fiber/generator only needs a cell for a *value-type* capture, where a by-value copy would drop the write. A captured heap object (string, array, hash, ...) is already shared by pointer -- in-place mutation reaches the enclosing scope -- and the cell machinery does not handle some of those types (e.g. a mutable-string buffer), so leave them by value. */ int fib_create = a_is_fiber_or_gen_create(c, id); /* a lifted iteration block is consumed while its call runs; everything else here may hold its cells past the call (see LocalVar.cell_outlives) */ int outlives = !(a_block_is_lifted(c, id) && !is_proc_create(c, id) && !fib_create && !is_handler_proc_block(c, id) && !a_block_forwarded_to_proc(c, id)); /* ...nor does the capture-wrap lambda (desugar_block_capture_wrap): it is called where it is made and returns inside the iteration, so its cells end with the call. Counted as outliving, a parameter it captured lost the by-reference ABI, and every append to lobsters' `io` in show_into landed in a copy the caller never saw (#5087). */ if (nt_int(nt, id, "cap_iife", 0)) outlives = 0; int body = a_proc_body(c, id); if (body < 0) continue; int encl = c->nscope[id]; ANameSet params = {0}, used = {0}; int pn = a_proc_params_node(c, id); /* keyword params bind BOXED (extracted from the call-site kwargs hash), so their static type is poly -- an untyped one lets the body's tail mis-derive (a `[a, k]` settled INT_ARRAY and misread the PolyArray, #2728). Seeded: the write pass's reset must not wipe it. */ { int kn2 = 0; const int *kws2 = pn >= 0 ? nt_arr(nt, pn, "keywords", &kn2) : NULL; Scope *pbs2 = kn2 > 0 ? comp_scope_of(c, id) : NULL; for (int k2 = 0; k2 < kn2 && pbs2; k2++) { const char *kname2 = nt_str(nt, kws2[k2], "name"); LocalVar *klv2 = kname2 ? scope_local_intern(pbs2, kname2) : NULL; if (klv2 && klv2->type == TY_UNKNOWN) { lv_set_type(klv2, TY_POLY); klv2->rbs_seeded = 1; } } /* `**kw` binds the whole boxed kwargs hash: poly likewise */ int kwrest2 = pn >= 0 ? nt_ref(nt, pn, "keyword_rest") : -1; NodeKind kwrty2_k = nt_kind(nt, kwrest2); if (kwrty2_k == NK_KeywordRestParameterNode) { const char *krn2 = nt_str(nt, kwrest2, "name"); Scope *pbs3 = comp_scope_of(c, id); LocalVar *krv2 = (krn2 && pbs3) ? scope_local_intern(pbs3, krn2) : NULL; if (krv2 && krv2->type == TY_UNKNOWN) { lv_set_type(krv2, TY_POLY); krv2->rbs_seeded = 1; } } } /* ENV's block mutators (delete_if/keep_if/...) pass (key, value) string pairs; seed the params so the proc binds them as strings (#2832) */ { const char *cnm2 = nt_str(nt, id, "name"); int crecv2 = nt_ref(nt, id, "receiver"); if (cnm2 && crecv2 >= 0 && nt_kind(nt, crecv2) == NK_ConstantReadNode && nt_str(nt, crecv2, "name") && sp_streq(nt_str(nt, crecv2, "name"), "ENV")) { int ern = 0; const int *ereqs = pn >= 0 ? nt_arr(nt, pn, "requireds", &ern) : NULL; Scope *epbs = comp_scope_of(c, id); for (int ek = 0; ek < ern && ek < 2 && epbs; ek++) { const char *ernm = nt_str(nt, ereqs[ek], "name"); LocalVar *erlv = ernm ? scope_local_intern(epbs, ernm) : NULL; if (erlv && (erlv->type == TY_UNKNOWN || erlv->type == TY_INT)) { lv_set_type(erlv, TY_STRING); erlv->rbs_seeded = 1; } } } } /* Fiber.blocking { |fiber| } passes the running fiber */ { const char *fbn = nt_str(nt, id, "name"); int fbr = nt_ref(nt, id, "receiver"); if (fbn && sp_streq(fbn, "blocking") && fbr >= 0 && nt_kind(nt, fbr) == NK_ConstantReadNode && nt_str(nt, fbr, "name") && sp_streq(nt_str(nt, fbr, "name"), "Fiber")) { int frn = 0; const int *freqs = pn >= 0 ? nt_arr(nt, pn, "requireds", &frn) : NULL; Scope *fbs = frn > 0 ? comp_scope_of(c, id) : NULL; const char *fpn = fbs ? nt_str(nt, freqs[0], "name") : NULL; LocalVar *flv = fpn ? scope_local_intern(fbs, fpn) : NULL; if (flv && flv->type == TY_UNKNOWN) { lv_set_type(flv, TY_FIBER); flv->rbs_seeded = 1; } } } /* a named &block param binds an sp_Proc* from the block side-channel: type it so `b.call(...)` rides the TY_PROC dispatch (#2648) */ { int bpar = pn >= 0 ? nt_ref(nt, pn, "block") : -1; NodeKind bpk = nt_kind(nt, bpar); if (bpk == NK_BlockParameterNode) { const char *bpn = nt_str(nt, bpar, "name"); Scope *pbs = comp_scope_of(c, id); LocalVar *blv = (bpn && pbs) ? scope_local_intern(pbs, bpn) : NULL; if (blv && blv->type == TY_UNKNOWN) { lv_set_type(blv, TY_PROC); blv->is_block_param = 1; } if (bpn) aname_add(¶ms, bpn); } } if (pn >= 0) { int rn = 0; const int *reqs = nt_arr(nt, pn, "requireds", &rn); for (int k = 0; k < rn; k++) aname_add(¶ms, nt_str(nt, reqs[k], "name")); } /* an optional is the proc's own too: a later default that writes it (`|a = 1, b = (a = 4)|`) is no capture of an enclosing local */ if (pn >= 0) { int on = 0; const int *opts = nt_arr(nt, pn, "optionals", &on); for (int k = 0; k < on; k++) aname_add(¶ms, nt_str(nt, opts[k], "name")); } a_collect_used(c, body, &used); a_collect_alias_used(c, body, &used, 0); /* an optional or keyword default is evaluated in the proc's frame */ a_collect_used(c, pn, &used); Scope *es = &c->scopes[encl]; for (int u = 0; u < used.n; u++) { const char *nm = used.v[u]; if (aname_has(¶ms, nm)) continue; /* the proc's own param */ LocalVar *lv = scope_local(es, nm); if (!lv) continue; /* not an enclosing local */ int owned = lv->is_param; int myframe = procof ? procof[id] : -1; int nwn = 0; const int *wns = mp_get(&wix, nm, encl, &nwn); for (int wi = 0; wi < nwn && !owned; wi++) { int w = wns[wi]; if (c->nscope[w] != encl) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, nm)) continue; if (!procof) { if (!inproc[w]) owned = 1; continue; } /* The write's frame is this proc's own, or one that ENCLOSES it. An enclosing frame's write is just as capturable: a_collect_used is deep, so every proc between that write and this reader has the name in its own used set and cells it too, and the chain of cells is what this proc reads through. Accepting only myframe refused a block nested inside a Thread whose body never names the variable directly -- the write sat in the outermost frame, two levels up, and the compiler reported an uncaptured outer variable (#4349). */ int wf = procof[w], f = myframe; for (;;) { if (wf == f) { owned = 1; break; } if (f < 0) break; f = procof[f]; } } /* a param of an ENCLOSING proc: it lives in that proc's frame (a "later slice"), and this proc closes over it -- the enclosing proc's prologue materializes the cell (#2648). Only proc-fn enclosers: an inlined iterator block's params bind in the loop, where no cell exists yet. */ /* The binding-site walk below runs whether or not a write already settled `owned`: a param of an INLINED iteration block that the body also REASSIGNS is owned by the write, but the loop still binds it by writing the plain C slot, so it needs the shadow slot all the same (the cell alone left `lv_i` undeclared, and the C build failed). */ int shadow = 0; { int owned_q = 0; int nqn = 0; const int *qns = mp_get(&qix, nm, -1, &nqn); for (int qi = 0; qi < nqn && !owned_q; qi++) { int q = qns[qi]; if (q == id) continue; /* An INLINED iteration block binds its params in the loop, where the emitters write the plain C slot -- so celling one needs the slot kept alongside the cell (LocalVar.cell_shadow) and copied in at the top of the body. Refusing instead left a proc lifted out of a nested block with nowhere to read the enclosing loop variable, and the emitter had to reject the whole program. */ int q_is_proc = qproc[q]; int q_is_block = !q_is_proc; /* listed: a proc or a block call */ /* The cell is fresh for every iteration (#4462), the binding Ruby gives each, so a `proc {}` / lambda / Fiber / Thread body that outlives its iteration shares it too: kept by value, its writes to the parameter were lost. */ if (!q_is_proc && !q_is_block) continue; int qpn = a_proc_params_node(c, q); int qrn = 0; const int *qreqs = qpn >= 0 ? nt_arr(nt, qpn, "requireds", &qrn) : NULL; int has = 0; for (int k = 0; k < qrn && !has; k++) { const char *qn = nt_str(nt, qreqs[k], "name"); if (qn && sp_streq(qn, nm)) has = 1; } if (!has) continue; int qb = a_proc_body(c, q); if (qb >= 0 && a_subtree_contains(nt, qb, id, 0)) { owned_q = 1; shadow = !q_is_proc; } } if (owned_q) owned = 1; } if (owned) { /* A fiber/generator now cells a captured heap object too (string / array / hash / object), via a typed-pointer cell, so a reassignment in the body reaches the enclosing scope. In-place mutation of a non-reassigned capture stays by pointer (the var isn't `owned`, so it never reaches here). Value-type objects have no stable pointer. */ /* an sp_Proc* is a heap pointer with a stable identity, so a fiber or thread body can hold it in a typed cell -- which is what a lowered method's forwarded block needs to reach a yield inside such a body */ int heap_ptr = (lv->type == TY_STRING || lv->type == TY_STRBUF || lv->type == TY_PROC || /* `f = Fiber.new { f.resume }` reads its own handle */ lv->type == TY_FIBER || lv->type == TY_THREAD || ty_is_array(lv->type) || ty_is_hash(lv->type) || ty_is_object(lv->type)) && !comp_ty_value_obj(c, lv->type); if (fib_create && lv->type != TY_INT && lv->type != TY_BOOL && lv->type != TY_FLOAT && lv->type != TY_POLY && !heap_ptr) continue; /* capture type without a usable cell: leave by value */ { lv->is_cell = 1; comp_touch_scope(lv->owner); } if (outlives) lv->cell_outlives = 1; if (shadow) lv->cell_shadow = 1; /* A write in this proc's body, its own frame's or a block's nested in it, assigns the cell wherever the proc is later called from. A block the call consumes counts too: one bound to a `&b` parameter may be kept and called after it (`def keep(&b) = $kept = b`). So does a write in a parameter's default, which runs in the proc's frame when a call leaves the parameter out (`->(q = (v = 0)) { 5 }`); procof stamps only the body. */ for (int wi = 0; wi < nwn && !lv->proc_rebinds; wi++) { int w = wns[wi]; const char *wn = nt_str(nt, w, "name"); if (c->nscope[w] != encl || !wn || !sp_streq(wn, nm)) continue; if (pn >= 0 && a_subtree_contains(nt, pn, w, 0)) { lv->proc_rebinds = 1; continue; } if (!procof) { lv->proc_rebinds = a_subtree_contains(nt, body, w, 0); continue; } for (int f = procof[w]; f >= 0; f = procof[f]) if (f == id) { lv->proc_rebinds = 1; break; } } } } free(params.v); free(used.v); } free(procof); free(inproc); free(wl); free(ql); free(qproc); mp_free(&wix); mp_free(&qix); } /* ---- bigint loop-variable detection ---- */ /* Scan a while-loop body for `x = x * y` or `x *= y` patterns and collect the variable names in a heap-allocated array. Returns the count; caller must free the returned array. */ void bigint_scan_body(const NodeTable *nt, int id, char ***names, int *n, int *cap) { if (id < 0) return; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) return; /* x *= y (LocalVariableOperatorWriteNode with * or **) */ if (ty_k == NK_LocalVariableOperatorWriteNode) { const char *op = nt_str(nt, id, "binary_operator"); if (op && (is_mul_or_pow(op))) { const char *nm = nt_str(nt, id, "name"); if (nm) { for (int k = 0; k < *n; k++) if (sp_streq((*names)[k], nm)) goto skip_mul; if (*n >= *cap) { *cap = (*cap * 2) + 4; *names = (char **)realloc(*names, (size_t)*cap * sizeof(char *)); } (*names)[(*n)++] = (char *)nm; skip_mul:; } } } /* x = x * y (LocalVariableWriteNode where value is CallNode * with recv = x) */ if (ty_k == NK_LocalVariableWriteNode) { const char *nm = nt_str(nt, id, "name"); int val = nt_ref(nt, id, "value"); if (nm && val >= 0 && nt_kind(nt, val) == NK_CallNode) { const char *op2 = nt_str(nt, val, "name"); int recv2 = nt_ref(nt, val, "receiver"); if (op2 && (is_mul_or_pow(op2)) && recv2 >= 0 && nt_kind(nt, recv2) == NK_LocalVariableReadNode && sp_streq(nt_str(nt, recv2, "name") ? nt_str(nt, recv2, "name") : "", nm)) { for (int k = 0; k < *n; k++) if (sp_streq((*names)[k], nm)) goto skip_lv; if (*n >= *cap) { *cap = (*cap * 2) + 4; *names = (char **)realloc(*names, (size_t)*cap * sizeof(char *)); } (*names)[(*n)++] = (char *)nm; skip_lv:; } } } /* Recurse into body / stmts / subsequent */ bigint_scan_body(nt, nt_ref(nt, id, "body"), names, n, cap); int sn = 0; const int *stmts2 = nt_arr(nt, id, "body", &sn); for (int k = 0; k < sn; k++) bigint_scan_body(nt, stmts2[k], names, n, cap); bigint_scan_body(nt, nt_ref(nt, id, "subsequent"), names, n, cap); } void detect_bigint_loop_vars(Compiler *c) { if (g_wrap_mode) return; /* (see infer_bigint_loop_locals) */ const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_WhileNode) continue; int body = nt_ref(nt, id, "statements"); if (body < 0) continue; char **cands = NULL; int ncands = 0, cap = 0; bigint_scan_body(nt, body, &cands, &ncands, &cap); /* Promote matching TY_INT locals to TY_BIGINT */ for (int k = 0; k < ncands; k++) { Scope *s = comp_scope_of(c, id); LocalVar *lv = s ? scope_local(s, cands[k]) : NULL; if (lv && lv->type == TY_INT && !lv->rbs_seeded) lv_set_type(lv, TY_BIGINT); } free(cands); } } /* The value a write actually CARRIES, looking through a chain. infer_type of a write node answers the target's slot type, which is right for the expression's value (`a = (b = x)` evaluates to b's slot) but wrong as evidence about this assignment: `a = b = 0` puts 0 in both, and b widening to Bignum later says nothing about a. Reading the slot promoted a through the chain, and a return of `[a]` then declared a typed array while the body built a poly one -- a C function whose result type is not what it returns (#4686). */ static int bigint_cascade_value_node(const NodeTable *nt, int id) { int v = nt_ref(nt, id, "value"); for (int guard = 0; v >= 0 && guard < 64; guard++) { NodeKind k = nt_kind(nt, v); if (k != NK_LocalVariableWriteNode) break; v = nt_ref(nt, v, "value"); } return v; } /* After detect_bigint_loop_vars promotes some locals to TY_BIGINT, cascade the promotion to variables assigned from bigint-typed expressions. */ void propagate_bigint_cascade(Compiler *c) { const NodeTable *nt = c->nt; int changed = 1; while (changed) { changed = 0; for (int id = 0; id < nt->count; id++) { const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty) continue; if (k == NK_LocalVariableWriteNode) { const char *nm = nt_str(nt, id, "name"); Scope *s = comp_scope_of(c, id); LocalVar *lv = nm ? scope_local(s, nm) : NULL; if (!lv || lv->type != TY_INT || lv->rbs_seeded) continue; TyKind vt = infer_type(c, bigint_cascade_value_node(nt, id)); if (vt == TY_BIGINT) { lv_set_type(lv, TY_BIGINT); changed = 1; } } else if (k == NK_LocalVariableOperatorWriteNode) { const char *nm = nt_str(nt, id, "name"); Scope *s = comp_scope_of(c, id); LocalVar *lv = nm ? scope_local(s, nm) : NULL; if (!lv || lv->type != TY_INT || lv->rbs_seeded) continue; TyKind vt = infer_type(c, nt_ref(nt, id, "value")); if (vt == TY_BIGINT) { lv_set_type(lv, TY_BIGINT); changed = 1; } } } } } /* For nodes inside an instance_eval/exec block, the receiver's class id; -1 elsewhere. Lets bare calls/ivar refs in the block resolve against the receiver's class during inference (codegen mirrors this via an_ie_class_id). */ int *g_ie_node_class = NULL; static int g_ie_node_class_cap = 0; /* nodes inside an instance_eval / exec block over a receiver of no class of the program's (nil, a number, a String): its ivars are nil there. Sized as g_ie_node_class. */ static unsigned char *g_ie_nonobj = NULL; static void mark_ie_nonobj(Compiler *c, int node) { if (node < 0 || node >= g_ie_node_class_cap) return; NodeKind k = nt_kind(c->nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return; if (!g_ie_nonobj[node]) { g_ie_nonobj[node] = 1; comp_touch_node(c, node); } int nr = nt_num_refs(c->nt, node); for (int i = 0; i < nr; i++) mark_ie_nonobj(c, nt_ref_at(c->nt, node, i)); int na = nt_num_arrs(c->nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, node, i, &n); for (int j = 0; j < n; j++) mark_ie_nonobj(c, ids[j]); } } int ie_nonobj_node(int node) { return g_ie_nonobj && node >= 0 && node < g_ie_node_class_cap && g_ie_nonobj[node]; } void mark_ie_subtree(Compiler *c, int node, int cls) { if (node < 0) return; const char *ty = nt_type(c->nt, node); NodeKind ty_k = nt_kind(c->nt, node); if (!ty) return; /* a nested def/class starts a fresh self; don't bleed the rebind into it */ if (ty_k == NK_DefNode || ty_k == NK_ClassNode || ty_k == NK_ModuleNode) return; if (g_ie_node_class[node] != cls) { g_ie_node_class[node] = cls; comp_touch_node(c, node); } int nr = nt_num_refs(c->nt, node); for (int i = 0; i < nr; i++) mark_ie_subtree(c, nt_ref_at(c->nt, node, i), cls); int na = nt_num_arrs(c->nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(c->nt, node, i, &n); for (int k = 0; k < n; k++) mark_ie_subtree(c, ids[k], cls); } } /* `ENV` is compile-time modeled only as a call RECEIVER (`ENV[...]`, `ENV.fetch`, ...). Flowing as a VALUE -- an argument, a parameter default, an assignment, a Hash value (`params["e"] = ENV`) -- it is the environment's StrStr-hash snapshot taken where ENV is read, the same value the enumeration surface and ENV.update answer (#2742): reads see the variables set at that point, and writing through it leaves the environment alone. defined?(ENV) evaluates nothing and is exempt. */ static void desugar_env_values(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; char *ok = calloc((size_t)nt->count, 1); if (!ok) { perror("calloc"); exit(1); } NT_FOREACH_KIND(nt, NK_CallNode, id) { int r = nt_ref(nt, id, "receiver"); if (r >= 0) ok[r] = 1; } NT_FOREACH_KIND(nt, NK_DefinedNode, id) { int v = nt_ref(nt, id, "value"); if (v >= 0) ok[v] = 1; } /* `ENV[k] op= v` with one plain key: desugar_index_assign_user_recv writes it as the [] and []= calls (when ENV is the environment's) */ static const NodeKind opw[] = { NK_IndexOperatorWriteNode, NK_IndexOrWriteNode, NK_IndexAndWriteNode }; for (size_t w = 0; w < sizeof opw / sizeof *opw && !comp_const(c, "ENV"); w++) NT_FOREACH_KIND(nt, opw[w], id) { int r = nt_ref(nt, id, "receiver"), args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; NodeKind ak = an == 1 ? nt_kind(nt, av[0]) : NK_SplatNode; if (r >= 0 && nt_ref(nt, id, "block") < 0 && ak != NK_SplatNode && ak != NK_BlockArgumentNode && ak != NK_KeywordHashNode) ok[r] = 1; } /* a program that assigns its own ENV constant reads that one */ int n0 = comp_const(c, "ENV") ? 0 : nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_ConstantReadNode || ok[id]) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "ENV")) continue; nt_node_set_type(nt, id, "CallNode"); nt_node_set_str(nt, id, "name", "__env_to_h"); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); /* (ENV itself as a value, a store into which copies as ENV's does; the one representation's emitter reads it) */ nt_node_set_int(nt, id, "env_value", 1); } free(ok); } /* `include x` / `extend x` / `prepend x` in a class or module body (or at the top level) whose argument is not a constant -- `include w(Registry)` -- names a module only the run time knows. The mixin is resolved at compile time by its constant, so such an argument was skipped and the module's methods were silently missing; refuse it instead. */ static void reject_dynamic_mixin_args(Compiler *c) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); int body = -1; if (k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) body = nt_ref(nt, id, "body"); else if (prog_is_root(c, id)) body = nt_ref(nt, id, "statements"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); for (int i = 0; i < bn; i++) { int st = bb[i]; if (nt_kind(nt, st) != NK_CallNode || nt_ref(nt, st, "receiver") >= 0) continue; const char *nm = nt_str(nt, st, "name"); if (!nm || !(sp_streq(nm, "include") || sp_streq(nm, "extend") || sp_streq(nm, "prepend"))) continue; int args = nt_ref(nt, st, "arguments"), ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; for (int a = 0; a < ac; a++) { NodeKind ak = nt_kind(nt, av[a]); if (ak == NK_ConstantReadNode || ak == NK_ConstantPathNode || ak == NK_SelfNode) continue; int ln = (int)nt_int(nt, st, "node_line", 0); int fid = (int)nt_int(nt, st, "node_file", 0); const char *file = nt_file_path(nt, fid); if (!file || !*file) file = nt->source_file; fprintf(stderr, "spinel: %s:%d: `%s` of a module named by an expression, not a " "constant, is not supported (the mixin is resolved at compile time)\n", file ? file : "source.rb", ln, nm); exit(1); } } } } /* The renames rewrite_const_alias_receivers has decided and not yet made: a constant node and the class name it takes (an owned copy), and whether its scope holds the name the program wrote (held: the rename keeps it). */ typedef struct AliasRename { int node; char *name; int held; } AliasRename; typedef struct AliasRenames { AliasRename *v; int n, cap; } AliasRenames; static void alias_rename_add(AliasRenames *q, int node, const char *name, int held) { if (q->n == q->cap) { q->cap = q->cap ? q->cap * 2 : 64; q->v = realloc(q->v, sizeof *q->v * (size_t)q->cap); if (!q->v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } q->v[q->n].node = node; q->v[q->n].name = strdup(name); q->v[q->n].held = held; q->n++; } /* The name a read or write had before qualify_colliding_consts renamed it (`X8::S` read as `X8::X8__S`), kept for the classes pass, which resolves the module path a constant alias names in the names the program wrote. The path lookups take a kept name as proof that the scope holds the constant (const_node_class_index): qualify_colliding_consts renames a path only after it finds the member, and an alias rename keeps the name only for a path whose scope holds the name and finds it assigned (alias_read_held, asked before the edit). */ static void qc_keep_orig_name(const NodeTable *nt, int node) { const char *nm = nt_str(nt, node, "name"); if (nm && !nt_str(nt, node, "qc_orig_name")) nt_node_set_str((NodeTable *)nt, node, "qc_orig_name", nm); } /* Makes the queued renames and frees the queue. */ static void alias_renames_apply(Compiler *c, AliasRenames *q) { for (int i = 0; i < q->n; i++) { if (q->v[i].held) qc_keep_orig_name(c->nt, q->v[i].node); /* the path as written, for the NameError text alone (const_path_written): qc_orig_name additionally says the scope holds the name and drives the lookups, which a rename whose scope does not hold it must not claim */ const char *was = nt_str(c->nt, q->v[i].node, "name"); if (was && !nt_str(c->nt, q->v[i].node, "written_name")) nt_node_set_str((NodeTable *)c->nt, q->v[i].node, "written_name", was); nt_set_str((NodeTable *)c->nt, q->v[i].node, "name", q->v[i].name); free(q->v[i].name); } free(q->v); } /* Whether a renamed alias read keeps the name the program wrote: its scope holds that name (const_path_misses_member), and a path read finds the alias assigned where it runs (an_const_read_finds, the presence pass's question): `M::Top2` ahead of `module M; Top2 = Top; end` is CRuby's NameError, which the renamed leaf's class would otherwise answer. Asked before any edit, while the node still carries the written name. */ static int alias_read_held(Compiler *c, int node) { if (const_path_misses_member(c, node)) return 0; return nt_kind(c->nt, node) != NK_ConstantPathNode || an_const_read_finds(c, node, nt_str(c->nt, node, "name")); } /* Asks the class a constant node names when it names a class alias, and queues its rename to the underlying class. */ static void ask_const_alias_name(Compiler *c, int node, AliasRenames *q) { const NodeTable *nt = c->nt; if (node < 0 || !nt_type(nt, node)) return; NodeKind k = nt_kind(nt, node); /* A ConstantPathNode whose LEAF is the alias (`Outer::OC.five` where `OC = Util`) resolves the same way: constants register by leaf name, and a path receiver with a real class leaf already dispatches. */ if (k != NK_ConstantReadNode && k != NK_ConstantPathNode) return; const char *rn = nt_str(nt, node, "name"); if (!rn || comp_class_index(c, rn) >= 0) return; /* already a class name */ const char *real = resolve_class_alias(c, rn); if (real && !sp_streq(real, rn)) alias_rename_add(q, node, real, alias_read_held(c, node)); } /* `extend Alias` / `include Alias` / `prepend Alias` with `Alias = Mod` (also through a chain of aliases): queue naming the module itself, since the extend pass and the singleton prepend it carries match a module's own name only, and dropped an alias silently. */ static void ask_mixin_alias_args(Compiler *c, int call, AliasRenames *q) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); if (!nm || nt_ref(nt, call, "receiver") >= 0 || !is_mixin_call(nm)) return; int anode = nt_ref(nt, call, "arguments"), an = 0; const int *av = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { NodeKind ak = nt_kind(nt, av[j]); if (ak != NK_ConstantReadNode && ak != NK_ConstantPathNode) continue; const char *rn = nt_str(nt, av[j], "name"); if (!rn || comp_class_index(c, rn) >= 0) continue; const char *real = resolve_class_alias_chain(c, rn); if (!real || sp_streq(real, rn) || comp_class_index(c, real) < 0) continue; alias_rename_add(q, av[j], real, alias_read_held(c, av[j])); } } /* `A = SomeClass` (a constant aliasing a class) then `A.foo`: rewrite the ConstantRead receiver's name to the underlying class so class-method dispatch resolves it exactly like the direct `SomeClass.foo`. The class argument of is_a?/kind_of?/instance_of? (`x.is_a?(A)`) and each `when` condition (`when A`) resolve the same way. Mirrors the `class CONST` reopening rewrite in walk_scope. Runs once after classes are registered. */ void rewrite_const_alias_receivers(Compiler *c) { const NodeTable *nt = c->nt; AliasRenames q = {0}; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_WhenNode) { int wn = 0; const int *wc = nt_arr(nt, id, "conditions", &wn); for (int i = 0; i < wn; i++) ask_const_alias_name(c, wc[i], &q); continue; } if (k != NK_CallNode) continue; ask_const_alias_name(c, nt_ref(nt, id, "receiver"), &q); ask_mixin_alias_args(c, id, &q); const char *nm = nt_str(nt, id, "name"); if (nm && is_kind_query(nm)) { int args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an == 1 && av) ask_const_alias_name(c, av[0], &q); } } /* The asks above are the load record's, which rebuilds its point map after an edit: every rename waits until the last one is asked. */ alias_renames_apply(c, &q); } /* Asks the class a `raise` argument or a `rescue` exception names when it is a class alias, and queues its rename (rewrite_const_alias_raise_targets). */ static void ask_raise_target(Compiler *c, int t, AliasRenames *q) { const NodeTable *nt = c->nt; if (nt_kind(nt, t) != NK_ConstantReadNode && nt_kind(nt, t) != NK_ConstantPathNode) return; const char *rn = nt_str(nt, t, "name"); if (!rn || comp_class_index(c, rn) >= 0) return; /* a read that can run before the alias is assigned is CRuby's NameError, which the constant's presence flag raises: keep it */ LocalVar *lv = comp_const(c, rn); if (lv && lv->const_early) return; const char *real = resolve_class_alias(c, rn); if (real && !sp_streq(real, rn)) alias_rename_add(q, t, real, alias_read_held(c, t)); } /* The exception classes named by `raise Alias, msg` and by `rescue Alias` resolve as an `Alias.foo` receiver does (rewrite_const_alias_receivers): raising the alias raises the class it names, which a `rescue` of that class's ancestor catches (Thor's AmbiguousTaskError = AmbiguousCommandError, rescued as Thor::Error). Runs after an_const_presence, so an alias some read can reach before its assignment (LocalVar.const_early) is left alone. */ static void rewrite_const_alias_raise_targets(Compiler *c) { const NodeTable *nt = c->nt; AliasRenames q = {0}; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_CallNode && nt_ref(nt, id, "receiver") < 0 && nt_str(nt, id, "name") && is_raise_alias(nt_str(nt, id, "name"))) { int args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an >= 1 && av) ask_raise_target(c, av[0], &q); } else if (k == NK_RescueNode) { int en = 0; const int *ev = nt_arr(nt, id, "exceptions", &en); for (int i = 0; i < en; i++) ask_raise_target(c, ev[i], &q); } } alias_renames_apply(c, &q); } /* For a receiverless instance_eval/exec CallNode with a literal block inside an instance method, the receiver is self (CRuby resolves it to self.instance_exec). Return that class index, else -1. The literal-block requirement (a BlockNode, not a `&b` BlockArgumentNode) keeps this distinct from a trampoline body's `instance_exec(args, &b)`, which codegen lowers via its own trampoline detector. */ int ie_implicit_self_class(Compiler *c, int id) { const NodeTable *nt = c->nt; if (nt_ref(nt, id, "receiver") >= 0) return -1; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "instance_eval") && !sp_streq(nm, "instance_exec"))) return -1; int blk = nt_ref(nt, id, "block"); if (blk < 0) return -1; NodeKind bk = nt_kind(nt, blk); /* A literal block, or a `&b` forward of the enclosing method's block (which resolves to the literal active where the method inlines). */ if (bk != NK_BlockNode && bk != NK_BlockArgumentNode) return -1; Scope *s = comp_scope_of(c, id); if (!s || s->class_id < 0 || s->is_cmethod) return -1; return s->class_id; } static int ie_self_call_names(Compiler *c, int node, const char **names, int n, int max) { const NodeTable *nt = c->nt; if (node < 0) return n; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) return n; int skip = -1; if (k == NK_CallNode) { int r = nt_ref(nt, node, "receiver"); const char *nm = nt_str(nt, node, "name"); int self_call = r < 0 || nt_kind(nt, r) == NK_SelfNode; if (nm && self_call && !ie_kernel_global(nm)) { int seen = 0; for (int i = 0; i < n; i++) if (sp_streq(names[i], nm)) seen = 1; if (!seen && n < max) names[n++] = nm; } if (self_call || (nm && (is_instance_eval_family(nm)))) skip = nt_ref(nt, node, "block"); } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, node, i); if (ch != skip) n = ie_self_call_names(c, ch, names, n, max); } int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int j = 0; j < m; j++) n = ie_self_call_names(c, ids[j], names, n, max); } return n; } /* The ivars an instance_eval/exec body reads or writes on its self, outside nested defs and nested self-rebinding blocks. */ static int ie_ivar_names(Compiler *c, int node, const char **names, int n, int max) { const NodeTable *nt = c->nt; if (node < 0) return n; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) return n; const char *ty = nt_type(nt, node); if (ty && strncmp(ty, "InstanceVariable", 16) == 0) { const char *nm = nt_str(nt, node, "name"); int seen = 0; for (int i = 0; nm && i < n; i++) if (sp_streq(names[i], nm)) seen = 1; if (nm && !seen && n < max) names[n++] = nm; } int skip = -1; if (k == NK_CallNode) { const char *cn = nt_str(nt, node, "name"); if (cn && (is_instance_eval_family(cn))) skip = nt_ref(nt, node, "block"); } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, node, i); if (ch != skip) n = ie_ivar_names(c, ch, names, n, max); } int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int j = 0; j < m; j++) n = ie_ivar_names(c, ids[j], names, n, max); } return n; } static int ie_class_answers(Compiler *c, int k, const char *nm) { return comp_method_in_chain(c, k, nm, NULL) >= 0 || comp_reader_in_chain(c, k, nm, NULL); } static int ie_poly_class_ok(Compiler *c, int k) { return !c->classes[k].is_native_class && c->classes[k].ctor_reachable && builtin_class_id(c->classes[k].name) == 0; } /* The classes a poly receiver can run an instance_eval/exec body as: those answering every self call that some class, and no top-level def, answers (*need: the first). 0 when the body makes no such call. */ int ie_poly_self_classes(Compiler *c, const char *name, int body, int *out, int max, const char **need) { const char *names[64]; int nn = ie_self_call_names(c, body, names, 0, 64); if (nn == 64) return 0; int ask[64], nask = 0; for (int i = 0; i < nn; i++) { if (comp_method_index(c, names[i]) >= 0) continue; for (int k = 0; k < c->nclasses; k++) { if (ie_poly_class_ok(c, k) && ie_class_answers(c, k, names[i])) { ask[nask++] = i; break; } } } /* A body that makes no such call but reads or writes ivars on its self (`object.instance_exec { @content = block }`, Phlex's SGML.new) runs on the classes that own every one of them. */ const char *ivs[64]; int niv = 0; if (nask == 0) { niv = ie_ivar_names(c, body, ivs, 0, 64); if (niv == 0 || niv == 64) return 0; if (need) *need = name; } else if (need) *need = names[ask[0]]; int n = 0; for (int k = 0; k < c->nclasses && n < max; k++) { if (!ie_poly_class_ok(c, k) || comp_method_in_chain(c, k, name, NULL) >= 0) continue; int all = 1; for (int i = 0; i < nask && all; i++) all = ie_class_answers(c, k, names[ask[i]]); for (int i = 0; i < niv && all; i++) all = comp_ivar_index(&c->classes[k], ivs[i]) >= 0; if (all) out[n++] = k; } return n; } static int ie_poly_mark(Compiler *c, int id, TyKind rt) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); int blk = nt_ref(nt, id, "block"); if (rt != TY_POLY || !nm || blk < 0 || nt_kind(nt, blk) != NK_BlockNode || (!sp_streq(nm, "instance_eval") && !sp_streq(nm, "instance_exec"))) return -1; int k[2]; int n = ie_poly_self_classes(c, nm, nt_ref(nt, blk, "body"), k, 2, NULL); if (n == 0) return -2 - id; /* self is still rebound when no call constrains it */ return n == 1 ? k[0] : n > 1 ? -2 - id : -1; } /* The names the shadow rename invents (`k__bp12` for a keyword `k`), each with the length of the name the program wrote, in the Compiler's open-addressed table (bp_name, bp_len, bp_mask, nbp). Only a name recorded there is an invented one: a program can write `k__bp12` itself, and that name is its own. */ static int bp_rename_slot(const Compiler *c, const char *name) { if (!c->bp_name || !name) return -1; unsigned i = sp_strhash(name) & c->bp_mask; while (c->bp_name[i]) { if (sp_streq(c->bp_name[i], name)) return (int)i; i = (i + 1) & c->bp_mask; } return -1; } static void bp_rename_insert(Compiler *c, char *name, size_t len, int blk) { unsigned i = sp_strhash(name) & c->bp_mask; while (c->bp_name[i]) i = (i + 1) & c->bp_mask; c->bp_name[i] = name; c->bp_len[i] = len; c->bp_blk[i] = blk; } static void bp_rename_record(Compiler *c, const char *name, size_t len, int blk) { int s = bp_rename_slot(c, name); if (s >= 0) { c->bp_len[s] = len; c->bp_blk[s] = blk; return; } unsigned ocap = c->bp_name ? c->bp_mask + 1 : 0; if ((unsigned)(c->nbp + 1) * 2 > ocap) { char **on = c->bp_name; size_t *ol = c->bp_len; int *ob = c->bp_blk; unsigned cap = ocap ? ocap * 2 : 64; c->bp_name = calloc(cap, sizeof(char *)); c->bp_len = calloc(cap, sizeof(size_t)); c->bp_blk = calloc(cap, sizeof(int)); c->bp_mask = cap - 1; for (unsigned i = 0; i < ocap; i++) if (on[i]) bp_rename_insert(c, on[i], ol[i], ob[i]); free(on); free(ol); free(ob); } bp_rename_insert(c, strdup(name), len, blk); c->nbp++; } int block_param_is_renamed(const Compiler *c, const char *name) { return bp_rename_slot(c, name) >= 0; } /* The length of the name a block parameter was written with: an invented `k__bp12` is still the keyword `k`; any other name is its whole self. */ size_t block_param_written_len(const Compiler *c, const char *name) { int s = bp_rename_slot(c, name); return s >= 0 ? c->bp_len[s] : strlen(name); } /* True when the program itself uses `name`. Only a name with `__bp` or `__bq` in it can clash with an invented one, so those are collected once. */ static int bp_user_uses(Compiler *c, const char *name) { const NodeTable *nt = c->nt; if (!c->bp_user_built) { c->bp_user_built = 1; int cap = 0; for (int id = 0; id < nt->count; id++) { const char *nm = nt_str(nt, id, "name"); if (!nm || (!strstr(nm, "__bp") && !strstr(nm, "__bq")) || block_param_is_renamed(c, nm)) continue; if (c->nbp_user == cap) { cap = cap ? cap * 2 : 8; c->bp_user = realloc(c->bp_user, (size_t)cap * sizeof(char *)); } c->bp_user[c->nbp_user++] = strdup(nm); } } for (int i = 0; i < c->nbp_user; i++) if (sp_streq(c->bp_user[i], name)) return 1; return 0; } /* Is `name` one block_param_invent_name made for block `blk`? The record says: it holds each invented name with its block. */ int block_param_made_for(const Compiler *c, const char *name, int blk) { int s = bp_rename_slot(c, name); return s >= 0 && c->bp_blk[s] == blk; } /* Invent the slot name for the parameter `written` of block `blk` and record it, stepping past a name the program already uses. The caller frees it. */ char *block_param_invent_name(Compiler *c, const char *written, int blk) { /* `x__bp12` for a program block, `x__bqmin_by_12` for a builtin's */ const char *tag = comp_node_tag(c, blk); const char *p = tag[0] == 'q' || tag[0] == 'p' ? "" : "p"; size_t n = strlen(written) + strlen(tag) + 32; /* "__bp", "_" */ char *buf = malloc(n); if (!buf) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } snprintf(buf, n, "%s__b%s%s", written, p, tag); for (int k = 1; bp_user_uses(c, buf); k++) snprintf(buf, n, "%s__b%s%s_%d", written, p, tag, k); bp_rename_record(c, buf, strlen(written), blk); return buf; } /* In a call-site KeywordHashNode (`k: 9, j: 2`), the value node bound to the keyword `name` -- the last when the key is written twice, as CRuby binds it -- or -1. Used to match keyword block params. */ int ie_kwhash_value(Compiler *c, int kwhash, const char *name) { const NodeTable *nt = c->nt; if (kwhash < 0 || !name) return -1; size_t nlen = block_param_written_len(c, name); int en = 0, v = -1; const int *els = nt_arr(nt, kwhash, "elements", &en); for (int i = 0; i < en; i++) { NodeKind ek = nt_kind(nt, els[i]); if (ek != NK_AssocNode) continue; int key = nt_ref(nt, els[i], "key"); NodeKind kk = nt_kind(nt, key); if (kk != NK_SymbolNode) continue; const char *kn = nt_str(nt, key, "value"); if (kn && strlen(kn) == nlen && !strncmp(kn, name, nlen)) v = nt_ref(nt, els[i], "value"); } return v; } /* The type the values of `kwhash` whose key is an expression carry (`k(1) => v`): such a key may name any keyword, so each keyword parameter takes it too. TY_UNKNOWN when every key is a literal. */ TyKind ie_kwhash_computed_type(Compiler *c, int kwhash) { const NodeTable *nt = c->nt; TyKind t = TY_UNKNOWN; int en = 0; const int *els = kwhash >= 0 ? nt_arr(nt, kwhash, "elements", &en) : NULL; for (int i = 0; i < en; i++) { if (nt_kind(nt, els[i]) != NK_AssocNode) continue; int key = nt_ref(nt, els[i], "key"), v = nt_ref(nt, els[i], "value"); if (key < 0 || v < 0 || nt_kind(nt, key) == NK_SymbolNode || nt_kind(nt, key) == NK_StringNode) continue; TyKind vt = infer_type(c, v); if (vt == TY_UNKNOWN || vt == TY_NIL) vt = TY_POLY; /* see bind_call_params */ t = t == TY_UNKNOWN ? vt : ty_unify(t, vt); } return t; } /* The trailing KeywordHashNode of a call's arguments (`k: 1`), or -1. */ int ie_call_kwhash(Compiler *c, int id) { const NodeTable *nt = c->nt; int args = nt_ref(nt, id, "arguments"); int ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; return nt_argv_kwh(nt, av, ac); } /* For a call `recv.m(cargs) { ... }` to an instance_exec trampoline `def m(p..., &b); instance_exec(tbody..., &b); end`, the node to bind/emit for the block's p-th parameter: the p-th trampoline-body arg, with a read of one of the trampoline's own positional params rewritten to the matching caller argument. Returns -1 when out of range, when not such a trampoline, or when tbody uses a splat (the existing 1:1 forwarding path handles that). ie_tramp_effective_argc returns the tbody arg count (or -1 to bail). */ static int ie_tramp_body_args(Compiler *c, int caller_id, const int **tav_out, Scope **ms_out) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, caller_id, "receiver"); if (recv < 0) return -1; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) return -1; const char *nm = nt_str(nt, caller_id, "name"); int mi = nm ? call_target(c, caller_id, ty_object_class(rt), nm, 0, NULL) : -1; if (mi < 0) return -1; Scope *ms = &c->scopes[mi]; if (ms->body < 0) return -1; int bn = 0; const int *bb = nt_arr(nt, ms->body, "body", &bn); if (bn != 1 || !bb) return -1; int targs = nt_ref(nt, bb[0], "arguments"); int tac = 0; const int *tav = targs >= 0 ? nt_arr(nt, targs, "arguments", &tac) : NULL; if (nt_argv_has_splat(nt, tav, tac)) return -1; /* forwarding path handles splat */ if (tav_out) *tav_out = tav; if (ms_out) *ms_out = ms; return tac; } int ie_tramp_effective_argc(Compiler *c, int caller_id) { return ie_tramp_body_args(c, caller_id, NULL, NULL); } int ie_tramp_effective_arg(Compiler *c, int caller_id, int p) { const NodeTable *nt = c->nt; const int *tav = NULL; Scope *ms = NULL; int tac = ie_tramp_body_args(c, caller_id, &tav, &ms); if (tac < 0 || p < 0 || p >= tac) return -1; int arg = tav[p]; NodeKind ak = nt_kind(nt, arg); if (ak == NK_LocalVariableReadNode) { const char *an = nt_str(nt, arg, "name"); for (int j = 0; j < ms->nparams; j++) { if (ms->pnames[j] && an && sp_streq(ms->pnames[j], an)) { int cargs = nt_ref(nt, caller_id, "arguments"); int cac = 0; const int *cav = cargs >= 0 ? nt_arr(nt, cargs, "arguments", &cac) : NULL; return j < cac ? cav[j] : -1; } } } return arg; /* ivar / literal / other: evaluated in the rebound-self context */ } static int ie_forward_target(Compiler *c, int call, int cls, int cm, const char *name, int receiverless); static int ie_class_value_target(Compiler *c, int id, int recv, TyKind rt, int blk); static int ie_forward_hops; /* Not re-walked per call: a poly receiver re-walks every class's method at every hop, exponential in the hop depth. */ static struct { int mi, key, val; } *ie_forward_memo; static int ie_forward_memo_cap, ie_forward_memo_n; static void ie_forward_memo_reset(void) { free(ie_forward_memo); ie_forward_memo = NULL; ie_forward_memo_cap = ie_forward_memo_n = 0; } static unsigned ie_forward_memo_hash(int mi, int key, int cap) { return ((unsigned)mi * 2654435761u ^ (unsigned)key * 40503u) & (unsigned)(cap - 1); } static int *ie_forward_memo_slot(int mi, int key) { if (ie_forward_memo_n * 2 >= ie_forward_memo_cap) { int cap = ie_forward_memo_cap ? ie_forward_memo_cap * 2 : 1024; __typeof__(ie_forward_memo) grown = malloc(sizeof(*grown) * (size_t)cap); if (!grown) return NULL; for (int i = 0; i < cap; i++) grown[i].mi = -1; for (int i = 0; i < ie_forward_memo_cap; i++) { if (ie_forward_memo[i].mi < 0) continue; unsigned h = ie_forward_memo_hash(ie_forward_memo[i].mi, ie_forward_memo[i].key, cap); while (grown[h].mi >= 0) h = (h + 1) & (unsigned)(cap - 1); grown[h] = ie_forward_memo[i]; } free(ie_forward_memo); ie_forward_memo = grown; ie_forward_memo_cap = cap; } unsigned h = ie_forward_memo_hash(mi, key, ie_forward_memo_cap); while (ie_forward_memo[h].mi >= 0) { if (ie_forward_memo[h].mi == mi && ie_forward_memo[h].key == key) return &ie_forward_memo[h].val; h = (h + 1) & (unsigned)(ie_forward_memo_cap - 1); } ie_forward_memo[h].mi = mi; ie_forward_memo[h].key = key; ie_forward_memo[h].val = INT_MIN; ie_forward_memo_n++; return &ie_forward_memo[h].val; } static int ie_forward_find(Compiler *c, int node, const Scope *s, int self_cls, int depth) { const NodeTable *nt = c->nt; if (node < 0 || depth > 64) return -1; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return -1; if (k == NK_CallNode) { const char *cn = nt_str(nt, node, "name"); int barg = nt_ref(nt, node, "block"); if (cn && barg >= 0 && nt_kind(nt, barg) == NK_BlockArgumentNode) { int bexpr = nt_ref(nt, barg, "expression"); const char *bvn = bexpr >= 0 ? nt_str(nt, bexpr, "name") : NULL; if (bexpr < 0 ? !s->blk_param[0] : (bvn && sp_streq(bvn, s->blk_param))) { int r = nt_ref(nt, node, "receiver"); TyKind rt = r >= 0 ? infer_type(c, r) : TY_UNKNOWN; int cm = r < 0 ? s->is_cmethod : rt == TY_CLASS; int rcls = r < 0 ? self_cls : ty_is_object(rt) ? ty_object_class(rt) : cm ? class_recv_static_ci(c, r) : -1; if (is_instance_eval_family(cn)) { if (!cm && (r < 0 || rcls >= 0)) return rcls; } else if (ie_forward_hops < 8) { ie_forward_hops++; int t = ie_forward_target(c, node, rcls, cm, cn, r < 0); if (t < 0 && rt == TY_POLY) t = ie_class_value_target(c, node, r, rt, barg); ie_forward_hops--; if (t >= 0) return t; } } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) { int f = ie_forward_find(c, nt_ref_at(nt, node, i), s, self_cls, depth + 1); if (f >= 0) return f; } int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) { int f = ie_forward_find(c, ids[j], s, self_cls, depth + 1); if (f >= 0) return f; } } return -1; } /* A method on `cls` that hands its own block to `X.instance_eval/exec(&blk)` runs a call site's literal block with self = X: the class of X, or -1. `name` is what call `call` runs on `cls`. */ static int ie_forward_target(Compiler *c, int call, int cls, int cm, const char *name, int receiverless) { if (!name) return -1; int mi = cls < 0 ? -1 : call_target(c, call, cls, name, cm ? CT_CMETHOD : 0, NULL); if (mi < 0 && cm && cls >= 0 && sp_streq(name, "new")) mi = comp_method_in_chain(c, cls, "initialize", NULL); if (mi < 0 && receiverless) { mi = call_target(c, call, -1, name, CT_TOP, NULL); if (mi >= 0 && c->scopes[mi].class_id >= 0) mi = -1; } if (mi < 0) return -1; const Scope *s = &c->scopes[mi]; if (!s->blk_param || s->body < 0) return -1; int *memo = ie_forward_memo_slot(mi, (cls + 1) * 16 + ie_forward_hops); if (memo && *memo != INT_MIN) return *memo; int t = ie_forward_find(c, s->body, s, cls, 0); memo = ie_forward_memo_slot(mi, (cls + 1) * 16 + ie_forward_hops); if (memo) *memo = t; return t; } static int ie_receiverless_self_class(Compiler *c, int id) { int v = ie_class_of(c, id); if (v >= 0) return v; if (v < -1) return -1; Scope *s = comp_scope_of(c, id); return (s && s->class_id >= 0 && !s->is_cmethod) ? s->class_id : -1; } int ie_block_body(Compiler *c, int blk) { const NodeTable *nt = c->nt; int x = nt_kind(nt, blk) == NK_BlockArgumentNode ? nt_ref(nt, blk, "expression") : -1; if (x < 0 || nt_kind(nt, x) != NK_LocalVariableReadNode) return nt_ref(nt, blk, "body"); Scope *sc = comp_scope_of(c, x); const char *vn = nt_str(nt, x, "name"); int body = -1; for (int w = comp_lvw_first_sc(c, (int)(sc - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn) || comp_scope_of(c, w) != sc) continue; int v = nt_ref(nt, w, "value"); if (body >= 0 || !is_proc_create(c, v)) return -1; body = nt_ref(nt, nt_kind(nt, v) == NK_LambdaNode ? v : nt_ref(nt, v, "block"), "body"); } return body; } /* The names a method that takes a block can be reached by: its own, and any alias (method lookup resolves one to another name). Listed once per build_ie_map, whose walk alone asks; a name in neither forwards nothing on any class, so a boxed receiver's call need not ask each class. `new` reaches an initialize and is always asked. */ static ANameHash g_ief_names; static int g_ief_ready; static int ie_name_may_forward(Compiler *c, const char *nm) { if (!nm) return 0; if (sp_streq(nm, "new")) return 1; if (!g_ief_ready) { anh_free(&g_ief_names); memset(&g_ief_names, 0, sizeof g_ief_names); for (int s = 0; s < c->nscopes; s++) if (c->scopes[s].name && c->scopes[s].blk_param && c->scopes[s].body >= 0 && !anh_has(&g_ief_names, c->scopes[s].name)) anh_add(&g_ief_names, c->scopes[s].name); for (int k = 0; k < c->nclasses; k++) for (int a = 0; a < c->classes[k].naliases; a++) if (!anh_has(&g_ief_names, c->classes[k].alias_new[a])) anh_add(&g_ief_names, c->classes[k].alias_new[a]); g_ief_ready = 1; } return anh_has(&g_ief_names, nm); } static int ie_class_value_target(Compiler *c, int id, int recv, TyKind rt, int blk) { const char *nm = nt_str(c->nt, id, "name"); int arg = nt_kind(c->nt, blk) == NK_BlockArgumentNode, cls = -1; if (rt == TY_CLASS && is_instance_eval_family(nm)) return -2 - id; if (rt == TY_CLASS) return ie_forward_target(c, id, class_recv_static_ci(c, recv), 1, nm, 0); int may = rt == TY_POLY && ie_name_may_forward(c, nm); /* The classes whose chain answers the name, on each side, in place of every class on both: a class whose chain does not answer forwards nothing (ie_forward_target: no method, no target), `new` aside, which reaches an initialize and so asks every class. Asked class by class, a boxed call with a block cost a chain lookup per class and side, 2,600 on a Rails application, every round: half of build_ie_map. The answer is the one target every class agrees on, or the ambiguity, whichever order the classes come in. The lists are copied out: a forward's walk may ask the chain memo, which keeps them. */ int listed = may && !sp_streq(nm, "new"); for (int side = 0; listed && cls >= -1 && side < 2; side++) { int n = 0; const int *rs = comp_name_responders(c, nm, side, &n); if (n < 0) { listed = 0; break; } int *ks = n > 0 ? malloc(sizeof(int) * (size_t)n) : NULL; if (n > 0 && !ks) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < n; i++) ks[i] = rs[2 * i]; for (int i = 0; i < n && cls >= -1; i++) { int t = ie_forward_target(c, id, ks[i], side, nm, 0); if (t >= 0) cls = cls == -1 || cls == t ? t : -2 - id; } free(ks); } for (int k = 0; may && !listed && cls >= -1 && k < 2 * c->nclasses; k++) { int t = ie_forward_target(c, id, k / 2, k % 2, nm, 0); if (t >= 0) cls = cls == -1 || cls == t ? t : -2 - id; } const char *names[64]; int nn = rt == TY_POLY && cls >= 0 ? ie_self_call_names(c, ie_block_body(c, blk), names, 0, 64) : 0; for (int i = 0; i < nn; i++) for (int k = 0; k < c->nclasses && !ie_class_answers(c, cls, names[i]) && comp_method_index(c, names[i]) < 0; k++) if (ie_poly_class_ok(c, k) && ie_class_answers(c, k, names[i])) return -1; return cls < -1 && !arg && sp_streq(nm, "new") ? -1 : cls; } /* `recv.instance_exec(args, &pr)` where `pr` is a positional or keyword parameter of the enclosing def: the proc literals its call sites hand that parameter are the blocks run with self = `cls`, so their bodies are marked (#7213). Answers 1 when every call site passes a proc literal, nil or nothing there, 0 when one passes anything else (which this cannot trace), -1 when `pr` is not such a parameter. */ /* applied after the enclosing blocks' own marks, which would cover them */ static int ie_param_marks[256][2], ie_param_n; static int ie_param_literals(Compiler *c, int blk, int cls) { const NodeTable *nt = c->nt; int x = nt_ref(nt, blk, "expression"); if (x < 0 || nt_kind(nt, x) != NK_LocalVariableReadNode) return -1; const char *pn = nt_str(nt, x, "name"); Scope *sc = comp_scope_of(c, x); if (!pn || !sc || sc->def_node < 0 || nt_kind(nt, sc->def_node) != NK_DefNode || !sc->name) return -1; if (sc->blk_param && sp_streq(sc->blk_param, pn)) return -1; int def = sc->def_node, params = nt_ref(nt, def, "parameters"); if (params < 0) return -1; /* its position among the positionals, or its keyword */ int pos = -1, kw = 0, at = 0; static const char *const pos_lists[] = { "requireds", "optionals" }; for (int l = 0; l < 2 && pos < 0; l++) { int n = 0; const int *ps = nt_arr(nt, params, pos_lists[l], &n); for (int i = 0; i < n; i++, at++) if (sp_streq(nt_str(nt, ps[i], "name"), pn)) { pos = at; break; } } if (pos < 0) { int n = 0; const int *ks = nt_arr(nt, params, "keywords", &n); for (int i = 0; i < n && !kw; i++) if (sp_streq(nt_str(nt, ks[i], "name"), pn)) kw = 1; if (!kw) return -1; } /* a write to it in the body could hand instance_exec another proc */ if (comp_lvw_any_sc(c, (int)(sc - c->scopes), pn)) return 0; int traced = 1; NT_FOREACH_KIND(nt, NK_CallNode, u) { if (!sp_streq(nt_str(nt, u, "name"), sc->name)) continue; int args = nt_ref(nt, u, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; int npos = an, kh = nt_argv_kwh(nt, av, an); if (kh >= 0) npos--; if (nt_argv_has_splat(nt, av, npos)) { traced = 0; npos = 0; } int v = -1; if (!kw) v = pos < npos ? av[pos] : -1; else if (kh >= 0) { int en = 0; const int *el = nt_arr(nt, kh, "elements", &en); for (int i = 0; i < en; i++) { if (nt_kind(nt, el[i]) != NK_AssocNode) { traced = 0; continue; } int k = nt_ref(nt, el[i], "key"); if (nt_kind(nt, k) == NK_SymbolNode && sp_streq(nt_str(nt, k, "value"), pn)) v = nt_ref(nt, el[i], "value"); } } if (v < 0 || nt_kind(nt, v) == NK_NilNode) continue; if (!is_proc_create(c, v)) { traced = 0; continue; } int body = nt_ref(nt, nt_kind(nt, v) == NK_LambdaNode ? v : nt_ref(nt, v, "block"), "body"); if (body >= 0 && cls >= 0 && ie_param_n < (int)(sizeof ie_param_marks / sizeof ie_param_marks[0])) { ie_param_marks[ie_param_n][0] = body; ie_param_marks[ie_param_n++][1] = cls; } } return traced; } static int ie_builtin_recv(Compiler *c, int recv) { TyKind t = infer_type(c, recv); return t != TY_UNKNOWN && t != TY_POLY && t != TY_VOID && t != TY_CLASS && !ty_is_object(t); } static int ie_untraced_site(Compiler *c, int id) { const NodeTable *nt = c->nt; int blk = nt_ref(nt, id, "block"); if (!is_instance_eval_family(nt_str(nt, id, "name")) || nt_ref(nt, id, "receiver") < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockArgumentNode) return 0; int x = nt_ref(nt, blk, "expression"); NodeKind k = x >= 0 ? nt_kind(nt, x) : NK_NONE; if (k == NK_LocalVariableReadNode) { Scope *sc = comp_scope_of(c, x); const char *pn = nt_str(nt, x, "name"); int params = sc && sc->def_node >= 0 ? nt_ref(nt, sc->def_node, "parameters") : -1; if (!pn || (sc && sc->blk_param && sp_streq(sc->blk_param, pn))) return 0; if (ie_block_body(c, blk) >= 0) return ie_builtin_recv(c, nt_ref(nt, id, "receiver")); static const char *const lists[] = { "requireds", "optionals", "keywords" }; for (int l = 0; l < 3 && params >= 0; l++) { int n = 0; const int *ps = nt_arr(nt, params, lists[l], &n); for (int i = 0; i < n; i++) if (sp_streq(nt_str(nt, ps[i], "name"), pn)) return ie_builtin_recv(c, nt_ref(nt, id, "receiver")); } return 1; } return k == NK_InstanceVariableReadNode || k == NK_CallNode; } static int ie_rself_name_selfish(Compiler *c, const char *nm) { static const char *const kn[] = { "send", "__send__", "public_send", "instance_variable_get", "instance_variable_set", "instance_variable_defined?", "instance_variables", "method", "methods", "public_method", "respond_to?", "singleton_class", "define_singleton_method", "extend", "is_a?", "kind_of?", "instance_of?", "class", "frozen?", "freeze", "object_id", "hash", "inspect", "to_s", "dup", "clone", "tap", "then", "itself", "display", "instance_eval", "instance_exec", "binding", NULL }; for (int i = 0; kn[i]; i++) if (sp_streq(nm, kn[i])) return 1; for (int s = 0; s < c->nscopes; s++) if (c->scopes[s].name && c->scopes[s].class_id >= 0 && !c->scopes[s].is_cmethod && sp_streq(c->scopes[s].name, nm)) return 1; for (int k = 0; k < c->nclasses; k++) if (comp_reader_in_chain(c, k, nm, NULL)) return 1; return 0; } /* The self_call marker tracks receiver identity through rebinding, rather than private-call permission (nt_ignore_visibility). A call whose receiver is the block's self though no SelfNode spells it: a receiverless call to a name only a class method answers, or a class body's `self.m` / `m` the body desugar gave its class as the receiver. It rebinds with the block's self, so it takes a `self.` receiver before marking. */ static int ie_rself_cmethod_call(Compiler *c, int n) { const NodeTable *nt = c->nt; if (nt_kind(nt, n) != NK_CallNode) return 0; int r = nt_ref(nt, n, "receiver"); if (r >= 0) return nt_int(nt, n, "self_call", 0) && (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode); const char *nm = nt_str(nt, n, "name"); if (!nm || is_kernel_module_function(nm) || call_target(c, n, -1, nm, CT_TOP, NULL) >= 0) return 0; for (int s = 0; s < c->nscopes; s++) if (c->scopes[s].name && c->scopes[s].class_id >= 0 && c->scopes[s].is_cmethod && sp_streq(c->scopes[s].name, nm)) return 1; return 0; } static void ie_rself_give_self(Compiler *c, int n) { NodeTable *nt = (NodeTable *)c->nt; if (n < 0) return; NodeKind k = nt_kind(nt, n); if (k == NK_BlockNode || k == NK_LambdaNode) return; if (ie_rself_cmethod_call(c, n)) { int sn = nt_new_node(nt, "SelfNode"); if (sn >= 0) { comp_grow_node_arrays(c); c->nscope[sn] = c->nscope[n]; if (c->node_cbody && n < c->node_cap && sn < c->node_cap) c->node_cbody[sn] = c->node_cbody[n]; nt_node_set_ref(nt, n, "receiver", sn); nt_set_vcall(nt, n, 0); } } for (int i = 0, nr = nt_num_refs(nt, n); i < nr; i++) ie_rself_give_self(c, nt_ref_at(nt, n, i)); for (int i = 0, na = nt_num_arrs(nt, n); i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) { ie_rself_give_self(c, ids[j]); ids = nt_arr_at(nt, n, i, &m); } } } static int ie_rself_use(Compiler *c, int n, int nested) { const NodeTable *nt = c->nt; if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_SelfNode) return nested ? 2 : 1; const char *ty = nt_type(nt, n); if (k == NK_SuperNode || k == NK_ForwardingSuperNode || k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || (ty && !strncmp(ty, "InstanceVariable", 16))) return 2; if (k == NK_CallNode && nt_ref(nt, n, "receiver") < 0 && nt_str(nt, n, "name") && ie_rself_name_selfish(c, nt_str(nt, n, "name"))) return 2; if (ie_rself_cmethod_call(c, n)) return nested ? 2 : 1; int in = nested || k == NK_BlockNode || k == NK_LambdaNode, r = 0; for (int i = 0, nr = nt_num_refs(nt, n); i < nr; i++) r |= ie_rself_use(c, nt_ref_at(nt, n, i), in); for (int i = 0, na = nt_num_arrs(nt, n); i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) r |= ie_rself_use(c, ids[j], in); } return r; } static int ie_rself_mark(NodeTable *nt, int n) { if (n < 0) return -1; NodeKind k = nt_kind(nt, n); if (k == NK_BlockNode || k == NK_LambdaNode) return -1; int at = k == NK_SelfNode ? n : -1; if (at >= 0) nt_node_set_int(nt, n, "ie_rself", 1); for (int i = 0, nr = nt_num_refs(nt, n); i < nr; i++) { int x = ie_rself_mark(nt, nt_ref_at(nt, n, i)); if (at < 0) at = x; } for (int i = 0, na = nt_num_arrs(nt, n); i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) { int x = ie_rself_mark(nt, ids[j]); if (at < 0) at = x; } } return at; } static void ie_rself_literal(Compiler *c, int lit) { NodeTable *nt = (NodeTable *)c->nt; int body = nt_ref(nt, lit, "body"), params = nt_ref(nt, lit, "parameters"); int u = ie_rself_use(c, body, 0) | ie_rself_use(c, params, 0); if (body < 0 || !u) return; nt_node_set_int(nt, body, "ie_rself_lit", u & 2 ? 2 : 1); if (u & 2) return; ie_rself_give_self(c, params); ie_rself_give_self(c, body); int at = ie_rself_mark(nt, params), bt = ie_rself_mark(nt, body); nt_node_set_int(nt, body, "ie_rself_at", (at >= 0 ? at : bt) + 1); } static int ie_new_takes_block(Compiler *c, int recv) { const NodeTable *nt = c->nt; NodeKind rk = nt_kind(nt, recv); int ci = rk == NK_ConstantReadNode || rk == NK_ConstantPathNode ? const_node_class_index(c, recv) : -1; if (ci < 0) return -1; int mi = comp_cmethod_in_chain(c, ci, "new", NULL); if (mi < 0) mi = comp_method_in_chain(c, ci, "initialize", NULL); return mi >= 0 && c->scopes[mi].blk_param; } static void ie_rself_mark_literals(Compiler *c); static void mark_ie_rebindable_literals(Compiler *c) { const NodeTable *nt = c->nt; int any = 0; NT_FOREACH_KIND(nt, NK_CallNode, id) if (ie_untraced_site(c, id)) { nt_node_set_int((NodeTable *)nt, id, "ie_usite", 1); any = 1; } if (any) ie_rself_mark_literals(c); } static void ie_rself_mark_literals(Compiler *c) { static int done; const NodeTable *nt = c->nt; if (done++) return; NT_FOREACH_KIND(nt, NK_LambdaNode, l) ie_rself_literal(c, l); NT_FOREACH_KIND(nt, NK_CallNode, id) { int blk = nt_ref(nt, id, "block"), r = nt_ref(nt, id, "receiver"); const char *nm = nt_str(nt, id, "name"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || !nm) continue; int take = r < 0 && (sp_streq(nm, "lambda") || sp_streq(nm, "proc")); take |= r >= 0 && sp_streq(nm, "new") && nt_kind(nt, r) == NK_ConstantReadNode && sp_streq(nt_str(nt, r, "name"), "Proc"); int known = !take && r >= 0 && sp_streq(nm, "new") ? ie_new_takes_block(c, r) : -1; if (known >= 0) take = known; for (int s = 0; s < c->nscopes && !take && known < 0; s++) take = c->scopes[s].name && c->scopes[s].blk_param && sp_streq(c->scopes[s].name, sp_streq(nm, "new") ? "initialize" : nm); if (take) ie_rself_literal(c, blk); } } /* (Re)build the instance_eval/exec node→class map from current receiver types. */ void build_ie_map(Compiler *c) { const NodeTable *nt = c->nt; /* sized to nt->count, which can grow mid-analysis when forwarded callables are desugared into synthetic blocks; resize so per-node writes stay bounded */ if (g_ie_node_class_cap < nt->count) { int *grown = realloc(g_ie_node_class, sizeof(int) * (size_t)nt->count); if (!grown) return; /* OOM: keep the old map rather than leak/deref NULL */ unsigned char *gn = realloc(g_ie_nonobj, (size_t)nt->count); if (!gn) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = g_ie_node_class_cap; i < nt->count; i++) { grown[i] = -1; gn[i] = 0; } g_ie_node_class = grown; g_ie_nonobj = gn; g_ie_node_class_cap = nt->count; } /* (each entry that changes moves its node's scope: the build runs inferences of its own, which read the map as it stands) */ for (int i = 0; i < g_ie_node_class_cap; i++) if (g_ie_nonobj[i]) { g_ie_nonobj[i] = 0; comp_touch_node(c, i); } int *pend = malloc(sizeof(int) * (size_t)nt->count); if (!pend) return; int pend_n = nt->count; ie_forward_memo_reset(); g_ief_ready = 0; for (int i = 0; i < nt->count; i++) { pend[i] = -1; if (g_ie_node_class[i] != -1) { g_ie_node_class[i] = -1; comp_touch_node(c, i); } } ie_param_n = 0; for (int pass = 0; pass < 2; pass++) for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int recv = nt_ref(nt, id, "receiver"); int blk = nt_ref(nt, id, "block"); if (blk < 0 || (!pass && nt_kind(nt, blk) != NK_BlockArgumentNode)) continue; int cls; if (recv < 0) { /* receiverless instance_eval/exec inside an instance method: self. */ cls = ie_implicit_self_class(c, id); if (cls < 0) cls = ie_forward_target(c, id, ie_receiverless_self_class(c, id), 0, nm, 1); if (cls < 0 && id < c->node_cap) cls = ie_forward_target(c, id, c->node_cbody[id], 1, nm, 0); if (cls < 0) continue; } else { TyKind rt = infer_type(c, recv); cls = ty_is_object(rt) ? ty_object_class(rt) : ie_poly_mark(c, id, rt); if (cls == -1 && !ty_is_object(rt)) cls = ie_class_value_target(c, id, recv, rt, blk); /* (a boxed receiver no class of the program's can be -- Object.new -- counts too: ie_poly_mark found none, or found the self rebound with no class it must be, -2 - id; either way its ivars can read nil. The mark is the nil question's alone, so it only widens.) */ if (cls < 0 && pass && rt != TY_UNKNOWN && !ty_is_object(rt) && (sp_streq(nm, "instance_eval") || sp_streq(nm, "instance_exec")) && nt_kind(nt, blk) == NK_BlockNode) mark_ie_nonobj(c, blk); if (cls == -1 && !pass && !nt_int(nt, id, "ie_usite", 0) && ie_untraced_site(c, id)) { nt_node_set_int((NodeTable *)nt, id, "ie_usite", 1); ie_rself_mark_literals(c); } if (cls == -1) continue; if (ty_is_object(rt) && !sp_streq(nm, "instance_eval") && !sp_streq(nm, "instance_exec")) { /* not a direct instance_eval/exec: maybe a trampoline method on `cls`? */ if (!comp_trampoline_kind(c, cls, nm, NULL)) { cls = ie_forward_target(c, id, cls, 0, nm, 0); if (cls < 0) continue; } } } int body = ie_block_body(c, blk); if (body < 0 && !pass && nt_kind(nt, blk) == NK_BlockArgumentNode && is_instance_eval_family(nm)) { int tr = ie_param_literals(c, blk, cls); if (tr >= 0) nt_node_set_int((NodeTable *)nt, id, "ie_param_traced", tr + 1); } if (body < 0) continue; if (pass) pend[body] = pend[body] == -1 || pend[body] == cls ? cls : -2 - id; mark_ie_subtree(c, body, pass ? pend[body] : cls); } for (int i = 0; i < pend_n; i++) if (pend[i] != -1) mark_ie_subtree(c, i, pend[i]); for (int i = 0; i < ie_param_n; i++) mark_ie_subtree(c, ie_param_marks[i][0], ie_param_marks[i][1]); ie_param_n = 0; free(pend); } /* The receiver class for a node inside an instance_eval/exec block, or -1. */ int ie_class_of(Compiler *c, int node) { (void)c; /* g_ie_node_class is sized to nt->count per fixpoint iteration; a node synthesized mid-iteration (id >= cap) has no instance_eval receiver yet. */ return (g_ie_node_class && node >= 0 && node < g_ie_node_class_cap) ? g_ie_node_class[node] : -1; } int *ie_body_retype(Compiler *c, int body, int cls) { int n = c->nt->count; if (body < 0 || !g_ie_node_class || g_ie_node_class_cap < n || g_ie_node_class[body] == cls) return NULL; int *snap = malloc(sizeof(int) * (2 * (size_t)n + 1)); if (!snap) return NULL; snap[0] = n; for (int i = 0; i < n; i++) { snap[1 + i] = (int)c->ntype[i]; snap[1 + n + i] = g_ie_node_class[i]; } mark_ie_subtree(c, body, cls); if (cls >= 0) infer_subtree(c, body); return snap; } void ie_body_restore(Compiler *c, int *snap) { if (!snap) return; int n = snap[0]; for (int i = 0; i < n; i++) { comp_set_ntype(c, i, (TyKind)snap[1 + i]); if (g_ie_node_class[i] != snap[1 + n + i]) { g_ie_node_class[i] = snap[1 + n + i]; comp_touch_node(c, i); } } free(snap); } int ie_poly_classes_at(Compiler *c, int node, int *out, int max) { int v = ie_class_of(c, node); if (v >= -1) return 0; int call = -2 - v; int blk = nt_ref(c->nt, call, "block"); return ie_poly_self_classes(c, nt_str(c->nt, call, "name"), nt_ref(c->nt, blk, "body"), out, max, NULL); } /* Whether `self` at node is top-level self, the main object (#4926): no enclosing class and no instance_eval/exec receiver rebinding it. */ int self_is_main(Compiler *c, int node) { return !nt_int(c->nt, node, "ie_rself", 0) && self_is_main_raw(c, node); } int self_is_main_raw(Compiler *c, int node) { Scope *s = comp_scope_of(c, node); return s && s->class_id < 0 && an_ie_class_id < 0 && ie_class_of(c, node) == -1 && self_class_body(c, node) < 0; } /* The class whose body `self` at node sits in -- a statement of a class or module body (or a block there), outside any def and not rebound by an instance_eval/exec -- where `self` is that class object: cgi/escape.rb's `target = defined?(CGI::EscapeExt) && ... ? CGI::EscapeExt : self` in the body of CGI::Escape. -1 elsewhere (a method, top level, a rebound self). */ int self_class_body(Compiler *c, int node) { Scope *s = comp_scope_of(c, node); if (!s || s->class_id >= 0) return -1; if (an_ie_class_id >= 0 || ie_class_of(c, node) != -1) return -1; if (!c->node_cbody || node < 0 || node >= c->node_cap) return -1; return c->node_cbody[node]; } /* A receiver-less private / protected / public / module_function in a class or module body, bare or naming its methods by Symbol literals, `def`s or one Array of Symbols. Its value is nil bare (VIS_DECL_NIL), the name for one Symbol or def (VIS_DECL_SYM), and the Array of names otherwise (VIS_DECL_ARRAY). private_class_method / public_class_method naming Symbols or `def self.m`s answer the class itself (VIS_DECL_SELF). 0 when `id` is not one of these. */ int vis_decl_call(Compiler *c, int id) { const NodeTable *nt = c->nt; if (id < 0 || nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) return 0; if (nt_ref(nt, id, "block") >= 0) return 0; const char *nm = nt_str(nt, id, "name"); if (!nm) return 0; int cm = sp_streq(nm, "private_class_method") || sp_streq(nm, "public_class_method"); if (!cm && !sp_streq(nm, "private") && !sp_streq(nm, "protected") && !sp_streq(nm, "public") && !sp_streq(nm, "module_function")) return 0; if (self_class_body(c, id) < 0) return 0; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an == 0) return cm ? 0 : VIS_DECL_NIL; if (an == 1 && nt_kind(nt, av[0]) == NK_ArrayNode) { int en = 0; const int *ev = nt_arr(nt, av[0], "elements", &en); for (int i = 0; i < en; i++) if (nt_kind(nt, ev[i]) != NK_SymbolNode) return 0; return cm ? VIS_DECL_SELF : VIS_DECL_ARRAY; } for (int i = 0; i < an; i++) { NodeKind k = nt_kind(nt, av[i]); if (k == NK_SymbolNode) continue; if (k == NK_DefNode && (nt_ref(nt, av[i], "receiver") >= 0) == cm) continue; return 0; } return cm ? VIS_DECL_SELF : an == 1 ? VIS_DECL_SYM : VIS_DECL_ARRAY; } /* A receiver-less attr_reader / attr_writer / attr_accessor / attr in a class body whose names are all Symbol literals: its value is the Array of the method names it defines. 1 when `id` is one. */ int attr_decl_call(Compiler *c, int id) { const NodeTable *nt = c->nt; if (id < 0 || nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) return 0; if (nt_ref(nt, id, "block") >= 0) return 0; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "attr_reader") && !sp_streq(nm, "attr_writer") && !sp_streq(nm, "attr_accessor") && !sp_streq(nm, "attr"))) return 0; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an < 1) return 0; for (int i = 0; i < an; i++) { if (nt_kind(nt, av[i]) != NK_SymbolNode) return 0; } return self_class_body(c, id) >= 0; } /* Register an ivar first assigned inside an instance_exec/instance_eval block on the block's receiver class. register_locals only interns ivar writes whose enclosing scope is a class body or method; an ivar written solely inside a lifted iexec block has no such scope, so without this it gets no struct slot (and any read of it fails to resolve). Runs in the fixpoint right after build_ie_map; returns 1 when it adds a new slot so the fixpoint re-runs and infers the new ivar's type from its assignment. */ int register_ie_block_ivars(Compiler *c) { const NodeTable *nt = c->nt; if (!g_ie_node_class) return 0; int changed = 0; for (int id = 0; id < nt->count; id++) { int cls = g_ie_node_class[id]; if (cls < 0) continue; const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty) continue; if (k != NK_InstanceVariableWriteNode && k != NK_InstanceVariableOperatorWriteNode && k != NK_InstanceVariableOrWriteNode && k != NK_InstanceVariableAndWriteNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; ClassInfo *ci = &c->classes[cls]; int iv = comp_ivar_index(ci, nm); if (iv < 0) { iv = comp_ivar_intern(ci, nm); changed = 1; } /* Type the slot from the assignment value. The general ivar-write inference keys on the write's enclosing scope class_id, which for a lifted iexec block is the toplevel/method, not the receiver -- so it never types this slot. An ivar written only inside iexec blocks has no competing writer, so widening it here from the value type is safe. */ int v = nt_ref(nt, id, "value"); if (v >= 0 && !class_ivar_pinned(ci, nm)) { /* --rbs seed pins are authoritative */ TyKind vt = infer_type(c, v); if (vt != TY_UNKNOWN && vt != TY_NIL) { TyKind cur = ci->ivar_types[iv]; TyKind nu = (cur == TY_UNKNOWN) ? vt : ty_unify(cur, vt); if (nu != cur) { ivar_set_type(ci, iv, nu); changed = 1; } } } } return changed; } /* ---- Block/lambda parameter alpha-renaming ---------------------------- * Block and lambda parameters are interned into the *enclosing* scope, so a * parameter sharing a name with an enclosing local collapses onto a single * LocalVar (hence one type), corrupting both. Ruby semantics say the two are * distinct (the parameter shadows). When the name is also assigned outside the * block body -- the case that pollutes the shared type -- rename the parameter * and its in-body references to a fresh, collision-free name so they become * separate variables. Runs before walk_scope so all downstream interning and * codegen see the disambiguated names. */ /* The ParametersNode for a block (BlockParametersNode -> ParametersNode) or a lambda (ParametersNode directly). -1 if none / not a plain ParametersNode. */ int blkp_params_node(Compiler *c, int create) { const NodeTable *nt = c->nt; int pn = nt_ref(nt, create, "parameters"); if (pn < 0) return -1; NodeKind pk = nt_kind(nt, pn); if (pk == NK_BlockParametersNode) pn = nt_ref(nt, pn, "parameters"); return pn; } /* Does parameter node `p` bind `name`, directly or inside a destructuring `(a, (b, *c))`? */ static int blkp_param_binds(const NodeTable *nt, int p, const char *name) { if (p < 0) return 0; NodeKind pk = nt_kind(nt, p); if (pk == NK_MultiTargetNode) { const char *arrs[2] = { "lefts", "rights" }; for (int a = 0; a < 2; a++) { int n = 0; const int *ids = nt_arr(nt, p, arrs[a], &n); for (int i = 0; i < n; i++) if (blkp_param_binds(nt, ids[i], name)) return 1; } int r = nt_ref(nt, p, "rest"); if (r >= 0 && blkp_param_binds(nt, nt_ref(nt, r, "expression"), name)) return 1; return 0; } const char *pn = nt_str(nt, p, "name"); return pn && sp_streq(pn, name); } int blkp_binds_param(Compiler *c, int create, const char *name) { const NodeTable *nt = c->nt; int bp = nt_ref(nt, create, "parameters"); if (bp >= 0 && nt_kind(nt, bp) == NK_BlockParametersNode) { int ln = 0; const int *locs = nt_arr(nt, bp, "locals", &ln); for (int i = 0; i < ln; i++) if (blkp_param_binds(nt, locs[i], name)) return 1; } /* A block-local (`|b; q|`) is only in the block's comma-joined `locals` string, which lists every name new to the block; an outer name the block merely reads or writes is not in it. */ const char *ls = nt_str(nt, create, "locals"); size_t nl = strlen(name); for (const char *p = ls; p && *p; ) { const char *e = strchr(p, ','); size_t seg = e ? (size_t)(e - p) : strlen(p); if (seg == nl && !strncmp(p, name, nl)) return 1; if (!e) break; p = e + 1; } int pn = blkp_params_node(c, create); if (pn < 0) return 0; NodeKind pk = nt_kind(nt, pn); if (pk != NK_ParametersNode) return 0; const char *arrs[4] = { "requireds", "optionals", "posts", "keywords" }; for (int a = 0; a < 4; a++) { int n = 0; const int *ids = nt_arr(nt, pn, arrs[a], &n); for (int i = 0; i < n; i++) if (blkp_param_binds(nt, ids[i], name)) return 1; } const char *refs[3] = { "rest", "keyword_rest", "block" }; for (int a = 0; a < 3; a++) if (blkp_param_binds(nt, nt_ref(nt, pn, refs[a]), name)) return 1; return 0; } /* Rewrite references to `oldn` -> `newn`, stopping at nested defs/classes and at nested blocks/lambdas that re-bind `oldn`. */ void blkp_rewrite_refs(Compiler *c, int node, const char *oldn, const char *newn) { if (node < 0) return; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return; /* a def's receiver (`def x.m`) is read in the enclosing scope; the rest of a def, and a class or module body, is a scope of its own */ if (ty_k == NK_DefNode) { blkp_rewrite_refs(c, nt_ref(nt, node, "receiver"), oldn, newn); return; } if (ty_k == NK_ClassNode || ty_k == NK_ModuleNode) return; if ((ty_k == NK_BlockNode || ty_k == NK_LambdaNode) && blkp_binds_param(c, node, oldn)) return; if (a_is_local_node(ty)) { const char *nm = nt_str(nt, node, "name"); if (nm && sp_streq(nm, oldn)) nt_set_str((NodeTable *)nt, node, "name", newn); } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) blkp_rewrite_refs(c, nt_ref_at(nt, node, i), oldn, newn); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int k = 0; k < n; k++) blkp_rewrite_refs(c, ids[k], oldn, newn); } } void blkp_mark_subtree(const NodeTable *nt, int node, char *marks) { if (node < 0) return; marks[node] = 1; int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) blkp_mark_subtree(nt, nt_ref_at(nt, node, i), marks); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int k = 0; k < n; k++) blkp_mark_subtree(nt, ids[k], marks); } } /* Generation-stamping variant: writes `gen` instead of 1, so the membership array can be reused across blocks without an O(n) memset per block (a node is "in body" iff stamp[node] == gen). */ static void blkp_stamp_subtree(const NodeTable *nt, int node, int *stamp, int gen) { if (node < 0) return; stamp[node] = gen; int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) blkp_stamp_subtree(nt, nt_ref_at(nt, node, i), stamp, gen); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int k = 0; k < n; k++) blkp_stamp_subtree(nt, ids[k], stamp, gen); } } /* ---- Colliding nested-constant qualification -------------------------- * Constants live in a flat cst_ namespace, so `RootNS::Mid::LEAF` and * `Lex::RootNS::Mid::LEAF` collide. When the same constant name is written * under 2+ distinct module paths, rename each nested write to a qualified * `__..__` and rewrite every path read to whichever qualified * constant it denotes (relative reads prefer the lexically enclosing module * chain; `::`-anchored reads resolve from the root). Collision-gated: programs * with unique constant names are untouched. */ /* QCWrite: moved to analyze_internal.h */ /* The module path segments a compact header names before its leaf -- `class A::B::C` gives ["A", "B"] -- into segs, outermost first: the body of `class A::B::C` is lexically inside A::B::C, as with `module A; module B; class C`, so these passes push A and B too. Answers their count, or 0 for a plain `class C`, a `class ::C`, or a path that is not all plain constant names or reaches into a builtin's namespace (`class IO::Buffer`, whose runtime and package classes are registered by the leaf). */ /* The constants holding a module (`A = ::P`), with the module path each stands for (tl segments, 0 when unknown), recorded by qc_resolve_headers in source order. qc_hdr_alias[cp] is the entry, without a name, that holds the full module path of a compact header reached through one, or -1. */ typedef struct { int depth; char scope[QC_MAXDEPTH][64]; char name[64], orig[64]; int tl; char target[QC_MAXDEPTH][64]; int sd; } QCAlias; static QCAlias *qc_aliases = NULL; /* a lookup scope: the body path[0..n) */ typedef struct { char (*path)[64]; int n; } QCScope; static int qc_naliases = 0, qc_caliases = 0; static int *qc_hdr_alias = NULL; /* the named entries again as writes (node: the entry's index), for the ancestor lookup reads use, and the program the walk is over */ static QCWrite *qc_alias_ws = NULL; static int qc_nalias_ws = 0, qc_calias_ws = 0; static const NodeTable *qc_hdr_nt = NULL; static Compiler *qc_hdr_cc = NULL; static int qc_nhdr = 0; static int qc_builtin_head(char (*chain)[64], int cl, int *abs_anchor); static int qc_header_segs(const NodeTable *nt, int cp, char (*segs)[64]) { if (cp < 0 || nt_kind(nt, cp) != NK_ConstantPathNode) return 0; const char *rev[QC_MAXDEPTH]; int ns = 0, builtin = 0; for (int q = nt_ref(nt, cp, "parent"); ; ) { if (q < 0 || ns >= QC_MAXDEPTH) return 0; NodeKind qk = nt_kind(nt, q); if (qk != NK_ConstantReadNode && qk != NK_ConstantPathNode) return 0; const char *nm = nt_str(nt, q, "name"); if (!nm) return 0; builtin |= is_builtin_class_name(nm) || is_builtin_module_name(nm); rev[ns++] = nm; if (qk == NK_ConstantReadNode) break; q = nt_ref(nt, q, "parent"); } /* a path through a compact header under a builtin (`module Regexp::Expression` before `module Regexp::Expression::Escape`) is the body that header opened ([Expression]); any other builtin segment leaves the header registered by its leaf */ if (builtin) { char bchain[QC_MAXDEPTH + 1][64]; for (int i = 0; i < ns; i++) snprintf(bchain[i], 64, "%s", rev[ns - 1 - i]); int abs_anchor = 0, bl = qc_builtin_head(bchain, ns, &abs_anchor); if (!abs_anchor || bl < 1 || bl >= QC_MAXDEPTH) return 0; for (int i = 0; i < bl; i++) { if (is_builtin_class_name(bchain[i]) || is_builtin_module_name(bchain[i])) return 0; snprintf(segs[i], 64, "%s", bchain[i]); } return bl; } /* a head that is a constant holding a module (`M = X; class M::Foo`) stands for the module's own path, X::Foo */ char chain[QC_MAXDEPTH * 2][64]; int cl = 0; int a = qc_hdr_alias && cp < qc_nhdr ? qc_hdr_alias[cp] : -1; if (a >= 0) for (int i = 0; i < qc_aliases[a].tl; i++) snprintf(chain[cl++], 64, "%s", qc_aliases[a].target[i]); else for (int i = ns - 1; i >= 0; i--) snprintf(chain[cl++], 64, "%s", rev[i]); for (int i = 0; i < cl; i++) { if (is_builtin_class_name(chain[i]) || is_builtin_module_name(chain[i])) return 0; snprintf(segs[i], 64, "%s", chain[i]); } return cl; } /* Where each compact header's path starts: qc_hdr_base[node] is the depth of the enclosing path its first segment resolves under -- for `module A; class A::Foo` that is 0, the top-level A, not A::A -- or -1 for a node that is not a compact header. Ruby looks the first segment up in the lexical scopes innermost first, then at the top level, among the classes and modules defined before it; qc_resolve_headers does the same and the passes below read the answer. A first segment it finds no definition for (`A = Module.new`, a constant set some other way) stays relative to the enclosing body, the depth the passes used before this lookup, rather than guessing the top level. A head that names a constant holding a module (`A = ::P`) starts at the top level with that module's path; see qc_note_alias. */ static int *qc_hdr_base = NULL; /* A body whose compact header's path starts above the enclosing body's depth: its own path buffer, since the caller's is still in use for the siblings, and the lexical flags it overwrote. */ typedef struct { char path[QC_MAXDEPTH][64]; unsigned char lex[QC_MAXDEPTH + 1]; } QCRebase; /* Push a class or module header onto a pass's lexical path: the compact header's segments, then the leaf. Answers the new depth; *pathp moves to a buffer of its own (*own, for qc_pop_header) when the header's path starts above `depth`. lex[d] (when given) is set for the depths that close a lexical scope -- the leaf's, and a segment's only where it is one of the enclosing bodies, since `class A::B` does not see A's constants. */ static int qc_push_header(const NodeTable *nt, int node, char (**pathp)[64], int depth, unsigned char *lex, QCRebase **own) { *own = NULL; int cp = nt_ref(nt, node, "constant_path"); const char *mn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (!mn) return depth; char segs[QC_MAXDEPTH][64]; int ns = qc_header_segs(nt, cp, segs); /* `class ::Foo` is Foo at the top level, wherever it is written */ int anchored = nt_kind(nt, cp) == NK_ConstantPathNode && nt_ref(nt, cp, "parent") < 0; int base = anchored ? 0 : ns > 0 && node < qc_nhdr && qc_hdr_base[node] >= 0 && qc_hdr_base[node] < depth ? qc_hdr_base[node] : depth; if (base + ns + 1 > QC_MAXDEPTH) { ns = 0; base = depth; } char (*old)[64] = *pathp; const unsigned char *olex = lex; if (base < depth) { QCRebase *r = malloc(sizeof *r); if (!r) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < base; i++) snprintf(r->path[i], 64, "%s", old[i]); if (lex) { memcpy(r->lex, lex, sizeof r->lex); olex = r->lex; } *own = r; *pathp = r->path; } char (*path)[64] = *pathp; int same = 1; /* path[0..d) is still the enclosing path's */ for (int i = 0; i < ns; i++) { snprintf(path[base + i], 64, "%s", segs[i]); same = same && base + i < depth && sp_streq(old[base + i], segs[i]); if (lex) lex[base + i + 1] = same && olex[base + i + 1]; } snprintf(path[base + ns], 64, "%s", mn); if (lex) lex[base + ns + 1] = 1; return base + ns + 1; } static void qc_pop_header(QCRebase *own, unsigned char *lex) { if (!own) return; if (lex) memcpy(lex, own->lex, sizeof own->lex); free(own); } /* Every class and module body with its path in the model qc_rewrite_reads walks with (the enclosing bodies' names, then its own), in source order: qc_resolve_headers looks a compact header's first segment up among the ones before it, and the ancestor lookup below uses them all. */ typedef struct { int node; int depth; char path[QC_MAXDEPTH][64]; } QCDef; static QCDef *qc_defs = NULL; static int qc_ndefs = 0, qc_cdefs = 0; static void qc_free_defs(void) { free(qc_defs); qc_defs = NULL; qc_ndefs = qc_cdefs = 0; } /* the body defined so far that has the path path[0..n) + head, or -1 */ static int qc_def_find(char (*path)[64], int n, const char *head) { for (int d = 0; d < qc_ndefs; d++) { if (qc_defs[d].depth != n + 1 || !sp_streq(qc_defs[d].path[n], head)) continue; int ok = 1; for (int i = 0; i < n && ok; i++) if (!sp_streq(qc_defs[d].path[i], path[i])) ok = 0; if (ok) return d; } return -1; } static int qc_known_has(char (*path)[64], int n, const char *head) { return qc_def_find(path, n, head) >= 0; } /* the latest alias written so far as `name` directly in the body path[0..n) */ static int qc_alias_find(char (*path)[64], int n, const char *name) { for (int a = qc_naliases - 1; a >= 0; a--) { if (qc_aliases[a].depth != n) continue; if (!sp_streq(qc_aliases[a].name, name) && !sp_streq(qc_aliases[a].orig, name)) continue; int ok = 1; for (int i = 0; i < n && ok; i++) if (!sp_streq(qc_aliases[a].scope[i], path[i])) ok = 0; if (ok) return a; } return -1; } /* A head found in no lexical scope may still be a constant an ancestor holds (`class Base; A = P; end; class Sub < Base; class A::Foo`): follow the alias when it is the program's only write of that name and no class is registered under the name, the rule this lookup had before it knew scopes. */ static int qc_alias_unique(const char *head) { int found = -1; for (int a = 0; a < qc_naliases; a++) { if (!sp_streq(qc_aliases[a].name, head) && !sp_streq(qc_aliases[a].orig, head)) continue; if (found >= 0) return -1; found = a; } if (found < 0 || (qc_hdr_cc && comp_class_index(qc_hdr_cc, head) >= 0)) return -1; return found; } /* The scopes a name read in the body path[0..depth) is looked up in by the header passes, innermost first, the top level last. */ static int qc_hdr_scopes(char (*path)[64], int depth, const unsigned char *lex, QCScope *out) { int k = 0; for (int p = depth; p >= 1; p--) if (lex[p]) { out[k].path = path; out[k++].n = p; } out[k].path = path; out[k++].n = 0; return k; } static int qc_ancestor_lookup(const NodeTable *nt, char (*path)[64], int n, const char *nm, QCWrite *ws, int wn, int own, int depth); /* The module path a constant path in[0..n) names, from the top level, for a read made in the scopes sc: the head is looked up in them innermost first, among the bodies and aliases defined so far, and each segment that is an alias rather than a body (`B = S::M` where S has `M = X`) is replaced by the path the alias stands for. Answers its length (0 when the head is not found, or the path grows past QC_MAXDEPTH), and sets *via when an alias was followed. A head no lexical scope has is looked for in the innermost body's ancestors, then taken from the program's only write of that name. */ static int qc_holder = -1; /* the alias of the last lookup whose head holds no module, or -1 */ static int qc_alias_chain(const QCScope *sc, int nsc, char (*in)[64], int n, char (*out)[64], int *via) { *via = 0; qc_holder = -1; if (n < 1) return 0; int len = -1, a = -1, anc = 0; for (int q = 0; q < nsc && len < 0 && a < 0; q++) { /* the innermost body's ancestors come before the top level (`class Sub < Base` sees Base's A) */ if (sc[q].n == 0 && !anc && q > 0 && sc[0].n > 0 && qc_hdr_nt) { anc = 1; int m = qc_ancestor_lookup(qc_hdr_nt, sc[0].path, sc[0].n, in[0], qc_alias_ws, qc_nalias_ws, 1, 0); if (m >= 0) { a = qc_alias_ws[m].node; break; } /* or a class or module an ancestor defines (`class Sub < Base; class A::Foo` where Base has a class A) */ m = qc_ancestor_lookup(qc_hdr_nt, sc[0].path, sc[0].n, in[0], NULL, -1, 1, 0); if (m >= 0) { len = 0; for (int i = 0; i < qc_defs[m].depth; i++) snprintf(out[len++], 64, "%s", qc_defs[m].path[i]); *via = 1; break; } } if (qc_known_has(sc[q].path, sc[q].n, in[0])) { len = 0; for (int i = 0; i < sc[q].n; i++) snprintf(out[len++], 64, "%s", sc[q].path[i]); snprintf(out[len++], 64, "%s", in[0]); } else a = qc_alias_find(sc[q].path, sc[q].n, in[0]); } /* then the only write of that name */ if (len < 0 && a < 0) a = qc_alias_unique(in[0]); if (len < 0 && a < 0) return 0; if (len < 0 && qc_aliases[a].tl == 0) { /* a constant holding something else (`S = Struct.new`) is its own namespace, where it is written */ len = 0; for (int i = 0; i < qc_aliases[a].depth; i++) snprintf(out[len++], 64, "%s", qc_aliases[a].scope[i]); snprintf(out[len++], 64, "%s", qc_aliases[a].orig); qc_holder = a; } else if (len < 0) { len = 0; for (int i = 0; i < qc_aliases[a].tl; i++) snprintf(out[len++], 64, "%s", qc_aliases[a].target[i]); *via = 1; } for (int i = 1; i < n; i++) { if (len >= QC_MAXDEPTH) return 0; if (!qc_known_has(out, len, in[i])) { int b = qc_alias_find(out, len, in[i]); if (b >= 0 && qc_aliases[b].tl > 0) { len = 0; for (int j = 0; j < qc_aliases[b].tl; j++) snprintf(out[len++], 64, "%s", qc_aliases[b].target[j]); *via = 1; continue; } } snprintf(out[len++], 64, "%s", in[i]); } /* a head found in a body QC_MAXDEPTH deep leaves no room for a path that a QCAlias target or a header's segments can hold */ return len <= QC_MAXDEPTH ? len : 0; } static int qc_read_chain_as(const NodeTable *nt, int node, char (*chain)[64], int *abs_anchor, int orig); static QCAlias *qc_alias_new(void) { if (qc_naliases == qc_caliases) { qc_caliases = qc_caliases ? qc_caliases * 2 : 16; qc_aliases = realloc(qc_aliases, sizeof(QCAlias) * (size_t)qc_caliases); if (!qc_aliases) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } QCAlias *al = &qc_aliases[qc_naliases++]; memset(al, 0, sizeof *al); return al; } /* Is the node `Struct.new(...)` or `Data.define(...)`, a call that makes a class? */ static int qc_struct_or_data_value(const NodeTable *nt, int v) { NodeKind vk = nt_kind(nt, v); if (vk != NK_CallNode) return 0; const char *vn = nt_str(nt, v, "name"); int vr = nt_ref(nt, v, "receiver"); const char *rn = vr >= 0 && nt_kind(nt, vr) == NK_ConstantReadNode ? nt_str(nt, vr, "name") : NULL; return rn && vn && ((sp_streq(rn, "Struct") && sp_streq(vn, "new")) || (sp_streq(rn, "Data") && sp_streq(vn, "define"))); } /* `A = ::P`, `A = P`, `A = B::C`: record the module path A stands for, resolved where the write is, the way a header's head is (so `module Q; module P; end; A = P` is Q::P), through an alias written before it. An anchored value starts at the top level. The value's tail need not be a body (`A = X::S` where S is a Struct). tl is 0 when the value's head names nothing defined around the write, and a header under A then keeps its written path. A value under a builtin's name is kept as written, which leaves a header under it registered by its leaf as before. */ static void qc_note_alias(const NodeTable *nt, int w, char (*path)[64], int depth, const unsigned char *lex) { const char *wn = nt_str(nt, w, "name"); int val = nt_ref(nt, w, "value"); if (!wn || val < 0) return; NodeKind vk = nt_kind(nt, val); char vchain[QC_MAXDEPTH + 1][64]; int vabs = 0; int vl = vk == NK_ConstantReadNode || vk == NK_ConstantPathNode ? qc_read_chain_as(nt, val, vchain, &vabs, 1) : 0; char out[QC_MAXDEPTH + 1][64]; int n = 0, via; if (vl >= 1 && vl <= QC_MAXDEPTH) { if (is_builtin_class_name(vchain[0]) || is_builtin_module_name(vchain[0])) { for (int i = 0; i < vl; i++) snprintf(out[n++], 64, "%s", vchain[i]); } else { QCScope sc[QC_MAXDEPTH + 2]; int nsc = vabs ? 1 : qc_hdr_scopes(path, depth, lex, sc); if (vabs) { sc[0].path = path; sc[0].n = 0; } n = qc_alias_chain(sc, nsc, vchain, vl, out, &via); } } QCAlias *al = qc_alias_new(); al->depth = depth; snprintf(al->name, 64, "%s", wn); /* a write qualify_colliding_consts renamed (`Q__A`) is still read as A by a header's head, which that pass leaves alone */ const char *on = nt_str(nt, w, "qc_orig_name"); snprintf(al->orig, 64, "%s", on ? on : wn); for (int i = 0; i < depth; i++) snprintf(al->scope[i], 64, "%s", path[i]); al->tl = n; al->sd = qc_struct_or_data_value(nt, val); for (int i = 0; i < n; i++) snprintf(al->target[i], 64, "%s", out[i]); for (int k = 0; k < (sp_streq(al->orig, al->name) ? 1 : 2); k++) { if (qc_nalias_ws == qc_calias_ws) { qc_calias_ws = qc_calias_ws ? qc_calias_ws * 2 : 16; qc_alias_ws = realloc(qc_alias_ws, sizeof(QCWrite) * (size_t)qc_calias_ws); if (!qc_alias_ws) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } QCWrite *aw = &qc_alias_ws[qc_nalias_ws++]; aw->node = qc_naliases - 1; aw->depth = depth; aw->salt = 0; aw->head[0] = 0; for (int i = 0; i < depth; i++) snprintf(aw->path[i], 64, "%s", path[i]); snprintf(aw->name, sizeof aw->name, "%s", k ? al->orig : al->name); } } /* `class Object::Foo` is `class ::Foo`: Object is the top level, so Foo is not defined under the body around the header. The passes and walk_scope read the anchored form, which they already place at the top level. */ static void qc_anchor_object_header(const NodeTable *nt, int cp) { for (int q = cp; q >= 0 && nt_kind(nt, q) == NK_ConstantPathNode; ) { int par = nt_ref(nt, q, "parent"); if (par >= 0 && nt_kind(nt, par) == NK_ConstantReadNode && sp_streq(nt_str(nt, par, "name"), "Object")) { nt_node_set_ref((NodeTable *)nt, q, "parent", -1); return; } q = par; } } /* The compact headers whose head is a constant holding no module (`Point = Struct.new`), for qc_refuse_holder_headers: the header node, the constant's alias, and whether the body path the header is written in is not the constant's own. */ typedef struct { int node, alias, away; } QCHolderHdr; static QCHolderHdr *qc_holder_hdrs = NULL; static int qc_nholder_hdrs = 0, qc_cholder_hdrs = 0; static void qc_note_holder_header(int node, int alias, char (*path)[64], int depth) { if (qc_nholder_hdrs == qc_cholder_hdrs) { qc_cholder_hdrs = qc_cholder_hdrs ? qc_cholder_hdrs * 2 : 8; qc_holder_hdrs = realloc(qc_holder_hdrs, sizeof(QCHolderHdr) * (size_t)qc_cholder_hdrs); if (!qc_holder_hdrs) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } QCHolderHdr *h = &qc_holder_hdrs[qc_nholder_hdrs++]; h->node = node; h->alias = alias; h->away = qc_aliases[alias].depth != depth; for (int i = 0; i < depth && !h->away; i++) h->away = !sp_streq(qc_aliases[alias].scope[i], path[i]); } /* The compact headers under a builtin's name (`module Regexp::Syntax`, `class Regexp::Syntax::V1 < ...`), whose segments qc_header_segs leaves out: the path as written, builtin head first, and the path of the body it opens in the passes' model ([Syntax], [V1]). A read of the written path names that body (qc_builtin_head). */ typedef struct { int n; char chain[QC_MAXDEPTH + 1][64]; int depth; char path[QC_MAXDEPTH][64]; } QCBuiltinHdr; static QCBuiltinHdr *qc_bhdrs = NULL; static int qc_nbhdrs = 0, qc_cbhdrs = 0; static void qc_note_builtin_header(const NodeTable *nt, int cp, char (*path)[64], int depth) { char chain[QC_MAXDEPTH + 1][64]; int abs_anchor = 0; int n = qc_read_chain(nt, cp, chain, &abs_anchor); if (n < 2 || !(is_builtin_class_name(chain[0]) || is_builtin_module_name(chain[0]))) return; if (qc_nbhdrs == qc_cbhdrs) { qc_cbhdrs = qc_cbhdrs ? qc_cbhdrs * 2 : 8; qc_bhdrs = realloc(qc_bhdrs, sizeof(QCBuiltinHdr) * (size_t)qc_cbhdrs); if (!qc_bhdrs) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } QCBuiltinHdr *h = &qc_bhdrs[qc_nbhdrs++]; h->n = n; h->depth = depth; for (int i = 0; i < n; i++) snprintf(h->chain[i], 64, "%s", chain[i]); for (int i = 0; i < depth; i++) snprintf(h->path[i], 64, "%s", path[i]); } /* The Ruby path of the body PATH[0..depth) names, each segment followed by `::`, into OUT: a prefix that is the body of a compact header under a builtin (`module Comparable::Ext` holds [Ext]) is spelled as the header wrote it, builtin head first (the longest such header). */ static int qc_path_eq(char (*a)[64], int an, char (*b)[64], int bn); static void qc_ruby_path(char (*path)[64], int depth, char *out, size_t cap) { int best = -1; for (int i = 0; i < qc_nbhdrs; i++) { const QCBuiltinHdr *h = &qc_bhdrs[i]; if (h->depth < 1 || h->depth > depth || (best >= 0 && h->depth <= qc_bhdrs[best].depth)) continue; if (qc_path_eq((char (*)[64])h->path, h->depth, path, h->depth)) best = i; } out[0] = 0; int from = 0; if (best >= 0) { for (int k = 0; k < qc_bhdrs[best].n; k++) { strncat(out, qc_bhdrs[best].chain[k], cap - strlen(out) - 1); strncat(out, "::", cap - strlen(out) - 1); } from = qc_bhdrs[best].depth; } for (int k = from; k < depth; k++) { strncat(out, path[k], cap - strlen(out) - 1); strncat(out, "::", cap - strlen(out) - 1); } } /* A path read chain[0..cl) whose head is a builtin's name and that starts with the written path of such a header (the longest) names that header's body: the prefix is replaced by the body's path, and the read is anchored at the top level. Answers the new length, or cl unchanged. */ static int qc_builtin_head(char (*chain)[64], int cl, int *abs_anchor) { if (cl < 2 || !(is_builtin_class_name(chain[0]) || is_builtin_module_name(chain[0]))) return cl; int best = -1; for (int i = 0; i < qc_nbhdrs; i++) { const QCBuiltinHdr *h = &qc_bhdrs[i]; if (h->n > cl || (best >= 0 && h->n <= qc_bhdrs[best].n)) continue; int ok = 1; for (int j = 0; j < h->n && ok; j++) ok = sp_streq(h->chain[j], chain[j]); if (ok) best = i; } if (best < 0) return cl; const QCBuiltinHdr *h = &qc_bhdrs[best]; int nl = h->depth + cl - h->n; if (nl > QC_MAXDEPTH + 1) return cl; char out[QC_MAXDEPTH + 1][64]; for (int i = 0; i < h->depth; i++) snprintf(out[i], 64, "%s", h->path[i]); for (int i = h->n; i < cl; i++) snprintf(out[h->depth + i - h->n], 64, "%s", chain[i]); for (int i = 0; i < nl; i++) snprintf(chain[i], 64, "%s", out[i]); *abs_anchor = 1; return nl; } static void qc_resolve_walk(const NodeTable *nt, int node, char (*path)[64], int depth, unsigned char *lex) { if (node < 0) return; NodeKind k = nt_kind(nt, node); QCRebase *own = NULL; if (k == NK_ConstantWriteNode) qc_note_alias(nt, node, path, depth, lex); if ((k == NK_ClassNode || k == NK_ModuleNode) && depth < QC_MAXDEPTH) { int cp = nt_ref(nt, node, "constant_path"); qc_anchor_object_header(nt, cp); const char *mn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; char segs[QC_MAXDEPTH][64]; int aliased = 0, ns = mn ? qc_header_segs(nt, cp, segs) : 0; int builtin_hdr = mn && ns == 0 && nt_kind(nt, cp) == NK_ConstantPathNode && nt_ref(nt, cp, "parent") >= 0; if (ns > 0) { int p = depth; while (p >= 0 && !((p == 0 || lex[p]) && qc_known_has(path, p, segs[0]))) p--; qc_hdr_base[node] = p >= 0 ? p : depth; /* a header reached through an alias (`A = ::P; class A::Foo`) is recorded with the module path it resolved to, from the top */ QCScope sc[QC_MAXDEPTH + 2]; char full[QC_MAXDEPTH + 1][64]; int via, fl = qc_alias_chain(sc, qc_hdr_scopes(path, depth, lex, sc), segs, ns, full, &via); if (fl > 0 && !via && qc_holder >= 0 && qc_aliases[qc_holder].sd) qc_note_holder_header(node, qc_holder, path, depth); if (fl > 0 && via && cp < qc_nhdr) { QCAlias *al = qc_alias_new(); /* nameless: only the header reads it */ al->tl = fl; for (int i = 0; i < fl; i++) snprintf(al->target[i], 64, "%s", full[i]); qc_hdr_alias[cp] = qc_naliases - 1; qc_hdr_base[node] = 0; aliased = 1; } } if (mn) { depth = qc_push_header(nt, node, &path, depth, lex, &own); if (builtin_hdr) qc_note_builtin_header(nt, cp, path, depth); if (qc_ndefs == qc_cdefs) { qc_cdefs = qc_cdefs ? qc_cdefs * 2 : 32; qc_defs = realloc(qc_defs, sizeof(QCDef) * (size_t)qc_cdefs); if (!qc_defs) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } QCDef *d = &qc_defs[qc_ndefs++]; d->node = node; d->depth = depth; for (int i = 0; i < depth; i++) snprintf(d->path[i], 64, "%s", path[i]); /* an aliased header's Ruby name is the module path it resolved to, which the enclosing class chain registration builds does not spell */ int ta = aliased ? qc_hdr_alias[cp] : -1; int rebased = ta >= 0 && depth > qc_aliases[ta].tl; for (int i = 0; rebased && i < qc_aliases[ta].tl; i++) if (!sp_streq(path[i], qc_aliases[ta].target[i])) rebased = 0; if (rebased) { char rn[QC_MAXDEPTH * 66]; rn[0] = 0; for (int i = 0; i < depth; i++) { if (i) strncat(rn, "::", sizeof rn - strlen(rn) - 1); strncat(rn, path[i], sizeof rn - strlen(rn) - 1); } nt_node_set_str((NodeTable *)nt, cp, "qc_ruby_name", rn); } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) qc_resolve_walk(nt, nt_ref_at(nt, node, i), path, depth, lex); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int j = 0; j < m; j++) qc_resolve_walk(nt, ids[j], path, depth, lex); } qc_pop_header(own, lex); } /* A compact header under a constant that holds a Struct or Data class (`Point = Struct.new`) names the class Point::Formatter. Two forms put it in the wrong place, and are refused. The first is a header in another body than the one that writes the constant: its class stays under its own body. The second is a header before a `class Point` body: spinel makes Point a parent only when that body starts. Run after the walk, when every body is known. */ static void qc_refuse_holder_headers(Compiler *c) { for (int i = 0; i < qc_nholder_hdrs; i++) { const QCAlias *al = &qc_aliases[qc_holder_hdrs[i].alias]; int reopened = qc_def_find((char (*)[64])al->scope, al->depth, al->orig) >= 0; if (!qc_holder_hdrs[i].away && !reopened) continue; char msg[512]; snprintf(msg, sizeof msg, "a class or module under %s, a constant that holds a Struct or Data class, " "is not supported here; write it in the body that writes %s, after any `class %s` body", al->orig, al->orig, al->orig); unsupported_feature(c, qc_holder_hdrs[i].node, msg); } } static void qc_free_headers(void) { free(qc_holder_hdrs); qc_holder_hdrs = NULL; qc_nholder_hdrs = qc_cholder_hdrs = 0; free(qc_hdr_base); qc_hdr_base = NULL; free(qc_hdr_alias); qc_hdr_alias = NULL; qc_nhdr = 0; free(qc_aliases); qc_aliases = NULL; qc_naliases = qc_caliases = 0; free(qc_alias_ws); qc_alias_ws = NULL; qc_nalias_ws = qc_calias_ws = 0; free(qc_bhdrs); qc_bhdrs = NULL; qc_nbhdrs = qc_cbhdrs = 0; qc_hdr_nt = NULL; qc_hdr_cc = NULL; qc_free_defs(); } /* One top-level statement (or a file's root) of the header walk, from the top level's path. */ static void qc_resolve_top(void *ud, int node, int file) { (void)ud; (void)file; char path[QC_MAXDEPTH][64]; unsigned char lex[QC_MAXDEPTH + 1] = {1}; qc_resolve_walk(qc_hdr_nt, node, path, 0, lex); } static void qc_resolve_headers(Compiler *c) { const NodeTable *nt = c->nt; qc_free_headers(); qc_nhdr = nt->count; qc_hdr_base = malloc(sizeof(int) * (size_t)(qc_nhdr > 0 ? qc_nhdr : 1)); qc_hdr_alias = malloc(sizeof(int) * (size_t)(qc_nhdr > 0 ? qc_nhdr : 1)); if (!qc_hdr_base || !qc_hdr_alias) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < qc_nhdr; i++) qc_hdr_base[i] = qc_hdr_alias[i] = -1; qc_hdr_nt = nt; qc_hdr_cc = c; /* in load order (prog_visit_files): a header reads the classes, modules and aliases defined before it, a required file's among them */ prog_visit_files(c, qc_resolve_top, NULL); qc_refuse_holder_headers(c); } /* Does the program have a `class` or `module` whose name ends in leaf? (The classes are not registered yet when the constants are qualified.) */ static int qc_program_defines_class(const NodeTable *nt, const char *leaf) { static const NodeKind hk[] = { NK_ClassNode, NK_ModuleNode }; for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, hk[h], id) { int cp = nt_ref(nt, id, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (cn && sp_streq(cn, leaf)) return 1; } return 0; } /* Does the program write `leaf = Struct.new(...)` or `leaf = Data.define(...)`? Such a constant names a class as a `class` header does. */ static int qc_program_defines_struct_or_data(const NodeTable *nt, const char *leaf) { NT_FOREACH_KIND(nt, NK_ConstantWriteNode, id) { const char *cn = nt_str(nt, id, "name"); if (cn && sp_streq(cn, leaf) && qc_struct_or_data_value(nt, nt_ref(nt, id, "value"))) return 1; } return 0; } /* `M.const_set(:N, v)` with a literal name sets M's constant N and answers v, as `M::N = v` does; `const_set(:N, v)` or `self.const_set(:N, v)` in a class or module body sets the body's own, as `N = v` there does. Written so, each is the constant write every pass below names, stores, orders and qualifies: beside a top-level G, `Values.const_set(:G, 13)` is Values's G and leaves G alone. M is a constant naming Object or a class or module the program defines, so it is a module whichever way the call runs. A program that defines a const_set of its own keeps its calls. */ int desugar_const_set_write_gate(Compiler *c, LowerCtx *lc) { (void)lc; return !ct_method_defined(c->nt, "const_set"); } int desugar_const_set_write_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (!is_const_set_name(nt_str(nt, id, "name")) || nt_ref(nt, id, "block") >= 0 || ac != 2) return 0; const char *n = vis_arg_name(nt, av[0]); NodeKind vk = nt_kind(nt, av[1]); if (!n || n[0] < 'A' || n[0] > 'Z' || !is_c_ident(n) || vk == NK_SplatNode || vk == NK_KeywordHashNode) return 0; int rv = nt_ref(nt, id, "receiver"); if (rv < 0 || nt_kind(nt, rv) == NK_SelfNode) { /* self is the class where the body runs, not in a method, block or `class << x` body in it, nor in its header */ const int *par = lower_parents(c, lc); for (int p = id, q = par[id]; q >= 0; p = q, q = par[q]) { NodeKind k = nt_kind(nt, q); if (k == NK_ClassNode || k == NK_ModuleNode) return p == nt_ref(nt, q, "body"); if (k == NK_DefNode || k == NK_BlockNode || k == NK_LambdaNode || k == NK_SingletonClassNode) return 0; } return 0; } const char *m = nt_str(nt, rv, "name"); for (int p = rv; p >= 0; p = nt_kind(nt, p) == NK_ConstantPathNode ? nt_ref(nt, p, "parent") : -1) if (nt_kind(nt, p) != NK_ConstantReadNode && nt_kind(nt, p) != NK_ConstantPathNode) return 0; return m && (is_object_base_name(m) || qc_program_defines_class(nt, m)); } /* node n stands where the call stood */ static void cs_place(NodeTable *nt, int n, const long long *at) { nt_node_set_int(nt, n, "node_line", at[0]); nt_node_set_int(nt, n, "node_file", at[1]); nt_node_set_int(nt, n, "node_col", at[2]); } /* the constant N, or M::N for a receiver M: a read, or a write's target */ static int cs_const(NodeTable *nt, int m, const char *n, const long long *at) { int k = nt_new_node(nt, m >= 0 ? "ConstantPathNode" : "ConstantReadNode"); if (k < 0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nt_node_set_str(nt, k, "name", n); if (m >= 0) nt_node_set_ref(nt, k, "parent", m); cs_place(nt, k, at); return k; } int desugar_const_set_write_at(Compiler *c, LowerCtx *lc, int id) { if (!desugar_const_set_write_left(c, lc, id)) return 0; NodeTable *nt = (NodeTable *)c->nt; int rv = nt_ref(nt, id, "receiver"), a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = nt_arr(nt, a, "arguments", &ac); int v = av[1], w = id; char *n = strdup(vis_arg_name(nt, av[0])); if (!n) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (rv >= 0 && nt_kind(nt, rv) == NK_SelfNode) rv = -1; long long at[3] = { nt_int(nt, id, "node_line", 0), nt_int(nt, id, "node_file", 0), nt_int(nt, id, "node_col", 0) }; /* used as a value, the call answers v, which the constant then holds: `(M::N = v; M::N)` */ int used = !an_value_dropped(nt, lower_parents(c, lc), id); nt_node_set_arr(nt, a, "arguments", NULL, 0); /* the value moves to the write */ if (used) { int s = nt_new_node(nt, "StatementsNode"); w = nt_new_node(nt, "ConstantWriteNode"); if (w < 0 || s < 0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int b[2] = { w, cs_const(nt, rv >= 0 ? nt_clone_subtree(nt, rv) : -1, n, at) }; nt_node_set_arr(nt, s, "body", b, 2); cs_place(nt, s, at); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "body", s); cs_place(nt, id, at); } nt_node_reset(nt, w, rv >= 0 ? "ConstantPathWriteNode" : "ConstantWriteNode"); if (rv >= 0) nt_node_set_ref(nt, w, "target", cs_const(nt, rv, n, at)); else nt_node_set_str(nt, w, "name", n); nt_node_set_ref(nt, w, "value", v); cs_place(nt, w, at); free(n); return 1; } /* A top-level path write's target `A::B::NAME`, as a write of NAME under the module path A::B names from the top level (through an alias, `A = Base`) -- when each segment of it is a class or module of the program, or a constant that holds `Struct.new(...)` or `Data.define(...)`. */ static void qc_note_path_write(Compiler *c, int tgt, QCWrite **ws, int *n, int *cap) { char chain[QC_MAXDEPTH + 1][64], mods[QC_MAXDEPTH + 1][64]; int abs = 0, cn = tgt >= 0 ? qc_read_chain(c->nt, tgt, chain, &abs) : 0; if (cn < 2 || cn > QC_MAXDEPTH) return; QCScope top = { chain, 0 }; int via, md = qc_alias_chain(&top, 1, chain, cn - 1, mods, &via); if (md < 1 || (qc_holder >= 0 && !qc_aliases[qc_holder].sd)) return; for (int i = 0; i < md; i++) if (!qc_program_defines_class(c->nt, mods[i]) && !qc_program_defines_struct_or_data(c->nt, mods[i])) return; if (!qc_program_defines_class(c->nt, mods[md - 1]) && holder_subclass_defines(c->nt, mods[md - 1], chain[cn - 1])) return; if (*n >= *cap) { *cap = *cap ? *cap * 2 : 16; *ws = realloc(*ws, sizeof(QCWrite) * (size_t)*cap); } if (!*ws) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } QCWrite *w = &(*ws)[(*n)++]; w->node = tgt; w->depth = md; w->salt = 0; w->head[0] = 0; for (int i = 0; i < md; i++) snprintf(w->path[i], 64, "%s", mods[i]); snprintf(w->name, sizeof w->name, "%s", chain[cn - 1]); /* the module the write is under, by its Ruby name, for the class's constant lookup (const_value_owned_key) */ char own[(QC_MAXDEPTH + 1) * 66] = ""; for (int i = 0; i < md; i++) { if (i) strcat(own, "::"); strcat(own, mods[i]); } const char *was = nt_str(c->nt, tgt, "qc_owner"); if (!was || !sp_streq(was, own)) nt_node_set_str((NodeTable *)c->nt, tgt, "qc_owner", own); } void qc_collect_writes(Compiler *c, int node, char (*path)[64], int depth, QCWrite **ws, int *n, int *cap) { const NodeTable *nt = c->nt; if (node < 0) return; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return; QCRebase *own = NULL; if ((ty_k == NK_ModuleNode || ty_k == NK_ClassNode) && depth < QC_MAXDEPTH) depth = qc_push_header(nt, node, &path, depth, NULL, &own); else if (ty_k == NK_ConstantWriteNode) { const char *nm = nt_str(nt, node, "name"); if (nm) { if (*n >= *cap) { *cap = *cap ? *cap * 2 : 16; *ws = realloc(*ws, sizeof(QCWrite) * (size_t)*cap); } QCWrite *w = &(*ws)[(*n)++]; w->node = node; w->depth = depth; w->salt = 0; w->head[0] = 0; for (int i = 0; i < depth; i++) snprintf(w->path[i], 64, "%s", path[i]); snprintf(w->name, sizeof w->name, "%s", nm); } } /* `Base::NAME = v` at the top level writes NAME under Base, as `NAME = v` in Base's body does: the target is that write's node */ else if (depth == 0 && (ty_k == NK_ConstantPathWriteNode || ty_k == NK_ConstantPathOrWriteNode || ty_k == NK_ConstantPathAndWriteNode || ty_k == NK_ConstantPathOperatorWriteNode)) qc_note_path_write(c, nt_ref(nt, node, "target"), ws, n, cap); int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) qc_collect_writes(c, nt_ref_at(nt, node, i), path, depth, ws, n, cap); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int k = 0; k < m; k++) qc_collect_writes(c, ids[k], path, depth, ws, n, cap); } qc_pop_header(own, NULL); } /* Reconstruct a path read's chain (["RootNS","Mid","LEAF"]) and whether it is root-anchored. Returns the chain length, or 0 if unsupported. */ int qc_read_chain(const NodeTable *nt, int node, char (*chain)[64], int *abs_anchor) { return qc_read_chain_as(nt, node, chain, abs_anchor, 0); } /* ... with orig set, in the names written before that pass renamed them */ static int qc_read_chain_as(const NodeTable *nt, int node, char (*chain)[64], int *abs_anchor, int orig) { char rev[QC_MAXDEPTH + 1][64]; int n = 0; int cur = node; *abs_anchor = 0; while (cur >= 0 && n <= QC_MAXDEPTH) { const char *ty = nt_type(nt, cur); NodeKind k = nt_kind(nt, cur); const char *nm = nt_str(nt, cur, "name"); const char *on = orig ? nt_str(nt, cur, "qc_orig_name") : NULL; if (!ty || !nm) return 0; snprintf(rev[n++], 64, "%s", on ? on : nm); if (k == NK_ConstantReadNode) break; if (k != NK_ConstantPathNode) return 0; int par = nt_ref(nt, cur, "parent"); if (par < 0) { *abs_anchor = 1; break; } cur = par; } for (int i = 0; i < n; i++) snprintf(chain[i], 64, "%s", rev[n - 1 - i]); return n; } void qc_qualified_name(char *out, size_t cap, const QCWrite *w) { out[0] = 0; for (int i = 0; i < w->salt; i++) strncat(out, "_", cap - strlen(out) - 1); for (int i = 0; i < w->depth; i++) { strncat(out, w->path[i], cap - strlen(out) - 1); strncat(out, "__", cap - strlen(out) - 1); } strncat(out, w->name, cap - strlen(out) - 1); } /* Reverse-reference flags for qc_rewrite_reads, built once per top-level call (not rescanned per constant node, which made the pass O(constants * nodes) on a flattened runtime). qc_cpath_parent[id]: some ConstantPathNode has parent == id. qc_def_cpath[id]: some Class/ModuleNode has constant_path == id. */ static unsigned char *qc_cpath_parent = NULL; static unsigned char *qc_def_cpath = NULL; static void qc_build_reverse_flags(Compiler *c) { const NodeTable *nt = c->nt; int n = nt->count; qc_cpath_parent = calloc((size_t)n, 1); qc_def_cpath = calloc((size_t)n, 1); if (!qc_cpath_parent || !qc_def_cpath) return; for (int q = 0; q < n; q++) { const char *qt = nt_type(nt, q); NodeKind qk = nt_kind(nt, q); if (!qt) continue; if (qk == NK_ConstantPathNode) { int p = nt_ref(nt, q, "parent"); if (p >= 0 && p < n) qc_cpath_parent[p] = 1; } else if (qk == NK_ClassNode || qk == NK_ModuleNode) { int cp = nt_ref(nt, q, "constant_path"); if (cp >= 0 && cp < n) qc_def_cpath[cp] = 1; } /* a `::X = v` / `::X ||= v` target is written through its path node */ else if (strncmp(qt, "ConstantPath", 12) == 0) { int t = nt_ref(nt, q, "target"); if (t >= 0 && t < n) qc_def_cpath[t] = 1; } } } static void qc_free_reverse_flags(void) { free(qc_cpath_parent); qc_cpath_parent = NULL; free(qc_def_cpath); qc_def_cpath = NULL; } static int qc_path_eq(char (*a)[64], int an, char (*b)[64], int bn) { if (an != bn) return 0; for (int i = 0; i < an; i++) if (!sp_streq(a[i], b[i])) return 0; return 1; } /* The class or module a constant written as `ref` (a read or a path) names from inside a body at `scope` (scope_n names deep): the enclosing bodies' paths innermost first, then the top level, as Ruby resolves it. Its path goes in `out`; the answer is its depth, or 0 when nothing defines it. */ static int qc_resolve_chain(char (*chain)[64], int cl, int abs_anchor, char (*scope)[64], int scope_n, char (*out)[64]); static int qc_resolve_ref(const NodeTable *nt, int ref, char (*scope)[64], int scope_n, char (*out)[64]) { char chain[QC_MAXDEPTH + 1][64]; int abs_anchor = 0; int cl = ref >= 0 ? qc_read_chain(nt, ref, chain, &abs_anchor) : 0; return qc_resolve_chain(chain, cl, abs_anchor, scope, scope_n, out); } /* Does a body enclosing scope[0..scope_n) define `head` (a class or module of the program at scope[0..p) + head, p >= 1)? */ static int qc_head_shadowed(const char *head, char (*scope)[64], int scope_n) { for (int p = scope_n; p >= 1; p--) { if (p + 1 > QC_MAXDEPTH) continue; char cand[QC_MAXDEPTH][64]; for (int i = 0; i < p; i++) snprintf(cand[i], 64, "%s", scope[i]); snprintf(cand[p], 64, "%s", head); for (int d = 0; d < qc_ndefs; d++) if (qc_path_eq(qc_defs[d].path, qc_defs[d].depth, cand, p + 1)) return 1; } return 0; } /* ... the same for a chain already read (cl names) */ static int qc_resolve_chain(char (*chain)[64], int cl, int abs_anchor, char (*scope)[64], int scope_n, char (*out)[64]) { if (cl <= 0) return 0; /* a path through a header under a builtin (`Regexp::Syntax::Base`) is the body that header opened, unless an enclosing body defines a module of the head's name, which the read finds first, as Ruby's lexical lookup does */ char bchain[QC_MAXDEPTH + 1][64]; if (cl <= QC_MAXDEPTH + 1 && (is_builtin_class_name(chain[0]) || is_builtin_module_name(chain[0])) && (abs_anchor || !qc_head_shadowed(chain[0], scope, scope_n))) { for (int i = 0; i < cl; i++) snprintf(bchain[i], 64, "%s", chain[i]); cl = qc_builtin_head(bchain, cl, &abs_anchor); chain = bchain; } for (int p = abs_anchor ? 0 : scope_n; p >= 0; p--) { if (p + cl > QC_MAXDEPTH) continue; char cand[QC_MAXDEPTH][64]; for (int i = 0; i < p; i++) snprintf(cand[i], 64, "%s", scope[i]); for (int i = 0; i < cl; i++) snprintf(cand[p + i], 64, "%s", chain[i]); for (int d = 0; d < qc_ndefs; d++) if (qc_path_eq(qc_defs[d].path, qc_defs[d].depth, cand, p + cl)) { for (int i = 0; i < p + cl; i++) snprintf(out[i], 64, "%s", cand[i]); return p + cl; } } return 0; } /* Is write `a` later in the source than write `b`? Only told apart within one file, by position; a write in another file counts as earlier (the walk order is not source order once passes have moved bodies). */ static int qc_write_after(const NodeTable *nt, int a, int b) { if (nt_int(nt, a, "node_file", -1) != nt_int(nt, b, "node_file", -1)) return 0; long long la = nt_int(nt, a, "node_line", 0), lb = nt_int(nt, b, "node_line", 0); if (la != lb) return la > lb; return nt_int(nt, a, "node_col", 0) > nt_int(nt, b, "node_col", 0); } /* The header a superclass read is resolved for (`class Foo < Foo`): the class it defines does not exist yet when CRuby reads the superclass, so lookups skip its write and any later reopening of it. */ static const QCWrite *qc_skip_write = NULL; static int qc_skipped(const QCWrite *w) { return qc_skip_write && sp_streq(w->name, qc_skip_write->name) && qc_path_eq((char (*)[64])w->path, w->depth, (char (*)[64])qc_skip_write->path, qc_skip_write->depth); } /* The colliding write of `nm` made directly in the body at `path`, or -1. */ static int qc_write_in(char (*path)[64], int n, const char *nm, QCWrite *ws, int wn) { for (int i = 0; i < wn; i++) if (!qc_skipped(&ws[i]) && sp_streq(ws[i].name, nm) && qc_path_eq(ws[i].path, ws[i].depth, path, n)) return i; return -1; } /* A read of `nm` that no enclosing body defines is looked up in the ancestors of the innermost one, at `path`: the modules it prepends, the modules it includes, the one prepended or included last first, and theirs in turn, then its superclass and on up. A prepended module comes before the body's own constants. Each superclass and module is resolved from the body that names it, by its whole path, so `B` inside `Outer` is `Outer::B` where there is one. The walk is bounded, so an include cycle ends. Without this, a constant a superclass defines, where another class defines the same name, read as undefined. With a negative wn it looks for a class or module body of that name instead, and answers its index in qc_defs. */ static int qc_ancestor_lookup(const NodeTable *nt, char (*path)[64], int n, const char *nm, QCWrite *ws, int wn, int own, int depth); static int qc_mixin_lookup(const NodeTable *nt, char (*path)[64], int n, const char *mixer, const char *nm, QCWrite *ws, int wn, int depth) { for (int d = qc_ndefs - 1; d >= 0; d--) { if (!qc_path_eq(qc_defs[d].path, qc_defs[d].depth, path, n)) continue; int body = nt_ref(nt, qc_defs[d].node, "body"); int bn = 0; const int *st = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; for (int q = bn - 1; q >= 0; q--) { if (nt_kind(nt, st[q]) != NK_CallNode || nt_ref(nt, st[q], "receiver") >= 0) continue; const char *cn = nt_str(nt, st[q], "name"); if (!cn || !sp_streq(cn, mixer)) continue; int args = nt_ref(nt, st[q], "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; for (int a = 0; a < an; a++) { char inc[QC_MAXDEPTH][64]; int incn = qc_resolve_ref(nt, av[a], path, n, inc); if (incn <= 0 || qc_path_eq(inc, incn, path, n)) continue; int m = qc_ancestor_lookup(nt, inc, incn, nm, ws, wn, 0, depth + 1); if (m >= 0) return m; } } } return -1; } static int qc_ancestor_lookup(const NodeTable *nt, char (*path)[64], int n, const char *nm, QCWrite *ws, int wn, int own, int depth) { if (n <= 0 || depth > 32) return -1; int m = qc_mixin_lookup(nt, path, n, "prepend", nm, ws, wn, depth); if (m >= 0) return m; if (!own && (m = wn >= 0 ? qc_write_in(path, n, nm, ws, wn) : qc_def_find(path, n, nm)) >= 0) return m; if ((m = qc_mixin_lookup(nt, path, n, "include", nm, ws, wn, depth)) >= 0) return m; char sup[QC_MAXDEPTH][64]; int supn = 0; for (int d = qc_ndefs - 1; d >= 0 && !supn; d--) { if (!qc_path_eq(qc_defs[d].path, qc_defs[d].depth, path, n)) continue; int dn = qc_defs[d].node; if (nt_kind(nt, dn) == NK_ClassNode) supn = qc_resolve_ref(nt, nt_ref(nt, dn, "superclass"), path, n - 1, sup); } return supn > 0 ? qc_ancestor_lookup(nt, sup, supn, nm, ws, wn, 0, depth + 1) : -1; } static int qc_ancestor_write(Compiler *c, char (*path)[64], int n, const char *nm, QCWrite *ws, int wn) { return qc_ancestor_lookup(c->nt, path, n, nm, ws, wn, 1, 0); } /* A read no body and no top-level write defines is looked up in the modules the top level includes (`include Outer::Helpers`): they are Object's, and Object is the last ancestor of every lookup, so their constants are visible from everywhere. The one included last first, as for any body. */ static int qc_object_mixin_write(Compiler *c, const char *nm, QCWrite *ws, int wn) { const NodeTable *nt = c->nt; int sn = 0; const int *st = nt->root_id < 0 ? NULL : nt_arr(nt, nt_ref(nt, nt->root_id, "statements"), "body", &sn); char top[1][64]; for (int q = sn - 1; q >= 0; q--) { if (nt_kind(nt, st[q]) != NK_CallNode || nt_ref(nt, st[q], "receiver") >= 0) continue; const char *cn = nt_str(nt, st[q], "name"); if (!cn || !sp_streq(cn, "include")) continue; int args = nt_ref(nt, st[q], "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; for (int a = 0; a < an; a++) { char inc[QC_MAXDEPTH][64]; int incn = qc_resolve_ref(nt, av[a], top, 0, inc); if (incn <= 0) continue; int m = qc_ancestor_lookup(nt, inc, incn, nm, ws, wn, 0, 1); if (m >= 0) return m; } } return -1; } /* qc_lex[d]: the body at depth d of qc_rewrite_reads' path is a lexical scope of its own -- not a segment a compact header (`class A::B`) pushed for A, whose constants the body does not see. */ static unsigned char qc_lex[QC_MAXDEPTH + 1] = {1}; /* The lexical scopes a compact header whose path starts above the body around it left behind: in `module M; class Top::Foo`, the body's path is Top::Foo, but M is still a lexical scope of it. Innermost last. */ static struct { char (*path)[64]; int depth; const unsigned char *lex; } qc_outer[QC_MAXDEPTH]; static int qc_nouter = 0; #define QC_MAXSCOPES (QC_MAXDEPTH * (QC_MAXDEPTH + 1) + 1) static QCScope qc_scopes[QC_MAXSCOPES]; /* a lookup's, never held across the walk */ /* The scopes a constant read in the body at `path` (depth deep) is looked up in, innermost first, the top level (n == 0) last. */ static int qc_lex_scopes(char (*path)[64], int depth, QCScope *out) { int k = 0; for (int p = depth; p >= 1; p--) if (qc_lex[p]) { out[k].path = path; out[k++].n = p; } for (int o = qc_nouter - 1; o >= 0; o--) for (int p = qc_outer[o].depth; p >= 1; p--) if (qc_outer[o].lex[p]) { out[k].path = qc_outer[o].path; out[k++].n = p; } out[k].path = path; out[k++].n = 0; return k; } /* A path read whose head is a constant HOLDING a module (`S = Web::Status`; `S::EOFError`) names what the module's own path does: the path is resolved as a header's is, and when it goes through an alias the chain becomes the module path, anchored at the top level, so it matches the nested class the colliding-name rename gave a qualified name. Answers the new length, or cl unchanged. */ static int qc_expand_read_alias(char (*path)[64], int depth, char (*chain)[64], int cl, int *abs_anchor) { if (cl < 2) return cl; QCScope *sc = qc_scopes; int nsc = *abs_anchor ? 1 : qc_lex_scopes(path, depth, sc); if (*abs_anchor) { sc[0].path = path; sc[0].n = 0; } char out[QC_MAXDEPTH + 1][64]; int via, n = qc_alias_chain(sc, nsc, chain, cl - 1, out, &via); if (n <= 0 || !via || n + 1 > QC_MAXDEPTH + 1) return cl; snprintf(out[n++], 64, "%s", chain[cl - 1]); for (int i = 0; i < n; i++) snprintf(chain[i], 64, "%s", out[i]); *abs_anchor = 1; return n; } void qc_rewrite_reads(Compiler *c, int node, char (*mods)[64], int mdepth, QCWrite *ws, int wn) { const NodeTable *nt = c->nt; if (node < 0) return; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return; int depth = mdepth; char (*path)[64] = mods; QCRebase *own = NULL; int outer = 0; if ((ty_k == NK_ModuleNode || ty_k == NK_ClassNode) && depth < QC_MAXDEPTH) { /* `class Foo < Foo` in A: CRuby reads the superclass in the scope around the header, before the constant A::Foo exists, so it names the Foo found from there, not the class being defined. Resolved here with the class's writes skipped, and left alone by the walk below. A reopen (A::Foo written earlier) keeps the old reading. */ int sup = ty_k == NK_ClassNode ? nt_ref(nt, node, "superclass") : -1; int hcp = nt_ref(nt, node, "constant_path"); const char *hn = hcp >= 0 ? nt_str(nt, hcp, "name") : NULL; if (sup >= 0 && hn && nt_kind(nt, sup) == NK_ConstantReadNode && nt_str(nt, sup, "name") && sp_streq(nt_str(nt, sup, "name"), hn)) { int h = -1, reopen = 0; for (int i = 0; i < wn; i++) if (ws[i].node == hcp) { h = i; break; } for (int i = 0; h >= 0 && i < wn && !reopen; i++) if (i != h && sp_streq(ws[i].name, ws[h].name) && qc_path_eq(ws[i].path, ws[i].depth, ws[h].path, ws[h].depth) && !qc_write_after(nt, ws[i].node, hcp)) reopen = 1; if (!reopen) { const QCWrite *sv = qc_skip_write; QCWrite self_w; memset(&self_w, 0, sizeof self_w); if (h >= 0) self_w = ws[h]; else { snprintf(self_w.name, sizeof self_w.name, "%s", hn); self_w.depth = -1; } qc_skip_write = &self_w; qc_rewrite_reads(c, sup, mods, mdepth, ws, wn); qc_skip_write = sv; nt_node_set_int((NodeTable *)nt, sup, "qc_outer_super", 1); } } depth = qc_push_header(nt, node, &path, depth, qc_lex, &own); if (own && qc_nouter < QC_MAXDEPTH) { qc_outer[qc_nouter].path = mods; qc_outer[qc_nouter].depth = mdepth; qc_outer[qc_nouter++].lex = own->lex; outer = 1; } } else if (ty_k == NK_ConstantReadNode) { /* bare constant read inside a module body: resolve lexically innermost- first against the colliding writes. Skip reads that are a path's parent (handled via the chain) or a class/module definition name. */ const char *nm = nt_str(nt, node, "name"); int part_of_other = (qc_cpath_parent && qc_cpath_parent[node]) || (qc_def_cpath && qc_def_cpath[node]) || nt_int(nt, node, "qc_outer_super", 0); if (nm && !part_of_other) { int involved = 0; for (int i = 0; i < wn; i++) if (sp_streq(ws[i].name, nm)) { involved = 1; break; } if (involved) { /* the enclosing modules innermost-first, then the ancestors of the innermost one, then the top level, as Ruby looks a constant up */ int matched = -1; QCScope *sc = qc_scopes; int nsc = qc_lex_scopes(path, depth, sc); for (int q = 0; q < nsc && matched < 0; q++) { int pref = sc[q].n; if (pref == 0) break; for (int i = 0; i < wn && matched < 0; i++) { if (qc_skipped(&ws[i]) || !sp_streq(ws[i].name, nm) || ws[i].depth != pref) continue; int ok = 1; for (int j = 0; j < pref && ok; j++) if (!sp_streq(ws[i].path[j], sc[q].path[j])) ok = 0; if (ok) matched = i; } } if (matched < 0 && depth > 0) matched = qc_ancestor_write(c, path, depth, nm, ws, wn); for (int i = 0; i < wn && matched < 0; i++) if (!qc_skipped(&ws[i]) && sp_streq(ws[i].name, nm) && ws[i].depth == 0) matched = i; if (matched < 0) matched = qc_object_mixin_write(c, nm, ws, wn); if (matched >= 0 && ws[matched].depth > 0) { char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[matched]); qc_keep_orig_name(nt, node); nt_set_str((NodeTable *)nt, node, "name", qn); } } } } else if (ty_k == NK_ConstantPathNode) { /* only process path heads: skip if this node is some other path's parent */ int is_parent = qc_cpath_parent && qc_cpath_parent[node]; if (!is_parent) { char chain[QC_MAXDEPTH + 1][64]; int abs_anchor = 0; int cl = qc_read_chain(nt, node, chain, &abs_anchor); /* `::X` for a colliding class leaf: every nested X is about to be renamed to its qualified name, so the bare name X is left naming only the top-level X (a user class or the builtin). Read it as the bare constant, which is what the builtin receivers (`File.binwrite`, `Time.at`) are recognised by. */ if (cl == 1 && abs_anchor && !(qc_def_cpath && qc_def_cpath[node])) { for (int i = 0; i < wn; i++) { if (!sp_streq(ws[i].name, chain[0])) continue; nt_node_set_type((NodeTable *)nt, node, "ConstantReadNode"); nt_node_set_int((NodeTable *)nt, node, "root_anchored", 1); /* Object's X, for the load record */ break; } } if (cl >= 2) { const char *cname = chain[cl - 1]; /* does this name participate in a collision? */ int involved = 0; for (int i = 0; i < wn; i++) if (sp_streq(ws[i].name, cname)) { involved = 1; break; } if (involved) { /* `Regexp::Syntax::Token::X` reads X in the body `module Regexp::Syntax` opened, which the passes keep at [Syntax], unless an enclosing body defines its own `Regexp` */ if (abs_anchor || !qc_head_shadowed(chain[0], path, depth)) cl = qc_builtin_head(chain, cl, &abs_anchor); cl = qc_expand_read_alias(path, depth, chain, cl, &abs_anchor); cname = chain[cl - 1]; } if (involved) { /* try lexical prefixes innermost-first (relative), or only the root (::) */ QCScope *sc = qc_scopes; int nsc = abs_anchor ? 0 : qc_lex_scopes(path, depth, sc); if (abs_anchor) { sc[0].path = path; sc[0].n = 0; nsc = 1; } for (int q = 0; q < nsc; q++) { int pref = sc[q].n; int matched = -1; for (int i = 0; i < wn && matched < 0; i++) { if (!sp_streq(ws[i].name, cname)) continue; if (ws[i].depth != pref + (cl - 1)) continue; int ok = 1; for (int j = 0; j < pref && ok; j++) if (!sp_streq(ws[i].path[j], sc[q].path[j])) ok = 0; for (int j = 0; j < cl - 1 && ok; j++) if (!sp_streq(ws[i].path[pref + j], chain[j])) ok = 0; if (ok) matched = i; } if (matched >= 0) { if (ws[matched].depth > 0) { char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[matched]); qc_keep_orig_name(nt, node); nt_set_str((NodeTable *)nt, node, "name", qn); } break; } /* no body at the written path holds it: the class or module the path's head names may still reach it through its ancestors (`Sub::NAME` for a NAME its superclass or an included module holds), as Ruby looks a scoped constant up */ if (pref == 0) { char owner[QC_MAXDEPTH][64]; int on = qc_resolve_chain(chain, cl - 1, abs_anchor, path, depth, owner); int m = on > 0 ? qc_ancestor_lookup(nt, owner, on, cname, ws, wn, 0, 0) : -1; if (m >= 0 && ws[m].depth > 0) { char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[m]); nt_set_str((NodeTable *)nt, node, "name", qn); } } } } } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) qc_rewrite_reads(c, nt_ref_at(nt, node, i), path, depth, ws, wn); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int k = 0; k < m; k++) qc_rewrite_reads(c, ids[k], path, depth, ws, wn); } if (outer) qc_nouter--; qc_pop_header(own, qc_lex); } /* The qualified names qualify_colliding_consts gave a constant written under a module path, for qualify_colliding_classes. */ static char **qc_const_quals; static int qc_nconst_quals; int qc_const_qualified(const char *qn) { for (int i = 0; i < qc_nconst_quals; i++) if (sp_streq(qc_const_quals[i], qn)) return 1; return 0; } /* Did that pass qualify some constant of this leaf name? */ int qc_leaf_qualified(const char *leaf) { size_t ll = strlen(leaf); for (int i = 0; i < qc_nconst_quals; i++) { size_t l = strlen(qc_const_quals[i]); if (l > ll + 2 && sp_streq(qc_const_quals[i] + l - ll, leaf) && !strncmp(qc_const_quals[i] + l - ll - 2, "__", 2)) return 1; } return 0; } /* Is the name `w` writes also defined at another place by an entry of `os` -- the same leaf under a different module path? An entry that is the same node (a `Point = Struct.new(...)`, both a constant write and a class definition) is the write itself, not another definition. */ static int qc_is_class_write(const QCWrite *w, const QCWrite *os, int on) { for (int j = 0; j < on; j++) if (os[j].node == w->node) return 1; return 0; } static int qc_leaf_elsewhere(const QCWrite *w, const QCWrite *os, int on) { for (int j = 0; j < on; j++) { if (os[j].node == w->node || !sp_streq(os[j].name, w->name)) continue; if (os[j].depth != w->depth) return 1; for (int k = 0; k < w->depth; k++) if (!sp_streq(os[j].path[k], w->path[k])) return 1; } return 0; } void qualify_colliding_consts(Compiler *c) { const NodeTable *nt = c->nt; QCWrite *ws = NULL; int wn = 0, wcap = 0; char path[QC_MAXDEPTH][64]; qc_resolve_headers(c); for (int r = 0; r < nt_nroots(nt); r++) /* each file's tree */ qc_collect_writes(c, nt_root(nt, r), path, 0, &ws, &wn, &wcap); /* the class and module definitions, for a constant that shares a class's leaf name: `module Config; FileHandler = {...}` beside `module Servlet; class FileHandler`. The constant was stored under the bare leaf, so every read of the class by its path (`Servlet::FileHandler`) answered the Hash. */ QCWrite *cws = NULL; int cwn = 0, cwcap = 0; for (int r = 0; r < nt_nroots(nt); r++) qc_collect_class_writes(c, nt_root(nt, r), path, 0, &cws, &cwn, &cwcap); /* keep only names written under 2+ distinct module paths */ int any = 0; for (int i = 0; i < wn; i++) { int collide = 0; for (int j = 0; j < wn && !collide; j++) { if (i == j || !sp_streq(ws[i].name, ws[j].name)) continue; if (ws[i].depth != ws[j].depth) { collide = 1; break; } for (int k = 0; k < ws[i].depth; k++) if (!sp_streq(ws[i].path[k], ws[j].path[k])) { collide = 1; break; } } if (!collide && ws[i].depth > 0 && builtin_class_id(ws[i].name) != 0) collide = 1; /* ... and a nested constant named like a class defined elsewhere. A class-defining write (`Point = Struct.new`) is the class pass's to qualify, as a class. */ if (!collide && ws[i].depth > 0 && !qc_is_class_write(&ws[i], cws, cwn) && qc_leaf_elsewhere(&ws[i], cws, cwn)) collide = 1; if (!collide) { ws[i] = ws[--wn]; i--; continue; } any = 1; } free(cws); if (any) { /* rewrite reads first (they match against the original write names) */ char mods[QC_MAXDEPTH][64]; qc_build_reverse_flags(c); for (int r = 0; r < nt_nroots(nt); r++) qc_rewrite_reads(c, nt_root(nt, r), mods, 0, ws, wn); qc_free_reverse_flags(); /* then qualify the nested writes themselves */ for (int i = 0; i < wn; i++) { if (ws[i].depth == 0) continue; char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[i]); qc_keep_orig_name(nt, ws[i].node); nt_set_str((NodeTable *)nt, ws[i].node, "name", qn); qc_const_quals = realloc(qc_const_quals, sizeof(char *) * (size_t)(qc_nconst_quals + 1)); qc_const_quals[qc_nconst_quals++] = strdup(qn); } } qc_free_headers(); free(ws); } /* ---- Colliding class/module-name qualification ------------------------ * Classes/modules live in a flat sp_ C namespace keyed by the leaf * name, so `Web::Response` and `Chat::Response` collapse onto one ClassInfo * (struct + methods merge, the last `initialize` wins -> uninitialized ivars * -> SIGSEGV). This is the class-definition analogue of the nested-constant * pass above: when the same class/module leaf name is defined under 2+ * distinct module paths, rename each nested definition to a qualified * `__..__` and rewrite every reference to whichever qualified * class it denotes (relative reads prefer the lexically enclosing module * chain; `::`-anchored reads resolve from the root). Collision-gated: a * program whose class names are all unique is untouched (so the common case, * optcarrot, and the self-host build see zero change). The rewrite happens * before walk_scope, so registration (comp_class_new) and every reference * lookup (comp_class_index) naturally key on the now-distinct names, and the * emitted C identifier (sp_) is distinct too -- no change to the many * leaf-name read sites. */ void qc_collect_class_writes(Compiler *c, int node, char (*path)[64], int depth, QCWrite **ws, int *n, int *cap) { const NodeTable *nt = c->nt; if (node < 0) return; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return; QCRebase *own = NULL; if ((ty_k == NK_ModuleNode || ty_k == NK_ClassNode) && depth < QC_MAXDEPTH) { int cp = nt_ref(nt, node, "constant_path"); const char *mn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (mn) { /* `class A::B`: B is defined in A, so it is recorded as a write under A as well as under the module it is written in -- `class X::Foo` and `class Y::Foo` are two classes, as `module X; class Foo` and `module Y; class Foo` are -- and its body's lexical path is A::B (#8369). ws[i].node is the constant_path node whose name the write-rewrite pass will qualify. */ depth = qc_push_header(nt, node, &path, depth, NULL, &own); if (*n >= *cap) { *cap = *cap ? *cap * 2 : 16; *ws = realloc(*ws, sizeof(QCWrite) * (size_t)*cap); } QCWrite *w = &(*ws)[(*n)++]; w->node = cp; w->depth = depth - 1; w->salt = 0; w->head[0] = 0; for (int i = 0; i < depth - 1; i++) snprintf(w->path[i], 64, "%s", path[i]); snprintf(w->name, sizeof w->name, "%s", mn); /* a builtin parent (`class String::Helper`) is no body the passes place the class under: it stays registered by its leaf, under the body it is written in. The parent's name tells two of them apart. */ int par = nt_kind(nt, cp) == NK_ConstantPathNode ? nt_ref(nt, cp, "parent") : -1; const char *hn = par >= 0 && nt_kind(nt, par) == NK_ConstantReadNode ? nt_str(nt, par, "name") : NULL; if (hn && (is_builtin_class_name(hn) || is_builtin_module_name(hn))) snprintf(w->head, sizeof w->head, "%s", hn); } } /* `Node = Struct.new(...)` / `Data.define(...)` inside a class or module defines a class just as `class Node` does, and its leaf name collides the same way -- two sibling namespaces each with their own `TreeNode` bound every reference to whichever was registered first. */ if (ty_k == NK_ConstantWriteNode && depth > 0) { const char *cn = nt_str(nt, node, "name"); if (cn && qc_struct_or_data_value(nt, nt_ref(nt, node, "value"))) { if (*n >= *cap) { *cap = *cap ? *cap * 2 : 16; *ws = realloc(*ws, sizeof(QCWrite) * (size_t)*cap); } QCWrite *w = &(*ws)[(*n)++]; w->node = node; w->depth = depth; w->salt = 0; w->head[0] = 0; for (int i = 0; i < depth; i++) snprintf(w->path[i], 64, "%s", path[i]); snprintf(w->name, sizeof w->name, "%s", cn); } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) qc_collect_class_writes(c, nt_ref_at(nt, node, i), path, depth, ws, n, cap); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int k = 0; k < m; k++) qc_collect_class_writes(c, ids[k], path, depth, ws, n, cap); } qc_pop_header(own, NULL); } /* how a `native_struct "A::B::Leaf", ...` declaration names w's leaf: 2 when it spells w's own path -- `class OpenSSL::X509::Store` beside "OpenSSL::X509::Store" is that native class reopened -- 1 when it is under another qualified path, 0 when no declaration names it */ static int qc_native_leaf_declared(Compiler *c, const QCWrite *w) { const NodeTable *nt = c->nt; int found = 0; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "native_struct") || nt_ref(nt, id, "receiver") >= 0) continue; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an < 1 || nt_kind(nt, av[0]) != NK_StringNode) continue; const char *cn = nt_str(nt, av[0], "content"); const char *sep = cn ? strrchr(cn, ':') : NULL; if (!sep || sep == cn || sep[-1] != ':') continue; if (!sp_streq(sep + 1, w->name)) continue; char own[512]; own[0] = 0; for (int i = 0; i < w->depth; i++) { strncat(own, w->path[i], sizeof own - strlen(own) - 1); strncat(own, "::", sizeof own - strlen(own) - 1); } strncat(own, w->name, sizeof own - strlen(own) - 1); if (sp_streq(own, cn)) return 2; found = 1; } return found; } /* whether the program defines a class or module whose header names `nm` */ static int qc_class_name_defined(Compiler *c, const char *nm) { const NodeTable *nt = c->nt; for (int pass = 0; pass < 2; pass++) NT_FOREACH_KIND(nt, pass ? NK_ModuleNode : NK_ClassNode, id) { int cp = nt_ref(nt, id, "constant_path"); const char *mn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (mn && sp_streq(mn, nm)) return 1; } return 0; } void qualify_colliding_classes(Compiler *c) { const NodeTable *nt = c->nt; QCWrite *ws = NULL; int wn = 0, wcap = 0; char path[QC_MAXDEPTH][64]; qc_resolve_headers(c); for (int r = 0; r < nt_nroots(nt); r++) /* each file's tree */ qc_collect_class_writes(c, nt_root(nt, r), path, 0, &ws, &wn, &wcap); /* A class under a builtin parent (`class String::Helper`) stays under the body it is written in, registered by its leaf. Another definition of that leaf in the same body, with another parent or none, would merge with it. When the class is nested, any other definition of the leaf makes the passes qualify it, and a read of its path then reaches the other one. A top-level class is left alone beside a nested one. */ for (int i = 0; i < wn; i++) { if (!ws[i].head[0]) continue; for (int j = 0; j < wn; j++) { if (j == i || !sp_streq(ws[j].name, ws[i].name)) continue; int same_body = qc_path_eq(ws[i].path, ws[i].depth, ws[j].path, ws[j].depth); if (same_body && sp_streq(ws[j].head, ws[i].head)) continue; if (!same_body && ws[i].depth == 0) continue; char msg[512]; snprintf(msg, sizeof msg, "a class or module named %s under the builtin %s is not supported beside " "another definition of %s: spinel cannot keep the two apart; rename one of them", ws[i].name, ws[i].head, ws[i].name); unsupported_feature(c, ws[i].node, msg); } } /* the plain constant writes left under a bare leaf (the constant pass qualified the nested ones that collide), for a nested class named like a top-level constant: `Config = {...}` beside `module Rack; class Config` */ QCWrite *kws = NULL; int kwn = 0, kwcap = 0; for (int r = 0; r < nt_nroots(nt); r++) qc_collect_writes(c, nt_root(nt, r), path, 0, &kws, &kwn, &kwcap); for (int i = 0; i < kwn; i++) if (qc_is_class_write(&kws[i], ws, wn)) { kws[i] = kws[--kwn]; i--; } /* keep only leaf names defined under 2+ distinct module paths */ int any = 0; for (int i = 0; i < wn; i++) { int collide = 0; for (int j = 0; j < wn && !collide; j++) { if (i == j || !sp_streq(ws[i].name, ws[j].name)) continue; if (ws[i].depth != ws[j].depth) { collide = 1; break; } for (int k = 0; k < ws[i].depth; k++) if (!sp_streq(ws[i].path[k], ws[j].path[k])) { collide = 1; break; } } /* A class defined INSIDE a module whose leaf name is a builtin's -- `module M; class Array < Bench` -- collides with the BUILTIN rather than with another user class, and every builtin-name test (Array.new, the reopen checks) would take it for the builtin. Qualify it too (#3781). */ if (!collide && ws[i].depth > 0 && builtin_class_id(ws[i].name) != 0) collide = 1; /* ... and a native class declared under a qualified path (openssl's `native_struct "OpenSSL::X509::Store"`), which registers under its leaf: a nested `class Store` elsewhere (activesupport's Cache::Store) shares that key and was refused as a merge; qualified, it is its own class. */ int native = ws[i].depth > 0 ? qc_native_leaf_declared(c, &ws[i]) : 0; if (!collide && native == 1) collide = 1; /* ... and the builtin classes the id table does not carry (Encoding is a tag of its own, not a class id): walk_scope refuses a nested `module Encoding` -- activesupport's ActiveSupport::JSON::Encoding -- by is_builtin_class_name, so qualify by the same predicate and the refusal is never reached. A builtin MODULE's name (Comparable, Math) reopens at any depth and is left alone. */ if (!collide && ws[i].depth > 0 && is_builtin_class_name(ws[i].name) && !is_builtin_module_name(ws[i].name)) collide = 1; /* ... and the builtins with no row in that table at all (Monitor, and OpenStruct once required), whose leaf the builtin's own checks match as well: graphql's `class MonitorTrace::Monitor` took over a top-level `Monitor.new`. */ if (!collide && ws[i].depth > 0 && is_untabled_native_class_name(ws[i].name)) collide = 1; /* ... and the builtin exceptions the id table carries without a class id (LoadError, SystemCallError): builtins/gem.rb's `module Gem; class LoadError < ::LoadError` took the builtin's name, so a top-level `rescue LoadError` named Gem's class (and, once a builtin exception's reopening adds to the runtime's class, lost its C type). */ if (!collide && ws[i].depth > 0 && is_builtin_exception_name(ws[i].name)) collide = 1; /* ... and a body reopening a constant the constant pass qualified -- `C64 = Data.define(...)` then `class C64` in the same module, with another module's C64 beside it -- or the body becomes a class of its own under the bare leaf and the Data class never gets its methods. */ if (!collide && ws[i].depth > 0) { char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[i]); if (qc_const_qualified(qn)) collide = 1; } /* ... and a nested class sharing its leaf with a plain constant written elsewhere, which keeps the bare leaf as its key */ if (!collide && ws[i].depth > 0 && qc_leaf_elsewhere(&ws[i], kws, kwn)) collide = 1; /* ... except the native class itself, written under the path its declaration spells: the native class registers under its bare leaf, so the reopening keeps the leaf whatever else shares it, and the others (Cache::Store) are the ones qualified away from it. */ if (native == 2) collide = 0; /* ... and when the qualified name is one the program gives a class or module of its own (`class Foo__Mutex` beside `class Foo::Mutex`), the name takes a leading underscore, as often as it needs to be new. The leaf is no key for it: another definition of the leaf (`Bar::Mutex`) would merge with it. A top-level definition of the leaf is still refused. */ if (collide && ws[i].depth > 0) { char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[i]); if (qc_class_name_defined(c, qn)) { for (int j = 0; j < wn; j++) if (j != i && ws[j].depth == 0 && sp_streq(ws[j].name, ws[i].name)) { char msg[768]; snprintf(msg, sizeof msg, "a class or module named %s under a module path takes the " "internal name %s, which the program's own class %s already has, and a " "top-level %s is defined too; rename one of them", ws[i].name, qn, qn, ws[i].name); unsupported_feature(c, ws[i].node, msg); } while (qc_class_name_defined(c, qn)) { ws[i].salt++; qc_qualified_name(qn, sizeof qn, &ws[i]); } } } if (!collide) { ws[i] = ws[--wn]; i--; continue; } any = 1; } free(kws); if (any) { /* rewrite references first (they match against the original leaf names), then qualify the nested definitions themselves */ char mods[QC_MAXDEPTH][64]; qc_build_reverse_flags(c); for (int r = 0; r < nt_nroots(nt); r++) qc_rewrite_reads(c, nt_root(nt, r), mods, 0, ws, wn); qc_free_reverse_flags(); for (int i = 0; i < wn; i++) { if (ws[i].depth == 0) continue; char qn[512]; qc_qualified_name(qn, sizeof qn, &ws[i]); nt_set_str((NodeTable *)nt, ws[i].node, "name", qn); /* and the Ruby name the path resolved to, for registration: the header's own parent segments (an alias, a module whose name is qualified too) need not name the class that encloses it */ char rn[512]; qc_ruby_path(ws[i].path, ws[i].depth, rn, sizeof rn); strncat(rn, ws[i].name, sizeof rn - strlen(rn) - 1); nt_node_set_str((NodeTable *)nt, ws[i].node, "qc_ruby_name", rn); } } qc_free_headers(); free(ws); } /* True if `name` is used as a local write or block parameter OUTSIDE the given block body (stamp `gen`) -- i.e. a block-local of that name would shadow a real enclosing use, so it must be split into its own slot. */ /* Name index over the write/param node set. The collision checks below ask "is this name used outside the block body?", and scanning every write and parameter node per block parameter was O(blocks * params * n) -- 12.7% of a 37k-line compile. Bucketing by name makes each check touch only the nodes that actually carry it. A rename during the pass changes node names, so the index is rebuilt lazily when that happens (renames are the exception). */ typedef struct { const Compiler *c; const int *wp; int wpn; int *head; int *next; unsigned mask; } BlkpIdx; static void blkp_idx_build(BlkpIdx *ix, const NodeTable *nt) { for (unsigned b = 0; b <= ix->mask; b++) ix->head[b] = -1; for (int i = 0; i < ix->wpn; i++) { const char *nm = nt_str(nt, ix->wp[i], "name"); if (!nm) { ix->next[i] = -1; continue; } unsigned b = sp_strhash(nm) & ix->mask; ix->next[i] = ix->head[b]; ix->head[b] = i; } } static int blkp_idx_init(BlkpIdx *ix, const Compiler *c, const int *wp, int wpn) { const NodeTable *nt = c->nt; unsigned nb = 16; while (nb < (unsigned)wpn * 2u && nb < (1u << 22)) nb <<= 1; ix->c = c; ix->wp = wp; ix->wpn = wpn; ix->mask = nb - 1; ix->head = malloc((size_t)nb * sizeof(int)); ix->next = malloc((size_t)(wpn > 0 ? wpn : 1) * sizeof(int)); if (!ix->head || !ix->next) { free(ix->head); free(ix->next); ix->head = ix->next = NULL; return 0; } blkp_idx_build(ix, nt); return 1; } static void blkp_idx_free(BlkpIdx *ix) { free(ix->head); free(ix->next); ix->head = ix->next = NULL; } /* Scan the wp entries bucketed under `name`. The index is built ONCE and never rebuilt: a rename only ever moves a node from its source name to an invented `x__bpN`, and callers re-read and compare the name, so an entry left in a stale bucket is simply rejected. The one shape that could MISS an entry is a query for an invented name, which falls back to scanning every entry. */ typedef struct { const BlkpIdx *ix; int all; int i; } BlkpScan; static BlkpScan blkp_scan(const BlkpIdx *ix, const char *name) { BlkpScan sc; sc.ix = ix; sc.all = block_param_is_renamed(ix->c, name); sc.i = sc.all ? 0 : ix->head[sp_strhash(name) & ix->mask]; return sc; } static int blkp_scan_next(BlkpScan *sc, int *out) { if (sc->all) { if (sc->i >= sc->ix->wpn) return 0; *out = sc->ix->wp[sc->i++]; return 1; } if (sc->i < 0) return 0; *out = sc->ix->wp[sc->i]; sc->i = sc->ix->next[sc->i]; return 1; } static int name_written_outside(const NodeTable *nt, const char *name, BlkpIdx *ix, const int *inbody, int gen) { BlkpScan sc = blkp_scan(ix, name); int w; while (blkp_scan_next(&sc, &w)) { if (inbody[w] == gen) continue; const char *wn = nt_str(nt, w, "name"); if (wn && sp_streq(wn, name)) return 1; } return 0; } /* True if `name` is bound as a parameter anywhere in the params subtree (leading / optional / rest / keyword / post / destructured), as opposed to a block-local (`; x`) shadow declaration. Block-locals share the BlockNode's comma-joined `locals` string with the real params, so the two must be told apart before a locals entry is treated as a shadow. */ static int params_bind_name(const NodeTable *nt, int id, const char *name) { if (id < 0) return 0; const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (ty && (strstr(ty, "ParameterNode") || k == NK_LocalVariableTargetNode)) { const char *pn = nt_str(nt, id, "name"); if (pn && sp_streq(pn, name)) return 1; } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) if (params_bind_name(nt, nt_ref_at(nt, id, i), name)) return 1; int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, id, i, &n); for (int j = 0; j < n; j++) if (params_bind_name(nt, ids[j], name)) return 1; } return 0; } /* Rename any block-local (`; x`) whose name collides with a local used outside the block body so the shadow becomes a distinct slot, mirroring the block parameter rename below. The declaration lives only in the BlockNode's comma-joined `locals` string (spinel's parser carries no BlockLocalVariableNode array), which is rewritten in lock-step so emit_block_locals_reset nils the renamed slot rather than the enclosing one. */ static void rename_shadowing_block_locals(Compiler *c, int L, int pn, int body, const int *inbody, int gen, BlkpIdx *ix) { const NodeTable *nt = c->nt; const char *locs0 = nt_str(nt, L, "locals"); if (!locs0 || !*locs0) return; /* copy: nt_set_str below frees locs0's storage while we still read it */ size_t llen = strlen(locs0); char *locs = malloc(llen + 1); if (!locs) return; memcpy(locs, locs0, llen + 1); size_t ocap = llen + 32; char *out = malloc(ocap); if (!out) { free(locs); return; } out[0] = 0; size_t out_len = 0; int changed = 0; char *save = NULL; for (char *tok = strtok_r(locs, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { char *newn = NULL; const char *emit = tok; if (!params_bind_name(nt, pn, tok) && !block_param_made_for(c, tok, L) && name_written_outside(nt, tok, ix, inbody, gen)) { newn = block_param_invent_name(c, tok, L); blkp_rewrite_refs(c, body, tok, newn); emit = newn; changed = 1; } /* track out_len + memcpy so the append is O(1), keeping the whole rebuild O(N) rather than rescanning `out` with strlen/strcat each iteration. */ size_t emit_len = strlen(emit); size_t need = out_len + emit_len + 2; if (need > ocap) { ocap = need * 2; char *t = realloc(out, ocap); if (!t) { free(newn); free(out); free(locs); return; } out = t; } if (out_len > 0) out[out_len++] = ','; memcpy(out + out_len, emit, emit_len); out_len += emit_len; out[out_len] = '\0'; free(newn); } if (changed) nt_set_str((NodeTable *)nt, L, "locals", out); free(out); free(locs); } /* A native class's own methods, called on an implicit self from a method the program adds by reopening the class. The native surface is dispatched through the receiver, and an implicit self has no receiver node at all, so every one of them was a NameError: class StringIO def rest = gets # undefined local variable or method 'gets' end Give it the receiver it means. Same shape as the Enumerable-on-self redirect, and for the same reason. Only names the class's native surface actually declares, so a genuine typo still reports as one, and only when no Ruby method of the class already answers -- that one binds first. */ static int give_native_self_calls_a_receiver(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; Scope *osc = comp_scope_of(c, id); int ocid = osc ? osc->class_id : -1; if (ocid < 0 || osc->is_cmethod) continue; /* and in a subclass of a native class, which answers with its bindings (#9269) */ if (ocid >= c->nclasses || (!c->classes[ocid].is_native_class && c->classes[ocid].nsub_root <= 0)) continue; if (call_target(c, id, ocid, nm, 0, NULL) >= 0) continue; /* the class's own Ruby method */ int argc = 0; { int an = nt_ref(nt, id, "arguments"); if (an >= 0) nt_arr(nt, an, "arguments", &argc); } if (comp_native_method_find(c, ocid, nm, argc, 0) < 0) continue; int selfn = nt_new_node(nt, "SelfNode"); if (selfn < 0) continue; nt_set_self_receiver(nt, id, selfn); comp_grow_node_arrays(c); c->nscope[selfn] = c->nscope[id]; changed = 1; } return changed; } /* Exception's own methods, called on an implicit self from a method of the program's exception class (`def label = "w:#{message}"`): they are read off the receiver, and with none the bare call was a NameError where `self.message` answered. Give it the receiver, unless a Ruby method of the chain (a reopening of the builtin among them) answers the name. */ static int give_exception_self_calls_a_receiver(Compiler *c) { static const struct { const char *name; int argc_min, argc_max; } own[] = { { "message", 0, 0 }, { "backtrace", 0, 0 }, { "backtrace_locations", 0, 0 }, { "cause", 0, 0 }, { "full_message", 0, 1 }, { "detailed_message", 0, 1 }, { "set_backtrace", 1, 1 }, }; /* (the renderings' one argument is their keywords, `highlight: false`) */ NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0 || nt_ref(nt, id, "block") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int an = 0; const int *av = NULL; { int a = nt_ref(nt, id, "arguments"); if (a >= 0) av = nt_arr(nt, a, "arguments", &an); } int hit = 0; for (size_t k = 0; k < sizeof own / sizeof own[0] && !hit; k++) hit = sp_streq(nm, own[k].name) && an >= own[k].argc_min && an <= own[k].argc_max; if (!hit || (an == 1 && !sp_streq(nm, "set_backtrace") && nt_kind(nt, av[0]) != NK_KeywordHashNode)) continue; Scope *osc = comp_scope_of(c, id); int ocid = osc ? osc->class_id : -1; if (ocid < 0 || ocid >= c->nclasses || osc->is_cmethod || !class_is_exc_subclass(c, ocid)) continue; if (call_target(c, id, ocid, nm, 0, NULL) >= 0) continue; int selfn = nt_new_node(nt, "SelfNode"); if (selfn < 0) continue; nt_set_self_receiver(nt, id, selfn); comp_grow_node_arrays(c); c->nscope[selfn] = c->nscope[id]; changed = 1; } return changed; } /* An implicit-self call inside a class or module method, of a name the program defines as an instance method of Object that no class method of the chain answers: self is the class or module, an Object, so CRuby finds Object's method (`def self.go = current_part` beside `class Object; def current_part`). It was the NameError for a bare identifier, where `self.current_part` already answered. Give it that receiver, as the native surface's implicit self gets one above. Module's own private methods come before Object's and keep their bare form. */ static int module_private_builtin(const char *nm) { static const char *const names[] = { "attr", "attr_accessor", "attr_reader", "attr_writer", "define_method", "include", "extend", "prepend", "private", "public", "protected", "module_function", "private_constant", "public_constant", "alias_method", "remove_method", "undef_method", "private_class_method", "public_class_method", "refine", "using", "new", NULL }; for (int i = 0; names[i]; i++) if (sp_streq(nm, names[i])) return 1; return 0; } static int give_object_methods_a_class_self(Compiler *c) { int ob = comp_core_class(c, CORE_OBJECT); if (ob < 0) return 0; NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || module_private_builtin(nm)) continue; Scope *osc = comp_scope_of(c, id); if (!osc || osc->class_id < 0 || osc->class_id == ob || !osc->is_cmethod) continue; if (call_target(c, id, osc->class_id, nm, CT_CMETHOD, NULL) >= 0) continue; if (call_target(c, id, ob, nm, CT_OWN, NULL) < 0) continue; int selfn = nt_new_node(nt, "SelfNode"); if (selfn < 0) continue; nt_set_self_receiver(nt, id, selfn); comp_grow_node_arrays(c); c->nscope[selfn] = c->nscope[id]; changed = 1; } return changed; } /* The same missing receiver, for the universal Object predicates. `is_a?` and friends are dispatched through the receiver, so written on an implicit self they had no receiver node and were rejected outright: class Room < ApplicationRecord def open? = is_a?(Rooms::Open) # unsupported call end which is how Rails spells single-table inheritance, and how anyone spells a type predicate on self. Give it the receiver it means (#4142). The list was deliberately short at first -- only predicates that are Object's and have no sensible meaning as a bare call -- and being short is what brought it back: `to_s`, `inspect`, `hash`, `dup`, `itself` and `object_id` on an implicit self were all still the NameError, in EVERY class, so `def label = "x: " + to_s` did not compile anywhere (#4387). A hand-kept list of what an object answers goes stale exactly this way; the note on POLY_RAW in analyze_infer.c says so about its own twin, and this is the second time here. So the names come from that table rather than from a list of their own. It is the compiler's statement of what a receiver answers universally, every consumer already reads it, and a name added there is answered here without anyone remembering to. What stays local is the part POLY_RAW cannot know: the two arg-taking type predicates, and the renderers whose answer is a String rather than a raw scalar. A method the class itself defines wins, as does a top-level def of the same name -- both bind ahead of Object's. */ static int give_self_predicates_a_receiver(Compiler *c) { /* What AN_POLY_RAW cannot carry: it states what a receiver answers as a RAW C scalar, so a name whose answer is a String (the renderers), an object (`dup`, `itself`), or self (`freeze`, `tap`) has no row there and is named here instead. Every one of these is verified against an explicit `self.` receiver by test/implicit_self_universal_surface.rb, so a name that stops being answered fails there rather than going quiet. Only names that were actually refused. `freeze` and `respond_to?` already answered an implicit self by another route, and naming them here moved them onto this one -- which widened an ivar from sp_int to sp_RbVal in test/respond_to_implicit_self.rb. A redirect that changes the route of a call that already worked is a cost with no benefit. Not everything receiver-less can join this: the bare Kernel surface is NOT receiver-transparent. `raise "x"` inside a method compiles to the raise; rewritten to `self.raise("x")` it becomes a NoMethodError, which is what a list-free version of this pass did to 226 test programs. The sweep is what caught it -- a probe that wrapped the call in `rescue` had reported it working. */ static const struct { const char *name; int argc; } preds[] = { { "is_a?", 1 }, { "kind_of?", 1 }, { "instance_of?", 1 }, { "to_s", 0 }, { "inspect", 0 }, { "itself", 0 }, { "dup", 0 }, { "clone", 0 }, { "tap", 0 }, { "display", 0 }, { "instance_variables", 0 }, { "instance_variable_get", 1 }, { "instance_variable_set", 2 }, }; NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int want = -1; for (size_t k = 0; k < sizeof preds / sizeof preds[0]; k++) if (sp_streq(nm, preds[k].name)) { want = preds[k].argc; break; } if (want < 0) want = an_poly_raw_argc(nm); /* the universal table decides the rest */ if (want < 0) continue; Scope *osc = comp_scope_of(c, id); int ocid = osc ? osc->class_id : -1; /* At the top level and in a top-level def, self is main, an Object: a name Object answers is given self too, unless the program's own method has it (a top-level def, or one on Object or Kernel), which the bare call already reaches. Not in a class body or an instance_eval block, whose self is another object. */ if (object_public_method_name(nm) && self_is_main(c, id)) { if (std_stream_bare_call_own(c, id, nm)) continue; /* or a module the top level includes */ int inc = 0; for (int k = 0; k < c->ntoplevel_includes && !inc; k++) inc = call_target(c, id, c->toplevel_includes[k], nm, 0, NULL) >= 0; if (inc) continue; /* main's ivars are not the slots an explicit receiver reaches */ if (strncmp(nm, "instance_variable", 17) == 0) continue; } else { if (ocid < 0 || osc->is_cmethod) continue; if (ocid >= c->nclasses) continue; if (call_target(c, id, ocid, nm, 0, NULL) >= 0) continue; if (call_target(c, id, -1, nm, CT_TOP, NULL) >= 0) continue; } int argc = 0; { int an = nt_ref(nt, id, "arguments"); if (an >= 0) nt_arr(nt, an, "arguments", &argc); } if (argc != want) continue; int selfn = nt_new_node(nt, "SelfNode"); if (selfn < 0) continue; nt_set_self_receiver(nt, id, selfn); comp_grow_node_arrays(c); c->nscope[selfn] = c->nscope[id]; changed = 1; } return changed; } /* Give each block its own numbered parameters, where a scope holds more than one block that uses them. `_1` .. `_9` (and `it`, which the parser lowers to `_1`) are names spinel synthesizes rather than names the author wrote, and blocks share their enclosing scope's local table -- so two such blocks in one method interned the SAME slot and their types merged. A scope with one of them gets `sp_int lv__1`; add a second over strings and both become a boxed `sp_RbVal`. Values stayed right (each block writes the slot before reading it), so what this costs is the type. It also left two passes typing that one slot from different call shapes on every round, which is the last program in the suite whose inference fixpoint ran to its cap (#4116). Only where the names actually collide, which is what rename_shadowing_block_params does for a named parameter and for the same reason: a scope with a single numbered-param block is already correct, and renaming it would churn the emitted identifier for nothing. The rewrite is textual on the AST, like that pass: the body's reads are rewritten, the block's comma-joined `locals` string with them, and the generated name is recorded on the NumberedParametersNode where numbered_param_name reads it back. The node keeps its shape, so every pass that recognises a numbered-param block still does. A nested block with its own numbered parameters is a SyntaxError in CRuby ("numbered parameter is already used in outer block"), so the walk stops at one rather than guessing which block a name belongs to. */ static int numbered_params_node_of(Compiler *c, int L) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, L); if (k != NK_BlockNode && k != NK_LambdaNode) return -1; int bp = nt_ref(nt, L, "parameters"); if (bp < 0) return -1; NodeKind bpk = nt_kind(nt, bp); return (bpk == NK_NumberedParametersNode) ? bp : -1; } static void numbered_rename_reads(NodeTable *nt, int id, const char *from, const char *to, int depth) { if (id < 0 || id >= nt->count || depth > 200) return; const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty) return; if (k == NK_BlockNode || k == NK_LambdaNode) { int bp = nt_ref(nt, id, "parameters"); NodeKind bpk = nt_kind(nt, bp); if (bpk == NK_NumberedParametersNode) return; /* binds it itself */ } if (k == NK_LocalVariableReadNode || k == NK_LocalVariableWriteNode || k == NK_LocalVariableTargetNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode) { const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, from)) nt_set_str(nt, id, "name", to); } const SpNode *nd = &nt->nodes[id]; for (int i = 0; i < nd->nr; i++) numbered_rename_reads(nt, nd->r[i].ref, from, to, depth + 1); for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) numbered_rename_reads(nt, nd->a[i].ids[j], from, to, depth + 1); } /* Replace `from` with `to` in a block's comma-joined `locals` string. emit_block_locals_reset nils every name in it that is not a parameter, so a stale entry there nils the renamed slot right after the bind, once per iteration, and the body reads nil. */ void numbered_rename_locals_str(NodeTable *nt, int L, const char *from, const char *to) { const char *locs = nt_str(nt, L, "locals"); if (!locs || !*locs) return; size_t flen = strlen(from), cap = strlen(locs) + strlen(to) + 8, w = 0; char *out = (char *)malloc(cap); if (!out) return; out[0] = 0; for (const char *p = locs; ; ) { const char *e = strchr(p, ','); size_t seg = e ? (size_t)(e - p) : strlen(p); const char *put = p; size_t putn = seg; if (seg == flen && !strncmp(p, from, seg)) { put = to; putn = strlen(to); } if (w) out[w++] = ','; memcpy(out + w, put, putn); w += putn; out[w] = 0; if (!e) break; p = e + 1; } nt_set_str(nt, L, "locals", out); free(out); } int scope_numbered_block_params_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; /* how many numbered-param blocks each scope holds */ int *per_scope = (int *)calloc((size_t)(c->nscopes > 0 ? c->nscopes : 1), sizeof(int)); if (!per_scope) return 0; for (int L = 0; L < n0; L++) { if (numbered_params_node_of(c, L) < 0) continue; int sc = c->nscope[L]; if (sc >= 0 && sc < c->nscopes) per_scope[sc]++; } for (int L = 0; L < n0; L++) { int bp = numbered_params_node_of(c, L); if (bp < 0) continue; int sc = c->nscope[L]; if (sc < 0 || sc >= c->nscopes || per_scope[sc] < 2) continue; /* no collision */ int maxn = (int)nt_int(nt, bp, "maximum", 0); if (maxn <= 0 || maxn > 9) continue; int body = nt_ref(nt, L, "body"); for (int k = 1; k <= maxn; k++) { char from[8], to[64], key[8]; snprintf(from, sizeof from, "_%d", k); snprintf(to, sizeof to, "_%d__b%s", k, comp_node_tag(c, L)); snprintf(key, sizeof key, "n%d", k); if (body >= 0) numbered_rename_reads(nt, body, from, to, 0); numbered_rename_locals_str(nt, L, from, to); /* nt_node_set_str, not nt_set_str: the latter only UPDATES a key the node already carries and silently drops a new one. */ nt_node_set_str(nt, bp, key, to); } } free(per_scope); return 0; } /* Every parameter kind that binds a name: a block parameter or block-local shadowing any of them needs its own slot, not just a shadowed required parameter. Counting only RequiredParameterNode left `def m(q = 7)` with a `|x; q|` block writing the method's q, and `*q` / `**q` / `&q` sharing the C slot with a different C type, which did not compile. */ static int is_name_binding_param(const char *ty) { return ty && (sp_streq(ty, "RequiredParameterNode") || sp_streq(ty, "OptionalParameterNode") || sp_streq(ty, "RestParameterNode") || sp_streq(ty, "RequiredKeywordParameterNode") || sp_streq(ty, "OptionalKeywordParameterNode") || sp_streq(ty, "KeywordRestParameterNode") || sp_streq(ty, "BlockParameterNode")); } /* The names a destructuring parameter (`|(a, (b, *c))|`) binds: the RequiredParameterNode leaves under its MultiTargetNode / SplatNode. */ static void blkp_collect_destructured(const NodeTable *nt, int id, int *out, int *n, int cap) { const char *ty = id >= 0 ? nt_type(nt, id) : NULL; NodeKind ty_k = nt_kind(nt, id); if (!ty) return; if (ty_k == NK_RequiredParameterNode) { if (*n < cap && nt_str(nt, id, "name")) out[(*n)++] = id; return; } if (ty_k == NK_SplatNode) { blkp_collect_destructured(nt, nt_ref(nt, id, "expression"), out, n, cap); return; } if (ty_k != NK_MultiTargetNode) return; static const char *const parts[] = { "lefts", "rights" }; for (int k = 0; k < 2; k++) { int m = 0; const int *ids = nt_arr(nt, id, parts[k], &m); for (int i = 0; i < m; i++) blkp_collect_destructured(nt, ids[i], out, n, cap); } blkp_collect_destructured(nt, nt_ref(nt, id, "rest"), out, n, cap); } /* A block's or lambda's anonymous `**` takes every keyword no named one does, as a `**kw` would; named after its block, it is one to every binder that builds a keyword-rest and skips the unknown-keyword check for it. */ /* The anonymous `**` of block or lambda id's parameters, or -1 */ static int anon_block_kwrest(const NodeTable *nt, int id) { int bp = nt_ref(nt, id, "parameters"); if (bp < 0 || nt_kind(nt, bp) != NK_BlockParametersNode) return -1; int pn = nt_ref(nt, bp, "parameters"); int kr = pn >= 0 ? nt_ref(nt, pn, "keyword_rest") : -1; if (kr < 0 || nt_kind(nt, kr) != NK_KeywordRestParameterNode) return -1; const char *kn = nt_str(nt, kr, "name"); return kn && kn[0] ? -1 : kr; } int name_anon_block_kwrest_left(Compiler *c, LowerCtx *lc, int id) { return anon_block_kwrest(c->nt, id) >= 0; } int name_anon_block_kwrest_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int kr = anon_block_kwrest(nt, id); if (kr < 0) return 0; char nm[48]; snprintf(nm, sizeof nm, "__blk_kwrest%s", comp_node_tag(c, id)); nt_node_set_str(nt, kr, "name", nm); return 1; } /* What the pass knows of the program: each block's owning call (the node whose "block" ref it is), a generation stamp per node for "inside this block's body", and the program's local writes and parameters indexed by name, all over the nodes [0, upto). blkp_step_extend takes in the nodes made since, so the walk's spans (rename_shadowing_block_params_span) ask the program without rebuilding it per splice. */ typedef struct BlkpStep { int *owner, *inbody, *wp; int wpn, cap, upto, gen; BlkpIdx ix; } BlkpStep; static void blkp_step_extend(Compiler *c, BlkpStep *st) { const NodeTable *nt = c->nt; int n = nt->count, from = st->upto; if (n <= from) return; st->owner = realloc(st->owner, (size_t)n * sizeof(int)); st->inbody = realloc(st->inbody, (size_t)n * sizeof(int)); st->wp = realloc(st->wp, (size_t)n * sizeof(int)); if (!st->owner || !st->inbody || !st->wp) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = from; i < n; i++) { st->owner[i] = -1; st->inbody[i] = 0; } /* Reverse map block-node -> owning node, built in one O(n) pass. Finding a block's owner by scanning all n nodes per block made this whole pass O(blocks*n) on large inputs (a flattened runtime is ~500k nodes). */ for (int id = from; id < n; id++) { int b = nt_ref(nt, id, "block"); if (b >= 0 && b < n) st->owner[b] = id; } /* All local-variable-write and parameter nodes, collected once; the per-param collision scan iterates this set rather than all n nodes (it re-reads each name fresh, so a rename made earlier in this pass is still reflected). */ int wp0 = st->wpn; for (int w = from; w < n; w++) { const char *wty = nt_type(nt, w); if (a_is_write_node(wty) || is_name_binding_param(wty)) st->wp[st->wpn++] = w; } st->upto = n; if (!st->ix.head) { if (!blkp_idx_init(&st->ix, c, st->wp, st->wpn)) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } return; } st->ix.wp = st->wp; st->ix.next = realloc(st->ix.next, (size_t)(st->wpn > 0 ? st->wpn : 1) * sizeof(int)); if (!st->ix.next) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = wp0; i < st->wpn; i++) { const char *nm = nt_str(nt, st->wp[i], "name"); unsigned b = nm ? sp_strhash(nm) & st->ix.mask : 0; st->ix.next[i] = nm ? st->ix.head[b] : -1; if (nm) st->ix.head[b] = i; } st->ix.wpn = st->wpn; } void blkp_step_free(BlkpStep *st) { if (!st) return; blkp_idx_free(&st->ix); free(st->owner); free(st->inbody); free(st->wp); free(st); } static void blkp_rename_blocks(Compiler *c, BlkpStep *st, const int *ids, int nids); int rename_shadowing_block_params_at(Compiler *c, LowerCtx *lc, int id) { BlkpStep *st = calloc(1, sizeof *st); if (!st) return 0; blkp_step_extend(c, st); blkp_rename_blocks(c, st, NULL, st->upto); blkp_step_free(st); return 0; } /* The same over the blocks of a span of the walk's (lower_span), as it is spliced: a parameter that collides is renamed before the walk registers it (a define_method block's are the method's). The program's names are kept for the step (LowerCtx.blkp) and take in each span's nodes. */ int rename_shadowing_block_params_span(Compiler *c, LowerCtx *lc, int id) { if (!lc->blkp && !(lc->blkp = calloc(1, sizeof *lc->blkp))) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } blkp_step_extend(c, lc->blkp); int n = 0, *ids = lower_span_ids(lc, &n); blkp_rename_blocks(c, lc->blkp, ids, n); free(ids); return 0; } /* The blocks among ids[0..nids) (NULL: the nodes [0, nids)) */ static void blkp_rename_blocks(Compiler *c, BlkpStep *st, const int *ids, int nids) { const NodeTable *nt = c->nt; int *owner = st->owner, *inbody = st->inbody; BlkpIdx ix = st->ix; for (int q = 0; q < nids; q++) { int L = ids ? ids[q] : q; if (L >= st->upto) continue; const char *ty = nt_type(nt, L); NodeKind k = nt_kind(nt, L); if (!ty) continue; int is_lambda = k == NK_LambdaNode; if (!is_lambda && k != NK_BlockNode) continue; /* renameable: a lambda, or a block owned by a named call -- resolved in O(1) through the owner index. */ if (!is_lambda) { int o = owner[L]; if (o < 0 || nt_str(nt, o, "name") == NULL) continue; } int pn = blkp_params_node(c, L); NodeKind pk = nt_kind(nt, pn); /* Numbered parameters are renamed by scope_numbered_block_params, which runs just before this and keys on the NumberedParametersNode; this pass only ever sees an ordinary ParametersNode -- or no parameters at all, for a block whose only names are its body's own locals. */ if (pn >= 0 && (pk != NK_ParametersNode)) continue; int rn = 0; const int *reqs = pn >= 0 ? nt_arr(nt, pn, "requireds", &rn) : NULL; /* rest / post parameters shadow-rename the same way as the requireds: a rest param sharing an outer local's name shares its C slot, so the rest-packing assignment aliases the outer variable (a sole-rest block whose name matches the inlined method's own param read garbage). */ int extras[129]; int ne = 0; if (pn >= 0) { int rref = nt_ref(nt, pn, "rest"); if (rref >= 0 && nt_kind(nt, rref) == NK_RestParameterNode && nt_str(nt, rref, "name")) extras[ne++] = rref; int pon = 0; const int *posts = nt_arr(nt, pn, "posts", &pon); for (int q = 0; q < pon; q++) blkp_collect_destructured(nt, posts[q], extras, &ne, 128); /* a destructured required (`|(b, j)|`) binds its leaves like any other */ for (int q = 0; q < rn; q++) if (reqs[q] >= 0 && nt_kind(nt, reqs[q]) == NK_MultiTargetNode) blkp_collect_destructured(nt, reqs[q], extras, &ne, 128); int oon = 0; const int *opts = nt_arr(nt, pn, "optionals", &oon); for (int q = 0; q < oon && ne < 128; q++) if (opts[q] >= 0 && nt_kind(nt, opts[q]) == NK_OptionalParameterNode && nt_str(nt, opts[q], "name")) extras[ne++] = opts[q]; int kwn = 0; const int *kws = nt_arr(nt, pn, "keywords", &kwn); for (int q = 0; q < kwn && ne < 128; q++) if (kws[q] >= 0 && is_name_binding_param(nt_type(nt, kws[q])) && nt_str(nt, kws[q], "name")) extras[ne++] = kws[q]; int kwr = nt_ref(nt, pn, "keyword_rest"); if (kwr >= 0 && ne < 128 && nt_kind(nt, kwr) == NK_KeywordRestParameterNode && nt_str(nt, kwr, "name")) extras[ne++] = kwr; int bpr = nt_ref(nt, pn, "block"); if (bpr >= 0 && ne < 128 && nt_str(nt, bpr, "name")) extras[ne++] = bpr; } /* block-locals (`; a, b`) live only in the BlockNode's comma-joined `locals` string; a block may carry them with no required params (`{ |; x| ... }`), and the parser lists a body's own locals there too: a name first assigned inside the block, which Ruby scopes to the block even when the enclosing scope assigns the same name further down. A paramless block (`Thread.new do ... end`) was skipped here, so its local shared the enclosing slot with that later assignment -- one cell for every thread (found under #4528). */ const char *locs = nt_str(nt, L, "locals"); int have_locals = locs && *locs; if (rn == 0 && ne == 0 && !have_locals) continue; /* An EMPTY body (`tap do |a| end`) still binds the parameter, so it still collides: skipping it left the two names sharing one C slot, and the slot took the inner block's type while the outer block assigned its own (#4027). There is simply nothing inside to rewrite -- both helpers below take a negative body as the empty subtree it is. */ int body = nt_ref(nt, L, "body"); int gen = ++st->gen; blkp_stamp_subtree(nt, body, inbody, gen); for (int ii = 0; ii < rn + ne; ii++) { int pnode = ii < rn ? reqs[ii] : extras[ii - rn]; const char *p = nt_str(nt, pnode, "name"); if (!p) continue; /* a parameter this block has already been given a slot name for keeps it */ if (block_param_made_for(c, p, L)) continue; /* collision: the name is used outside this block's body -- as a local write/read or as another block's parameter (param-vs-param, e.g. two inject folds sharing `|a, b|` with different element types). Both pollute the shared LocalVar's type. */ int collide = 0; BlkpScan sc = blkp_scan(&ix, p); int w; while (!collide && blkp_scan_next(&sc, &w)) { if (inbody[w] == gen) continue; const char *wty = nt_type(nt, w); int is_param_node = is_name_binding_param(wty); /* don't let this block's own parameter nodes count as a collision */ if (is_param_node) { int own = 0; for (int q = 0; q < rn; q++) if (reqs[q] == w) { own = 1; break; } for (int q = 0; q < ne && !own; q++) if (extras[q] == w) own = 1; if (own) continue; } const char *wn = nt_str(nt, w, "name"); if (wn && sp_streq(wn, p)) collide = 1; } if (!collide) continue; char *oldn = strdup(p); /* copy: nt_set_str frees p's storage */ if (!oldn) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } char *newn = block_param_invent_name(c, oldn, L); /* `Proc#parameters` reports the name the program wrote, and the emitter recovers it by stripping this suffix -- but a stripped name that appears nowhere else is not in the generated symbol table, so interning it at emit time came too late and it rendered as the empty symbol (#4045). Put it in the table here, while the table is still open: the name written, when `oldn` is the slot of a parameter the body assigns. */ comp_sym_intern_n(c, oldn, reassigned_param_written_len(c, oldn)); nt_set_str((NodeTable *)nt, pnode, "name", newn); blkp_rewrite_refs(c, body, oldn, newn); /* an optional's default expression can reference a renamed sibling param (`|a, b=a|`); those reads live under the ParametersNode, not the body, so rewrite there too */ blkp_rewrite_refs(c, pn, oldn, newn); free(oldn); free(newn); } if (have_locals) rename_shadowing_block_locals(c, L, pn, body, inbody, gen, &ix); } } /* ---- --rbs advisory type seeds ---- spinel_rbs_extract emits line-oriented seeds, read from the file named by g_opt_rbs_seed. Before the fixpoint we pin the named params / returns / ivars to the seeded type; guards at the inference write sites then keep the fixpoint from widening a pinned slot (legacy "RBS wins" semantics). Type tokens with no precise C kind (poly*, sym_array, obj_X_ptr_array, unknown classes) are skipped, so a seed never makes inference worse. Entirely inert when g_opt_rbs_seed is unset -- the normal compile path is unchanged. */ static int seed_class_index(Compiler *c, const char *name); /* builtin_value_kind for a seed's `_`-joined name: a qualified builtin (`Errno::ENOENT`, `JSON::ParserError`) arrives as `Errno_ENOENT`. */ static TyKind seed_builtin_kind(const char *name) { TyKind t = builtin_value_kind(name); if (t != TY_UNKNOWN || !strchr(name, '_')) return t; char q[256]; size_t j = 0; for (const char *p = name; *p && j + 2 < sizeof q; p++) { if (*p == '_') { q[j++] = ':'; q[j++] = ':'; } else q[j++] = *p; } q[j] = '\0'; return builtin_value_kind(q); } /* Set by parse_seed_type when the token carried RBS's trailing `?`. An int or float pin keeps the unboxed kind and holds its nil beside the value (an oint), so every site that BOXES such a slot has to know -- otherwise the nil goes out as an ordinary number (a Hash key that misses a literal nil, #3493). Recorded here because the '?' is gone by the time the type is returned. */ static int g_seed_nilable; /* Set by parse_seed_type when it answers TY_UNKNOWN for a type it knows but pins nothing for (a Range, an exception): the slot keeps its inferred type, and an inline annotation naming it still applies. */ static int g_seed_unpinned; static TyKind parse_seed_type(Compiler *c, const char *tok) { g_seed_nilable = 0; g_seed_unpinned = 0; if (!tok || !*tok) return TY_UNKNOWN; size_t n = strlen(tok); char buf[128]; if (n >= sizeof buf) return TY_UNKNOWN; memcpy(buf, tok, n + 1); /* A trailing '?' is RBS's nilable form (`Integer?`, `bool?`). Pinning it to the base type is right only where that type's C slot still has a way to spell nil: a pointer kind (string too) uses NULL, and int / float hold a nil flag beside the value (an oint) every nil? / to_s / boxing site already tests for. `sp_bool` and `sp_sym` have none -- 0 is `false`, and symbol 0 is a real symbol -- so a `bool?` / `Symbol?` pin has nowhere to put nil and collapses it onto false / :"" (#3412). Those pin to the tagged union, which is what `bool | nil` means anyway. */ int nilable = 0; if (n > 0 && buf[n - 1] == '?') { buf[--n] = '\0'; nilable = 1; g_seed_nilable = 1; } if (sp_streq(buf, "int")) return TY_INT; if (sp_streq(buf, "float")) return TY_FLOAT; if (sp_streq(buf, "string") || sp_streq(buf, "str")) return TY_STRING; if (sp_streq(buf, "symbol") || sp_streq(buf, "sym")) return nilable ? TY_POLY : TY_SYMBOL; if (sp_streq(buf, "bool")) return nilable ? TY_POLY : TY_BOOL; /* `singleton(X)` and unions of them: a Class value, which has no nil of its own, so the nilable form is the boxed one */ if (sp_streq(buf, "class")) return nilable ? TY_POLY : TY_CLASS; if (sp_streq(buf, "nil")) return TY_NIL; if (sp_streq(buf, "void")) return TY_VOID; /* heterogeneous unions map to the bare poly tag (#1255); accepting the token pins the slot to sp_RbVal instead of silently dropping the seed */ if (sp_streq(buf, "poly")) return TY_POLY; if (sp_streq(buf, "int_array")) return TY_INT_ARRAY; if (sp_streq(buf, "float_array")) return TY_FLOAT_ARRAY; if (sp_streq(buf, "str_array")) return TY_STR_ARRAY; /* the extractor emits poly_array for Array[T-outside-subset] (its header documents it), but this parser never accepted it -- those seeds were silently dropped */ if (sp_streq(buf, "poly_array")) return TY_POLY_ARRAY; if (sp_streq(buf, "str_int_hash")) return TY_STR_INT_HASH; if (sp_streq(buf, "str_str_hash")) return TY_STR_STR_HASH; if (sp_streq(buf, "int_int_hash")) return TY_INT_INT_HASH; if (sp_streq(buf, "int_str_hash")) return TY_INT_STR_HASH; if (sp_streq(buf, "sym_poly_hash")) return TY_SYM_POLY_HASH; if (sp_streq(buf, "str_poly_hash")) return TY_STR_POLY_HASH; if (sp_streq(buf, "poly_poly_hash")) return TY_POLY_POLY_HASH; if (!strncmp(buf, "obj_", 4)) { /* the full seed matcher, not a bare table lookup: an obj_ token names a class the same way a `class` seed line does (module-nested leaf, collision-renamed form), and must match the same set */ /* obj_X_ptr_array (`Array[X]`) is not a pin: see seed_obj_array_class */ int ci = seed_class_index(c, buf + 4); /* A module names no object layout: its methods exist only as copies in its includers, and a value of the module type is one of them. It is the boxed value, as a union of the includers would be (#7169). */ if (ci >= 0 && comp_class_is_module(c, &c->classes[ci])) return TY_POLY; /* A builtin the program reopens is a class of the program, but its values are still the builtin's: a Time is Spinel's by-value Time, not an object of the reopen. The kind is the one `self` has in the reopen, and a nilable one with no nil of its own (a struct) is the boxed value. */ /* A builtin the program does not reopen names that kind as well (`Time`, `File`, `Errno::ENOENT`). */ TyKind t = ci >= 0 && is_builtin_reopen(c->classes[ci].name) ? infer_builtin_self(c, ci) : ci < 0 ? seed_builtin_kind(buf + 4) : TY_UNKNOWN; /* The type names every value of its class, so a kind that holds only some of them pins nothing and the slot keeps its inferred type: TY_RANGE is the Integer Range (a Float or a String one is another kind), and TY_EXCEPTION is not an exception class of the program's own (`class MyError < StandardError`). Boxing would do for both but a Range's box is a copy, so `equal?` would stop answering true. */ if (t == TY_RANGE || t == TY_EXCEPTION) { g_seed_unpinned = 1; return TY_UNKNOWN; } if (t != TY_UNKNOWN) { const TyTraits *tr = ty_traits_of(t); return nilable && tr && !tr->null_is_nil ? TY_POLY : t; } return ci >= 0 ? ty_object(ci) : TY_UNKNOWN; } return TY_UNKNOWN; } /* `Array[Vec]` arrives as obj_Vec_ptr_array: the homogeneous pointer array narrow_object_arrays derives from the uses. It cannot be pinned the way a scalar seed is -- the unboxed form has emitters for only a few operations (index, push, length, ...), and a pin on a slot that is also iterated or handed out would leave codegen with no arm -- so the ivar seed arm records it as a request the pass answers, and every other arm drops it as before (#4444). Answers the element class, or -1 for any other token. */ /* `Array[Array[Integer]]` / `Array[Array[Float]]` arrive as int_array_array / float_array_array. Like obj_X_ptr_array they name a kind only narrow_object_arrays produces, so they are requests rather than pins -- and here the distinction is not a nicety. parse_seed_type would have to answer TY_POLY_ARRAY for them (the tags map to no scalar kind), which PINS the ivar, and a pinned ivar is skipped by the very pass that would have narrowed it: a correct declaration made the program slower, silently. Answers the request, or 0 for any other token. */ static int seed_nested_array_req(const char *tok) { if (!tok) return 0; if (sp_streq(tok, "int_array_array")) return SEED_OA_INT_TABLE; if (sp_streq(tok, "float_array_array")) return SEED_OA_FLT_TABLE; return 0; } static int seed_obj_array_class(Compiler *c, const char *tok) { size_t n = tok ? strlen(tok) : 0; char buf[128]; if (n < 15 || n >= sizeof buf || strncmp(tok, "obj_", 4) != 0) return -1; memcpy(buf, tok, n + 1); if (!sp_streq(buf + n - 10, "_ptr_array")) return -1; buf[n - 10] = '\0'; return seed_class_index(c, buf + 4); } /* Build ci's fully-qualified name as `Outer_Inner_Leaf` -- the module path joined with `_`, matching the form spinel_rbs_extract emits for a seed `class` line. The compiler's class table stores only the leaf name (e.g. `Flash`) plus an enclosing_class link, so a qualified seed name needs this to match. */ static void class_qualified_name(Compiler *c, int ci, char *out, size_t cap) { int chain[64]; int n = 0; for (int x = ci; x >= 0 && n < (int)(sizeof chain / sizeof chain[0]); x = c->classes[x].enclosing_class) chain[n++] = x; /* A qualify_colliding_classes-renamed link (`Mod__Leaf`) already embeds its full enclosing path in its stored name; prepending its enclosers again would duplicate the module prefix (`Red::Base::Inner` with a renamed `Red__Base` link would reconstruct as `Red_Red__Base_Inner`, which no extractor-emitted seed name can match). Start at the outermost renamed link instead. */ int start = n - 1; for (int i = n - 1; i >= 0; i--) { const char *nm = c->classes[chain[i]].name; if (nm && strstr(nm, "__")) { start = i; break; } } size_t j = 0; for (int i = start; i >= 0; i--) { const char *nm = c->classes[chain[i]].name; if (!nm) continue; if (j && j + 1 < cap) out[j++] = '_'; for (const char *p = nm; *p && j + 1 < cap; p++) out[j++] = *p; } /* The loop only advances j while j + 1 < cap, so j < cap here. */ if (cap) out[j] = '\0'; } /* Class index for a seed `class` line, normalizing `::` to the `_` form used in the compiler's class table. -1 if no such user class. */ /* Equal with every run of consecutive underscores treated as one (see the fallback in seed_class_index). */ static int seed_name_und_eq(const char *a, const char *b) { while (*a && *b) { if (*a == '_' && *b == '_') { while (*a == '_') a++; while (*b == '_') b++; continue; } if (*a != *b) return 0; a++; b++; } return *a == '\0' && *b == '\0'; } static int seed_class_index(Compiler *c, const char *name) { char buf[256]; size_t j = 0; for (const char *p = name; *p && j < sizeof buf - 1; ) { if (p[0] == ':' && p[1] == ':') { buf[j++] = '_'; p += 2; } else buf[j++] = *p++; } buf[j] = '\0'; int direct = comp_class_index(c, buf); if (direct >= 0) return direct; /* A class whose leaf name collided across modules was renamed to the `__..__` form by qualify_colliding_classes; match that too. */ char buf2[256]; size_t j2 = 0; for (const char *p = name; *p && j2 < sizeof buf2 - 1; ) { if (p[0] == ':' && p[1] == ':') { if (j2 + 2 < sizeof buf2) { buf2[j2++] = '_'; buf2[j2++] = '_'; } p += 2; } else buf2[j2++] = *p++; } buf2[j2] = '\0'; if (!sp_streq(buf, buf2)) { int q2 = comp_class_index(c, buf2); if (q2 >= 0) return q2; } /* A native class declared under a qualified name (`native_struct "IO::Buffer"`) is keyed by its leaf with no enclosing link; its Ruby name keeps the path. Match that name in the `_` form. */ for (int i = 0; i < c->nclasses; i++) { const ClassInfo *k = &c->classes[i]; const char *rn = k->is_native_class && k->enclosing_class < 0 ? class_ruby_name(c, i) : NULL; if (!rn || !strstr(rn, "::")) continue; char und[256]; size_t r = 0; for (const char *p = rn; *p && r < sizeof und - 1; ) { if (p[0] == ':' && p[1] == ':') { und[r++] = '_'; p += 2; } else und[r++] = *p++; } und[r] = '\0'; if (sp_streq(und, buf)) return i; } /* A module-nested class (`module M; class C`) is stored under its leaf name `C` with enclosing_class = M, but the extractor emits the qualified `M_C`. Match against each class's reconstructed qualified name so the seed's ivar/method types are not silently dropped. */ for (int i = 0; i < c->nclasses; i++) { char qn[256]; class_qualified_name(c, i, qn, sizeof qn); if (sp_streq(qn, buf)) return i; } /* Last resort: compare with underscore runs collapsed on both sides. qualify_colliding_classes renames a colliding leaf to the `Mod__Leaf` (double-underscore) form -- stored as the class's own name, with the enclosing-module link intact -- while the extractor's path join is a single underscore (`Mod_Leaf`), so no exact form above can match and the seeds for exactly the collision-prone classes were silently dropped. Compare both the stored name (renamed classes) and the reconstructed qualified name (renamed classes nested deeper) with underscore runs collapsed. A fallback only: it can never shadow an exact match. */ for (int i = 0; i < c->nclasses; i++) { if (c->classes[i].name && seed_name_und_eq(c->classes[i].name, buf)) return i; char qn[256]; class_qualified_name(c, i, qn, sizeof qn); if (seed_name_und_eq(qn, buf)) return i; } return -1; } static Scope *find_method_scope(Compiler *c, int class_id, const char *name, int is_cmethod) { for (int si = 1; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (s->class_id != class_id) continue; if (!!s->is_cmethod != !!is_cmethod) continue; if (s->name && sp_streq(s->name, name)) return s; } return NULL; } int class_ivar_pinned(ClassInfo *ci, const char *name) { for (int i = 0; i < ci->n_rbs_pin_ivars; i++) if (sp_streq(ci->rbs_pin_ivars[i], name)) return 1; return 0; } /* Drop an --rbs ivar pin (a layout conflict with an ancestor demoted it; see the cast-compatibility note in inherit_members). */ void class_unpin_ivar(ClassInfo *ci, const char *name) { for (int i = 0; i < ci->n_rbs_pin_ivars; i++) { if (sp_streq(ci->rbs_pin_ivars[i], name)) { free(ci->rbs_pin_ivars[i]); ci->rbs_pin_ivars[i] = ci->rbs_pin_ivars[--ci->n_rbs_pin_ivars]; return; } } } /* A seed declares the Ruby type, not the C form (#8925). A Hash whose keys inference boxes (`{[s, 1][0] => 1}`, an sp_PolyPolyHash) has no conversion into a Hash the seed keys by one class (`Hash[String, Integer]`, an sp_StrIntHash): the store was refused, and a return did not compile. The seed of a slot such a Hash reaches gives way, and the slot takes the form its values give it, as it does with no seed. Where the emitter would convert such a Hash into the seed's (a Symbol-keyed parameter, a poly-valued return), the conversion is a copy, so a change through one name would not be seen through the other: those give way too. */ static int seed_form_gives_way(TyKind seed, TyKind val) { return ty_is_hash(seed) && seed != TY_POLY_POLY_HASH && val == TY_POLY_POLY_HASH; } /* A box written into an ivar, or answered by a return, the seed gives a container of one form: the box may hold a Hash with boxed keys (a parameter whose seed gave way to one, a method whose Hash did), or an Array of mixed values that holds only the seed's kind (`[1, "s"].select { ... }` under `Array[Integer]`), and the store would unbox it into a copy of the seed's form: another object, which a change through one name does not reach. The slot takes the box instead (#9692: a container seed keeps the object and checks its class). */ static int seed_box_gives_way(TyKind seed, TyKind val) { return val == TY_POLY && ((ty_is_hash(seed) && seed != TY_POLY_POLY_HASH) || (ty_is_array(seed) && seed != TY_POLY_ARRAY)); } /* A box reaching a parameter or a return the seed keys by Symbols: the box may hold a Hash with boxed keys (a Hash read out of an Array of mixed values), and unboxing it into the seed's form would rebuild it reading each key as a Symbol, whatever it is. The slot takes the box. */ static int seed_symkey_box_gives_way(TyKind seed, TyKind val) { return seed == TY_SYM_POLY_HASH && val == TY_POLY; } /* Does ivar `name`'s pin hold against a write of `vt`? One that gives way is dropped, and the slot re-derives from its writes. A box gives way only on the settled types, as for a parameter's seed (param_seed_holds). */ int class_ivar_pin_holds(ClassInfo *ci, const char *name, TyKind vt) { if (!class_ivar_pinned(ci, name)) return 0; int iv = comp_ivar_index(ci, name); if (iv < 0) return 1; int form = seed_form_gives_way(ci->ivar_types[iv], vt); if (!form && !seed_box_gives_way(ci->ivar_types[iv], vt)) return 1; if (!form && !g_seed_box_settled) { ci->ivar_box_held = 1; return 1; } class_unpin_ivar(ci, name); ivar_set_type(ci, iv, TY_UNKNOWN); return 0; } /* The same for a parameter's seed against an argument of `at`. */ int param_seed_holds(LocalVar *lv, TyKind at) { if (!lv->rbs_seeded) return 0; /* (a box handed to a container seed is the caller's object: unboxed into the seed's form it would be a copy the method changes in vain) */ int form = seed_form_gives_way(lv->rbs_type, at) || seed_symkey_box_gives_way(lv->rbs_type, at); int box = seed_box_gives_way(lv->rbs_type, at); if (!form && !box) return 1; /* (only a box the settled types still hand it: one seen mid-fixpoint may be a transient, and a seed it dropped never came back -- the parameter re-derived from the same transient and stayed boxed) */ if (!form && !g_seed_box_settled) { lv->rbs_box_held = 1; return 1; } lv->rbs_seeded = 0; lv->rbs_type = TY_UNKNOWN; lv_set_type(lv, TY_UNKNOWN); return 0; } /* And a return's against the value its body answers; a box, again, only on the settled types. */ int ret_seed_holds(Scope *s, TyKind r) { if (!s->ret_rbs_seeded) return 0; int symkey = seed_symkey_box_gives_way(s->ret, r); int boxed = seed_box_gives_way(s->ret, r) || symkey; int form = seed_form_gives_way(s->ret, r); if (!form && !boxed) return 1; if (!form && !symkey && !g_seed_box_settled) { s->ret_rbs_box_held = 1; return 1; } /* The box the body answers keeps its form, which only the run time knows, but the seed still says what it is: a Hash, or an Array. A yielding method can be spliced into its caller, past the check that makes it so. */ if (boxed && !s->yields && !s->is_proc_form) s->ret_rbs_cont = ty_is_hash(s->ret) || s->ret == TY_SYM_POLY_HASH ? RBS_CONT_HASH : RBS_CONT_ARRAY; s->ret_rbs_seeded = 0; s->ret_rbs_nilable = 0; return 0; } void class_pin_ivar(ClassInfo *ci, const char *name) { if (class_ivar_pinned(ci, name)) return; if (ci->n_rbs_pin_ivars >= ci->c_rbs_pin_ivars) { int nc = ci->c_rbs_pin_ivars ? ci->c_rbs_pin_ivars * 2 : 4; ci->rbs_pin_ivars = realloc(ci->rbs_pin_ivars, sizeof(char *) * (size_t)nc); ci->c_rbs_pin_ivars = nc; } ci->rbs_pin_ivars[ci->n_rbs_pin_ivars++] = strdup(name); } /* Does `name` belong to an override family -- defined on two related classes (one an ancestor of the other)? A return seed on any member would pin that member's C decl repr while poly-dispatch call sites keep the family (boxed) view: `sp_Base_s_instantiate` declared `sp_Base *` but unboxed `.v.p` at the call (#3203). Overridden methods keep their inferred return -- the same never-makes-it-worse rule the seed path applies to layout conflicts. Params still seed: they are per-scope and carry no cross-family repr. */ static int method_in_override_family(Compiler *c, int class_id, const char *name, int is_cmethod) { if (class_id < 0 || !name) return 0; for (int si = 1; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (!!s->is_cmethod != !!is_cmethod) continue; if (!s->name || !sp_streq(s->name, name)) continue; if (s->class_id == class_id || s->class_id < 0) continue; for (int ci = s->class_id; ci >= 0; ci = c->classes[ci].parent) if (ci == class_id) return 1; for (int ci = class_id; ci >= 0; ci = c->classes[ci].parent) if (ci == s->class_id) return 1; } return 0; } /* Does every value `id` can produce name a class (a constant that is one of the program's classes)? The class names met are appended to `names`. */ typedef struct { Compiler *c; char *names; size_t cap; int ok; } ClassLeaves; /* One value position: a constant naming a class adds its name, anything else (a nil by absence too) fails the walk. */ static int class_leaf_check(const VpLeaf *leaf, void *ud) { ClassLeaves *k = ud; const NodeTable *nt = k->c->nt; if (leaf->kind == VPK_EXPR && (nt_kind(nt, leaf->node) == NK_ConstantReadNode || nt_kind(nt, leaf->node) == NK_ConstantPathNode)) { const char *nm = nt_str(nt, leaf->node, "name"); if (nm && comp_class_index(k->c, nm) >= 0) { if (!strstr(k->names, nm) && strlen(k->names) + strlen(nm) + 3 < k->cap) { if (*k->names) strcat(k->names, ", "); strcat(k->names, nm); } return 0; } } k->ok = 0; return 1; } static int value_leaves_are_classes(Compiler *c, int id, char *names, size_t cap) { ClassLeaves k = { c, names, cap, 1 }; if (id < 0) return 0; for_each_value_position(c->nt, id, VP_AND_LEFT | VPS_ALL, class_leaf_check, &k); return k.ok; } /* A return seed naming an instance type (`-> Story`, or a union of them) on a method whose every value is a class (`def searched_model = Story`): the RBS means `singleton(Story)`. Pinned as it stands, a single class declared the C return as `sp_Story *` and returned the sp_Class into it, which the C compiler rejected without a word about the signature; a union compiles (both are boxed) but types every call on the value from the wrong side. Answers 1 when the seed must be dropped. */ static int seed_ret_contradicts_class_body(Compiler *c, Scope *s, TyKind rt) { if (!ty_is_object(rt) && rt != TY_POLY) return 0; if (s->body < 0) return 0; char names[256] = ""; if (!value_leaves_are_classes(c, s->body, names, sizeof names)) return 0; const NodeTable *nt = c->nt; int si = (int)(s - c->scopes); NT_FOREACH_KIND(nt, NK_ReturnNode, rid) { if (c->nscope[rid] != si) continue; int an = nt_ref(nt, rid, "arguments"), n = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; if (!av || n != 1 || !value_leaves_are_classes(c, av[0], names, sizeof names)) return 0; } const char *cls = s->class_id >= 0 && s->class_id < c->nclasses ? c->classes[s->class_id].name : NULL; fprintf(stderr, "spinel: warning: --rbs: %s%s%s is declared to return an instance, but it returns " "the class itself (%s); write singleton(...) in the signature. %s\n", cls ? cls : "", cls ? (s->is_cmethod ? "." : "#") : "", s->name ? s->name : "?", names, ty_is_object(rt) ? "The declaration is ignored." : "Calls on its value are typed from the declaration."); return ty_is_object(rt); } static int inline_pin_agrees(Compiler *c, Scope *s, int ci, const char *ivar, const char *ret, const char *params, const char *where, int from_rbs); static void inline_pin_record(Scope *s, int ci, const char *ivar, const char *ret, const char *params, const char *where, const char *head); /* A seed parameter field is `type`, applied to the next positional slot, or `name:type`, applied to the parameter of that name: a def orders its keywords as it likes, so no position can stand for one. Splits the field; returns the type and leaves the name (or "") in name. */ static const char *seed_field_split(const char *field, char *name, size_t n) { name[0] = '\0'; const char *colon = field ? strchr(field, ':') : NULL; if (!colon) return field; size_t l = (size_t)(colon - field); if (l >= n) l = n - 1; memcpy(name, field, l); name[l] = '\0'; return colon + 1; } static int seed_nfields(const char *list) { if (!list || !*list || sp_streq(list, "-")) return 0; int n = 1; for (const char *q = list; *q; q++) n += *q == ','; return n; } /* The index of the parameter the i-th field of `list` seeds: the slot of the name a `name:type` field carries, or the next slot a plain field counts to (empty fields count too, so a skipped middle parameter shifts nothing). -1 past the end or for a name the scope has no parameter of. */ static int seed_field_param(const Scope *s, const char *list, int i, char *buf, size_t n) { if (!list || !*list || sp_streq(list, "-")) return -1; int pos = 0; const char *p = list; for (int k = 0; ; k++) { const char *e = strchr(p, ','); size_t l = e ? (size_t)(e - p) : strlen(p); if (l >= n) l = n - 1; memcpy(buf, p, l); buf[l] = '\0'; char name[128]; const char *ty = seed_field_split(buf, name, sizeof name); int slot = pos; if (name[0]) { slot = -1; for (int j = 0; j < s->nparams; j++) if (s->pnames[j] && sp_streq(s->pnames[j], name)) { slot = j; break; } } else pos++; if (k == i) { memmove(buf, ty, strlen(ty) + 1); return slot; } if (!e) return -1; p = e + 1; } } /* The type `list` seeds parameter `slot` of `s` with, "" when no field does. The last field naming the slot wins, as it does in seed_method, which applies the fields in order. */ static const char *seed_type_of_param(const Scope *s, const char *list, int slot, char *buf, size_t n) { int nf = seed_nfields(list); int found = -1; for (int i = 0; i < nf; i++) { char probe[256]; if (seed_field_param(s, list, i, probe, sizeof probe) == slot) found = i; } if (found >= 0) { seed_field_param(s, list, found, buf, n); return buf; } buf[0] = '\0'; return buf; } /* Pin scope `s`'s return and each named parameter to its seeded type. ptypes is a comma-separated list: a plain field is aligned with the parameter index (empty fields preserved so a skipped middle param doesn't shift the rest), a `name:type` field with the parameter of that name. */ static void seed_method(Compiler *c, Scope *s, const char *ret_tok, char *ptypes, const char *where, const char *head, int from_rbs) { if (!s) return; TyKind rt = parse_seed_type(c, ret_tok); int nilable_seed = g_seed_nilable; if (rt != TY_UNKNOWN && seed_ret_contradicts_class_body(c, s, rt)) rt = TY_UNKNOWN; g_seed_nilable = nilable_seed; if (rt != TY_UNKNOWN) { scope_set_ret(s, rt); s->ret_rbs_seeded = 1; /* `String?` is as nilable as `Integer?`: a bare `const char *` slot carries nil as NULL, so the seed has to record it or the implicit nil arm returns the empty string instead (#4250). */ s->ret_rbs_nilable = g_seed_nilable && (rt == TY_INT || rt == TY_FLOAT || rt == TY_STRING); s->ret_rbs_cont_nil = g_seed_nilable && (ty_is_hash(rt) || ty_is_array(rt)); /* A memoized CONTAINER reader (`def self.table; @table ||= {}; end`) IS its ivar: pinning only the return left the slot poly, and the seeded narrowing then cast a PolyPolyHash payload to the declared hash -- a segfault on the first read (#3779). Pin the ivar the body answers too. Containers only: a scalar slot pinned this way loses the nil a boxed reader legitimately answers. */ if ((ty_is_hash(rt) || ty_is_array(rt)) && !g_seed_nilable && s->class_id >= 0 && s->class_id < c->nclasses && s->body >= 0) { const NodeTable *nt = c->nt; int bn = 0; const int *bb = nt_arr(nt, s->body, "body", &bn); if (bb && bn > 0) { int last = bb[bn - 1]; NodeKind lk = nt_kind(nt, last); if (lk == NK_InstanceVariableOrWriteNode || lk == NK_InstanceVariableReadNode) { const char *ivn = nt_str(nt, last, "name"); if (ivn && ivn[0] == '@') { /* The return also declares this ivar's representation: check it before replacing a pin and retain an inline origin for later ivar declarations, other readers and contradiction checks. */ if (!inline_pin_agrees(c, NULL, s->class_id, ivn, ret_tok, NULL, where, from_rbs)) return; ClassInfo *ci = &c->classes[s->class_id]; int idx = comp_ivar_intern(ci, ivn); ivar_set_type(ci, idx, rt); class_pin_ivar(ci, ivn); if (!from_rbs) inline_pin_record(NULL, s->class_id, ivn, ret_tok, NULL, where, head); } } } } } if (!ptypes) return; char field[256]; int nf = seed_nfields(ptypes); for (int i = 0; i < nf; i++) { int slot = seed_field_param(s, ptypes, i, field, sizeof field); if (slot < 0 || slot >= s->nparams) continue; /* a name the def has not */ /* A rest or keyword rest slot is typed by construction (the poly array or hash its callers' arguments are gathered into), and the extractor gives it an empty field. A field that still lands on one comes from a signature that declares an optional positional where the def gathers its arguments (`?Integer c` for `def m(*c)`): pinning the gathered array to the element's type would make every call pass an Array to a slot of another C type. */ if (slot == s->rest_idx || slot == s->kwrest_idx) continue; TyKind pt = parse_seed_type(c, field); if (pt != TY_UNKNOWN) { LocalVar *lv = scope_local(s, s->pnames[slot]); if (lv) { lv_set_type(lv, pt); lv->rbs_seeded = 1; lv->rbs_type = pt; /* a nilable int parameter can hold nil like any other: mark it so it is an oint and boxing it answers nil rather than a number */ if (g_seed_nilable && (pt == TY_INT || pt == TY_FLOAT)) lv->nullable_int = 1; } } } } static int is_empty_array_literal(const NodeTable *nt, int id, int cap); /* A method the seed file declares twice with different signatures (two .rbs of one directory, one per route helper, both naming `H.path`): the later record would replace the earlier one, so a call the first one contradicts builds. Said once, naming the method and both signatures. */ typedef struct { char *key; char *sig; } SeedDecl; static void seed_decl_note(SeedDecl **tab, int *n, int *cap, const char *kind, const char *cls, const char *name, const char *ret, const char *params) { char key[600]; snprintf(key, sizeof key, "%s %s %s", kind, cls ? cls : "Object", name); char sig[900]; snprintf(sig, sizeof sig, "%s %s", ret, params ? params : ""); for (int i = 0; i < *n; i++) { if (strcmp((*tab)[i].key, key) != 0) continue; if (strcmp((*tab)[i].sig, sig) != 0) { fprintf(stderr, "spinel: --rbs declares %s%s%s twice with different signatures:\n" " (%s) and (%s)\n" " A seed is trusted and the later one would replace the earlier: keep one declaration, " "or give the methods different names.\n", cls ? cls : "", cls ? (strcmp(kind, "cmeth") == 0 ? "." : "#") : "", name, (*tab)[i].sig, sig); exit(1); } return; } if (*n == *cap) { *cap = *cap ? *cap * 2 : 16; *tab = (SeedDecl *)realloc(*tab, sizeof(SeedDecl) * (size_t)*cap); } (*tab)[*n].key = strdup(key); (*tab)[*n].sig = strdup(sig); (*n)++; } /* Pin instance variable `a1` of class `cur_ci` to seed type `a2`, as an `ivar` seed line does (an Array[Foo] or nested table type is a request for narrow_object_arrays rather than a pin). */ static void seed_class_ivar(Compiler *c, int cur_ci, const char *a1, const char *a2) { int oac = seed_obj_array_class(c, a2); int nreq = oac >= 0 ? 0 : seed_nested_array_req(a2); TyKind t = (oac >= 0 || nreq) ? TY_UNKNOWN : parse_seed_type(c, a2); if (oac >= 0 || nreq) { /* a request for narrow_object_arrays, not a pin */ char ivn[300]; snprintf(ivn, sizeof ivn, "%s%s", a1[0] == '@' ? "" : "@", a1); int idx = comp_ivar_intern(&c->classes[cur_ci], ivn); c->classes[cur_ci].ivar_oa_seed[idx] = nreq ? nreq : oac + 1; /* `@t = []` under a nested seed: the empty literal's default kind is the int array, which would type the ivar as one and keep it out of the narrowing pass altogether. The seed says the literal is the table, so it starts as the poly array the pass narrows (#4484). */ if (nreq && c->arr_want) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, wid) { const char *wn = nt_str(nt, wid, "name"); int wv = nt_ref(nt, wid, "value"); if (!wn || !sp_streq(wn, ivn) || !is_empty_array_literal(nt, wv, c->node_cap)) continue; Scope *wsc = comp_scope_of(c, wid); if (!wsc || wsc->class_id != cur_ci) continue; if (c->arr_want[wv] == TY_UNKNOWN) comp_set_arr_want(c, wv, TY_POLY_ARRAY); } } } else if (t != TY_UNKNOWN) { ClassInfo *ci = &c->classes[cur_ci]; /* The extractor emits the name WITHOUT the sigil (`ivar w1 obj_Mat`), but ClassInfo interns parse-time ivars as `@w1`. Interning the bare token created a PHANTOM parallel ivar: the seed typed and pinned "w1" while every lookup asked about "@w1" -- so seeds never pinned the real ivar, and the phantom's static emission strips the first character (`ivars[j] + 1` assumes the sigil), colliding `w1`/`b1` into two `civ_..._1` statics of conflicting C types (the toy FFN double-emission). Normalize to the sigil form. */ char ivn[300]; snprintf(ivn, sizeof ivn, "%s%s", a1[0] == '@' ? "" : "@", a1); int idx = comp_ivar_intern(ci, ivn); ivar_set_type(ci, idx, t); class_pin_ivar(ci, ivn); } } /* ---- inline RBS (docs/inline-rbs.md) ---- The parser attaches each applied annotation to its node as attributes (rbs_ret / rbs_params on a def, rbs_ivar on an attr_* call, rbs_ivars on a class body; rbs_line / rbs_col / rbs_file name the comment, and rbs_names spells each tag the way the comment wrote it). apply_inline_rbs pins them through the same helpers as the seed file, so the slots and flags are the ones the equivalent --rbs signature gives; the method is found by its def node and the class by the node's class body, so no class name is matched. It runs just before apply_rbs_seeds, which then checks each .rbs signature against what an annotation pinned. */ typedef struct { Scope *s; /* a method's annotation, or NULL */ int def_node; /* its def node, which outlives the Scope pointer */ int ci; /* an ivar's class */ char *ivar; char *ret, *params; char where[300]; /* FILE:LINE of the annotation */ char head[300]; /* FILE:LINE:COL, for a warning about it */ } InlinePin; static InlinePin *g_inline_pins = NULL; static int g_n_inline_pins = 0; static int g_inline_rbs_bad = 0; static void inline_pin_record(Scope *s, int ci, const char *ivar, const char *ret, const char *params, const char *where, const char *head) { g_inline_pins = realloc(g_inline_pins, sizeof(InlinePin) * (size_t)(g_n_inline_pins + 1)); InlinePin *p = &g_inline_pins[g_n_inline_pins++]; p->s = s; p->ci = ci; p->def_node = s ? s->def_node : -1; p->ivar = ivar ? strdup(ivar) : NULL; p->ret = ret ? strdup(ret) : NULL; p->params = params ? strdup(params) : NULL; snprintf(p->where, sizeof p->where, "%s", where); snprintf(p->head, sizeof p->head, "%s", head); } static int inline_seed_tok_ok(Compiler *c, const char *tok) { if (!tok || !*tok || sp_streq(tok, "-")) return 1; int nil = g_seed_nilable; int ok = parse_seed_type(c, tok) != TY_UNKNOWN || g_seed_unpinned; g_seed_nilable = nil; return ok; } /* Do two seed tokens say the same type? Both sides are mapped by rbs_map.c, so one type is one token (`T | nil` is `T?`) and agreement is equality of the tokens -- not of the TyKind they parse to, which is the same for every array of objects. Unsaid on either side agrees. */ static int inline_tok_same(const char *a, const char *b) { if (!a || !*a || sp_streq(a, "-") || !b || !*b || sp_streq(b, "-")) return 1; return sp_streq(a, b); } /* the i-th comma field of a parameter list into buf ("" past the end) */ static const char *inline_field(const char *list, int i, char *buf, size_t n) { buf[0] = '\0'; if (!list) return buf; const char *p = list; for (int k = 0; k < i; k++) { p = strchr(p, ','); if (!p) return buf; p++; } const char *e = strchr(p, ','); size_t l = e ? (size_t)(e - p) : strlen(p); if (l >= n) l = n - 1; memcpy(buf, p, l); buf[l] = '\0'; return buf; } /* The annotation that pinned a slot, as FILE:LINE, or NULL when none did (the pin is an --rbs seed's). A contradiction found later names the annotation, which is what the program's reader wrote; a slot both pin agrees on names the annotation too. `param` < 0 asks about the return. */ static const char *inline_pin_at_method(const Scope *s, int param) { char f[128]; for (int i = 0; s && i < g_n_inline_pins; i++) { const InlinePin *p = &g_inline_pins[i]; if (!p->s || p->def_node != s->def_node) continue; const char *tok = param < 0 ? p->ret : inline_field(p->params, param, f, sizeof f); if (tok && *tok && !sp_streq(tok, "-")) return p->where; } return NULL; } static const InlinePin *inline_pin_of_ivar(int ci, const char *ivar) { const char *a = ivar[0] == '@' ? ivar + 1 : ivar; for (int i = 0; i < g_n_inline_pins; i++) { const InlinePin *p = &g_inline_pins[i]; if (p->s || !p->ivar || p->ci != ci) continue; if (sp_streq(p->ivar[0] == '@' ? p->ivar + 1 : p->ivar, a)) return p; } return NULL; } static const char *inline_pin_at_ivar(int ci, const char *ivar) { const InlinePin *p = inline_pin_of_ivar(ci, ivar); return p ? p->where : NULL; } /* A fact about to be applied -- a .rbs seed line (rbs_where is its position, "" when the extractor gave none), or another annotation (from_rbs 0) -- against every annotation already pinned on the same slot: the method (class, singleton or not, name: every definition of it, in any opening of the class) or the ivar of a class. They must agree; a disagreement is reported naming both and fails the compile once every fact has been read. */ static int inline_pin_agrees(Compiler *c, Scope *s, int ci, const char *ivar, const char *ret, const char *params, const char *where, int from_rbs) { for (int i = 0; i < g_n_inline_pins; i++) { InlinePin *p = &g_inline_pins[i]; const char *what = NULL; char wbuf[64]; if (s && p->s && p->s->class_id == s->class_id && !p->s->is_cmethod == !s->is_cmethod && p->s->name && s->name && sp_streq(p->s->name, s->name)) { if (!inline_tok_same(p->ret, ret)) what = "the return type"; else { /* slot by slot: an --rbs seed names its keywords (`a:int`), an annotation's fields follow the def; both resolve to a parameter */ char fa[128], fb[128]; int nf = seed_nfields(p->params), nb = seed_nfields(params); int nk = s->nparams > nf ? s->nparams : nf; if (nb > nk) nk = nb; for (int k = 0; k < nk && !what; k++) { if (!inline_tok_same(seed_type_of_param(s, p->params, k, fa, sizeof fa), seed_type_of_param(s, params, k, fb, sizeof fb))) { if (k < s->nparams && s->pnames[k]) snprintf(wbuf, sizeof wbuf, "the type of parameter `%s`", s->pnames[k]); else snprintf(wbuf, sizeof wbuf, "the type of parameter %d", k + 1); what = wbuf; } } } } else if (!s && ivar && p->ivar && p->ci == ci) { char iv[300]; snprintf(iv, sizeof iv, "%s%s", ivar[0] == '@' ? "" : "@", ivar); char pv[300]; snprintf(pv, sizeof pv, "%s%s", p->ivar[0] == '@' ? "" : "@", p->ivar); if (sp_streq(iv, pv) && !inline_tok_same(p->ret, ret)) what = "the type"; } if (!what) continue; const char *cn = (s ? s->class_id : ci) >= 0 ? c->classes[s ? s->class_id : ci].name : NULL; const char *slot = s ? (s->name ? s->name : "?") : p->ivar; const char *sep = s ? (s->is_cmethod ? "." : "#") : " "; if (from_rbs) fprintf(stderr, "spinel: %s: inline RBS: %s of %s%s%s disagrees with the .rbs signature at %s\n" " the two must say the same thing; correct one, or remove it\n", p->where, what, cn ? cn : "", sep, slot, where[0] ? where : "(a .rbs file)"); else fprintf(stderr, "spinel: %s: inline RBS: %s of %s%s%s disagrees with another annotation at %s\n" " the two must say the same thing; correct one, or remove it\n", where, what, cn ? cn : "", sep, slot, p->where); g_inline_rbs_bad = 1; return 0; } return 1; } static void inline_warn(const char *where, const char *fmt, const char *a, const char *b) { fprintf(stderr, "spinel: %s: warning: inline RBS: ", where); fprintf(stderr, fmt, a ? a : "", b ? b : ""); fputc('\n', stderr); } /* The class a class-level node belongs to: the struct's, for a direct statement of a `Name = Struct.new(...) do` / `Data.define do` block, whose node_cbody still names the class around the write (only the block's defs take the struct's class, from the load record); otherwise its class body's. */ static int *inline_struct_owner(Compiler *c) { const NodeTable *nt = c->nt; int *own = malloc(sizeof(int) * (size_t)(nt->count ? nt->count : 1)); for (int i = 0; i < nt->count; i++) own[i] = -1; for (int id = 0; id < nt->count; id++) { NodeKind wk = nt_kind(nt, id); if (wk != NK_ConstantWriteNode && wk != NK_LocalVariableWriteNode) continue; int bbody = class_def_body(c, id); if (bbody < 0) continue; char an[48]; const char *cname = nt_str(nt, id, "name"); if (wk == NK_LocalVariableWriteNode) { snprintf(an, sizeof an, "StructAnon_%s", comp_node_tag(c, id)); cname = an; } int ci = cname ? comp_class_index(c, cname) : -1; if (ci < 0) continue; own[bbody] = ci; int bn = 0; const int *stmts = nt_arr(nt, bbody, "body", &bn); for (int k = 0; k < bn; k++) own[stmts[k]] = ci; } return own; } /* FILE:LINE of a node's annotation, and FILE:LINE:COL for a warning about it: the position the parser's own warnings carry */ static void inline_where(const NodeTable *nt, int id, char *buf, size_t n) { const char *f = nt_file_path(nt, (int)nt_int(nt, id, "rbs_file", 0)); snprintf(buf, n, "%s:%lld", f ? f : "?", nt_int(nt, id, "rbs_line", 0)); } static void inline_head(const NodeTable *nt, int id, char *buf, size_t n) { inline_where(nt, id, buf, n); long long col = nt_int(nt, id, "rbs_col", 0); size_t l = strlen(buf); if (col > 0 && l < n) snprintf(buf + l, n - l, ":%lld", col); } /* A tag as the comment spelled it (`Time`, `Array[Time]`), from the node's rbs_names; a tag is never shown to the program's reader. */ static const char *inline_spelling(const NodeTable *nt, int id, const char *tok, char *buf, size_t n) { const char *names = nt_str(nt, id, "rbs_names"); size_t tl = tok ? strlen(tok) : 0; for (const char *e = names; e && *e && tl; ) { const char *nl = strchr(e, '\n'); size_t el = nl ? (size_t)(nl - e) : strlen(e); if (el > tl && e[tl] == '=' && !strncmp(e, tok, tl)) { snprintf(buf, n, "`%.*s`", (int)(el - tl - 1), e + tl + 1); return buf; } e = nl ? nl + 1 : NULL; } snprintf(buf, n, "a type it names"); return buf; } /* The node of the definition that answers calls to the method `ms` defines, when that is not `ms`'s own: a later definition of the name in the same class replaces an earlier one (comp_method_in_class keeps the last), whatever form it takes -- a `def`, a `def` in a class_eval block, a `define_method` call. -1 when `ms` is the live one. */ static int inline_replacing_def(Compiler *c, Scope *ms) { if (!ms->name) return -1; int li = ms->is_cmethod ? comp_cmethod_in_class(c, ms->class_id, ms->name) : comp_method_in_class(c, ms->class_id, ms->name); int dn = li >= 0 ? c->scopes[li].def_node : -1; return dn >= 0 && dn != ms->def_node ? dn : -1; } /* How the module method `ms` (def node `id`) is mixed in, when it is: the word for the mixin a copy made for an includer records ("include", "extend", "prepend"; a module mixed in more than one way, or copied away with no copy left, is all three), or NULL when it is not. Calls to such a method run the copies, each a Scope of its own that shares the def node, and no signature reaches them: --rbs seeds the module's own Scope, which is copied away. A module_function is not mixed in: its one function serves the includer's calls too (is_transplanted_source stays clear). */ static const char *inline_def_mixed_in(Compiler *c, int id, const Scope *ms) { if (ms->is_module_function) return NULL; const char *how = NULL; for (int si = 1; si < c->nscopes; si++) { const Scope *s = &c->scopes[si]; if (s == ms || s->def_node != id) continue; const char *w = s->is_prepend_copy ? "prepend" : s->is_extend_copy ? "extend" : s->is_include_copy ? "include" : s->origin_module_ci ? "extend" : NULL; if (w) how = how && !sp_streq(how, w) ? "include, extend or prepend" : w; } if (!how && ms->is_transplanted_source && ms->class_id >= 0 && comp_class_is_module(c, &c->classes[ms->class_id])) how = "include, extend or prepend"; return how; } /* Does the annotation on def node `id` apply: the parser kept a signature for it, its method was found and is not a mixed-in module's, and every type it names pins something in this program? The one test of "applied" for a method annotation, asked of the annotated def itself and of a later definition that replaces it, so the two answers cannot differ. `ms` gets the def's Scope (NULL when not found); `bad`, when not applicable for a type, gets that type's tag, and for a mixed-in method the mixin's word. */ enum { INLINE_DEF_NOT_FOUND, INLINE_DEF_MIXED_IN, INLINE_DEF_BAD_TYPE, INLINE_DEF_APPLIES }; static int inline_def_applicable(Compiler *c, const NodeTable *nt, int id, Scope **ms, char *bad, size_t n) { *ms = NULL; bad[0] = '\0'; if (nt_kind(nt, id) != NK_DefNode || !nt_int(nt, id, "rbs_line", 0)) return INLINE_DEF_NOT_FOUND; for (int si = 1; si < c->nscopes && !*ms; si++) if (c->scopes[si].def_node == id) *ms = &c->scopes[si]; if (!*ms) return INLINE_DEF_NOT_FOUND; const char *how = inline_def_mixed_in(c, id, *ms); if (how) { snprintf(bad, n, "%s", how); return INLINE_DEF_MIXED_IN; } const char *ret = nt_str(nt, id, "rbs_ret"), *params = nt_str(nt, id, "rbs_params"); if (!inline_seed_tok_ok(c, ret)) { snprintf(bad, n, "%s", ret); return INLINE_DEF_BAD_TYPE; } for (int f = 0; f < seed_nfields(params); f++) if (!inline_seed_tok_ok(c, inline_field(params, f, bad, n))) return INLINE_DEF_BAD_TYPE; bad[0] = '\0'; return INLINE_DEF_APPLIES; } static void apply_inline_rbs(Compiler *c) { const NodeTable *nt = c->nt; int any = 0; for (int r = 0; r < nt_nroots(nt) && !any; r++) /* each file's root says whether it has one */ any = nt_root(nt, r) >= 0 && nt_int(nt, nt_root(nt, r), "rbs_any", 0); if (!any) return; int *sown = inline_struct_owner(c); for (int id = 0; id < nt->count; id++) { if (!nt_int(nt, id, "rbs_line", 0) && !nt_str(nt, id, "rbs_ivars")) continue; char where[300], head[300], spell[300]; inline_where(nt, id, where, sizeof where); inline_head(nt, id, head, sizeof head); NodeKind k = nt_kind(nt, id); const char *ivt = nt_str(nt, id, "rbs_ivar"); if (k == NK_DefNode) { const char *ret = nt_str(nt, id, "rbs_ret"), *params = nt_str(nt, id, "rbs_params"); const char *name = nt_str(nt, id, "name"); Scope *ms = NULL, *rs = NULL; char bad[128], rbad[128]; int app = inline_def_applicable(c, nt, id, &ms, bad, sizeof bad); int rep = app == INLINE_DEF_APPLIES ? inline_replacing_def(c, ms) : -1; if (app == INLINE_DEF_NOT_FOUND) inline_warn(head, "the signature of `%s` is not applied: its method was not found after the parse%s", name, ""); else if (app == INLINE_DEF_MIXED_IN) inline_warn(head, "the signature of `%s` is not applied: it is a method of a module mixed in with %s, whose calls " "run a copy made for each class it is mixed into, which no signature reaches; the same signature " "through --rbs is not applied either", name, bad); else if (app == INLINE_DEF_BAD_TYPE) inline_warn(head, "the signature of `%s` is not applied: %s names no type Spinel can pin", name, inline_spelling(nt, id, bad, spell, sizeof spell)); else if (rep >= 0 && inline_def_applicable(c, nt, rep, &rs, rbad, sizeof rbad) != INLINE_DEF_APPLIES) { /* the annotated def never runs, and the one that does carries no applied annotation of its own (none, one the parser ignored, or one whose types pin nothing here): applying the signature to either would be wrong. A replacement whose annotation applies must agree with this one, as two openings of a class must (inline_pin_agrees). */ const char *rf = nt_file_path(nt, (int)nt_int(nt, rep, "node_file", 0)); char rw[300]; snprintf(rw, sizeof rw, "%s:%lld", rf ? rf : "?", nt_int(nt, rep, "node_line", 0)); inline_warn(head, "the signature of `%s` is not applied: a later definition without an applied annotation, at %s, replaces this one", name, rw); } else { if (!inline_pin_agrees(c, ms, -1, NULL, ret, params, where, 0)) continue; /* A replaced definition participates in signature agreement, but its body must not pin a backing ivar that the live definition never reads. */ if (rep < 0) { int fam = method_in_override_family(c, ms->class_id, ms->name, ms->is_cmethod); if (fam && ret && !sp_streq(ret, "-")) inline_warn(head, "the return type of %s is not applied: the method is overridden in a related class, " "whose calls share one return representation (its parameter types are applied)%s", name, ""); char *pcopy = params ? strdup(params) : NULL; seed_method(c, ms, fam ? NULL : ret, pcopy, where, head, 0); free(pcopy); } inline_pin_record(ms, -1, NULL, ret, params, where, head); } } else if (ivt && k == NK_CallNode) { int ci = sown[id] >= 0 ? sown[id] : c->node_cbody[id]; if (ci < 0 || ci >= c->nclasses) { inline_warn(head, "this annotation is not applied: no class was found for it%s%s", "", ""); continue; } if (!inline_seed_tok_ok(c, ivt) && seed_obj_array_class(c, ivt) < 0 && !seed_nested_array_req(ivt)) { inline_warn(head, "this type is not applied: %s names no type Spinel can pin%s", inline_spelling(nt, id, ivt, spell, sizeof spell), ""); continue; } { int an = nt_ref(nt, id, "arguments"), n = 0, nsym = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; for (int j = 0; j < n; j++) { const char *sym = nt_str(nt, av[j], "value"); if (nt_kind(nt, av[j]) != NK_SymbolNode || !sym) continue; nsym++; char ivn[300]; snprintf(ivn, sizeof ivn, "@%s", sym); if (!inline_pin_agrees(c, NULL, ci, ivn, ivt, NULL, where, 0)) continue; seed_class_ivar(c, ci, ivn, ivt); inline_pin_record(NULL, ci, ivn, ivt, NULL, where, head); } if (!nsym) inline_warn(head, "this type is not applied: the call names no attribute Spinel can see%s%s", "", ""); } } else if (nt_int(nt, id, "rbs_line", 0)) { /* every fact the parser wrote is applied or reported: one on a node an earlier pass rewrote into another kind would otherwise be lost */ inline_warn(head, "this annotation is not applied: its declaration became a %s before types were read%s", nt_type(nt, id) ? nt_type(nt, id) : "node", ""); } if (nt_str(nt, id, "rbs_ivars")) { int ci = sown[id] >= 0 ? sown[id] : c->node_cbody[id]; char *list = strdup(nt_str(nt, id, "rbs_ivars")); for (char *e = strtok(list, ","); e; e = strtok(NULL, ",")) { /* @x=@@@ */ char *eq = strchr(e, '='), *at1 = eq ? strchr(eq, '@') : NULL, *at2 = at1 ? strchr(at1 + 1, '@') : NULL; char *at3 = at2 ? strchr(at2 + 1, '@') : NULL; if (!eq || !at1 || !at2) continue; *eq = '\0'; *at1 = '\0'; *at2 = '\0'; if (at3) *at3 = '\0'; const char *f = nt_file_path(nt, atoi(at2 + 1)); snprintf(where, sizeof where, "%s:%s", f ? f : "?", at1 + 1); if (at3 && atoi(at3 + 1) > 0) snprintf(head, sizeof head, "%s:%d", where, atoi(at3 + 1)); else snprintf(head, sizeof head, "%s", where); if (ci < 0 || ci >= c->nclasses) { inline_warn(head, "the declaration of %s is not applied: no class was found for it%s", e, ""); continue; } if (!inline_seed_tok_ok(c, eq + 1) && seed_obj_array_class(c, eq + 1) < 0 && !seed_nested_array_req(eq + 1)) { inline_warn(head, "the declaration of %s is not applied: %s names no type Spinel can pin", e, inline_spelling(nt, id, eq + 1, spell, sizeof spell)); continue; } if (!inline_pin_agrees(c, NULL, ci, e, eq + 1, NULL, where, 0)) continue; seed_class_ivar(c, ci, e, eq + 1); inline_pin_record(NULL, ci, e, eq + 1, NULL, where, head); } free(list); } } free(sown); } /* A seed that names a class (`@memory: Bus`, `Array[Bus]`) the program has none of is ignored like any untyped seed, so say which one: an unqualified name the extractor could not resolve against the signatures' own declarations otherwise drops without a word. */ static void seed_warn_unknown_class(Compiler *c, int ci, const char *sep, const char *slot, const char *tok) { if (!tok || strncmp(tok, "obj_", 4) != 0) return; char nm[256]; snprintf(nm, sizeof nm, "%s", tok + 4); size_t n = strlen(nm); if (n && nm[n - 1] == '?') nm[--n] = '\0'; if (n > 10 && sp_streq(nm + n - 10, "_ptr_array")) nm[n - 10] = '\0'; if (builtin_class_id(nm) || seed_class_index(c, nm) >= 0) return; fprintf(stderr, "warning: --rbs: %s%s%s: %s names no class of the program; the seed is ignored\n", ci >= 0 ? class_ruby_name(c, ci) : "", ci >= 0 ? sep : "", slot, nm); } static void seed_warn_unknown_classes(Compiler *c, int ci, const char *sep, const char *meth, const char *ret, const char *ptypes) { seed_warn_unknown_class(c, ci, sep, meth, ret); for (const char *p = ptypes; p && *p; ) { const char *e = strchr(p, ','); size_t len = e ? (size_t)(e - p) : strlen(p); char field[256]; snprintf(field, sizeof field, "%.*s", (int)len, p); const char *colon = strchr(field, ':'); seed_warn_unknown_class(c, ci, sep, meth, colon ? colon + 1 : field); p = e ? e + 1 : NULL; } } static void apply_rbs_seeds(Compiler *c, const char *path) { FILE *f = fopen(path, "rb"); if (!f) return; SeedDecl *decls = NULL; int ndecls = 0, capdecls = 0; const char *cur_cname = NULL; char cur_cname_buf[300]; cur_cname_buf[0] = 0; int cur_ci = -1; /* current class index; -2 = top level (Object) */ char src[512] = ""; /* `src FILE:LINE` (--positions): where the next seed is from */ char line[2048]; while (fgets(line, sizeof line, f)) { size_t L = strlen(line); while (L && (line[L - 1] == '\n' || line[L - 1] == '\r')) line[--L] = '\0'; if (L == 0) continue; /* split into keyword + up to 3 fields (the 3rd holds the rest of line) */ char *kw = line, *a1 = NULL, *a2 = NULL, *a3 = NULL; char *s1 = strchr(line, ' '); if (s1) { *s1 = '\0'; a1 = s1 + 1; char *s2 = strchr(a1, ' '); if (s2) { *s2 = '\0'; a2 = s2 + 1; char *s3 = strchr(a2, ' '); if (s3) { *s3 = '\0'; a3 = s3 + 1; } } } if (sp_streq(kw, "class")) { snprintf(cur_cname_buf, sizeof cur_cname_buf, "%s", a1 ? a1 : ""); cur_cname = cur_cname_buf; if (a1 && sp_streq(a1, "Object")) cur_ci = -2; else cur_ci = a1 ? seed_class_index(c, a1) : -1; src[0] = '\0'; } else if (sp_streq(kw, "ivar") && a1 && a2 && cur_ci >= 0) { if (!inline_pin_agrees(c, NULL, cur_ci, a1, a2, NULL, src, 1)) continue; seed_warn_unknown_class(c, cur_ci, a1[0] == '@' ? " " : " @", a1, a2); seed_class_ivar(c, cur_ci, a1, a2); } else if (sp_streq(kw, "meth") && a1 && a2) { seed_decl_note(&decls, &ndecls, &capdecls, "meth", cur_cname, a1, a2, a3); int class_id = (cur_ci == -2) ? -1 : cur_ci; if (cur_ci == -2 || cur_ci >= 0) { Scope *ms = find_method_scope(c, class_id, a1, 0); if (ms && !inline_pin_agrees(c, ms, -1, NULL, a2, a3, src, 1)) continue; if (ms) seed_warn_unknown_classes(c, class_id, "#", a1, a2, a3); seed_method(c, ms, method_in_override_family(c, class_id, a1, 0) ? NULL : a2, a3, src, NULL, 1); } } else if (sp_streq(kw, "cmeth") && a1 && a2 && cur_ci >= 0) { seed_decl_note(&decls, &ndecls, &capdecls, "cmeth", cur_cname, a1, a2, a3); Scope *ms = find_method_scope(c, cur_ci, a1, 1); if (ms && !inline_pin_agrees(c, ms, -1, NULL, a2, a3, src, 1)) continue; if (ms) seed_warn_unknown_classes(c, cur_ci, ".", a1, a2, a3); seed_method(c, ms, method_in_override_family(c, cur_ci, a1, 1) ? NULL : a2, a3, src, NULL, 1); } else if (sp_streq(kw, "src") && a1) { snprintf(src, sizeof src, "%s", a1); /* --positions: the .rbs line the next seed is from */ } } fclose(f); for (int i = 0; i < ndecls; i++) { free(decls[i].key); free(decls[i].sig); } free(decls); } /* Iteration methods whose block binds a parameter to the receiver array's element type (the forms infer_block_params re-derives from the receiver). A param of such a block can lock to poly when the element type settles only late in the fixpoint (e.g. `arr.map{...}.map{...}`: the inner map's receiver becomes a typed array only after the outer map narrows). */ static int iter_elem_block_method(const char *n) { static const char *names[] = { "each","map","collect","select","reject","filter","find","detect", "find_all","flat_map","filter_map","reverse_each","each_entry", "take_while","drop_while","sort_by","sort_by!","min_by","max_by","group_by", "partition","count","sum","any?","all?","none?","one?","keep_if", "delete_if","uniq","find_index","each_with_index","reduce","inject", NULL }; return str_in(n, names); } /* Is local `nm` assigned anywhere in `node`'s subtree (a block body)? Stops at nested defs/classes and at nested blocks/lambdas that re-bind `nm` (its writes there are a different variable). Used to leave a reassigned block param locked: its widening contribution is not recoverable by re-derivation from the element type alone. */ static int blkp_name_written(Compiler *c, int node, const char *nm) { if (node < 0) return 0; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return 0; if (ty_k == NK_DefNode || ty_k == NK_ClassNode || ty_k == NK_ModuleNode) return 0; if ((ty_k == NK_BlockNode || ty_k == NK_LambdaNode) && blkp_binds_param(c, node, nm)) return 0; if (a_is_write_node(ty)) { const char *n = nt_str(nt, node, "name"); if (n && sp_streq(n, nm)) return 1; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (blkp_name_written(c, nt_ref_at(nt, node, i), nm)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int k = 0; k < m; k++) if (blkp_name_written(c, ids[k], nm)) return 1; } return 0; } /* Clear a transient poly lock on iteration-block params over a now-typed (non-poly) array, so the optimistic re-narrow can re-derive the element type. Only read-only params are reset: a param reassigned in the body has a widening contribution the element-type re-derivation cannot recover (the re-run would re-narrow it and silently miscompile), so it stays locked. Reset to UNKNOWN -- never the element type directly. Returns the count. */ static int reset_locked_iter_block_params(Compiler *c) { const NodeTable *nt = c->nt; int n = 0; for (int id = 0; id < nt->count; id++) { int block = nt_ref(nt, id, "block"); if (block < 0) continue; const char *cn = nt_str(nt, id, "name"); if (!cn || !iter_elem_block_method(cn)) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; TyKind rt = infer_type(c, recv); if (!ty_is_array(rt) || rt == TY_POLY_ARRAY) continue; Scope *bs = comp_scope_of(c, block); if (!bs) continue; int body = nt_ref(nt, block, "body"); for (int k = 0; ; k++) { const char *pn = block_param_name(c, block, k); if (!pn) break; LocalVar *lp = scope_local(bs, pn); if (lp && lp->type == TY_POLY && !blkp_name_written(c, body, pn)) { lv_set_type(lp, TY_UNKNOWN); n++; } } } return n; } /* If `id` is a `to_enum`/`enum_for` call, resolve the target method name into `buf` (the literal first symbol, `:each` by default, or the enclosing method name for `__method__`) and report the count of forwarded args after the symbol plus whether a size block is present. Returns 1 for a handled call. `recv.to_enum(:m, *a)` defers `recv.m(*a)`; the internal iterator is lowered per receiver kind by desugar_to_enum. */ static int to_enum_target(Compiler *c, int id, char *buf, int buflen, int *out_extra, int *out_has_block) { const NodeTable *nt = c->nt; if (nt_kind(nt, id) != NK_CallNode) return 0; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "enum_for") && !sp_streq(nm, "to_enum"))) return 0; if (out_has_block) *out_has_block = nt_ref(nt, id, "block") >= 0; int args = nt_ref(nt, id, "arguments"); int ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; const char *mname = NULL; int extra = 0; if (ac == 0) { mname = "each"; } /* to_enum/enum_for default to :each */ else { NodeKind ak = nt_kind(nt, av[0]); if (ak == NK_SymbolNode) mname = nt_str(nt, av[0], "value"); else if (ak == NK_CallNode && nt_ref(nt, av[0], "receiver") < 0) { const char *an = nt_str(nt, av[0], "name"); if (an && sp_streq(an, "__method__")) { /* enum_for(__method__) -> this method */ Scope *es = comp_scope_of(c, id); mname = es ? es->name : NULL; } } if (!mname) return 0; /* a dynamic method symbol: not handled */ extra = ac - 1; } if (!mname) return 0; snprintf(buf, buflen, "%s", mname); if (out_extra) *out_extra = extra; return 1; } /* Small synthetic-AST constructors for the to_enum generator helper. */ static int te_lvread(NodeTable *nt, const char *name) { int n = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, n, "name", name); return n; } static int te_const(NodeTable *nt, const char *name) { int n = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, n, "name", name); return n; } static int te_args1(NodeTable *nt, int a) { int n = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, n, "arguments", &a, 1); return n; } static int te_call(NodeTable *nt, int recv, const char *name, int argsnode, int block) { int n = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, n, "name", name); if (recv >= 0) nt_node_set_ref(nt, n, "receiver", recv); if (argsnode >= 0) nt_node_set_ref(nt, n, "arguments", argsnode); if (block >= 0) nt_node_set_ref(nt, n, "block", block); return n; } /* A generator step that hands on what the method yielded, `__ey.yield(*__ev)`: several values go on as one step of several (a pack, as Enumerator.new's `y.yield(a, b)` makes), which a block of map and its kin takes spread and a `|x|` reads the first of; one value as itself; none as nil */ static int te_yield_step(NodeTable *nt) { int evsplat = nt_new_node(nt, "SplatNode"); nt_node_set_ref(nt, evsplat, "expression", te_lvread(nt, "__ev")); return te_call(nt, te_lvread(nt, "__ey"), "yield", te_args1(nt, evsplat), -1); } static int te_stmts1(NodeTable *nt, int s) { int n = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, n, "body", &s, 1); return n; } static int te_lvwrite(NodeTable *nt, const char *name, int value) { int n = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, n, "name", name); nt_node_set_ref(nt, n, "value", value); return n; } /* `Enumerator.produce(init) { |prev| nxt }` -> a fiber-backed generator that yields init, then repeatedly applies the block. Rewritten (before the inference fixpoint, so the generator's block body is typed) to: Enumerator.new { |__py| __pv = init loop { __py << __pv; prev = __pv; __pv = begin nxt end } } The block's param name (`prev`) is bound to the current value each round and its body inlined via a begin-expression. `<<` lowers to Fiber.yield. (#2483) */ int desugar_enumerator_produce_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "produce")) return 0; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_ConstantReadNode) return 0; if (!nt_str(nt, recv, "name") || !sp_streq(nt_str(nt, recv, "name"), "Enumerator")) return 0; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an != 1 || !av) return 0; /* only produce(init) { } for now */ return nt_ref(nt, blk, "body") >= 0 && block_param_name(c, blk, 0); } int desugar_enumerator_produce_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; if (!desugar_enumerator_produce_left(c, lc, id)) return 0; int blk = nt_ref(nt, id, "block"); int an = 0; const int *av = nt_arr(nt, nt_ref(nt, id, "arguments"), "arguments", &an); int init = av[0]; int bbody = nt_ref(nt, blk, "body"); const char *pname = block_param_name(c, blk, 0); int pv_w = te_lvwrite(nt, "__pv", init); /* loop body: __py << __pv ; = __pv ; __pv = begin end */ int yshl = te_call(nt, te_lvread(nt, "__py"), "<<", te_args1(nt, te_lvread(nt, "__pv")), -1); /* Several block params autosplat the state: `produce([0, 1]) { |a, b| }` binds a = state[0], b = state[1] (#3582). */ int bpn2 = nt_ref(nt, blk, "parameters"); int preqn = 0; if (bpn2 >= 0) { int pp2 = nt_ref(nt, bpn2, "parameters"); if (pp2 >= 0) nt_arr(nt, pp2, "requireds", &preqn); } int loopbodyarr[10]; int nlb = 0; loopbodyarr[nlb++] = yshl; if (preqn > 1 && preqn <= 8) { for (int k = 0; k < preqn; k++) { const char *pk = block_param_name(c, blk, k); if (!pk) break; int idxc = te_call(nt, te_lvread(nt, "__pv"), "[]", te_args1(nt, nt_new_int(nt, k)), -1); loopbodyarr[nlb++] = te_lvwrite(nt, pk, idxc); } } else loopbodyarr[nlb++] = te_lvwrite(nt, pname, te_lvread(nt, "__pv")); int beginn = nt_new_node(nt, "BeginNode"); nt_node_set_ref(nt, beginn, "statements", bbody); int pv_w2 = te_lvwrite(nt, "__pv", beginn); loopbodyarr[nlb++] = pv_w2; int loopbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, loopbody, "body", loopbodyarr, nlb); int loopblk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, loopblk, "body", loopbody); int loopcall = te_call(nt, -1, "loop", -1, loopblk); /* generator block: |__py| __pv=...; loop {...} */ int genarr[2] = { pv_w, loopcall }; int genbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, genbody, "body", genarr, 2); int pyreq = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, pyreq, "name", "__py"); int genparams = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, genparams, "requireds", &pyreq, 1); int genbp = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, genbp, "parameters", genparams); int genblk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, genblk, "parameters", genbp); nt_node_set_ref(nt, genblk, "body", genbody); /* rewrite THIS node into Enumerator.new { |__py| ... } */ nt_node_set_str(nt, id, "name", "new"); nt_node_set_ref(nt, id, "block", genblk); nt_node_set_ref(nt, id, "arguments", -1); comp_grow_node_arrays(c); return 1; } /* Is `nm` a positional parameter (required, optional or post) or a keyword parameter of the def whose ParametersNode is `params`? */ static int ef_named_param(const NodeTable *nt, int params, const char *nm) { if (params < 0 || !nm) return 0; static const char *const arrs[] = { "requireds", "optionals", "posts", "keywords", NULL }; for (int a = 0; arrs[a]; a++) { int n = 0; const int *ids = nt_arr(nt, params, arrs[a], &n); for (int k = 0; k < n; k++) { const char *pn = nt_str(nt, ids[k], "name"); if (pn && sp_streq(pn, nm)) return 1; } } return 0; } /* The def's rest parameter's name, or NULL. */ static const char *ef_rest_param(const NodeTable *nt, int def) { int params = nt_ref(nt, def, "parameters"); int rest = params >= 0 ? nt_ref(nt, params, "rest") : -1; return rest >= 0 ? nt_str(nt, rest, "name") : NULL; } /* An argument of an enum_for whose value the Enumerator can read again when it runs: a literal, a positional parameter of the def that nothing in its body writes, or a splat of its rest parameter likewise (`*rest`, which the rewrite copies first, as CRuby's enum_for keeps its own array). */ static int ef_stable_arg(Compiler *c, int def, int arg) { const NodeTable *nt = c->nt; const char *ty = nt_type(nt, arg); NodeKind ty_k = nt_kind(nt, arg); if (!ty) return 0; if (ty_k == NK_SymbolNode || ty_k == NK_IntegerNode || ty_k == NK_FloatNode || ty_k == NK_NilNode || ty_k == NK_TrueNode || ty_k == NK_FalseNode) return 1; int body = nt_ref(nt, def, "body"); if (ty_k == NK_SplatNode) { int e = nt_ref(nt, arg, "expression"); const char *rn = ef_rest_param(nt, def); return e >= 0 && nt_kind(nt, e) == NK_LocalVariableReadNode && rn && nt_str(nt, e, "name") && sp_streq(nt_str(nt, e, "name"), rn) && !blkp_name_written(c, body, rn); } /* `include_self: include_self`: keywords whose values can each be read again, a keyword parameter's included */ if (ty_k == NK_KeywordHashNode) { int en = 0; const int *els = nt_arr(nt, arg, "elements", &en); for (int k = 0; k < en; k++) { int key = nt_kind(nt, els[k]) == NK_AssocNode ? nt_ref(nt, els[k], "key") : -1; if (key < 0 || nt_kind(nt, key) != NK_SymbolNode || !ef_stable_arg(c, def, nt_ref(nt, els[k], "value"))) return 0; } return 1; } if (ty_k != NK_LocalVariableReadNode) return 0; const char *nm = nt_str(nt, arg, "name"); return ef_named_param(nt, nt_ref(nt, def, "parameters"), nm) && !blkp_name_written(c, body, nm); } /* Is `node` an enum_for / to_enum call on self naming the method being defined (`dname`, literally or by `__method__`), with no block? */ static int ef_names_own_method(const NodeTable *nt, int node, const char *dname) { if (nt_kind(nt, node) != NK_CallNode) return 0; const char *nm = nt_str(nt, node, "name"); int recv = nt_ref(nt, node, "receiver"); int args = nt_ref(nt, node, "arguments"); int ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; if (!nm || (!sp_streq(nm, "enum_for") && !sp_streq(nm, "to_enum")) || ac == 0) return 0; if (recv >= 0 && nt_kind(nt, recv) != NK_SelfNode) return 0; if (nt_ref(nt, node, "block") >= 0) return 0; NodeKind ak = nt_kind(nt, av[0]); if (ak == NK_SymbolNode) { const char *t = nt_str(nt, av[0], "value"); return t && sp_streq(t, dname); } return ak == NK_CallNode && nt_ref(nt, av[0], "receiver") < 0 && nt_ref(nt, av[0], "arguments") < 0 && nt_str(nt, av[0], "name") && sp_streq(nt_str(nt, av[0], "name"), "__method__"); } /* Collect the statements `return enum_for(...)` (ef_names_own_method) of `node`, outside any block or nested definition, each as its StatementsNode and index there. Nothing in the method runs after such a return. */ static void ef_collect(const NodeTable *nt, int node, const char *dname, int *stmts, int *idx, int *nout, int cap) { if (node < 0) return; const char *ty = nt_type(nt, node); NodeKind ty_k = nt_kind(nt, node); if (!ty) return; if (ty_k == NK_DefNode || ty_k == NK_ClassNode || ty_k == NK_ModuleNode || ty_k == NK_SingletonClassNode || ty_k == NK_BlockNode || ty_k == NK_LambdaNode) return; if (ty_k == NK_StatementsNode) { int n = 0; const int *body = nt_arr(nt, node, "body", &n); for (int k = 0; k < n && *nout < cap; k++) { if (nt_kind(nt, body[k]) != NK_ReturnNode) continue; int ra = nt_ref(nt, body[k], "arguments"); int rc = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rc) : NULL; if (rc == 1 && ef_names_own_method(nt, rv[0], dname)) { stmts[*nout] = node; idx[*nout] = k; (*nout)++; } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) ef_collect(nt, nt_ref_at(nt, node, i), dname, stmts, idx, nout, cap); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int k = 0; k < m; k++) ef_collect(nt, ids[k], dname, stmts, idx, nout, cap); } } /* `return enum_for(__method__, a, *rest) unless block_given?` in a method m that takes arguments, or in a class method (ruby/rdoc's RDoc::RI::Paths.each). The generator helper desugar_to_enum retargets to takes no arguments and lives on instances, so these were refused. When every argument can be read again (ef_stable_arg), the Enumerator makes the call itself: __ef_rest = rest.dup return Enumerator.new { |__ey| m(a, *__ef_rest) { |*__ev| Fiber.yield(__ev.length <= 1 ? __ev[0] : __ev) } } which re-runs m on self with the arguments enum_for was given, as CRuby's Enumerator does (the collapse is CRuby's ary2sv, as in the helper). The return keeps anything after it from writing what the block reads. */ int desugar_enum_for_with_args_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int def = id, changed = 0; const char *dn = nt_str(nt, def, "name"); if (!dn) return 0; int drecv = nt_ref(nt, def, "receiver"); int singleton = drecv >= 0 && nt_kind(nt, drecv) == NK_SelfNode; if (drecv >= 0 && !singleton) return 0; int stmts[8], sidx[8], nfound = 0; ef_collect(nt, nt_ref(nt, def, "body"), dn, stmts, sidx, &nfound, 8); if (nfound == 0) return 0; char *dname = strdup(dn); /* survives nt_new_node reallocs */ if (!dname) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int k = 0; k < nfound; k++) { int sn = 0; const int *sbody = nt_arr(nt, stmts[k], "body", &sn); int ret = sbody[sidx[k]]; int ra = nt_ref(nt, ret, "arguments"); int rc = 0; const int *rv = nt_arr(nt, ra, "arguments", &rc); int ef = rv[0]; int args = nt_ref(nt, ef, "arguments"); int ac = 0; const int *av = nt_arr(nt, args, "arguments", &ac); /* an instance method's argumentless call is the helper's */ if (ac == 1 && !singleton) continue; int fwd[16], nfwd = 0, ok = ac - 1 <= 16, splat = -1; for (int j = 1; j < ac && ok; j++) { if (!ef_stable_arg(c, def, av[j])) ok = 0; else { if (nt_kind(nt, av[j]) == NK_SplatNode) splat = av[j]; fwd[nfwd++] = av[j]; } } if (!ok) continue; /* *rest -> *__ef_rest, a copy taken before the return */ int snap = -1; if (splat >= 0) { int rd = nt_ref(nt, splat, "expression"); snap = te_lvwrite(nt, "__ef_rest", te_call(nt, rd, "dup", -1, -1)); nt_node_set_ref(nt, splat, "expression", te_lvread(nt, "__ef_rest")); } int fargs = -1; if (nfwd > 0) { fargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, fargs, "arguments", fwd, nfwd); } int fyield = te_yield_step(nt); /* m(args) { |*__ev| __ey.yield(*__ev) } */ int evrest = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, evrest, "name", "__ev"); int inner_params = nt_new_node(nt, "ParametersNode"); nt_node_set_ref(nt, inner_params, "rest", evrest); int inner_bp = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, inner_bp, "parameters", inner_params); int inner_blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, inner_blk, "parameters", inner_bp); nt_node_set_ref(nt, inner_blk, "body", te_stmts1(nt, fyield)); int mcall = te_call(nt, -1, dname, fargs, inner_blk); /* Enumerator.new { |__ey| } */ int eyreq = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, eyreq, "name", "__ey"); int outer_params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, outer_params, "requireds", &eyreq, 1); int outer_bp = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, outer_bp, "parameters", outer_params); int outer_blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, outer_blk, "parameters", outer_bp); nt_node_set_ref(nt, outer_blk, "body", te_stmts1(nt, mcall)); /* rewrite the enum_for itself into Enumerator.new { |__ey| ... } */ nt_node_set_str(nt, ef, "name", "new"); nt_node_set_ref(nt, ef, "receiver", te_const(nt, "Enumerator")); nt_node_set_ref(nt, ef, "arguments", -1); nt_node_set_ref(nt, ef, "block", outer_blk); if (snap >= 0) { /* the copy goes just before the return */ sbody = nt_arr(nt, stmts[k], "body", &sn); int *nb = malloc(sizeof(int) * (size_t)(sn + 1)); if (!nb) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int j = 0, o = 0; j < sn; j++) { if (j == sidx[k]) nb[o++] = snap; nb[o++] = sbody[j]; } nt_node_set_arr(nt, stmts[k], "body", nb, sn + 1); free(nb); /* a later return in the same statements moved down one */ for (int j = k + 1; j < nfound; j++) if (stmts[j] == stmts[k] && sidx[j] > sidx[k]) sidx[j]++; } comp_grow_node_arrays(c); changed = 1; } free(dname); return changed; } /* Synthesize, on every class that defines an instance `#each` that yields, a helper method def __enum_to_a __enum_acc = [] each { |__enum_e| __enum_acc << __enum_e } __enum_acc end so an `enum_for(:each)` rewritten to `__enum_to_a` materializes the receiver eagerly into an array (then map/select/to_a/... use the array path). The helper is a normal method whose body is inferred and emitted by the usual machinery; when `#each` is force-lowered the inlined `each { }` becomes a real call passing the block as a proc. Unused helpers are pruned by reachability, so this is inert for programs that never call enum_for/to_enum (the self-host compiler included). */ /* Synthesize, on each Struct/Data class with no user #each, the standard member iterator def each; yield @m1; ...; yield @mN; self; end so every inherited Enumerable method (map/select/sum/min/include?/...) rides the __enum_to_a machinery synthesized right below. Unused copies prune by reachability. */ /* `StructConst[a, b]` is Struct::[], an alias for StructConst.new(a, b). Rewrite the `[]` call to `new` once the struct classes are registered so the whole .new machinery (positional/keyword member fill, typing) serves it. */ static void desugar_struct_index_ctor(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "[]")) continue; if (nt_ref(nt, id, "block") >= 0) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; NodeKind rk = nt_kind(nt, recv); int ci = -1; if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) ci = const_node_class_index(c, recv); else if (rk == NK_LocalVariableReadNode) ci = class_var_static_ci(c, recv); if (ci < 0 || !c->classes[ci].is_struct) continue; nt_node_set_str(nt, id, "name", "new"); } } /* Data.new hands its positional arguments to initialize as keywords named by the members in order, so a user `def initialize(x:, y: 5)` is reached by `D.new(1, 2)` too. Rewrite such a call to `D.new(x: 1, y: 2)`; a Data class without its own initialize fills the members positionally already. */ static void desugar_data_positional_new(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "new")) continue; int recv = nt_ref(nt, id, "receiver"); NodeKind rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) continue; int ci = const_node_class_index(c, recv); if (ci < 0 || comp_method_in_chain(c, ci, "initialize", NULL) < 0) continue; int dc = ci; for (int g = 0; dc >= 0 && !c->classes[dc].is_data && g < 64; g++) dc = c->classes[dc].parent; if (dc < 0 || !c->classes[dc].is_data) continue; ClassInfo *dcls = &c->classes[dc]; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an == 0 || an > dcls->nmembers) continue; if (nt_argv_has(nt, av, an, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK | NT_ARG_FORWARDING)) continue; int *els = malloc(sizeof(int) * (size_t)an); if (!els) continue; int first = nt->count; int kwh = nt_new_node(nt, "KeywordHashNode"); for (int a = 0; a < an; a++) { int sy = nt_new_node(nt, "SymbolNode"); nt_node_set_str(nt, sy, "value", dcls->ivars[a] + 1); int as = nt_new_node(nt, "AssocNode"); nt_node_set_ref(nt, as, "key", sy); nt_node_set_ref(nt, as, "value", av[a]); els[a] = as; } nt_node_set_arr(nt, kwh, "elements", els, an); nt_node_set_arr(nt, args, "arguments", &kwh, 1); free(els); int sc = id < c->node_cap ? c->nscope[id] : 0; comp_grow_node_arrays(c); for (int k = first; k < nt->count; k++) c->nscope[k] = sc; } } /* Does Struct class `cls` have a member named `nm`? Its accessor is the class's own and answers in place of Struct's iterator of that name. */ static int struct_has_member(const ClassInfo *cls, const char *nm) { for (int j = 0; j < cls->nmembers; j++) if (cls->ivars[j] && sp_streq(cls->ivars[j] + 1, nm)) return 1; return 0; } static void synth_struct_each(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int ncls0 = c->nclasses; for (int ci = 0; ci < ncls0; ci++) { ClassInfo *cls = &c->classes[ci]; /* Data is not Enumerable: each/each_pair/each_with_index are Struct's */ if (!cls->is_struct || cls->is_data || cls->nmembers == 0) continue; if (comp_method_in_class(c, ci, "each") >= 0) continue; /* a member of an iterator's name is read by its accessor, which the struct defines over the one it inherits (#8203) */ if (!struct_has_member(cls, "each")) { int stmts[65]; int nst = 0; for (int j = 0; j < cls->nmembers && nst < 64; j++) { int ivr = nt_new_node(nt, "InstanceVariableReadNode"); nt_node_set_str(nt, ivr, "name", cls->ivars[j]); int yargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, yargs, "arguments", &ivr, 1); int yn = nt_new_node(nt, "YieldNode"); nt_node_set_ref(nt, yn, "arguments", yargs); stmts[nst++] = yn; } stmts[nst++] = nt_new_node(nt, "SelfNode"); int body = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, body, "body", stmts, nst); int def = nt_new_node(nt, "DefNode"); nt_node_set_str(nt, def, "name", "each"); /* marked for the boxed-receiver dispatch, which answers a blockless call to the synthesized method with an Enumerator, as the typed receiver does, and for reflection, which keeps all three generated iterators out of the struct class's own method list */ nt_node_set_str(nt, def, "synth", "struct"); nt_node_set_ref(nt, def, "body", body); Scope *ms = comp_scope_new(c, "each", def); { ms->class_id = ci; comp_touch_scope(ms->idx); } { ms->body = body; comp_touch_scope(ms->idx); } ms->yields = 1; comp_grow_node_arrays(c); walk_scope(c, body, c->nscopes - 1, ci); } /* def each_pair; yield :m1, @m1; ...; self; end */ if (comp_method_in_class(c, ci, "each_pair") < 0 && !struct_has_member(cls, "each_pair")) { int pst[65]; int pn = 0; for (int j = 0; j < cls->nmembers && pn < 64; j++) { int sy = nt_new_node(nt, "SymbolNode"); nt_node_set_str(nt, sy, "value", cls->ivars[j] + 1); int ivr = nt_new_node(nt, "InstanceVariableReadNode"); nt_node_set_str(nt, ivr, "name", cls->ivars[j]); /* ONE argument, the [name, value] pair: CRuby's each_pair yields the pair, so a single-parameter block sees it whole and a two-parameter one destructures it. Yielding two values gave the 1-param form only the name. */ int ya[2] = { sy, ivr }; int pair = nt_new_node(nt, "ArrayNode"); nt_node_set_arr(nt, pair, "elements", ya, 2); int yargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, yargs, "arguments", &pair, 1); int yn = nt_new_node(nt, "YieldNode"); nt_node_set_ref(nt, yn, "arguments", yargs); pst[pn++] = yn; } pst[pn++] = nt_new_node(nt, "SelfNode"); int pbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, pbody, "body", pst, pn); int pdef = nt_new_node(nt, "DefNode"); nt_node_set_str(nt, pdef, "name", "each_pair"); nt_node_set_str(nt, pdef, "synth", "struct"); nt_node_set_ref(nt, pdef, "body", pbody); Scope *ps = comp_scope_new(c, "each_pair", pdef); { ps->class_id = ci; comp_touch_scope(ps->idx); } { ps->body = pbody; comp_touch_scope(ps->idx); } ps->yields = 1; comp_grow_node_arrays(c); walk_scope(c, pbody, c->nscopes - 1, ci); } /* def each_with_index; yield @m1, 0; yield @m2, 1; ...; self; end (returns the receiver, unlike the __enum_to_a redirect which would return the flat member array). The index is a literal per member. */ if (comp_method_in_class(c, ci, "each_with_index") < 0 && !struct_has_member(cls, "each_with_index")) { int wst[65]; int wn = 0; for (int j = 0; j < cls->nmembers && wn < 64; j++) { int ivr = nt_new_node(nt, "InstanceVariableReadNode"); nt_node_set_str(nt, ivr, "name", cls->ivars[j]); int ixn = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, ixn, "value", j); int ya[2] = { ivr, ixn }; int yargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, yargs, "arguments", ya, 2); int yn = nt_new_node(nt, "YieldNode"); nt_node_set_ref(nt, yn, "arguments", yargs); wst[wn++] = yn; } wst[wn++] = nt_new_node(nt, "SelfNode"); int wbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, wbody, "body", wst, wn); int wdef = nt_new_node(nt, "DefNode"); nt_node_set_str(nt, wdef, "name", "each_with_index"); nt_node_set_str(nt, wdef, "synth", "struct"); nt_node_set_ref(nt, wdef, "body", wbody); Scope *ws = comp_scope_new(c, "each_with_index", wdef); { ws->class_id = ci; comp_touch_scope(ws->idx); } { ws->body = wbody; comp_touch_scope(ws->idx); } ws->yields = 1; comp_grow_node_arrays(c); walk_scope(c, wbody, c->nscopes - 1, ci); } } } /* Does `class `'s body say `include Enumerable`? A builtin module has no class-table entry, so the AST is what records it (#3755). */ /* The names of the classes whose body includes Enumerable, from one walk of the ClassNodes; asked per enum call site (desugar_builtin_enum_calls, narrow_object_arrays), the walk of every node per ask was quadratic (rubys/roundhouse#72). Rebuilt whenever the node table changes. */ static ANameHash g_enum_cls; static const NodeTable *g_enum_cls_nt; static int g_enum_cls_cnt = -1; static int class_body_includes_enumerable(const NodeTable *nt, int id); int an_class_includes_enumerable(Compiler *c, int ci) { const NodeTable *nt = c->nt; if (ci < 0 || ci >= c->nclasses) return 0; const char *cn = c->classes[ci].name; if (!cn) return 0; /* Keyed on the class declarations, not the table's version: the pass that asks most (desugar_builtin_enum_calls) rewrites nodes between asks, and a version key rebuilt the set once per rewrite -- K rebuilds of K classes (rubys in #5035). An include comes only from a class body, and no pass adds one to an existing body; a new class grows the table. */ int ncls_now = c->nclasses; /* not the kind list: asking it rebuilds the kind index after every rewrite */ if (g_enum_cls_nt != nt || g_enum_cls_cnt != ncls_now) { anh_free(&g_enum_cls); memset(&g_enum_cls, 0, sizeof g_enum_cls); NT_FOREACH_KIND(nt, NK_ClassNode, id) { if (!class_body_includes_enumerable(nt, id)) continue; int cp = nt_ref(nt, id, "constant_path"); const char *n = cp >= 0 ? nt_str(nt, cp, "name") : nt_str(nt, id, "name"); if (n && !anh_has(&g_enum_cls, n)) anh_add(&g_enum_cls, n); } g_enum_cls_nt = nt; g_enum_cls_cnt = ncls_now; } return anh_has(&g_enum_cls, cn); } /* Enumerable reaches class `ci` through itself, a superclass, or a module any of those includes (and a module it includes in turn). The minmax gate asks it once per call site; an_class_includes_enumerable alone sees only a class whose own body includes Enumerable. */ static int an_enum_reaches_v(Compiler *c, int ci, unsigned char *seen) { const NodeTable *nt = c->nt; if (ci < 0 || ci >= c->nclasses || seen[ci]) return 0; seen[ci] = 1; for (int cur = ci; cur >= 0 && cur < c->nclasses; cur = c->classes[cur].parent) { if (an_class_includes_enumerable(c, cur)) return 1; for (int id = 0; id < nt->count; id++) { int k = nt_kind(nt, id); if (k != NK_ClassNode && k != NK_ModuleNode) continue; if (elab_header_class(c, id, NULL, NULL) != cur) continue; int body = nt_ref(nt, id, "body"); int bn = 0; const int *stmts = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; for (int s2 = 0; s2 < bn; s2++) { if (nt_kind(nt, stmts[s2]) != NK_CallNode || nt_ref(nt, stmts[s2], "receiver") >= 0) continue; const char *nm = nt_str(nt, stmts[s2], "name"); if (!nm || !sp_streq(nm, "include")) continue; int an = 0, anode = nt_ref(nt, stmts[s2], "arguments"); const int *aa = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { const char *mn = nt_str(nt, aa[j], "name"); if (!mn) continue; if (sp_streq(mn, "Enumerable")) return 1; int mi = comp_class_index(c, mn); if (mi >= 0 && an_enum_reaches_v(c, mi, seen)) return 1; } } } if (c->classes[cur].parent == cur) break; } return 0; } /* Each class or module is walked once (a visited set rather than a depth bound, so a long include chain is still followed to its end). */ static int an_enum_reaches(Compiler *c, int ci) { if (ci < 0 || ci >= c->nclasses) return 0; unsigned char *seen = calloc((size_t)c->nclasses, 1); if (!seen) return 0; int r = an_enum_reaches_v(c, ci, seen); free(seen); return r; } static int class_body_includes_enumerable(const NodeTable *nt, int id) { int body = nt_ref(nt, id, "body"); int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; for (int k = 0; k < bn; k++) { if (nt_kind(nt, bb[k]) != NK_CallNode) continue; const char *nm = nt_str(nt, bb[k], "name"); if (!nm || !sp_streq(nm, "include") || nt_ref(nt, bb[k], "receiver") >= 0) continue; int an = nt_ref(nt, bb[k], "arguments"); int n2 = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n2) : NULL; for (int j = 0; j < n2; j++) { const char *mn = nt_str(nt, av[j], "name"); if (mn && sp_streq(mn, "Enumerable")) return 1; } } return 0; } static void synth_enum_to_a(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; if (c->nscopes == 0) return; /* collect target classes first (synthesis below grows c->scopes). At most one entry per scope, so pre-size both arrays to nscopes and avoid per-item growth. */ int *cls = malloc(sizeof(int) * (size_t)c->nscopes); int *eachdef = malloc(sizeof(int) * (size_t)c->nscopes); if (!cls || !eachdef) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int ncls = 0; for (int s = 0; s < c->nscopes; s++) { Scope *m = &c->scopes[s]; if (!m->name || m->is_cmethod || m->class_id < 0) continue; /* an each that drives the block via `yield`, or one that forwards an explicit `&blk` to an inner iterator (`def each(&b) = @xs.each(&b)`): both invoke the synthesized `each { |e| acc << e }` block per element. An anonymous `&` forward is excluded (blk_param is "") -- forwarding an anonymous block through the synthesized materializer is not yet wired and would yield an empty result, so it stays a loud reject. */ /* Before entries are built, the table already answers a prepend's hidden copy by its public name. The bridge serves that same name. */ if (!is_each_name(comp_scope_user_name(m)) || !(m->yields || (m->blk_param && m->blk_param[0]) || /* a class that mixes Enumerable in still gets the materializer even when its #each never yields: the mixin is simply empty (#3755) */ an_class_includes_enumerable(c, m->class_id))) continue; if (comp_method_in_class(c, m->class_id, "__enum_to_a") >= 0) continue; /* a module's methods are compiled into each class that includes it, which gets its own materializer; the module has no instances, and one of its own called methods that exist only in those copies */ if (comp_class_is_module(c, &c->classes[m->class_id])) continue; int dup = 0; for (int k = 0; k < ncls; k++) if (cls[k] == m->class_id) { dup = 1; break; } if (dup) continue; cls[ncls] = m->class_id; eachdef[ncls] = m->def_node; ncls++; } for (int k = 0; k < ncls; k++) { /* `each` may yield more than one value per element (`yield k, v`), and Enumerable packs those into one array element. The collector block takes as many parameters as the widest yield and pushes them as an array; with a single parameter it saw only the first value (#3754). */ int yarity = 1; /* yields of one value and of several, or a splat, whose count is known only at run time: Enumerable packs only a yield of several values, so the collector takes them all as a rest and packs by the count */ int ymany = 0, yone = 0, ysplat = 0; { int esi = comp_method_in_class(c, cls[k], "each"); if (esi >= 0) for (int nid = 0; nid < nt->count; nid++) { if (c->nscope[nid] != esi) continue; NodeKind ynk = nt_kind(nt, nid); if (ynk != NK_YieldNode) continue; int ya = nt_ref(nt, nid, "arguments"); int yn2 = 0; const int *yav = ya >= 0 ? nt_arr(nt, ya, "arguments", &yn2) : NULL; if (nt_argv_has_splat(nt, yav, yn2)) ysplat = 1; if (yn2 >= 2) ymany = 1; else yone = 1; if (yn2 > yarity) yarity = yn2; } /* The same method written with an explicit block parameter drives it through `block.call(k, v)` rather than `yield`: count those sites too, or the collector took one parameter and every element arrived as the first value alone (#3792). */ if (esi >= 0 && c->scopes[esi].blk_param && c->scopes[esi].blk_param[0]) { const char *bpn = c->scopes[esi].blk_param; for (int nid = 0; nid < nt->count; nid++) { if (c->nscope[nid] != esi) continue; if (nt_kind(nt, nid) != NK_CallNode) continue; const char *cnm2 = nt_str(nt, nid, "name"); if (!cnm2 || !is_call_or_yield(cnm2)) continue; int crv = nt_ref(nt, nid, "receiver"); if (crv < 0 || nt_kind(nt, crv) != NK_LocalVariableReadNode) continue; const char *crn = nt_str(nt, crv, "name"); if (!crn || !sp_streq(crn, bpn)) continue; int ca2 = nt_ref(nt, nid, "arguments"); int cn2 = 0; const int *cav = ca2 >= 0 ? nt_arr(nt, ca2, "arguments", &cn2) : NULL; if (nt_argv_has_splat(nt, cav, cn2)) ysplat = 1; if (cn2 >= 2) ymany = 1; else yone = 1; if (cn2 > yarity) yarity = cn2; } } if (yarity > 8) yarity = 8; c->classes[cls[k]].enum_yield_arity = yarity; } int packed = ysplat || (ymany && yone); c->classes[cls[k]].enum_yield_packed = packed; if (packed) yarity = 1; /* a packed class also gets the values as yielded, for the methods that hand them to their block as yielded (map, flat_map, `for`) */ for (int raw = 0; raw <= packed; raw++) { int arr = nt_new_node(nt, "ArrayNode"); nt_node_set_arr(nt, arr, "elements", NULL, 0); int accw = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, accw, "name", "__enum_acc"); nt_node_set_ref(nt, accw, "value", arr); char enames[8][16]; int eps[8]; for (int q = 0; q < yarity; q++) { if (q == 0) snprintf(enames[q], sizeof enames[q], "__enum_e"); else snprintf(enames[q], sizeof enames[q], "__enum_e%d", q); eps[q] = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, eps[q], "name", enames[q]); } int params = nt_new_node(nt, "ParametersNode"); int accr1 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, accr1, "name", "__enum_acc"); int eread; if (packed) { /* |*__enum_e|, pushed as __enum_e.length <= 1 ? __enum_e[0] : __enum_e: nothing yielded is nil, one value itself, several an Array; the raw collector pushes the values as yielded */ int rest = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, rest, "name", "__enum_e"); nt_node_set_ref(nt, params, "rest", rest); } if (packed && raw) eread = te_lvread(nt, "__enum_e"); else if (packed) { int len = te_call(nt, te_lvread(nt, "__enum_e"), "length", -1, -1); int cmp = te_call(nt, len, "<=", te_args1(nt, nt_new_int(nt, 1)), -1); int first = te_call(nt, te_lvread(nt, "__enum_e"), "[]", te_args1(nt, nt_new_int(nt, 0)), -1); int els = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, els, "statements", te_stmts1(nt, te_lvread(nt, "__enum_e"))); eread = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, eread, "predicate", cmp); nt_node_set_ref(nt, eread, "statements", te_stmts1(nt, first)); nt_node_set_ref(nt, eread, "subsequent", els); } else { nt_node_set_arr(nt, params, "requireds", eps, yarity); int ereads[8]; for (int q = 0; q < yarity; q++) { ereads[q] = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ereads[q], "name", enames[q]); } if (yarity == 1) eread = ereads[0]; else { eread = nt_new_node(nt, "ArrayNode"); nt_node_set_arr(nt, eread, "elements", ereads, yarity); } } int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int pushargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, pushargs, "arguments", &eread, 1); int push = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, push, "name", "<<"); nt_node_set_ref(nt, push, "receiver", accr1); nt_node_set_ref(nt, push, "arguments", pushargs); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &push, 1); int blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk, "parameters", bparams); nt_node_set_ref(nt, blk, "body", blkbody); int eachcall = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, eachcall, "name", "each"); nt_node_set_ref(nt, eachcall, "block", blk); int accr2 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, accr2, "name", "__enum_acc"); int body = nt_new_node(nt, "StatementsNode"); int stmts[3] = { accw, eachcall, accr2 }; nt_node_set_arr(nt, body, "body", stmts, 3); Scope *ms = comp_scope_new(c, raw ? "__enum_to_a_raw" : "__enum_to_a", eachdef[k]); elab_names_add(c, ms->name); /* a call of it runs the program's #each */ { ms->class_id = cls[k]; comp_touch_scope(ms->idx); } { ms->body = body; comp_touch_scope(ms->idx); } int ms_idx = c->nscopes - 1; /* register_locals already ran (before synthesis), so intern this scope's locals here; types are filled in by the inference fixpoint that follows. */ scope_local_intern(ms, "__enum_acc"); for (int q = 0; q < yarity; q++) { LocalVar *ev = scope_local_intern(ms, enames[q]); if (ev) ev->is_block_param = 1; } comp_grow_node_arrays(c); walk_scope(c, body, ms_idx, cls[k]); } } free(cls); free(eachdef); } /* Blockless builtin iterators that already lower to a materialized Enumerator; a builtin-receiver `to_enum(:m)` retargets to the plain blockless `recv.m`. */ static int to_enum_builtin_method(const char *m) { static const char *const ok[] = { "each", "reverse_each", "each_with_index", "each_index", "each_char", "each_line", "each_slice", "each_cons", "each_pair", "times", "each_byte", NULL }; return str_in(m, ok); } /* Synthesize, on each user class whose yielding instance method `m` is the target of a `to_enum(:m)`/`enum_for(:m)` call, a lazy generator helper def __to_enum_m Enumerator.new do |__ey| m do |*__ev| Fiber.yield(__ev.length <= 1 ? __ev[0] : __ev) end end end so `recv.to_enum(:m)` (rewritten to `recv.__to_enum_m` by desugar_to_enum) yields a real fiber-backed Enumerator. `m`'s yields flow through the rest param `__ev`; the `length<=1 ? [0] : self` collapse is CRuby's ary2sv (`[][0]` is nil), and `y << v` is emitted as a direct `Fiber.yield` so the inner block captures nothing (no proc-in-proc capture). The method call is the construction-time snapshot of the receiver (recv -> self via cap_self). Runs pre-fixpoint like synth_enum_to_a; unused helpers prune by reachability. */ static void synth_to_enum_generators(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; if (c->nscopes == 0) return; /* Collect the set of target method names across all to_enum/enum_for calls. */ int nnames = 0, cnames = 8; char **names = malloc(sizeof(char *) * (size_t)cnames); if (!names) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int n0 = nt->count; /* A short-circuiting Enumerable call is served by the same generator: the redirect below routes `obj.first`/`take`/`find`/`take_while`/`include?`/ `lazy` through `__to_enum_each` so a class whose #each never ends still answers (#3756). Seed "each" whenever such a call is in the program; an unused helper prunes by reachability like every other. */ for (int id = 0; id < n0 && nnames == 0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *cn = nt_str(nt, id, "name"); if (!cn) continue; if (sp_streq(cn, "first") || sp_streq(cn, "take") || sp_streq(cn, "find") || sp_streq(cn, "detect") || sp_streq(cn, "take_while") || sp_streq(cn, "lazy") || sp_streq(cn, "include?") || sp_streq(cn, "member?")) names[nnames++] = strdup("each"); } for (int id = 0; id < n0; id++) { char buf[128]; if (!to_enum_target(c, id, buf, sizeof buf, NULL, NULL)) continue; int dup = 0; for (int k = 0; k < nnames; k++) if (sp_streq(names[k], buf)) { dup = 1; break; } if (dup) continue; if (nnames >= cnames) { cnames *= 2; char **tmp = realloc(names, sizeof(char *) * (size_t)cnames); if (!tmp) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } names = tmp; } names[nnames++] = strdup(buf); } if (nnames == 0) { free(names); return; } /* Collect (class, method-name, def_node) triples: a yielding instance method whose name is a to_enum target. Snapshot before synthesis grows c->scopes. */ int ntr = 0, ctr = c->nscopes; int *tc = malloc(sizeof(int) * (size_t)ctr); int *tdef = malloc(sizeof(int) * (size_t)ctr); char **tm = malloc(sizeof(char *) * (size_t)ctr); if (!tc || !tdef || !tm) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int s = 0; s < c->nscopes; s++) { Scope *m = &c->scopes[s]; if (!m->name || m->is_cmethod) continue; /* a module's own copy of the method has no receiver to iterate (the includers' copies get their helpers): a generator on the module -- activesupport's Enumerable#index_by, `to_enum(:index_by)` on its blockless branch -- would compile that copy, whose bare `each` has no self, and was refused */ if (m->class_id >= 0 && comp_class_is_module(c, &c->classes[m->class_id])) continue; if (!(m->yields || (m->blk_param && m->blk_param[0]))) continue; int hit = 0; for (int k = 0; k < nnames; k++) if (sp_streq(names[k], m->name)) { hit = 1; break; } if (!hit) continue; char hname[160]; snprintf(hname, sizeof hname, "__to_enum_%s", m->name); if (comp_method_in_class(c, m->class_id, hname) >= 0) continue; /* already synthesized */ int dup = 0; for (int k = 0; k < ntr; k++) if (tc[k] == m->class_id && sp_streq(tm[k], m->name)) { dup = 1; break; } if (dup) continue; tc[ntr] = m->class_id; tdef[ntr] = m->def_node; tm[ntr] = strdup(m->name); ntr++; } for (int k = 0; k < ntr; k++) { int ci = tc[k]; const char *m = tm[k]; /* stable strdup: survives nt_new_node reallocs */ char hname[160]; snprintf(hname, sizeof hname, "__to_enum_%s", m); int fyield = te_yield_step(nt); /* inner block: m { |*__ev| __ey.yield(*__ev) } */ int evrest = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, evrest, "name", "__ev"); int inner_params = nt_new_node(nt, "ParametersNode"); nt_node_set_ref(nt, inner_params, "rest", evrest); int inner_bp = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, inner_bp, "parameters", inner_params); int inner_blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, inner_blk, "parameters", inner_bp); nt_node_set_ref(nt, inner_blk, "body", te_stmts1(nt, fyield)); int mcall = te_call(nt, -1, m, -1, inner_blk); /* implicit self.m -> cap_self */ /* outer block: Enumerator.new { |__ey| } */ int eyreq = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, eyreq, "name", "__ey"); int outer_params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, outer_params, "requireds", &eyreq, 1); int outer_bp = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, outer_bp, "parameters", outer_params); int outer_blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, outer_blk, "parameters", outer_bp); nt_node_set_ref(nt, outer_blk, "body", te_stmts1(nt, mcall)); int enumnew = te_call(nt, te_const(nt, "Enumerator"), "new", -1, outer_blk); int mbody = te_stmts1(nt, enumnew); Scope *ms = comp_scope_new(c, hname, tdef[k]); elab_names_add(c, hname); /* a call of it runs the program's method m */ { ms->class_id = ci; comp_touch_scope(ms->idx); } { ms->body = mbody; comp_touch_scope(ms->idx); } int ms_idx = c->nscopes - 1; LocalVar *ey = scope_local_intern(ms, "__ey"); if (ey) ey->is_block_param = 1; LocalVar *ev = scope_local_intern(ms, "__ev"); if (ev) ev->is_block_param = 1; comp_grow_node_arrays(c); walk_scope(c, mbody, ms_idx, ci); } for (int k = 0; k < nnames; k++) free(names[k]); for (int k = 0; k < ntr; k++) free(tm[k]); free(names); free(tc); free(tdef); free(tm); } /* Enumerable methods that work on an array receiver in Spinel; a bare call to one of these on a user `#each` class (that does not define it) is redirected through the materialized array. Kept to methods the array path supports. */ static int is_array_enum_method(const char *nm) { static const char *const names[] = { "map", "collect", "select", "filter", "reject", "to_a", "entries", "find", "detect", "find_index", "count", "sum", "min", "max", "include?", "first", "sort", "sort_by", "min_by", "max_by", "reduce", "inject", "flat_map", "collect_concat", "any?", "all?", "none?", "one?", "take", "drop", "take_while", "drop_while", "filter_map", "partition", "group_by", "each_with_object", "tally", "find_all", "zip", "grep", "grep_v", "to_h", "uniq", "reverse", "member?", "each_with_index", "join", "index", "each", "each_cons", "each_slice", "chunk", "chunk_while", "slice_when", "minmax_by", "cycle", "lazy", "each_entry", "reverse_each", "compact", "chain", "slice_before", "slice_after", "minmax", NULL }; return str_in(nm, names); } /* Redirect a bare Enumerable call -- `obj.map { }`, `obj.to_a`, ... -- on a user class that defines `#each` but not that method, through the synthesized `__enum_to_a` helper: `obj.{blk}` becomes `obj.__enum_to_a.{blk}`, so the array path handles it without an explicit `enum_for(:each)`. Runs in the inference fixpoint (needs the receiver's type, and the inserted node is typed on a later iteration). Returns 1 if anything changed. */ /* One-shot chain normalizations before scope registration: - str.each_char.with_index { } -> str.chars.each.with_index { } (the each.with_index chain machinery keys on the `each` shape) - enum_recv.each.with_object(x) { } -> enum_recv.each_with_object(x) { } Both rewrites produce constructs the existing emitters already serve. */ /* --share-strings: is the blockless call id an Enumerator kept past the expression it is made in (no call's receiver: held, handed on or answered)? `s.each_char.with_index` kept so stays an Enumerator over each_char's, which holds a String the rule shares and reads it when it is walked (sp_enum_str_each_h); `chars` would read it here. */ /* builtins/enumerator.rb's walk named `nm`, spliced into this program (the pass runs before the top-level defs are registered as methods) */ static int ecw_helper_spliced(const NodeTable *nt, const char *nm) { NT_FOREACH_KIND(nt, NK_DefNode, d) if (nt_ref(nt, d, "receiver") < 0 && nt_str(nt, d, "name") && sp_streq(nt_str(nt, d, "name"), nm)) return 1; return 0; } /* Does the program define a method of its own named for the String walk `rn` (each_char, each_line, each_byte, each_codepoint) or for the reader the rewrite puts in its place (chars, lines, bytes, codepoints)? Then the receiver may be the program's own object, or a reopened String, which answers its own method: the call stays as written. The pass runs before the methods are registered, so it reads the definitions. */ static int walk_rewrite_shadowed(Compiler *c, const char *rn) { const char *rd = sp_streq(rn, "each_char") ? "chars" : sp_streq(rn, "each_line") ? "lines" : sp_streq(rn, "each_byte") ? "bytes" : "codepoints"; NT_FOREACH_KIND(c->nt, NK_DefNode, d) { const char *dn = nt_str(c->nt, d, "name"); if (dn && (sp_streq(dn, rn) || sp_streq(dn, rd))) return 1; } return 0; } /* a Hash literal, or a merge into one (`{}.merge!(a)` in `{}.merge!(a).merge!(b)`): a Hash nothing else holds */ static int fresh_hash_merge_recv(const NodeTable *nt, int recv) { recv = nt_paren_single(nt, recv); if (recv < 0) return 0; if (nt_kind(nt, recv) == NK_HashNode) return 1; const char *rn = nt_kind(nt, recv) == NK_CallNode ? nt_str(nt, recv, "name") : NULL; return rn && (sp_streq(rn, "merge") || is_hash_merge_bang(rn)) && fresh_hash_merge_recv(nt, nt_ref(nt, recv, "receiver")); } static int lit_name_is(const NodeTable *nt, int n, const char *nm) { NodeKind k = nt_kind(nt, n); const char *s = k == NK_SymbolNode ? nt_str(nt, n, "value") : k == NK_StringNode ? nt_str(nt, n, "content") : NULL; return s && sp_streq(s, nm); } /* `nm` bound as a method under `root`: a def, an alias, or an alias_method / define_method naming it or a name only the run time knows (`%i[merge! update].each { |m| define_method(m) ... }`); or a module prepended, whose methods are not followed here (`Hash.prepend(M)` is a `prepend M` in a `class Hash` by now) */ /* Call `call` is an alias_method or define_method that may bind `nm`: of that name, or of a name not written out. */ static int dyn_def_binds_name(const NodeTable *nt, int call, const char *nm) { const char *cn = nt_kind(nt, call) == NK_CallNode ? nt_str(nt, call, "name") : NULL; if (!cn || (!sp_streq(cn, "alias_method") && !sp_streq(cn, "define_method"))) return 0; int ca = nt_ref(nt, call, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; NodeKind ak = an >= 1 ? nt_kind(nt, av[0]) : NK_NONE; return an >= 1 && ((ak != NK_SymbolNode && ak != NK_StringNode) || lit_name_is(nt, av[0], nm)); } static int binds_name_under(const NodeTable *nt, int root, const char *nm) { if (root < 0 || root >= nt->count) return 0; NodeKind k = nt_kind(nt, root); if (k == NK_DefNode && nt_ref(nt, root, "receiver") < 0 && nt_str(nt, root, "name") && sp_streq(nt_str(nt, root, "name"), nm)) return 1; if (k == NK_AliasMethodNode && lit_name_is(nt, nt_ref(nt, root, "new_name"), nm)) return 1; const char *cn = k == NK_CallNode ? nt_str(nt, root, "name") : NULL; if (cn && sp_streq(cn, "prepend") && nt_ref(nt, root, "receiver") < 0) return 1; if (dyn_def_binds_name(nt, root, nm)) return 1; for (int j = 0; j < nt_num_refs(nt, root); j++) if (binds_name_under(nt, nt_ref_at(nt, root, j), nm)) return 1; for (int j = 0; j < nt_num_arrs(nt, root); j++) { int an = 0; const int *ids = nt_arr_at(nt, root, j, &an); for (int i = 0; i < an; i++) if (binds_name_under(nt, ids[i], nm)) return 1; } return 0; } static int is_hash_const(const NodeTable *nt, int n) { return n >= 0 && nt_kind(nt, n) == NK_ConstantReadNode && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), "Hash"); } /* the program may give Hash its own `nm` (the pass runs before the classes are registered): its `class Hash` binds the name, or it reaches Hash's methods another way -- a module prepended or included, a class_eval, a refinement -- whose bindings are not followed here */ static int hash_reopen_defines(const NodeTable *nt, const char *nm) { NT_FOREACH_KIND(nt, NK_ClassNode, k) { int cp = nt_ref(nt, k, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (cn && sp_streq(cn, "Hash") && binds_name_under(nt, nt_ref(nt, k, "body"), nm)) return 1; } static const char *const reach[] = { "prepend", "include", "class_eval", "class_exec", "module_eval", "module_exec", "define_method", "alias_method", "send", "__send__", NULL }; NT_FOREACH_KIND(nt, NK_CallNode, call) { const char *cn = nt_str(nt, call, "name"); if (!cn) continue; for (int i = 0; reach[i]; i++) if (sp_streq(cn, reach[i]) && is_hash_const(nt, nt_ref(nt, call, "receiver"))) return 1; if (sp_streq(cn, "refine")) { int ca = nt_ref(nt, call, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (an >= 1 && is_hash_const(nt, av[0])) return 1; } } return 0; } /* hash_reopen_defines of `nm` (merge! or update) or of merge, asked once per pass for each (`memo[]` -1 until then) */ static int hash_merges_own(const NodeTable *nt, const char *nm, int memo[3]) { int i = sp_streq(nm, "merge!") ? 0 : 1; if (memo[i] < 0) memo[i] = hash_reopen_defines(nt, nm); if (memo[2] < 0) memo[2] = hash_reopen_defines(nt, "merge"); return memo[i] || memo[2]; } /* Whether node `d`, a method's definition, takes `an` arguments. A def with keywords (`**opts`, `...` included) is said to take them, since the last argument may be its keywords, and so is a definition that is no def (define_method's block). */ static int def_takes_argc(const NodeTable *nt, int d, int an) { if (d < 0 || nt_kind(nt, d) != NK_DefNode) return 1; int pn = nt_ref(nt, d, "parameters"); if (pn < 0) return an == 0; int rn = 0, on = 0, qn = 0, kn = 0; nt_arr(nt, pn, "requireds", &rn); nt_arr(nt, pn, "optionals", &on); nt_arr(nt, pn, "posts", &qn); nt_arr(nt, pn, "keywords", &kn); if (kn > 0 || nt_ref(nt, pn, "keyword_rest") >= 0) return 1; return an >= rn + qn && (nt_ref(nt, pn, "rest") >= 0 || an <= rn + qn + on); } /* update with a block was not folded before: a def of it of any arity keeps the call, so its own ArgumentError still shows */ static int merge_def_takes_argc(const NodeTable *nt, const char *nm, int d, int an) { return sp_streq(nm, "update") || def_takes_argc(nt, d, an); } /* Whether the program may give some object its own `nm` (merge, merge! or update) that takes `an` arguments in one call: a def of it on any receiver that takes them, an alias to it, an alias_method or define_method of it or of a name not written out, or a method_missing. The classes are not registered yet, so every such binding counts. */ static int hash_merge_name_owned(const NodeTable *nt, const char *nm, int an) { NT_FOREACH_KIND(nt, NK_DefNode, d) { const char *dn = nt_str(nt, d, "name"); if (dn && ((sp_streq(dn, nm) && merge_def_takes_argc(nt, nm, d, an)) || sp_streq(dn, "method_missing"))) return 1; } NT_FOREACH_KIND(nt, NK_AliasMethodNode, a) if (lit_name_is(nt, nt_ref(nt, a, "new_name"), nm)) return 1; NT_FOREACH_KIND(nt, NK_CallNode, call) if (dyn_def_binds_name(nt, call, nm)) return 1; return 0; } /* `r.merge(a, b, ...)` folds left into `r.merge(a).merge(b)...`; merge! and update chain the same way because they answer self. `typed`: asked in the inference rounds, so new nodes take the call's scope. Answers 1 when it folded. */ static int hash_merge_fold(Compiler *c, int id, int typed) { NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av0 = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; if (!nm || recv < 0 || an < 2 || an > 64) return 0; const char *mname = sp_streq(nm, "merge!") ? "merge!" : sp_streq(nm, "update") ? "update" : "merge"; int base = nt->count; int av[64]; memcpy(av, av0, (size_t)an * sizeof(int)); int mblk = nt_ref(nt, id, "block"); /* An argument with a side effect after the first runs in order with the receiver and the arguments before it: each is held in a local, and the whole chain is one parenthesized sequence, since C leaves the order in which a call's operands are evaluated open (a read placed beside the sequence that assigns it could run first). */ int late = 0; for (int j = 1; j < an; j++) late |= subtree_has_side_effect(c, av[j]); int seq[66], ns = 0; if (late) { for (int j = -1; j < an; j++) { char tn[48]; snprintf(tn, sizeof tn, "__mrg%d_%d", id, j + 1); int w = nt_new_node(nt, "LocalVariableWriteNode"), r = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, w, "name", tn); nt_node_set_int(nt, w, "depth", 0); nt_node_set_ref(nt, w, "value", j < 0 ? recv : av[j]); nt_node_set_str(nt, r, "name", tn); nt_node_set_int(nt, r, "depth", 0); seq[ns++] = w; if (j < 0) recv = r; else av[j] = r; if (typed) scope_local_intern(comp_scope_of(c, id), tn); } } int cur = recv; for (int j = 0; j < an - 1; j++) { int call = nt_new_node(nt, "CallNode"); int one = nt_new_node(nt, "ArgumentsNode"); if (call < 0 || one < 0) return 0; nt_node_set_arr(nt, one, "arguments", &av[j], 1); nt_node_set_str(nt, call, "name", mname); nt_node_set_ref(nt, call, "receiver", cur); nt_node_set_ref(nt, call, "arguments", one); /* a conflict block applies at every merge step; a `break` in it leaves the whole call, so an inner step is no break target of its own (call_breaks) and its break reaches the outer call's */ if (mblk >= 0) { nt_node_set_ref(nt, call, "block", mblk); nt_node_set_str(nt, call, "merge_fold_step", "1"); } cur = call; } int last = nt_new_node(nt, "ArgumentsNode"); if (last < 0) return 0; if (late) { int fin = nt_new_node(nt, "CallNode"), st = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, last, "arguments", &av[an - 1], 1); nt_node_set_str(nt, fin, "name", mname); nt_node_set_ref(nt, fin, "receiver", cur); nt_node_set_ref(nt, fin, "arguments", last); if (mblk >= 0) nt_node_set_ref(nt, fin, "block", mblk); seq[ns++] = fin; nt_node_set_arr(nt, st, "body", seq, ns); /* the call node becomes the sequence: its own operands move into the chain above */ if (typed) { int line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); } else { nt_node_set_type(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); } nt_node_set_ref(nt, id, "body", st); } else { nt_node_set_arr(nt, last, "arguments", &av[an - 1], 1); nt_node_set_ref(nt, id, "receiver", cur); nt_node_set_ref(nt, id, "arguments", last); } comp_grow_node_arrays(c); if (typed) { int encl = c->nscope[id]; for (int j = base; j < nt->count; j++) c->nscope[j] = encl; } return 1; } /* Whether method mi, of `nm`, takes `an` arguments in one call (merge_def_takes_argc). */ static int scope_takes_argc(Compiler *c, int mi, const char *nm, int an) { return merge_def_takes_argc(c->nt, nm, c->scopes[mi].def_node, an); } /* Whether class k answers `nm` with a method of its own (a Hash subclass's or Hash's reopened one included) that takes `an` arguments. */ static int class_takes_merge_argc(Compiler *c, int k, const char *nm, int an) { int mi = comp_method_in_chain(c, k, nm, NULL); return mi >= 0 && scope_takes_argc(c, mi, nm, an); } /* The fold of hash_merge_fold, for a call desugar_enum_chain_shapes left (marked merge_fold_wait) because the program may own the name. Once the receiver is typed, a Hash or an object of a class with no method of the name that takes every argument in one call folds as it would have before. Any other receiver (a class, a String, ...) keeps the call. */ int desugar_hash_merge_fold(Compiler *c) { /* the fold cannot be taken back: a block parameter typed a Hash in an optimistic round may hold the program's own object after */ if (g_infer_optimistic) return 0; NodeTable *nt = (NodeTable *)c->nt; int changed = 0; NT_FOREACH_KIND(nt, NK_CallNode, id) { if (!nt_int(nt, id, "merge_fold_wait", 0)) continue; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"), argsn = nt_ref(nt, id, "arguments"), an = 0; if (!nm || recv < 0 || argsn < 0) continue; nt_arr(nt, argsn, "arguments", &an); if (an < 2) continue; TyKind t = infer_type(c, recv); int own = 0; if (ty_is_object(t)) own = class_takes_merge_argc(c, ty_object_class(t), nm, an); else if (ty_is_hash(t)) { int mi = comp_builtin_kind_reopen_mi(c, t, nm); own = mi >= 0 && scope_takes_argc(c, mi, nm, an); } /* a value of either kind: the dispatch's merge! / update arms take every argument, to a Hash and to an object alike (its merge arm reads the first one only, so a merge still folds) */ else if (t == TY_POLY) { for (int k = 0; k < c->nclasses && !own && is_hash_merge_bang(nm); k++) own = class_takes_merge_argc(c, k, nm, an); } /* (a receiver nothing typed, such as a `{}` only these calls fill) */ else own = t != TY_UNKNOWN; if (!own) changed |= hash_merge_fold(c, id, 1); } return changed; } /* The nodes from `base` on, made for call `id`: once the scopes are known (`late`) they take the call's scope and line. */ static void hash_brackets_adopt(Compiler *c, int id, int base, int late) { NodeTable *nt = (NodeTable *)c->nt; comp_grow_node_arrays(c); if (!late) return; long long ln = nt_int(nt, id, "node_line", 0), fl = nt_int(nt, id, "node_file", 0); for (int j = base; j < nt->count; j++) { c->nscope[j] = c->nscope[id]; c->node_cbody[j] = c->node_cbody[id]; if (ln) nt_node_set_int(nt, j, "node_line", ln); if (fl) nt_node_set_int(nt, j, "node_file", fl); } } /* Whether the program gives Hash its own class method `[]`: a `def self.[]` or a `class << self` binding it in `class Hash`, or a `def Hash.[]`. Hash[...] is then that method's call. */ static int hash_cmethod_brackets_under(const NodeTable *nt, int root, int in_hash) { if (root < 0 || root >= nt->count) return 0; NodeKind k = nt_kind(nt, root); if (k == NK_DefNode) { const char *dn = nt_str(nt, root, "name"); int r = nt_ref(nt, root, "receiver"); return dn && sp_streq(dn, "[]") && r >= 0 && (in_hash ? nt_kind(nt, r) == NK_SelfNode : is_hash_const(nt, r)); } if (k == NK_SingletonClassNode && in_hash) return binds_name_under(nt, nt_ref(nt, root, "body"), "[]"); if (k == NK_ClassNode) { int cp = nt_ref(nt, root, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; in_hash = cn && sp_streq(cn, "Hash"); } else if (k == NK_ModuleNode) in_hash = 0; for (int j = 0; j < nt_num_refs(nt, root); j++) if (hash_cmethod_brackets_under(nt, nt_ref_at(nt, root, j), in_hash)) return 1; for (int j = 0; j < nt_num_arrs(nt, root); j++) { int an = 0; const int *ids = nt_arr_at(nt, root, j, &an); for (int i = 0; i < an; i++) if (hash_cmethod_brackets_under(nt, ids[i], in_hash)) return 1; } return 0; } static int hash_cmethod_brackets(const NodeTable *nt) { NT_FOREACH_KIND(nt, NK_DefNode, d) if (hash_cmethod_brackets_under(nt, d, 0)) return 1; NT_FOREACH_KIND(nt, NK_ClassNode, k) if (hash_cmethod_brackets_under(nt, k, 0)) return 1; return 0; } /* `Hash[...]` (call `id`, no block) as the to_h of its pairs, or for a lone splat the runtime's reading of the list. `late`: asked after the scopes are known (a Hash subclass's `H[...]`), so new nodes take the call's scope. Answers 1 when it rewrote the call. */ static int hash_brackets_desugar(Compiler *c, int id, int late) { NodeTable *nt = (NodeTable *)c->nt; int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av0 = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; if (an < 1) return 0; if (an == 1 && nt_kind(nt, av0[0]) == NK_ArrayNode) { /* Hash[[[k, v], ...]] == pairs.to_h */ nt_node_set_str(nt, id, "name", "to_h"); nt_node_set_ref(nt, id, "receiver", av0[0]); nt_node_set_ref(nt, id, "arguments", -1); /* Hash[] is laxer about pair shape than Array#to_h: a one-element sub-array gives a nil value rather than raising */ nt_node_set_int(nt, id, "hash_brackets", 1); return 1; } /* Hash[*args]: the splatted list is the argument list, so whether it is one Hash or pair list or alternating keys and values is known only at run time -- reading it as the pairs (the rewrite below) took the list itself for them */ if (an == 1 && nt_kind(nt, av0[0]) == NK_SplatNode && nt_ref(nt, av0[0], "expression") >= 0) { nt_node_set_str(nt, id, "name", "__hash_brackets_splat"); nt_node_set_ref(nt, id, "receiver", nt_ref(nt, av0[0], "expression")); nt_node_set_ref(nt, id, "arguments", -1); return 1; } /* Hash[arg] with any single non-literal argument: the same pairs.to_h rewrite (this desugar runs pre-type, so no array check is possible; to_h itself raises on a non-convertible receiver, as Hash[] does). */ if (an == 1) { nt_node_set_str(nt, id, "name", "to_h"); nt_node_set_ref(nt, id, "receiver", av0[0]); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_int(nt, id, "hash_brackets", 1); return 1; } /* a splat among them (`Hash[*a, *b]`, `Hash[k, *rest]`) makes the count known only at run time: the list below */ int splat = 0; for (int j = 0; j < an; j++) splat |= nt_kind(nt, av0[j]) == NK_SplatNode; if (an >= 2 && an % 2 == 0 && an <= 64 && !splat) { /* Hash[k1, v1, k2, v2, ...] == [[k1, v1], [k2, v2], ...].to_h */ int base = nt->count; int av[64]; memcpy(av, av0, (size_t)an * sizeof(int)); int pairs[32]; for (int j = 0; j < an / 2; j++) { int pair = nt_new_node(nt, "ArrayNode"); if (pair < 0) return 0; nt_node_set_arr(nt, pair, "elements", &av[j * 2], 2); pairs[j] = pair; } int outer = nt_new_node(nt, "ArrayNode"); if (outer < 0) return 0; nt_node_set_arr(nt, outer, "elements", pairs, an / 2); nt_node_set_str(nt, id, "name", "to_h"); nt_node_set_ref(nt, id, "receiver", outer); nt_node_set_ref(nt, id, "arguments", -1); hash_brackets_adopt(c, id, base, late); return 1; } if (an >= 2) { /* an odd count, a splat, or past 64: the runtime reads the list as the arguments and raises Hash[]'s ArgumentError for an odd one */ int base = nt->count; int list = nt_new_node(nt, "ArrayNode"); if (list < 0) return 0; nt_node_set_arr(nt, list, "elements", av0, an); nt_node_set_str(nt, id, "name", "__hash_brackets_splat"); nt_node_set_ref(nt, id, "receiver", list); nt_node_set_ref(nt, id, "arguments", -1); hash_brackets_adopt(c, id, base, late); return 1; } return 0; } static int enum_chain_kept(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; if (nt_ref(nt, id, "block") >= 0) return 0; /* (the parent map, made at the first such call of the run) */ const int *par = lower_parents(c, lc); int p = par[id]; while (p >= 0 && nt_kind(nt, p) == NK_ParenthesesNode) p = par[p]; if (p >= 0 && nt_kind(nt, p) == NK_StatementsNode) { /* a parenthesized sequence ending in id */ int q = par[p]; int bn = 0; const int *bb = nt_arr(nt, p, "body", &bn); if (q >= 0 && nt_kind(nt, q) == NK_ParenthesesNode && bn > 0 && bb[bn - 1] == id) { for (p = par[q]; p >= 0 && nt_kind(nt, p) == NK_ParenthesesNode; p = par[p]) {} } } return !(p >= 0 && nt_kind(nt, p) == NK_CallNode && nt_paren_single(nt, nt_ref(nt, p, "receiver")) == id); } /* The shapes of an iterator chain at the call id, lowered to the forms the Enumerable and Enumerator routes read: each_char.with_index, the each.with_object family and the rest. A run sees shapes an earlier one made (map(&:sym.to_proc): to_proc becomes a lambda first, then the lambda block attaches), so the row runs twice. */ static void enum_chain_node(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm) return; int recv = nt_ref(nt, id, "receiver"); /* `.lazy.each_cons(n)` / `.each_slice(n)`: each_cons/each_slice are not element-wise, so the lazy pipeline cannot fuse them and the chain falls to a NoMethodError. For a finite source (a bounded range literal or an array) the lazy is a no-op -- drop it so the eager each_cons/each_slice Enumerator, and a following .first(m), materialize (#3171). */ if ((is_each_window(nm)) && recv >= 0 && nt_kind(nt, recv) == NK_CallNode && nt_str(nt, recv, "name") && sp_streq(nt_str(nt, recv, "name"), "lazy") && nt_ref(nt, recv, "block") < 0 && nt_ref(nt, recv, "arguments") < 0) { int lz_recv = nt_ref(nt, recv, "receiver"); /* `p (2..10).lazy...` parses the source as a ParenthesesNode; look through it (and nested parens) to the real range/array. */ lz_recv = nt_paren_single(nt, lz_recv); NodeKind lk = nt_kind(nt, lz_recv); int finite = 0; if (lk == NK_ArrayNode) finite = 1; else if (lk == NK_RangeNode) { int rgt = nt_ref(nt, lz_recv, "right"); /* endless (nil right) stays lazy */ finite = rgt >= 0 && nt_kind(nt, rgt) == NK_IntegerNode; } if (finite) { nt_node_set_ref(nt, id, "receiver", lz_recv); return; } } /* `{}.merge!(h)` / `{"a" => 1}.update(h)`: nothing else holds the literal, so a merge into a new Hash answers the same, and merge folds a hash of another kind into one that holds both; merge! keeps the literal's own variant, which has no arm for it */ if (is_hash_merge_bang(nm) && fresh_hash_merge_recv(nt, recv) && !hash_merges_own(nt, nm, lc->prog_memo)) { nt_node_set_str(nt, id, "name", "merge"); nm = "merge"; } /* (update without a block keeps its own several-Hash arm) */ if ((sp_streq(nm, "merge") || sp_streq(nm, "merge!") || (sp_streq(nm, "update") && nt_ref(nt, id, "block") >= 0)) && recv >= 0) { int argsn = nt_ref(nt, id, "arguments"), an = 0; if (argsn >= 0) nt_arr(nt, argsn, "arguments", &an); /* a receiver that may be the program's own object waits for its type (desugar_hash_merge_fold) */ if (an >= 2 && hash_merge_name_owned(nt, nm, an)) nt_node_set_int(nt, id, "merge_fold_wait", 1); else hash_merge_fold(c, id, 0); return; } /* `m(&lambda_literal)`: attach the lambda's block directly as m's block (the :sym.to_proc desugar below produces exactly this shape when the proc is passed inline as a block argument). */ { int blkref = nt_call_block_arg(nt, id); if (blkref >= 0) { int bex = nt_ref(nt, blkref, "expression"); if (bex >= 0 && nt_kind(nt, bex) == NK_CallNode) { const char *lname = nt_str(nt, bex, "name"); int lblk = nt_ref(nt, bex, "block"); if (lname && (is_proc_constructor(lname)) && nt_ref(nt, bex, "receiver") < 0 && lblk >= 0 && nt_kind(nt, lblk) == NK_BlockNode) nt_node_set_ref(nt, id, "block", lblk); } } } /* each_entry (block form) yields each element like #each but must return the RECEIVER (Enumerable#each_entry), unlike a user #each whose own return value leaks through. Leave the name intact so it rides the same receiver-returning redirect as reverse_each (#2621); blockless #each_entry stays for the Enumerable / struct enumerator path to materialize an Enumerator. */ /* max(n) { cmp } / min(n) { cmp }: the n extremes by the comparator -- sort { cmp } then take from the appropriate end (max descends). A block argument read off a local, a constant or a Symbol literal (`&pr`, `&:<=>`) goes to the sort the same way, which serves it as its own. */ if ((is_minmax_query(nm)) && recv >= 0 && nt_ref(nt, id, "block") >= 0) { int margs = nt_ref(nt, id, "arguments"); int man = 0; if (margs >= 0) nt_arr(nt, margs, "arguments", &man); int mblk = nt_ref(nt, id, "block"); const char *mbty = mblk >= 0 ? nt_type(nt, mblk) : NULL; NodeKind mbk = nt_kind(nt, mblk); NodeKind mek = mbk == NK_BlockArgumentNode ? nt_kind(nt, nt_ref(nt, mblk, "expression")) : NK_NONE; if (man == 1 && mbty && (mbk == NK_BlockNode || mek == NK_LocalVariableReadNode || mek == NK_ConstantReadNode || mek == NK_SymbolNode)) { int sortc = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, sortc, "name", "sort"); nt_node_set_ref(nt, sortc, "receiver", recv); nt_node_set_ref(nt, sortc, "block", mblk); if (sp_streq(nm, "min")) { nt_node_set_str(nt, id, "name", "first"); nt_node_set_ref(nt, id, "receiver", sortc); nt_node_set_ref(nt, id, "block", -1); } else { int lastc = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, lastc, "name", "last"); nt_node_set_ref(nt, lastc, "receiver", sortc); nt_node_set_ref(nt, lastc, "arguments", margs); nt_node_set_str(nt, id, "name", "reverse"); nt_node_set_ref(nt, id, "receiver", lastc); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); } comp_grow_node_arrays(c); return; } } /* :sym.to_proc (explicit): rewrite to the equivalent lambda -- one parameter calling the method, or two for the binary operators (:+.to_proc adds its two arguments). The &:sym shorthand stays on its own (textual) path. */ if (sp_streq(nm, "to_proc") && recv >= 0 && nt_kind(nt, recv) == NK_SymbolNode && nt_ref(nt, id, "block") < 0) { const char *sym = nt_str(nt, recv, "value"); if (sym && *sym) { static const char *const binops[] = { "+", "-", "*", "/", "%", "**", "==", "!=", "<", "<=", ">", ">=", "<=>", "<<", ">>", "&", "|", "^", NULL }; int is_binop = 0; for (int j = 0; binops[j]; j++) if (sp_streq(sym, binops[j])) { is_binop = 1; break; } char pa[64], pb[64]; snprintf(pa, sizeof pa, "__stp_a_%s", comp_node_tag(c, id)); snprintf(pb, sizeof pb, "__stp_b_%s", comp_node_tag(c, id)); int kpa = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, kpa, "name", pa); int preq[2] = { kpa, -1 }; int npar = 1; if (is_binop) { int kpb = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, kpb, "name", pb); preq[1] = kpb; npar = 2; } int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", preq, npar); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int ra = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ra, "name", pa); int call = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, call, "name", sym); nt_node_set_ref(nt, call, "receiver", ra); enum_send_copy_take(nt, call); /* `:tally.to_proc`'s call: the builtin's copy */ if (is_binop) { int rb2 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rb2, "name", pb); int cargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, cargs, "arguments", &rb2, 1); nt_node_set_ref(nt, call, "arguments", cargs); } int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &call, 1); int blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk, "parameters", bparams); nt_node_set_ref(nt, blk, "body", blkbody); nt_node_set_str(nt, id, "name", "lambda"); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", blk); /* Symbol#to_proc always reports arity -2 (a required receiver plus optional arguments), whatever the underlying method takes; the synthesized lambda's own parameter count would say 1 or 2 (#3053) */ nt_node_set_int(nt, id, "stp_arity", 1); comp_grow_node_arrays(c); return; } } /* transform_keys(mapping) [no block]: key k maps to mapping[k] when present, else stays -- exactly `{ |k| mapping.fetch(k, k) }`. The mapping expression is re-evaluated per key, which is correct (and only wasteful) for the literal-hash common case. */ if ((sp_streq(nm, "transform_keys") || sp_streq(nm, "transform_keys!")) && recv >= 0) { int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av0 = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; int origblk = nt_ref(nt, id, "block"); if (an == 1) { int mnode = av0[0]; char pname[64]; snprintf(pname, sizeof pname, "__tk_k_%s", comp_node_tag(c, id)); int kp = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, kp, "name", pname); int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", &kp, 1); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int kr1 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, kr1, "name", pname); int kr2 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, kr2, "name", pname); int fargs = nt_new_node(nt, "ArgumentsNode"); if (origblk >= 0) { /* a block alongside the mapping handles the unmapped keys: mapping.fetch(k) { original_block } */ nt_node_set_arr(nt, fargs, "arguments", &kr1, 1); (void)kr2; } else { int fa[2] = { kr1, kr2 }; nt_node_set_arr(nt, fargs, "arguments", fa, 2); } int fetch = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, fetch, "name", "fetch"); nt_node_set_ref(nt, fetch, "receiver", mnode); nt_node_set_ref(nt, fetch, "arguments", fargs); if (origblk >= 0) nt_node_set_ref(nt, fetch, "block", origblk); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &fetch, 1); int blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk, "parameters", bparams); nt_node_set_ref(nt, blk, "body", blkbody); nt_node_set_ref(nt, id, "block", blk); nt_node_set_ref(nt, id, "arguments", -1); comp_grow_node_arrays(c); } /* fall through: the bang form wraps in replace below */ } /* transform_values! / transform_keys! { }: build the transformed hash, then splice it back into the receiver (aliases observe the mutation). */ /* transform_keys!/transform_values! on a direct hash literal has no alias to observe the in-place mutation, so it is just the non-bang transform. This also serves the key-type-changing case (sym -> str), where splicing a different hash variant back via #replace cannot work (#2355). */ if ((sp_streq(nm, "transform_values!") || sp_streq(nm, "transform_keys!")) && recv >= 0 && nt_ref(nt, id, "block") >= 0 && (nt_kind(nt, recv) == NK_HashNode || nt_kind(nt, recv) == NK_KeywordHashNode)) { nt_node_set_str(nt, id, "name", sp_streq(nm, "transform_keys!") ? "transform_keys" : "transform_values"); return; } if ((sp_streq(nm, "transform_values!") || sp_streq(nm, "transform_keys!")) && recv >= 0 && nt_ref(nt, id, "block") >= 0) { int blk = nt_ref(nt, id, "block"); int inner = nt_new_node(nt, "CallNode"); int rargs = nt_new_node(nt, "ArgumentsNode"); if (inner >= 0 && rargs >= 0) { nt_node_set_str(nt, inner, "name", sp_streq(nm, "transform_keys!") ? "transform_keys" : "transform_values"); nt_node_set_ref(nt, inner, "receiver", recv); nt_node_set_ref(nt, inner, "block", blk); nt_node_set_arr(nt, rargs, "arguments", &inner, 1); nt_node_set_str(nt, id, "name", "replace"); /* both calls name the receiver node: the poly replace arm reads this to bind it once (codegen_call_recv.c) */ nt_node_set_str(nt, id, "bang_splice", "1"); /* both are Hash's own, as CRuby's bang form is: a class deriving from Hash answers neither with its own replace or transform_values (rack's Headers#transform_values is `dup.transform_values!`) */ nt_node_set_int(nt, id, "builtin_only", 1); nt_node_set_int(nt, inner, "builtin_only", 1); nt_node_set_ref(nt, id, "block", -1); nt_node_set_ref(nt, id, "arguments", rargs); comp_grow_node_arrays(c); } return; } if (sp_streq(nm, "[]") && recv >= 0 && nt_kind(nt, recv) == NK_ConstantReadNode && nt_ref(nt, id, "block") < 0) { const char *cn = nt_str(nt, recv, "name"); if (cn && sp_streq(cn, "Hash")) { if (lc->prog_memo[3] < 0) lc->prog_memo[3] = hash_cmethod_brackets(nt); if (!lc->prog_memo[3] && hash_brackets_desugar(c, id, 0)) return; } } if (recv < 0 || nt_kind(nt, recv) != NK_CallNode) return; const char *rn = nt_str(nt, recv, "name"); if (!rn || nt_ref(nt, recv, "block") >= 0) return; /* str.each_char/each_line/each_byte(blockless, no args).with_index -> chars/lines/bytes.each.with_index, so the pair types as an array element plus an int index. Only each_char was rewritten, so the other two kept an opaque enumerator element and a BOXED index: passed to anything wanting an Integer -- a Struct field another site builds with a literal -- the boxed value met an sp_int parameter and the build failed (#3939). */ if (sp_streq(nm, "with_index") && nt_ref(nt, recv, "arguments") < 0 && (sp_streq(rn, "each_char") || sp_streq(rn, "each_line") || sp_streq(rn, "each_byte") || sp_streq(rn, "each_codepoint")) && !(enum_chain_kept(c, lc, id)) && !walk_rewrite_shadowed(c, rn)) { /* a block that breaks: the walk's helper answers the String, or the break's value through the call's own break wrapper -- the (walk; s) form below lets its tail take the break's place */ const LowerRow *swr = lower_row_nt(nt, id, LR_STR_WALK); int wblk = nt_ref(nt, id, "block"); int wsrc = nt_ref(nt, recv, "receiver"); int wa = nt_ref(nt, id, "arguments"), wan = 0; const int *wav = wa >= 0 ? nt_arr(nt, wa, "arguments", &wan) : NULL; NodeKind wsk = wsrc >= 0 ? nt_kind(nt, wsrc) : NK_NilNode; int wvar = wsk == NK_LocalVariableReadNode || wsk == NK_InstanceVariableReadNode; /* and a String that is no variable: the (walk; s) form below cannot read it again, and the walk alone answered the pieces (the shape is LR_STR_WALK's row) */ if (swr && (block_body_breaks(nt, nt_ref(nt, wblk, "body")) || !wvar) && ecw_helper_spliced(nt, swr->target)) { int nargs = nt_new_node(nt, "ArgumentsNode"); if (nargs < 0) return; int na[2] = { wsrc, wan ? wav[0] : -1 }; nt_node_set_arr(nt, nargs, "arguments", na, wan + 1); nt_node_set_str(nt, id, "name", swr->target); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", nargs); comp_grow_node_arrays(c); return; } nt_node_set_str(nt, recv, "name", sp_streq(rn, "each_char") ? "chars" : sp_streq(rn, "each_line") ? "lines" : sp_streq(rn, "each_byte") ? "bytes" : "codepoints"); int eachn = nt_new_node(nt, "CallNode"); if (eachn < 0) return; nt_node_set_str(nt, eachn, "name", "each"); nt_node_set_ref(nt, eachn, "receiver", recv); nt_node_set_ref(nt, id, "receiver", eachn); /* with a block the call answers the String, not the pieces the walk now goes over: (s.chars.each.with_index { }; s), for a String a variable names, read again rather than evaluated twice */ int srecv = nt_ref(nt, recv, "receiver"); NodeKind sk = nt_kind(nt, srecv); if (nt_ref(nt, id, "block") >= 0 && (sk == NK_LocalVariableReadNode || sk == NK_InstanceVariableReadNode)) { int inner = nt_new_node(nt, "CallNode"); int st = nt_new_node(nt, "StatementsNode"); int tail = nt_clone_subtree(nt, srecv); if (inner >= 0 && st >= 0 && tail >= 0) { nt_node_set_str(nt, inner, "name", "with_index"); nt_node_set_ref(nt, inner, "receiver", eachn); nt_node_set_ref(nt, inner, "arguments", nt_ref(nt, id, "arguments")); nt_node_set_ref(nt, inner, "block", nt_ref(nt, id, "block")); int items[2] = { inner, tail }; nt_node_set_arr(nt, st, "body", items, 2); nt_node_set_type(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "body", st); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); } } comp_grow_node_arrays(c); } /* str.each_char/each_line (blockless, no args) followed by an eager block-consuming Enumerable method -> chars/lines, so the block param types as String (a StrArray element) instead of an opaque enumerator element (#2901). Not for the lazy/enumerator-shaped methods, which keep their own machinery. */ else if (nt_ref(nt, id, "block") >= 0 && nt_ref(nt, recv, "arguments") < 0 && (sp_streq(rn, "each_char") || sp_streq(rn, "each_line")) && !walk_rewrite_shadowed(c, rn) && (sp_streq(nm, "map") || sp_streq(nm, "collect") || sp_streq(nm, "select") || sp_streq(nm, "filter") || sp_streq(nm, "reject") || sp_streq(nm, "flat_map") || sp_streq(nm, "collect_concat") || sp_streq(nm, "filter_map") || sp_streq(nm, "min_by") || sp_streq(nm, "max_by") || sp_streq(nm, "sort_by") || sp_streq(nm, "find") || sp_streq(nm, "detect") || sp_streq(nm, "count") || sp_streq(nm, "sum") || sp_streq(nm, "each_with_index") || sp_streq(nm, "partition") || sp_streq(nm, "group_by"))) { nt_node_set_str(nt, recv, "name", sp_streq(rn, "each_char") ? "chars" : "lines"); } else if ((is_lazy_force(nm)) && sp_streq(rn, "take") && nt_ref(nt, id, "block") < 0) { /* X.take(n).to_a == X.first(n) -- and first(n) is what the lazy pipeline terminates on, so a lazy .take(n).to_a materializes. */ int inner = nt_ref(nt, recv, "receiver"); int targs = nt_ref(nt, recv, "arguments"); int tan = 0; if (targs >= 0) nt_arr(nt, targs, "arguments", &tan); if (inner >= 0 && tan == 1) { nt_node_set_str(nt, recv, "name", "first"); nt_node_set_ref(nt, id, "receiver", inner); nt_node_set_str(nt, id, "name", "first"); nt_node_set_ref(nt, id, "arguments", targs); /* the outer node IS now the first(n) call; drop the duplicated inner */ nt_node_set_ref(nt, id, "receiver", inner); } } else if ((is_with_object_alias(nm)) && sp_streq(rn, "each")) { int inner = nt_ref(nt, recv, "receiver"); if (inner < 0) return; nt_node_set_str(nt, id, "name", "each_with_object"); nt_node_set_ref(nt, id, "receiver", inner); } } int desugar_enum_chain_shapes_at(Compiler *c, LowerCtx *lc, int id) { unsigned long edits = c->nt->edits; enum_chain_node(c, lc, id); return c->nt->edits != edits; } /* How many positional arguments the program passes a `method(:sym)` value through Method#call / #() / #[] / #===: the count when every such site agrees, BAM_VARIADIC when they differ or one passes a splat, and -1 when there is none, or a site passes a keyword hash or a block. The receiver is resolved the way the Method machinery resolves it (method_recv_node), so `m = s.method(:x); m.call(1, 2)` counts with `s.method(:x).call(1, 2)`. A synthesized wrapper takes that many parameters and forwards them: with the one __bam_r parameter it had, `[3, 1, 2].method(:rotate).call(2)` answered rotate's default (the argument fell off the C cast on a native target, and is a signature-mismatch trap on wasm32). A variadic one takes a rest and splats it into the builtin: with a fixed count, `pm = method(:puts); pm.call(*[1, 2])` printed only 1 and `m.call(*[6])` on `"ab".method(:center)` passed center nothing. *passed is set when the program also passes the Method as a block (`a.map(&4.method(:fdiv))`, or `&m` on a local holding it): the block's call is no site here. */ enum { BAM_VARIADIC = -2 }; static int splat_dispatch_on_length(Compiler *c, int id, const int *argv, int argc, int sp_at, int ex); /* The sites bam_call_argc counts, resolved once per desugar_builtin_method_obj run: which Method nodes the receiver of each Method#call / #[] / #=== and each block argument can hold (method_recv_nodes) does not depend on the Method being sized, and asking it again per Method walked the program once for each of them. A runtime name's send on a boxed receiver makes a Method per name the program defines (desugar_dynamic_method), and every wrapper made for one changed the tree, so each of those walks rebuilt the indexes over it as well: one `public_send(*args)` in a Rails program took a fixpoint round hours. Keyed by node: `site` chains a call site under each Method node its receiver can hold and under the receiver itself (a runtime-name Method's arms are matched by dyn_of), `passed` marks a Method node a block argument can hold. Nodes made later in the run (the wrappers) are no sites. */ static struct { int n, cap, nodes; int *head, *next_e, *site, *passed; } bam_tab = { 0, 0, -1 }; static void bam_tab_add(int key, int site) { if (key < 0 || key >= bam_tab.nodes) return; if (bam_tab.n == bam_tab.cap) { bam_tab.cap = bam_tab.cap ? bam_tab.cap * 2 : 1024; bam_tab.next_e = realloc(bam_tab.next_e, sizeof(int) * (size_t)bam_tab.cap); bam_tab.site = realloc(bam_tab.site, sizeof(int) * (size_t)bam_tab.cap); if (!bam_tab.next_e || !bam_tab.site) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } bam_tab.site[bam_tab.n] = site; bam_tab.next_e[bam_tab.n] = bam_tab.head[key]; bam_tab.head[key] = bam_tab.n++; } static void bam_tab_free(void) { free(bam_tab.head); free(bam_tab.next_e); free(bam_tab.site); free(bam_tab.passed); memset(&bam_tab, 0, sizeof bam_tab); bam_tab.nodes = -1; } static void bam_tab_build(Compiler *c) { const NodeTable *nt = c->nt; int nn = nt->count; bam_tab.nodes = nn; bam_tab.head = malloc(sizeof(int) * (size_t)(nn > 0 ? nn : 1)); bam_tab.passed = calloc((size_t)(nn > 0 ? nn : 1), 1); if (!bam_tab.head || !bam_tab.passed) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < nn; i++) bam_tab.head[i] = -1; NT_FOREACH_KIND(nt, NK_BlockArgumentNode, ba) { int ex = nt_ref(nt, ba, "expression"); if (ex < 0) continue; int *mns, nmn = method_recv_nodes(c, ex, &mns); if (ex < nn) ((char *)bam_tab.passed)[ex] = 1; for (int j = 0; j < nmn; j++) if (mns[j] >= 0 && mns[j] < nn) ((char *)bam_tab.passed)[mns[j]] = 1; free(mns); } NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || !(sp_streq(nm, "call") || sp_streq(nm, "[]") || sp_streq(nm, "==="))) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; int *mns, nmn = method_recv_nodes(c, recv, &mns); bam_tab_add(recv, id); for (int j = 0; j < nmn; j++) if (mns[j] != recv) bam_tab_add(mns[j], id); free(mns); } } static int bam_call_argc(Compiler *c, int mnode, int *passed) { const NodeTable *nt = c->nt; int n = -1, vary = 0; if (bam_tab.nodes < 0) bam_tab_build(c); *passed = mnode >= 0 && mnode < bam_tab.nodes && ((char *)bam_tab.passed)[mnode]; /* the sites whose receiver can hold this Method, and those whose receiver is the runtime-name call it is an arm of (a site under both is counted twice, which agrees with itself) */ int keys[2] = { mnode, nt_int(nt, mnode, "dyn_of", -1) }; for (int q = 0; q < 2; q++) for (int e = keys[q] >= 0 && keys[q] < bam_tab.nodes ? bam_tab.head[keys[q]] : -1; e >= 0; e = bam_tab.next_e[e]) { int id = bam_tab.site[e]; if (nt_ref(nt, id, "block") >= 0) return -1; int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; for (int k = 0; k < an; k++) { const char *aty = av[k] >= 0 ? nt_type(nt, av[k]) : NULL; NodeKind ak = nt_kind(nt, av[k]); if (!aty || ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode || nt_kind(nt, av[k]) == NK_ForwardingArgumentsNode) return -1; if (ak == NK_SplatNode) vary = 1; } if (n >= 0 && n != an) vary = 1; n = an; } return vary ? BAM_VARIADIC : n; } /* The parameter list and forwarded arguments of a bound builtin's wrapper: `recv_param` (the receiver, or NULL for a Kernel function), then `nfwd` required parameters forwarded as they are, or for BAM_VARIADIC one rest splatted into the call. */ static void bam_wrapper_params(NodeTable *nt, const char *recv_param, int nfwd, int *params_out, int *args_out) { int preqs[9], areads[8], nreq = 0, nargs = 0; if (recv_param) { preqs[nreq] = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, preqs[nreq++], "name", recv_param); } int params = nt_new_node(nt, "ParametersNode"); const char *rest = recv_param ? "__bam_a" : "__bam_r"; if (nfwd == BAM_VARIADIC) { int rp = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, rp, "name", rest); nt_node_set_ref(nt, params, "rest", rp); int rd = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rd, "name", rest); areads[nargs] = nt_new_node(nt, "SplatNode"); nt_node_set_ref(nt, areads[nargs++], "expression", rd); } for (int k = 0; k < nfwd; k++) { char an[16]; if (!recv_param && k == 0) snprintf(an, sizeof an, "__bam_r"); else if (k == (recv_param ? 0 : 1)) snprintf(an, sizeof an, "__bam_a"); else snprintf(an, sizeof an, "__bam_a%d", recv_param ? k : k - 1); preqs[nreq] = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, preqs[nreq++], "name", an); areads[nargs] = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, areads[nargs++], "name", an); } nt_node_set_arr(nt, params, "requireds", preqs, nreq); *params_out = params; *args_out = -1; if (nargs > 0) { *args_out = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, *args_out, "arguments", areads, nargs); } } /* `recv.method(:sym)` over a BUILTIN receiver (string/int/array/...) has no compiled function to bind, so calling the Method crashed. Synthesize a top-level wrapper `def __bam_(__bam_r) = __bam_r.sym` -- the wrapper's param is pinned to the receiver's type, the Method binds the wrapper (self is passed as the first argument by the bound-call ABI), and the existing resolved-target machinery types the .call. The symbol argument is rewritten to the wrapper's name. */ static int desugar_builtin_method_obj(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "method")) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) { /* receiverless method(:Integer) etc.: a Kernel builtin has no C target for the bound-method fn slot, so synthesize a forwarding top-level def and retarget the symbol at it (`def __bam_N(__bam_r) = Integer(__bam_r)`). The wrapper's param types from the Method's call sites like any def. */ static const char *const KFN0[] = { "Integer", "Float", "String", "Array", "Rational", "Complex", "puts", "print", "p", "pp", NULL }; const char *ksym = method_sym_arg(c, id); if (!ksym || !ksym[0] || ksym[0] == '_') continue; int kknown = 0; for (int k = 0; KFN0[k]; k++) if (sp_streq(ksym, KFN0[k])) { kknown = 1; break; } /* in a class method, self is the class: `method(:const_get)` binds Module#const_get to it, which regexp_parser hands on as a block (`constants.grep(re).flat_map(&method(:const_get))`). The wrapper names the class: `def __bam_N(__bam_r) = K.const_get(__bam_r)`. */ int kcls = -1; if (!kknown && (sp_streq(ksym, "const_get") || sp_streq(ksym, "const_defined?"))) { int ks = c->nscope[id]; /* only where self can be no other class: a subclass calling the inherited class method is its own self */ int kd = 0; if (ks >= 0 && ks < c->nscopes && c->scopes[ks].is_cmethod && c->scopes[ks].class_id >= 0 && comp_cmethod_in_chain(c, c->scopes[ks].class_id, ksym, NULL) < 0 && (comp_descendants(c, c->scopes[ks].class_id, &kd), kd == 0)) kcls = c->scopes[ks].class_id; if (kcls < 0) continue; } else if (!kknown) continue; if (comp_method_index(c, ksym) >= 0) continue; /* a user def wins */ char kwname[48]; snprintf(kwname, sizeof kwname, "__bam_%s", comp_node_tag(c, id)); if (comp_method_index(c, kwname) >= 0) continue; /* the wrapper takes what the call sites pass (bam_call_argc above), `def __bam_N(__bam_r, __bam_a, ...) = Integer(__bam_r, __bam_a, ...)`, or `def __bam_N(*__bam_r) = puts(*__bam_r)` when they disagree; one parameter when there are none */ int kpassed; int knf = bam_call_argc(c, id, &kpassed); if (knf > 8) knf = BAM_VARIADIC; if (knf != BAM_VARIADIC && knf < 1) knf = 1; /* puts, print, p and pp take any number of arguments of any kind, so their wrapper does too: `def __bam_N(*__bam_r) = puts(*__bam_r)`, which binds a thunk (emit_method_thunk) the Method is called through whatever a site passes. Sized from the sites alone, a Method held where no site is counted (an ivar) took one parameter typed from every boxed call of the program: a splat anywhere made it an Integer, and a String the program passed found no way in (the call raised NoMethodError for the Method). */ if (sp_streq(ksym, "puts") || sp_streq(ksym, "print") || sp_streq(ksym, "p") || sp_streq(ksym, "pp")) knf = BAM_VARIADIC; int kparams, kargs; bam_wrapper_params(nt, NULL, knf, &kparams, &kargs); int kcall = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, kcall, "name", ksym); nt_node_set_ref(nt, kcall, "arguments", kargs); if (kcls >= 0) { int krecv = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, krecv, "name", c->classes[kcls].name); nt_node_set_ref(nt, kcall, "receiver", krecv); } int kbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, kbody, "body", &kcall, 1); int kdef = nt_new_node(nt, "DefNode"); nt_node_set_str(nt, kdef, "name", kwname); nt_node_set_ref(nt, kdef, "parameters", kparams); nt_node_set_ref(nt, kdef, "body", kbody); nt_node_set_str(nt, kdef, "bam_sym", ksym); /* the Method's name */ Scope *kws = comp_scope_new(c, kwname, kdef); { kws->class_id = -1; comp_touch_scope(kws->idx); } { kws->body = kbody; comp_touch_scope(kws->idx); } int kws_idx = c->nscopes - 1; collect_def_params(c, kdef, kws); comp_grow_node_arrays(c); walk_scope(c, kbody, kws_idx, -1); int kargsn = nt_ref(nt, id, "arguments"); int kan = 0; const int *kav = kargsn >= 0 ? nt_arr(nt, kargsn, "arguments", &kan) : NULL; if (kan == 1 && kav) nt_node_set_str(nt, kav[0], "value", kwname); changed = 1; continue; } const char *sym = method_sym_arg(c, id); if (!sym || !sym[0] || sym[0] == '_') continue; /* already rewritten / internal */ /* a binary operator method (5.method(:+)) gets a 2-param wrapper below, a unary one (5.method(:-@)) the 1-param wrapper of a method that takes no argument; other non-letter syms have no wrapper shape */ static const char *const BAM_BINOPS[] = { "+", "-", "*", "/", "%", "**", "&", "|", "^", "<<", ">>", "<=>", "==", "!=", "<", "<=", ">", ">=", "=~", "[]", NULL }; static const char *const BAM_UNOPS[] = { "+@", "-@", "~", NULL }; int binop = 0, unop = 0; if (!(sym[0] >= 'a' && sym[0] <= 'z')) { for (int k = 0; BAM_BINOPS[k]; k++) if (sp_streq(sym, BAM_BINOPS[k])) { binop = 1; break; } for (int k = 0; !binop && BAM_UNOPS[k]; k++) if (sp_streq(sym, BAM_UNOPS[k])) { unop = 1; break; } if (!binop && !unop) continue; } TyKind rt = infer_type(c, recv); /* A user-object receiver whose sym is an attr/struct accessor with NO real def has no callable target (accessors inline at the call site), so method(:attr) / method(:attr=) would fail. Synthesize the same __bam_ wrapper whose body `__bam_r.attr` (or `__bam_r.attr = __bam_a`) inlines the accessor -- a writer takes the binop-style 2-param shape. A real def is already handled via the resolved-target path (#3110 follow-up). */ if (ty_is_object(rt)) { int ocid = ty_object_class(rt); int obj_accessor = 0; if (ocid >= 0 && comp_method_in_chain(c, ocid, sym, NULL) < 0) { char basew[256]; if (setter_base_name(sym, basew, sizeof basew)) { if (comp_writer_in_chain(c, ocid, basew, NULL)) { obj_accessor = 1; binop = 1; } } else if (comp_reader_in_chain(c, ocid, sym, NULL)) { obj_accessor = 1; } } if (!obj_accessor) continue; /* real def / non-accessor: handled elsewhere */ } else if (rt == TY_UNKNOWN || rt == TY_VOID || rt == TY_NIL || rt == TY_CLASS || rt == TY_METHOD || rt == TY_PROC) continue; /* A boxed receiver is not a C pointer the Method's self slot can carry: the wrapper takes it inside a one-element array instead, `def __bam_(__bam_r, ...) = __bam_r[0].(...)`, and the call goes through the poly dispatch (codegen boxes the self, bam_poly). Under promote an Integer, a Bignum's too, may be boxed though it infers as Integer here: a plain wrapper had no self slot for it, and binding raised NoMethodError naming the wrapper. A Float takes the same shape: the self slot is a pointer, and a double cast to one did not compile. */ int poly_self = rt == TY_POLY || rt == TY_FLOAT || (g_promote_mode && (rt == TY_INT || rt == TY_BIGINT)); /* the typed-array (kind, op) trampoline path owns these */ if (ty_is_array(rt) && sp_streq(sym, "push")) continue; if (comp_method_index(c, sym) >= 0) continue; /* a same-named top-level def wins */ int cmp_only = 0; /* a Comparable method the class does not define itself */ int num_only = 0; /* a Numeric method Integer or Float inherits */ int sym_arity = 0, have_arity = 0; /* the builtin's Method#arity */ /* an undefined name must reach codegen's immediate NameError, not become a wrapper whose body call aborts the build (#2752) */ { const char *bcls = rt == TY_STRING ? "String" : rt == TY_INT ? "Integer" : rt == TY_FLOAT ? "Float" : rt == TY_SYMBOL ? "Symbol" : ty_is_array(rt) ? "Array" : ty_is_hash(rt) ? "Hash" : rt == TY_RANGE ? "Range" : rt == TY_TIME ? "Time" : rt == TY_BOOL ? "Object" : NULL; /* TrueClass/FalseClass define the logical operators (#2835) */ int bool_op = rt == TY_BOOL && is_bit_op(sym); /* Comparable's methods (between?, clamp) and Numeric's (abs2, quo, positive?) are the class's through the module or the superclass, which the class's own table leaves out */ if (bcls && !bool_op && !builtin_method_known(bcls, sym) && !builtin_comparable_owns(bcls, sym) && !builtin_numeric_owns(bcls, sym) && !builtin_object_method_known(sym)) continue; cmp_only = bcls && !builtin_method_known(bcls, sym) && builtin_comparable_owns(bcls, sym); num_only = bcls && !builtin_method_known(bcls, sym) && builtin_numeric_owns(bcls, sym); if (bcls && builtin_method_arity(bcls, sym, &sym_arity)) have_arity = 1; else if (cmp_only) { sym_arity = builtin_comparable_arity(sym); have_arity = 1; } else if (num_only && builtin_numeric_arity(sym, &sym_arity)) have_arity = 1; } char wname[48]; snprintf(wname, sizeof wname, "__bam_%s", comp_node_tag(c, id)); if (comp_method_index(c, wname) >= 0) continue; /* synthesized on a prior pass */ /* def __bam_(__bam_r) = __bam_r.; for a binary operator def __bam_(__bam_r, __bam_a) = __bam_r __bam_a; and for a method whose call sites pass N arguments def __bam_(__bam_r, __bam_a, __bam_a1, ...) = __bam_r.(__bam_a, ...), or when they disagree def __bam_(__bam_r, *__bam_a) = __bam_r.(*__bam_a) */ int passed = 0; int sites = bam_call_argc(c, id, &passed); int nfwd = sites; if (binop) nfwd = 1; /* an Array's or a String's [] also takes a start and a length: its wrapper forwards the two when its call sites pass them */ if (binop && sp_streq(sym, "[]") && !poly_self && (ty_is_array(rt) || rt == TY_STRING) && (sites == 2 || sites == BAM_VARIADIC)) nfwd = sites; /* A Method passed as a block is called by the block's yield, which no site above counts, so a wrapper of the sites' count (none: no argument) dropped what the yield passed and the builtin raised ArgumentError. Such a Method is a lambda of the builtin's arity: it takes that many, or any number for a negative arity. */ if (passed && !binop) { int f = have_arity && sym_arity >= 0 ? sym_arity : BAM_VARIADIC; nfwd = nfwd == -1 || nfwd == f ? f : BAM_VARIADIC; } /* An Array's or a String's [] takes an index or a start and a length (arity -1): passed as a block, it takes the two values a yield like each_with_index's hands it, where the operand wrapper dropped the second. A Hash's takes its one key. */ if (passed && binop && sp_streq(sym, "[]") && !poly_self && (ty_is_array(rt) || rt == TY_STRING)) nfwd = BAM_VARIADIC; if (nfwd > 8) nfwd = BAM_VARIADIC; if (nfwd != BAM_VARIADIC && nfwd < 0) nfwd = 0; int params, cargs; bam_wrapper_params(nt, "__bam_r", nfwd, ¶ms, &cargs); int rread = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rread, "name", "__bam_r"); if (poly_self) { int zero = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, zero, "value", 0); int zargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, zargs, "arguments", &zero, 1); int elem = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, elem, "name", "[]"); nt_node_set_ref(nt, elem, "receiver", rread); nt_node_set_ref(nt, elem, "arguments", zargs); rread = elem; } int call = nt_new_node(nt, "CallNode"); /* An Array's or String's slice is its [] (the one method under two names), which the splat of a run-time length reaches by that count (splat_dispatch_on_length); the table leaves slice out for Hash#slice, whose keys are any number, so `a.method(:slice)` called with one and with two arguments put the splatted array in the index slot. */ int slice_alias = nfwd == BAM_VARIADIC && sp_streq(sym, "slice") && !poly_self && (ty_is_array(rt) || rt == TY_STRING); nt_node_set_str(nt, call, "name", slice_alias ? "[]" : sym); nt_node_set_ref(nt, call, "receiver", rread); if (name_is_plain_setter(sym)) nt_set_attr_write(nt, call, 1); /* `__bam_r.attr = __bam_a` */ if (nt_int(nt, id, "dyn_of", -1) >= 0) nt_node_set_int(nt, call, "dyn_arm", 1); if (cargs >= 0) nt_node_set_ref(nt, call, "arguments", cargs); int body = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, body, "body", &call, 1); int def = nt_new_node(nt, "DefNode"); nt_node_set_str(nt, def, "name", wname); nt_node_set_ref(nt, def, "parameters", params); nt_node_set_ref(nt, def, "body", body); if (poly_self) nt_node_set_int(nt, def, "bam_poly", 1); /* the builtin's own name, for Method#name, #arity and #inspect: the body call is renamed ([] for slice) or rewritten (a splat dispatched on its length) */ nt_node_set_str(nt, def, "bam_sym", sym); /* its Method#arity is Comparable's, which a boxed self reads off the Method at run time (method_scope_arity) */ if (cmp_only) nt_node_set_int(nt, def, "bam_cmp", 1); if (num_only) nt_node_set_int(nt, def, "bam_num", 1); /* passed as a block, it is a lambda of the count it takes: a yield of another count raises (fwd_bam_lambda_count) */ if (passed && nfwd != BAM_VARIADIC) nt_node_set_int(nt, def, "bam_lambda", 1); Scope *ws = comp_scope_new(c, wname, def); { ws->class_id = -1; comp_touch_scope(ws->idx); } { ws->body = body; comp_touch_scope(ws->idx); } int ws_idx = c->nscopes - 1; collect_def_params(c, def, ws); LocalVar *plv = scope_local(ws, "__bam_r"); if (plv) { lv_set_type(plv, poly_self ? TY_POLY_ARRAY : rt); plv->rbs_seeded = 1; } /* pin: no call sites exist */ /* A bound String keeps its Ruby type when sharing chooses its handle. */ if (plv && rt == TY_STRING) plv->rbs_type = TY_STRING; comp_grow_node_arrays(c); walk_scope(c, body, ws_idx, -1); /* the builtin takes the rest by its run-time length, as any `recv.m(*a)` of a range of counts does (the splat expansion ran before this wrapper existed) */ if (nfwd == BAM_VARIADIC) { int wn = 0; const int *wav = nt_arr(nt, cargs, "arguments", &wn); int wsp = wn == 1 ? wav[0] : -1; if (wsp >= 0) splat_dispatch_on_length(c, call, &wsp, 1, 0, nt_ref(nt, wsp, "expression")); } /* retarget the Method at the wrapper */ int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; if (an == 1 && av) nt_node_set_str(nt, av[0], "value", wname); changed = 1; } bam_tab_free(); return changed; } /* An under-supplied call (`m(1)` against `def m(a, b)`) raises ArgumentError at runtime (args_raise), so the missing params never carry a real value -- but with NO other call site they also never receive a type, the body can't infer, and the method's return stays TY_UNKNOWN, which rejects any VALUE use of the call (`puts m(1)`). Give such never-supplied required params a concrete placeholder type: every call missing them raises before the body runs, so the choice is unobservable; a later legitimate call site simply unifies over it. */ static int pad_unsupplied_params(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; if (nt_ref(nt, id, "block") >= 0) continue; const char *name = nt_str(nt, id, "name"); if (!name) continue; int mi = call_target(c, id, -1, name, CT_TOP, NULL); if (mi < 0) continue; Scope *m = &c->scopes[mi]; if (m->rest_idx >= 0 || m->kwrest_idx >= 0 || m->npost_rest > 0) continue; int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; int fuzzy = 0, kwh = -1; for (int j = 0; j < an; j++) { const char *aty = av[j] >= 0 ? nt_type(nt, av[j]) : NULL; NodeKind ak = nt_kind(nt, av[j]); if (aty && (ak == NK_SplatNode || nt_kind(nt, av[j]) == NK_ForwardingArgumentsNode)) fuzzy = 1; else if (ak == NK_KeywordHashNode && j == an - 1) kwh = av[j]; } if (fuzzy) continue; ArgLayout L; call_layout(c, m, av, an, &L); for (int i = 0; i < m->nparams; i++) { if (L.from[i] != ARG_DEFAULT && L.from[i] != ARG_BY_NAME) continue; /* supplied positionally */ if (!m->pnames[i]) continue; if (m->pdefault[i] >= 0) continue; /* optional: has a default */ if (kwh >= 0 && kwh_lookup(nt, kwh, m->pnames[i]) >= 0) continue; /* supplied by keyword */ if (m->pnames[i][0] == '_' && m->pnames[i][1] == '_') continue; /* synthesized */ LocalVar *lv = scope_local(m, m->pnames[i]); if (lv && lv->type == TY_UNKNOWN) { lv_set_type(lv, TY_INT); changed = 1; } } arg_layout_free(&L); } return changed; } /* An inline `Hash.new` / `Hash.new(default)` in ARGUMENT position has no variable whose key/value usage could pick its hash variant, so it infers TY_UNKNOWN forever and codegen rejects the call -- which breaks the common counting idiom each_with_object(Hash.new(0)). Rename the call to the internal `__hash_new_default`, which infers (stably) as the universally-boxed PolyPoly variant. */ /* `m(*[a, b])`: a splat of an ARRAY LITERAL expands to its elements at compile time so builtin methods and proc calls (which have no runtime splat path) take plain positionals -- min(*[2]), add.(*[3, 4]). USER methods keep the runtime splat shape: emit_args_filled handles it and carries the correct ArgumentError timing for arity mismatches. Type-aware (user-object receivers excluded), hence in the fixpoint. */ static int expand_literal_splat_args(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; for (int id = comp_kind_first(c, NK_CallNode); id >= 0; id = comp_kind_next(c, id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; /* the shape first: only a call with a `*[...]` argument is rewritten, and the receiver's type (asked below) cost a lookup for every call, every round */ { int argsn0 = nt_ref(nt, id, "arguments"), an0 = 0; const int *av0 = argsn0 >= 0 ? nt_arr(nt, argsn0, "arguments", &an0) : NULL; int lit = 0; for (int j = 0; j < an0 && !lit; j++) if (av0[j] >= 0 && nt_kind(nt, av0[j]) == NK_SplatNode) { int in0 = nt_ref(nt, av0[j], "expression"); lit = in0 >= 0 && nt_kind(nt, in0) == NK_ArrayNode; break; } if (!lit) continue; } int recv = nt_ref(nt, id, "receiver"); if (recv < 0) { if (call_target(c, id, -1, nm, CT_TOP, NULL) >= 0 || comp_included_method_index(c, nm, id) >= 0) continue; /* a user method: native splat path */ } else { TyKind rt = infer_type(c, recv); if (rt == TY_UNKNOWN || ty_is_object(rt) || rt == TY_CLASS) continue; } int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; int sp_at = -1; for (int j = 0; j < an; j++) { NodeKind ak = nt_kind(nt, av[j]); if (ak == NK_SplatNode) { sp_at = j; break; } } if (sp_at < 0 || an > 32) continue; int inner = nt_ref(nt, av[sp_at], "expression"); NodeKind ik = nt_kind(nt, inner); if (ik != NK_ArrayNode) continue; int en = 0; const int *ev = nt_arr(nt, inner, "elements", &en); int nested = 0; for (int j = 0; j < en; j++) { NodeKind ek = nt_kind(nt, ev[j]); if (ek == NK_SplatNode) nested = 1; } if (nested || en > 32 || an - 1 + en > 64) continue; int na[96]; int m0 = 0; for (int j = 0; j < sp_at; j++) na[m0++] = av[j]; for (int j = 0; j < en; j++) na[m0++] = ev[j]; for (int j = sp_at + 1; j < an; j++) na[m0++] = av[j]; nt_node_set_arr((NodeTable *)nt, (int)argsn, "arguments", na, m0); changed = 1; } return changed; } /* A Symbol receiver delegates its STRING-flavored methods (regex match, pattern slice, comparisons) through the name text: interpose .to_s so the string machinery serves them. Symbol-native fast paths (==, to_s, case conversions, succ) keep their own arms. */ /* `arr.lazy....`: uniq / chunk_while / slice_when / with_index / each_with_index carry state across elements, so they do not fuse into the lazy loop and the whole chain was rejected as opaque. Over a FINITE source the lazy and eager forms agree (CRuby's lazy uniq.to_a over an endless source never terminates either), so drop the `.lazy` and let the eager array path serve it. An endless range keeps its lazy chain and its (still unsupported) reject. (#2993) */ /* A string range serves only its endpoint/membership face natively; every other method rides the materialized element array. Type-driven, so it also catches a range held in a variable -- which is why it lives in the inference fixpoint rather than the one-shot shape pass (#3064). */ static int desugar_str_range_methods(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const range_native[] = { "begin", "end", "min", "max", "include?", "member?", "cover?", "===", "exclude_end?", "==", "!=", "eql?", "inspect", "to_s", "class", "frozen?", "freeze", "itself", "dup", "clone", "hash", /* Range's own overlap? and bsearch answer about the RANGE: the member Array has no overlap?, and its bsearch searched where CRuby raises TypeError for a String range */ "overlap?", "bsearch", /* the identity predicates answer about the RANGE, not its members: routing them through to_a made `("a".."e").is_a?(Range)` false (#3619) */ "is_a?", "kind_of?", "instance_of?", "nil?", "equal?", "respond_to?", /* to_a IS the materializer: interposing it would recurse */ "to_a", "entries", /* Range#size counts integer elements: nil for a string range */ "size", /* step / % walk the members by stride: their own arm answers, the block form's too when the stride is an Integer (see below, #3671). */ "step", "%", NULL }; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || recv < 0) continue; if (infer_type(c, recv) != TY_STR_RANGE) continue; /* `range % n { }` is `range.step(n) { }` (#3671): the walk has no row of its own under the `%` name, so name it what it is. The blockless form is the `%` row's, whose Enumerator inspects as `%(n)`. */ if (is_mod_operator(nm) && nt_ref(nt, id, "block") < 0) continue; if (sp_streq(nm, "%")) { nt_node_set_str(nt, id, "name", "step"); changed = 1; continue; } int argn = nt_ref(nt, id, "arguments"); int an = 0; if (argn >= 0) nt_arr(nt, argn, "arguments", &an); int native = 0; for (int j = 0; range_native[j]; j++) if (sp_streq(nm, range_native[j])) { native = 1; break; } /* minmax without a block is [min, max] off the endpoints, its own arm (an endless range has no maximum, where the walk could not convert); the comparator form walks the members */ if (sp_streq(nm, "minmax") && an == 0 && nt_ref(nt, id, "block") < 0) native = 1; /* min / max with a comparator block walk the members as Enumerable's do; the endpoint readers answered as though there were no block */ if ((is_minmax_query(nm)) && nt_ref(nt, id, "block") >= 0) native = 0; /* a block-driven step by an Integer has its own arm (the statement iteration's String-range step), which walks an endless range too and answers the range; any other stride is `step(n).each { }`: the blockless arm answers the Enumerator, whose each the array path already drives (#3671) */ int int_stride = an == 0 || (an == 1 && infer_type(c, nt_arr(nt, argn, "arguments", &an)[0]) == TY_INT); if (native && sp_streq(nm, "step") && nt_ref(nt, id, "block") >= 0 && !int_stride) { int inner = nt_new_node(nt, "CallNode"); if (inner < 0) continue; nt_node_set_str(nt, inner, "name", "step"); nt_node_set_ref(nt, inner, "receiver", recv); nt_node_set_ref(nt, inner, "arguments", argn); nt_node_set_ref(nt, inner, "block", -1); nt_node_set_ref(nt, id, "receiver", inner); nt_node_set_str(nt, id, "name", "each"); nt_node_set_ref(nt, id, "arguments", -1); /* the Enumerator#each fold in desugar_enum_method_recv would turn this straight back into the block-driven step, every round (#4962) */ nt_node_set_int(nt, id, "lowered_each", 1); comp_grow_node_arrays(c); c->nscope[inner] = c->nscope[id]; changed = 1; continue; } /* each walks the members itself, with a block (the statement iteration's String-range arm) and without (an Enumerator over the Range), so an endless range streams by succ */ { int eb = nt_ref(nt, id, "block"); /* a block passed as `&callable` keeps the member Array's route */ if (an == 0 && (is_each_name(nm) || is_each_entry(nm)) && (eb < 0 || nt_kind(nt, eb) == NK_BlockNode)) native = 1; } /* the blockless windows, each_with_index, each_entry and find_index(value) are Enumerator walks over the Range, which the rows build (so an endless range streams, and a Range typed after this pass ran gets the same) */ if (nt_ref(nt, id, "block") < 0 && ((an == 1 && (is_each_window(nm) || is_find_index(nm))) || (an == 0 && (is_with_index_alias(nm) || is_each_entry(nm))))) native = 1; /* ... and lazy is the source of a pipeline that streams the same way */ if (an == 0 && is_lazy_name(nm) && nt_ref(nt, id, "block") < 0) native = 1; /* ... and the walks that stop on the block's answer ride builtins/enumerable.rb over that each */ if (is_stop_walk(nm) && an == 0 && nt_ref(nt, id, "block") >= 0 && nt_kind(nt, nt_ref(nt, id, "block")) == NK_BlockNode) native = 1; /* first(n) and take(n) with an Integer count stop at n: their own row, so an endless range answers where the member Array could not be made */ if (an == 1 && is_first_or_take(nm) && nt_ref(nt, id, "block") < 0 && infer_type(c, nt_arr(nt, argn, "arguments", &an)[0]) == TY_INT) native = 1; /* first/last are the endpoints bare, a prefix/suffix ARRAY with a count */ if (!native && an == 0 && (is_endpoint_query(nm))) native = 1; /* Object's own face answers about the RANGE, not its members (#3619's identity predicates were the first of it): through to_a, tap and then yielded the member Array, tap answered it, and on an endless range instance_variables, `!` and `=~` raised RangeError where CRuby answers */ if (range_object_face(nm)) native = 1; if (native) continue; int toa = nt_new_node(nt, "CallNode"); if (toa < 0) continue; nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", recv); /* each, each_entry, reverse_each, each_with_index, each_slice and each_cons with a block walk the members but answer the RANGE: mark the hop, as the Hash and Enumerable routes do (#3842), so inference and the value emitter yield the receiver instead of the member Array */ { int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && ((an == 0 && is_each_walk_or_with_index(nm)) || (an == 1 && is_each_window(nm)))) nt_node_set_str(nt, toa, "enum_recv", "1"); } nt_node_set_ref(nt, id, "receiver", toa); /* an Enumerator the blockless call answers has no size (emit_call) */ if (nt_ref(nt, id, "block") < 0) nt_node_set_int(nt, id, "srange_enum", 1); comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; changed = 1; } return changed; } /* `SYM.to_proc.call(recv, *args)` is just `recv.SYM(*args)` -- Ruby's Symbol#to_proc proc takes the receiver first and forwards the rest. The synthesized lambda has a fixed parameter count (1, or 2 for a binary operator), so any other shape raised ArgumentError. Rewriting the direct chain sidesteps the proc entirely, and needs no dynamic dispatch. (#3097) */ int desugar_sym_to_proc_call_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm || !is_call_or_yield(nm)) return 0; if (nt_ref(nt, id, "block") >= 0) return 0; int tp = nt_ref(nt, id, "receiver"); if (tp < 0 || nt_kind(nt, tp) != NK_CallNode) return 0; const char *tpn = nt_str(nt, tp, "name"); if (!tpn || !sp_streq(tpn, "to_proc")) return 0; int symn = nt_ref(nt, tp, "receiver"); if (symn < 0) return 0; const char *sym = sym_static_value(c, symn); if (!sym || !*sym) return 0; int argn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argn >= 0 ? nt_arr(nt, argn, "arguments", &an) : NULL; if (!av || an < 1) return 0; /* needs at least the receiver */ if (nt_argv_has_splat(nt, av, an)) an = 0; /* a splat keeps the proc path */ if (an < 1) return 0; int rest = nt_new_node(nt, "ArgumentsNode"); if (rest < 0) return 0; nt_node_set_arr(nt, rest, "arguments", av + 1, an - 1); nt_node_set_str(nt, id, "name", sym); nt_node_set_ref(nt, id, "receiver", av[0]); nt_node_set_ref(nt, id, "arguments", an > 1 ? rest : -1); enum_send_copy_take(nt, id); /* `a.tally`: the builtin's copy, as a written call's */ /* the `:m.to_proc` call is left in the node table, unreached; cut its Symbol off so the to_proc -> lambda rewrite (desugar_enum_chain_shapes, which visits every node id) cannot turn it into a lambda whose parameters become locals and Symbols of the C */ nt_node_set_ref(nt, tp, "receiver", -1); comp_grow_node_arrays(c); c->nscope[rest] = c->nscope[id]; return 1; } /* `arr.reduce(:gcd)` / `inject(init, :lcm)`: a Symbol naming an ordinary method (not an operator) folds pairwise through that method. The operator symbols ride the typed/boxed fold emitters; rewrite the method-naming form into the equivalent block, which the block fold already serves. (#3125) */ static int desugar_reduce_method_symbol(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "reduce") && !sp_streq(nm, "inject"))) continue; if (nt_ref(nt, id, "block") >= 0) continue; int argn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argn >= 0 ? nt_arr(nt, argn, "arguments", &an) : NULL; if (!av || an < 1 || an > 2) continue; const char *sym = sym_static_value(c, av[an - 1]); if (!sym || !*sym) continue; /* only an identifier-named method: operators keep their fold emitters */ if (!((sym[0] >= 'a' && sym[0] <= 'z') || (sym[0] >= 'A' && sym[0] <= 'Z') || sym[0] == '_')) continue; char pa[64], pb[64]; snprintf(pa, sizeof pa, "__rms_a_%s", comp_node_tag(c, id)); snprintf(pb, sizeof pb, "__rms_b_%s", comp_node_tag(c, id)); int base = nt->count; int kpa = nt_new_node(nt, "RequiredParameterNode"); int kpb = nt_new_node(nt, "RequiredParameterNode"); if (kpa < 0 || kpb < 0) continue; nt_node_set_str(nt, kpa, "name", pa); nt_node_set_str(nt, kpb, "name", pb); int preq[2] = { kpa, kpb }; int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", preq, 2); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int ra = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ra, "name", pa); int rb2 = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rb2, "name", pb); int cargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, cargs, "arguments", &rb2, 1); int call = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, call, "name", sym); nt_node_set_ref(nt, call, "receiver", ra); nt_node_set_ref(nt, call, "arguments", cargs); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &call, 1); int blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk, "parameters", bparams); nt_node_set_ref(nt, blk, "body", blkbody); nt_node_set_ref(nt, id, "block", blk); /* Marks this block as SYNTHESIZED from a symbol argument, not written by the program: builtins/enumerable.rb's Ruby definition (0 extra args, a real block) would otherwise claim it too, once the seedless form drops its argument below and looks identical to a genuine 0-arg block call -- and its plain `yield` has no fold emitter's fallback for a symbol naming no real method (`[1, 2].inject(:nope)` must answer CRuby's NoMethodError, not fail the C build). See desugar_builtin_enum_calls's carve-out. */ nt_node_set_int(nt, id, "sym_fold", 1); /* drop the symbol argument, keeping any leading init */ if (an == 2) nt_node_set_arr(nt, argn, "arguments", av, 1); else nt_node_set_ref(nt, id, "arguments", -1); comp_grow_node_arrays(c); int encl = c->nscope[id]; for (int j = base; j < nt->count; j++) c->nscope[j] = encl; changed = 1; } return changed; } static int desugar_lazy_stateful_stage(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; static const char *const stateful[] = { "uniq", "chunk_while", "slice_when", "with_index", "each_with_index", /* not stateful, but unfused: over a finite source the eager forms agree (#3127) */ "compact", "zip", NULL }; for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "lazy") || nt_ref(nt, id, "block") >= 0) continue; int src = nt_ref(nt, id, "receiver"); if (src < 0) continue; /* only a finite source: a typed array, or an array literal */ TyKind st = infer_type(c, src); int finite = ty_is_array(st) || (nt_kind(nt, src) == NK_ArrayNode); if (!finite) continue; /* `src.lazy.each { }` runs the block over each element and answers src, as `src.each { }` does: over a finite source there is no laziness to keep, and a lazy enumerator's own `each` had no emitter */ { int first = -1; for (int k = 0; k < nt->count && first < 0; k++) if (nt_kind(nt, k) == NK_CallNode && nt_ref(nt, k, "receiver") == id) first = k; const char *fn = first >= 0 ? nt_str(nt, first, "name") : NULL; int fblk = first >= 0 ? nt_ref(nt, first, "block") : -1; if (fn && sp_streq(fn, "each") && fblk >= 0 && nt_kind(nt, fblk) == NK_BlockNode && nt_ref(nt, first, "arguments") < 0) { nt_node_set_ref(nt, first, "receiver", src); changed = 1; continue; } } /* does any consumer up the chain need cross-element state? */ int node = id, hit = 0; for (int depth = 0; depth < 16 && !hit; depth++) { int call = -1; for (int k = 0; k < nt->count; k++) if (nt_kind(nt, k) == NK_CallNode && nt_ref(nt, k, "receiver") == node) { call = k; break; } if (call < 0) break; const char *cn = nt_str(nt, call, "name"); if (!cn) break; for (int i = 0; stateful[i]; i++) if (sp_streq(cn, stateful[i])) { hit = 1; break; } /* with_index attaches to an enumerator, not to the bare array */ if (hit && sp_streq(cn, "with_index")) hit = 2; node = call; } if (!hit) continue; /* Rewrite the `lazy` call in place: `each` for the stages that want an enumerator receiver, otherwise splice it out so the consumer reads the source array directly. */ if (hit == 2) { nt_node_set_str(nt, id, "name", "each"); } else { for (int k = 0; k < nt->count; k++) if (nt_kind(nt, k) == NK_CallNode && nt_ref(nt, k, "receiver") == id) nt_node_set_ref(nt, k, "receiver", src); } changed = 1; } return changed; } static int desugar_symbol_string_methods(Compiler *c) { /* the rewrite cannot be taken back, so it waits for types that have settled: a local that looked like a Symbol in an optimistic round and widened after (`key = yield x` over nil and Symbols) was compared as a String against a Symbol and answered nil */ if (g_infer_optimistic) return 0; NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const SYMSTR[] = { "match", "match?", "=~", "[]", "slice", "start_with?", "end_with?", "between?", "<", "<=", ">", ">=", "<=>", "count", "index", "tr", "sub", "gsub", NULL }; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int hit = 0; for (int j = 0; SYMSTR[j]; j++) if (sp_streq(nm, SYMSTR[j])) { hit = 1; break; } if (!hit) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || infer_type(c, recv) != TY_SYMBOL) continue; /* an `at` desugar_array_at rewrote: Symbol has #[] but no #at */ if (nt_int(nt, id, "was_at", 0)) continue; /* a Symbol reopen's own method of the name is the call's target, which the call plan takes for a Symbol receiver (`class Symbol; def index(x) = 0; end`): the call stays on the Symbol */ if (an_reopen_owner(c, id, CORE_SYMBOL) >= 0) continue; /* Symbol#<=> is defined only between Symbols: reading both sides as text would make :a <=> "a" answer 0 where Ruby answers nil, so leave a non-Symbol operand alone and let codegen emit the nil (#3081). */ if (sp_streq(nm, "<=>")) { int cargs = nt_ref(nt, id, "arguments"); int cn = 0; const int *cv = cargs >= 0 ? nt_arr(nt, cargs, "arguments", &cn) : NULL; if (cn == 1 && cv && cv[0] >= 0) { TyKind cat = infer_type(c, cv[0]); if (cat != TY_SYMBOL && cat != TY_POLY && cat != TY_UNKNOWN) continue; } } /* symbol-to-symbol comparisons need BOTH sides as text */ int toa = nt_new_node(nt, "CallNode"); if (toa < 0) continue; nt_node_set_str(nt, toa, "name", "to_s"); nt_node_set_ref(nt, toa, "receiver", recv); nt_node_set_ref(nt, id, "receiver", toa); comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; /* symbol ARGUMENTS to the comparisons also read as their names */ int argsn = nt_ref(nt, id, "arguments"); int an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; for (int j = 0; j < an && j < 8; j++) { if (av[j] >= 0 && infer_type(c, av[j]) == TY_SYMBOL) { int t2 = nt_new_node(nt, "CallNode"); if (t2 < 0) continue; nt_node_set_str(nt, t2, "name", "to_s"); nt_node_set_ref(nt, t2, "receiver", av[j]); comp_grow_node_arrays(c); c->nscope[t2] = c->nscope[id]; int na2[8]; const int *av2 = nt_arr(nt, argsn, "arguments", &an); for (int k2 = 0; k2 < an && k2 < 8; k2++) na2[k2] = (k2 == j) ? t2 : av2[k2]; nt_node_set_arr(nt, argsn, "arguments", na2, an); av = nt_arr(nt, argsn, "arguments", &an); } } changed = 1; } return changed; } /* `m(&op)` where op is a SYMBOL variable: rewrite the block argument into a real block calling `send(op)` -- the dynamic-send desugar then expands the send over op's statically-known candidate set. Comparator consumers (reduce/inject/sort/min/max/minmax) get the two-parameter form so operator symbols (`op = :+`) apply both arguments. A `sym_var.to_proc` call gets the same treatment as an explicit lambda. */ /* `m(&method(:Integer))` where :Integer names a Kernel builtin (no user def): the Method-object route has no C target for a builtin, so rewrite the block argument into an explicit one-parameter block calling the builtin: m { |__bmk_N| Integer(__bmk_N) }. User-defined names keep the Method path. */ static int desugar_kernel_method_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const KFN[] = { "Integer", "Float", "String", "Array", "Rational", "Complex", "puts", "print", "p", "pp", NULL }; int *calls, ncalls = an_calls_with_block_arg(c, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_CallNode) continue; int blk = nt_call_block_arg(nt, id); if (blk < 0) continue; int ex = nt_ref(nt, blk, "expression"); if (ex < 0 || nt_kind(nt, ex) != NK_CallNode) continue; const char *exn = nt_str(nt, ex, "name"); /* `&p.to_proc` on a Proc is `&p`: to_proc is self there (#3687) */ if (exn && sp_streq(exn, "to_proc")) { int tpr = nt_ref(nt, ex, "receiver"); int targs = nt_ref(nt, ex, "arguments"); int tac = 0; if (targs >= 0) nt_arr(nt, targs, "arguments", &tac); if (tpr >= 0 && tac == 0 && infer_type(c, tpr) == TY_PROC) { nt_node_set_ref(nt, blk, "expression", tpr); changed = 1; } continue; } if (!exn || !sp_streq(exn, "method")) continue; if (nt_ref(nt, ex, "receiver") >= 0) continue; /* recv.method(:x): wrapper path */ const char *sym = method_sym_arg(c, ex); if (!sym) continue; int known = 0; for (int k = 0; KFN[k]; k++) if (sp_streq(sym, KFN[k])) { known = 1; break; } if (!known) continue; if (comp_method_index(c, sym) >= 0) continue; /* a user def wins */ char pn[64]; snprintf(pn, sizeof pn, "__bmk_%s", comp_node_tag(c, id)); int kpa = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, kpa, "name", pn); int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", &kpa, 1); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int ra = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ra, "name", pn); int sargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, sargs, "arguments", &ra, 1); int call = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, call, "name", sym); nt_node_set_ref(nt, call, "arguments", sargs); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &call, 1); int blk2 = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk2, "parameters", bparams); nt_node_set_ref(nt, blk2, "body", blkbody); comp_grow_node_arrays(c); int ns2[8] = { kpa, params, bparams, ra, sargs, call, blkbody, blk2 }; for (int j = 0; j < 8; j++) c->nscope[ns2[j]] = c->nscope[id]; Scope *sc2 = comp_scope_of(c, id); if (sc2) { LocalVar *lva = scope_local_intern(sc2, pn); if (lva) lva->is_block_param = 1; } nt_node_set_ref(nt, id, "block", blk2); changed = 1; } free(calls); return changed; } /* `m(&h)` where h is a Hash: Hash#to_proc -- rewrite the block argument into an explicit one-parameter block indexing the hash (`m { |x| h[x] }`). */ /* An empty block body IS nil: `{ }` and `{ nil }` are the same block, and CRuby answers accordingly (`[1,2].filter_map {}` -> [], `.group_by {}` -> {nil=>[1,2]}). The iterator arms each read the body's tail to type their result, and most of them decline outright when there is none -- so the call fell through to the unresolved-call gate and reported the METHOD as undefined, which is the one thing it is not (#4006). Write the nil the block already means, once, rather than teaching a dozen arms to special-case it. The arms that DO have an empty-body path keep answering the same thing: a body of `nil` maps every element to nil, which is what they hard-coded. */ static int desugar_empty_block_body(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_BlockNode) continue; int body = nt_ref(nt, id, "body"); if (body >= 0) { /* a `begin`/rescue body is not a StatementsNode and is never empty */ if (nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; nt_arr(nt, body, "body", &bn); if (bn > 0) continue; } int nilnode = nt_new_node(nt, "NilNode"); if (nilnode < 0) continue; if (body < 0) { body = nt_new_node(nt, "StatementsNode"); if (body < 0) continue; nt_node_set_ref(nt, id, "body", body); } nt_node_set_arr(nt, body, "body", &nilnode, 1); comp_grow_node_arrays(c); c->nscope[nilnode] = c->nscope[id]; c->nscope[body] = c->nscope[id]; changed = 1; } return changed; } static int desugar_hash_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; int *calls, ncalls = an_calls_with_block_arg(c, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; int blk = nt_call_block_arg(nt, id); if (blk < 0) continue; int ex = nt_ref(nt, blk, "expression"); if (ex < 0 || !nt_type(nt, ex)) continue; NodeKind exk = nt_kind(nt, ex); int ex_is_lit = exk == NK_HashNode || exk == NK_KeywordHashNode; if (!ex_is_lit && !(exk == NK_LocalVariableReadNode && ty_is_hash(infer_type(c, ex)))) continue; char pn[64]; snprintf(pn, sizeof pn, "__bhp_%s", comp_node_tag(c, id)); int kpa = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, kpa, "name", pn); int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", &kpa, 1); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int ra = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ra, "name", pn); int sargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, sargs, "arguments", &ra, 1); int call = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, call, "name", "[]"); nt_node_set_ref(nt, call, "receiver", ex); nt_node_set_ref(nt, call, "arguments", sargs); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &call, 1); int blk2 = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk2, "parameters", bparams); nt_node_set_ref(nt, blk2, "body", blkbody); comp_grow_node_arrays(c); int ns2[7] = { kpa, params, bparams, ra, sargs, call, blkbody }; for (int j = 0; j < 7; j++) c->nscope[ns2[j]] = c->nscope[id]; c->nscope[blk2] = c->nscope[id]; Scope *sc2 = comp_scope_of(c, id); if (sc2) { LocalVar *lva = scope_local_intern(sc2, pn); if (lva) lva->is_block_param = 1; } nt_node_set_ref(nt, id, "block", blk2); changed = 1; } free(calls); return changed; } /* `expr.to_proc` over any other Symbol value (a method's result, an ivar, `str.to_sym`) is bound to a local where it stands -- `(__svr_N = expr; __svr_N.to_proc)` -- which the next round sends as it sends any Symbol local. The value is read once, where to_proc runs, not once per call of the proc, and in its place among its neighbours. Under a block argument it is the Symbol itself that is passed (`&expr`), which desugar_poly_symbol_block_arg binds. A literal `:m.to_proc` keeps its own lowerings. */ static int sym_to_proc_bind_recv(Compiler *c, int id, int ex) { NodeTable *nt = (NodeTable *)c->nt; NodeKind ek = nt_kind(nt, ex); if (ek == NK_LocalVariableReadNode || ek == NK_SymbolNode || nt_safe_nav(nt, id) || infer_type(c, ex) != TY_SYMBOL) return 0; NT_FOREACH_KIND(nt, NK_BlockArgumentNode, ba) if (nt_ref(nt, ba, "expression") == id) { nt_node_set_ref(nt, ba, "expression", ex); nt_node_set_int(nt, id, "stp_detached", 1); /* (out of the tree, as below) */ return 1; } Scope *sc = comp_scope_of(c, id); if (!sc) return 0; char vn[48]; snprintf(vn, sizeof vn, "__svr_%s", comp_node_tag(c, id)); int base = nt->count; int w = nt_new_node(nt, "LocalVariableWriteNode"); int rd = nt_new_node(nt, "LocalVariableReadNode"); int tp = nt_new_node(nt, "CallNode"); int body = nt_new_node(nt, "StatementsNode"); if (w < 0 || rd < 0 || tp < 0 || body < 0) return 0; nt_node_set_str(nt, w, "name", vn); nt_node_set_ref(nt, w, "value", ex); nt_node_set_str(nt, rd, "name", vn); nt_node_set_str(nt, tp, "name", "to_proc"); nt_node_set_ref(nt, tp, "receiver", rd); long long line = nt_int(nt, id, "node_line", 0); if (line) nt_node_set_int(nt, tp, "node_line", line); int stm[2] = { w, tp }; nt_node_set_arr(nt, body, "body", stm, 2); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "body", body); if (line) nt_node_set_int(nt, id, "node_line", line); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = c->nscope[id]; LocalVar *lv = scope_local_intern(sc, vn); if (lv) lv_set_type(lv, TY_SYMBOL); return 1; } static int bo_shallow_copy(NodeTable *nt, int id); /* Make `vn` a local of the innermost block or lambda node `id` is in, as the parser lists a block's own locals: a block-local is fresh in each run of its block, so a proc made in one iteration keeps that iteration's value, where a slot of the method's frame would have handed every one the last. */ static void sv_block_local(NodeTable *nt, const int *parent, int id, const char *vn) { for (int p = parent[id]; p >= 0; p = parent[p]) { NodeKind k = nt_kind(nt, p); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) return; if (k != NK_BlockNode && k != NK_LambdaNode) continue; const char *locs = nt_str(nt, p, "locals"); size_t ln = locs ? strlen(locs) : 0; char *nl = malloc(ln + strlen(vn) + 2); if (!nl) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } snprintf(nl, ln + strlen(vn) + 2, "%s%s%s", ln ? locs : "", ln ? "," : "", vn); nt_node_set_str(nt, p, "locals", nl); free(nl); return; } } static int desugar_symbol_var_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int *parent = NULL; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind ty_k = nt_kind(nt, id); /* `super(a, &s)` too: left as the block argument, the parent's `&b` had no proc for a Symbol and the call was refused */ if (ty_k != NK_CallNode && ty_k != NK_SuperNode) continue; const char *nm = ty_k == NK_CallNode ? nt_str(nt, id, "name") : NULL; int is_toproc = nm && sp_streq(nm, "to_proc"); int ex = -1; if (is_toproc) { if (nt_int(nt, id, "stp_detached", 0)) continue; /* handed to its block argument, below */ ex = nt_ref(nt, id, "receiver"); if (nt_ref(nt, id, "block") >= 0 || nt_ref(nt, id, "arguments") >= 0) continue; } else { int blk = nt_call_block_arg(nt, id); if (blk < 0) continue; ex = nt_ref(nt, blk, "expression"); } if (ex < 0 || !nt_type(nt, ex)) continue; if (is_toproc && sym_to_proc_bind_recv(c, id, ex)) { changed = 1; continue; } if (nt_kind(nt, ex) != NK_LocalVariableReadNode) continue; if (infer_type(c, ex) != TY_SYMBOL) continue; /* `m(&sym_var.to_proc)` is `m(&sym_var)`: the block argument takes the Symbol itself and the call becomes the block below, whose parameter is typed from what the call yields. As a lambda's, it was boxed, and a name computed at run time reached no builtin's method on it. */ if (is_toproc) { int ba_of = -1; NT_FOREACH_KIND(nt, NK_BlockArgumentNode, ba) if (nt_ref(nt, ba, "expression") == id) { ba_of = ba; break; } if (ba_of >= 0) { nt_node_set_ref(nt, ba_of, "expression", ex); /* (the call is out of the tree now: a later round's walk of the node table must not rewrite it into the lambda, whose parameter was left declared in the scope) */ nt_node_set_int(nt, id, "stp_detached", 1); changed = 1; continue; } } /* as many parameters as the call passes its block: the first receives the send, the rest are its arguments */ int npar = is_toproc || ty_k != NK_CallNode ? 1 : sym_block_values(c, id); if (npar < 1) npar = 1; char pn[8][64]; int preq[8], sa[9]; int base = nt->count; for (int k = 0; k < npar; k++) { snprintf(pn[k], sizeof pn[k], "__svp_%c_%s", 'a' + k, comp_node_tag(c, id)); preq[k] = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, preq[k], "name", pn[k]); } int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", preq, npar); /* Symbol#to_proc's lambda is `|recv, *args|`, the rest being the method's arguments: with the one parameter it had, `c.to_proc.call("ab", 6)` raised ArgumentError where CRuby centers. A block takes the count sym_block_values gives, one parameter each. */ char rn[64]; int nsa = npar; if (is_toproc) { snprintf(rn, sizeof rn, "__svp_r_%s", comp_node_tag(c, id)); int rp = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, rp, "name", rn); nt_node_set_ref(nt, params, "rest", rp); } int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int ra = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, ra, "name", pn[0]); /* `&s` and `s.to_proc` take the Symbol s holds where they are evaluated: the block sends the local __svs_N bound there, not s, which a later write -- after the call, or between two of its yields -- would have renamed the method of a block the callee kept or was still running */ /* (A lambda called where it is made, `s.to_proc.call(x)`, runs before anything could write s, and stays a call the call desugars read; nor is one defined? asks about, which it never runs.) */ if (!parent && !(parent = an_parent_map(nt))) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int pc = parent[id]; int bound = !(is_toproc && pc >= 0 && ((nt_kind(nt, pc) == NK_CallNode && nt_ref(nt, pc, "receiver") == id) || nt_kind(nt, pc) == NK_DefinedNode)); char vn[64]; snprintf(vn, sizeof vn, "__svs_%s", comp_node_tag(c, id)); int bind = -1; if (bound) { bind = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, bind, "name", vn); nt_node_set_ref(nt, bind, "value", ex); sa[0] = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, sa[0], "name", vn); } else sa[0] = ex; int sargs = nt_new_node(nt, "ArgumentsNode"); for (int k = 1; k < npar; k++) { sa[k] = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, sa[k], "name", pn[k]); } if (is_toproc) { int rr = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rr, "name", rn); sa[nsa] = nt_new_node(nt, "SplatNode"); nt_node_set_ref(nt, sa[nsa++], "expression", rr); } nt_node_set_arr(nt, sargs, "arguments", sa, nsa); int call = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, call, "name", "send"); nt_node_set_ref(nt, call, "receiver", ra); nt_node_set_ref(nt, call, "arguments", sargs); int blkbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, blkbody, "body", &call, 1); int blk2 = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk2, "parameters", bparams); nt_node_set_ref(nt, blk2, "body", blkbody); comp_grow_node_arrays(c); /* new nodes belong to the call's scope */ for (int j = base; j < nt->count; j++) c->nscope[j] = c->nscope[id]; Scope *sc2 = comp_scope_of(c, id); for (int k = 0; sc2 && k < npar; k++) { LocalVar *lv = scope_local_intern(sc2, pn[k]); if (lv) lv->is_block_param = 1; } if (sc2 && is_toproc) { LocalVar *lv = scope_local_intern(sc2, rn); if (lv) lv->is_block_param = 1; } if (is_toproc) { /* sym_var.to_proc -> lambda { |x, *r| x.send(sym_var, *r) } */ nt_node_set_str(nt, id, "name", "lambda"); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "block", blk2); nt_node_set_int(nt, id, "stp_arity", 1); /* Symbol#to_proc is -2 (#3053) */ } else nt_node_set_ref(nt, id, "block", blk2); if (!bound) { changed = 1; continue; } /* (__svs_N = s; m { |x| x.send(__svs_N) }): the node keeps its id, line and file as the parentheses, as desugar_poly_symbol_block_arg's */ int nc = bo_shallow_copy(nt, id); int stmts = nt_new_node(nt, "StatementsNode"); int stm[2] = { bind, nc }; nt_node_set_arr(nt, stmts, "body", stm, 2); long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = c->nscope[id]; if (sc2) lv_set_type(scope_local_intern(sc2, vn), TY_SYMBOL); sv_block_local(nt, parent, id, vn); changed = 1; } free(parent); return changed; } /* A capacity value with nothing to run: an Integer literal, a local or an instance variable, or Integer arithmetic and a builtin collection's size over those (`items.size * 2`, `n + 1`). */ static int hash_new_capacity_pure(Compiler *c, int n, int depth) { NodeTable *nt = (NodeTable *)c->nt; if (n < 0 || depth > 8) return 0; switch (nt_kind(nt, n)) { case NK_IntegerNode: case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: return 1; case NK_ParenthesesNode: { int body = nt_ref(nt, n, "body"), bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; return bn == 1 && hash_new_capacity_pure(c, bb[0], depth + 1); } case NK_CallNode: break; default: return 0; } const char *nm = nt_str(nt, n, "name"); int recv = nt_ref(nt, n, "receiver"); if (!nm || recv < 0 || nt_ref(nt, n, "block") >= 0) return 0; int args = nt_ref(nt, n, "arguments"), ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; TyKind rt = infer_type(c, recv); if (ac == 0 && is_len_alias(nm)) return (ty_is_array(rt) || ty_is_hash(rt) || rt == TY_STRING) && hash_new_capacity_pure(c, recv, depth + 1); if (ac == 1 && rt == TY_INT && infer_type(c, av[0]) == TY_INT && is_add_sub_mul(nm)) return hash_new_capacity_pure(c, recv, depth + 1) && hash_new_capacity_pure(c, av[0], depth + 1); return 0; } /* `Hash.new(capacity: n)`: the capacity only sizes the table, so the keyword is dropped and the call is the Hash.new it would be without it. A value with nothing to run (hash_new_capacity_pure) is dropped with it; any other value is kept on the call to run after the Hash is built. Any other keyword is CRuby's ArgumentError (emit_hash_new_arg_guard). The keyword argument went in as a default value, and the call found no method. */ static int desugar_hash_new_capacity(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; for (int id = comp_kind_first(c, NK_CallNode); id >= 0; id = comp_kind_next(c, id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); int r = nt_ref(nt, id, "receiver"); if (!nm || !sp_streq(nm, "new") || r < 0 || nt_kind(nt, r) != NK_ConstantReadNode || !nt_str(nt, r, "name") || !sp_streq(nt_str(nt, r, "name"), "Hash")) continue; int argsn = nt_ref(nt, id, "arguments"), an = 0; const int *av = argsn >= 0 ? nt_arr(nt, argsn, "arguments", &an) : NULL; if (nt_argv_kwh(nt, av, an) < 0) continue; if (cplan_const_user(c, id, r, 1)) continue; int en = 0; const int *el = nt_arr(nt, av[an - 1], "elements", &en); if (en != 1 || nt_kind(nt, el[0]) != NK_AssocNode) continue; int key = nt_ref(nt, el[0], "key"), val = nt_ref(nt, el[0], "value"); if (key < 0 || nt_kind(nt, key) != NK_SymbolNode || !nt_str(nt, key, "value") || !sp_streq(nt_str(nt, key, "value"), "capacity")) continue; if (val < 0) continue; if (an - 1 > 64) continue; /* a value with something to run (a call) still runs, after the Hash is built, and must be an Integer: it rides on the call as `hash_capacity` (emit_hash_new_capacity_wrap) */ if (!hash_new_capacity_pure(c, val, 0)) nt_node_set_ref(nt, id, "hash_capacity", val); if (an == 1) nt_node_set_ref(nt, id, "arguments", -1); else { int keep[64]; if (an - 1 > 64) continue; memcpy(keep, av, sizeof(int) * (size_t)(an - 1)); nt_node_set_arr(nt, argsn, "arguments", keep, an - 1); } changed = 1; } return changed; } static int pin_arg_position_hash_new(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode) continue; int argsn = nt_ref(nt, id, "arguments"); if (argsn < 0) continue; int an = 0; const int *av = nt_arr(nt, argsn, "arguments", &an); for (int j = 0; j < an; j++) { int a = av[j]; if (a < 0) continue; NodeKind ak = nt_kind(nt, a); if (ak != NK_CallNode) continue; const char *anm = nt_str(nt, a, "name"); if (!anm || !sp_streq(anm, "new") || nt_ref(nt, a, "block") >= 0) continue; int arecv = nt_ref(nt, a, "receiver"); if (arecv < 0) continue; NodeKind rk = nt_kind(nt, arecv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) continue; const char *cn = nt_str(nt, arecv, "name"); if (cn && sp_streq(cn, "Hash")) { if (cplan_const_user(c, a, arecv, 1)) continue; nt_node_set_str(nt, a, "name", "__hash_new_default"); changed = 1; } } } /* An optional parameter's default is hoisted to the call site, which is argument position by another name: `def m(x, memo = Hash.new)` had no variable whose key usage could pick a variant either, and the call fell to the unresolved-call gate (#3878). */ for (int id = 0; id < nt->count; id++) { { NodeKind pk = nt_kind(nt, id); if (pk != NK_OptionalParameterNode) continue; } int a = nt_ref(nt, id, "value"); if (a < 0 || nt_kind(nt, a) != NK_CallNode) continue; const char *anm = nt_str(nt, a, "name"); if (!anm || !sp_streq(anm, "new") || nt_ref(nt, a, "block") >= 0) continue; int arecv = nt_ref(nt, a, "receiver"); if (arecv < 0) continue; NodeKind rk = nt_kind(nt, arecv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) continue; const char *cn = nt_str(nt, arecv, "name"); if (cn && sp_streq(cn, "Hash")) { if (cplan_const_user(c, a, arecv, 1)) continue; nt_node_set_str(nt, a, "name", "__hash_new_default"); changed = 1; } } return changed; } static int name_is_math_fn(const char *nm) { static const char *const fns[] = { "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", "exp", "log", "log2", "log10", "sqrt", "cbrt", "hypot", "ldexp", "erf", "erfc", "gamma", "lgamma", "frexp", "expm1", "log1p", NULL }; return str_in(nm, fns); } /* `include Math` exposes the module's functions as bare calls: rewrite `sqrt(x)` to `Math.sqrt(x)` so the existing Math-receiver machinery serves them (#2600). Only fires when the program includes Math and no user method of the same name shadows it; a bare call needs at least one argument (the Math functions are all n-ary), so a receiverless niladic call is never touched. */ /* `Kernel.puts x` / `Kernel.exit(1)` / `Kernel.format(...)`: Kernel's module functions are callable with the module as an explicit receiver, and mean exactly what the bare call means. Nothing served that receiver, so every one of them raised "undefined method 'exit' for class Kernel" -- including the `lambda { |status| Kernel.exit(status) }` idiom for making the exit path injectable in a CLI library. Drop the receiver and let the bare-call machinery handle it. Skipped when the program defines its own Kernel, where the name means whatever it says. A reopening now skips only the singleton methods it actually replaces. */ int desugar_kernel_recv(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; NodeKind rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode) continue; const char *rn = nt_str(nt, recv, "name"); if (!rn || !sp_streq(rn, "Kernel")) continue; const char *mn = nt_str(nt, id, "name"); if (!mn || !is_kernel_module_function(mn) || cplan_const_user(c, id, recv, 1)) continue; nt_node_set_ref(nt, id, "receiver", -1); /* still Kernel's: a reopening's pass that gives a bare call self (desugar_handle_reopen_self_recv) leaves it bare, and no method the program defines of the name answers it (`def warn(m) = Kernel.warn(m)` called itself) */ nt_node_set_int(nt, id, "kernel_recv", 1); nt_node_set_int(nt, id, "builtin_only", 1); changed = 1; } return changed; } /* The bare output calls once the program assigns $stdout. Kernel#puts is `$stdout.puts`, Kernel#print writes through `$stdout`, Kernel#p writes the inspected value with `$stdout.write` and Kernel#printf the formatted text, so a program that swaps a StringIO in (minitest's capture_io) or a File captures what the bare calls print. They wrote to the C stream directly and the swap was silently ignored. Each becomes its spelling through `$stdout`, which reads what the program stored there (the stream itself until then) and dispatches on it: puts a, b -> $stdout.puts(a, b) print a -> $stdout.print(a) p f(x) -> (t = f(x); $stdout.write(t.inspect + "\n"); $stdout.flush if $stdout.is_a?(IO); t) p x, y -> (t = x; u = y; ...write t, then u...; [t, u]) printf(f, a) -> ($stdout.write(format(f, a)); nil) (a plain read or literal is used twice, without the temporary). A name the program defines for itself (its own puts, on Kernel or Object, or on an instance_eval block's receiver) is left alone, as is a print with no argument (it prints $_), a puts / print with a splat, and a puts / print while $stdout can hold an object with a write but not that method (std_stream_foreign_writer). The shapes left over (pp, putc, a splatted p) are refused by the emitter (refuse_unredirected_output). */ static int sbo_gvar_read(Compiler *c, int at, const char *name) { NodeTable *nt = (NodeTable *)c->nt; int gv = nt_new_node(nt, "GlobalVariableReadNode"); if (gv < 0) return -1; nt_node_set_str(nt, gv, "name", name); nt_node_set_int(nt, gv, "node_line", nt_int(nt, at, "node_line", 0)); nt_node_set_int(nt, gv, "node_file", nt_int(nt, at, "node_file", 0)); return gv; } static int sbo_stdout_read(Compiler *c, int at) { return sbo_gvar_read(c, at, "$stdout"); } static int sbo_call(NodeTable *nt, int recv, const char *name, const int *argv, int argc, int at) { int cl = nt_new_node(nt, "CallNode"); if (cl < 0) return -1; nt_node_set_str(nt, cl, "name", name); nt_node_set_ref(nt, cl, "receiver", recv); int an = -1; if (argc > 0) { an = nt_new_node(nt, "ArgumentsNode"); if (an < 0) return -1; nt_node_set_arr(nt, an, "arguments", argv, argc); } nt_node_set_ref(nt, cl, "arguments", an); nt_node_set_ref(nt, cl, "block", -1); nt_node_set_int(nt, cl, "node_line", nt_int(nt, at, "node_line", 0)); nt_node_set_int(nt, cl, "node_file", nt_int(nt, at, "node_file", 0)); return cl; } /* Does the bare call `nm` at id reach a method of the program's own rather than Kernel's: the enclosing class's, a top-level def, one on Kernel or Object, or one on an instance_eval block's receiver? */ int std_stream_bare_call_own(Compiler *c, int id, const char *nm) { if (an_bare_call_class_owned(c, id) || call_target(c, id, -1, nm, CT_TOP, NULL) >= 0) return 1; static const int roots[] = { CORE_KERNEL, CORE_OBJECT, CORE_BASIC_OBJECT }; for (int r = 0; r < 3; r++) { int rc = comp_core_class(c, roots[r]); if (rc >= 0 && call_target(c, id, rc, nm, 0, NULL) >= 0) return 1; } int iec = ie_class_of(c, id); return iec >= 0 && call_target(c, id, iec, nm, 0, NULL) >= 0; } static int desugar_std_stream_bare_output(Compiler *c) { if (!std_stream_assigned(c, "$stdout")) return 0; NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) continue; if (nt_ref(nt, id, "block") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int is_puts = sp_streq(nm, "puts"), is_print = sp_streq(nm, "print"); int is_p = sp_streq(nm, "p"), is_printf = sp_streq(nm, "printf"); if (!is_puts && !is_print && !is_p && !is_printf) continue; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if ((is_print || is_printf) && argc == 0) continue; if ((is_puts || is_print) && std_stream_foreign_writer(c, nm)) continue; /* a splat or block argument to puts / print is left to the emitter */ if ((is_puts || is_print) && nt_argv_has(nt, argv, argc, NT_ARG_SPLAT | NT_ARG_BLOCK)) continue; /* p hands back its argument (several: an Array of them); a splat is left to the emitter, which refuses it */ if (is_p) { if (argc < 1 || argc > 16 || nt_argv_has(nt, argv, argc, NT_ARG_SPLAT | NT_ARG_BLOCK)) continue; } if (id >= c->node_cap) continue; if (std_stream_bare_call_own(c, id, nm)) continue; /* printf's text comes from Kernel#format: not when the program has a format of its own anywhere (a class's would shadow it there) */ if (is_printf) { int own_format = 0; for (int k = 0; k < c->nscopes && !own_format; k++) own_format = c->scopes[k].name && sp_streq(c->scopes[k].name, "format"); if (own_format) continue; } Scope *sc = comp_scope_of(c, id); if (!sc) continue; int first = nt->count; long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); if (is_puts || is_print) { /* `puts k: 1` prints the Hash */ for (int k = 0; k < argc; k++) if (nt_kind(nt, argv[k]) == NK_KeywordHashNode) nt_node_set_type(nt, argv[k], "HashNode"); int gv = sbo_stdout_read(c, id); if (gv < 0) continue; nt_node_set_ref(nt, id, "receiver", gv); nt_set_vcall(nt, id, 0); } else { /* the text to write, and what the call answers */ int stm[64], ns = 0, ok = 1; if (is_p) { /* every argument is evaluated before anything prints */ int reads[16], writes[16]; for (int k = 0; k < argc && ok; k++) { /* `p k: 1` prints the Hash */ if (nt_kind(nt, argv[k]) == NK_KeywordHashNode) nt_node_set_type(nt, argv[k], "HashNode"); /* a read, a literal or self is used as it is, twice; anything else is held in a temporary */ NodeKind ak = nt_kind(nt, argv[k]); int simple = ak == NK_LocalVariableReadNode || ak == NK_InstanceVariableReadNode || ak == NK_SelfNode || ak == NK_NilNode || ak == NK_TrueNode || ak == NK_FalseNode || ak == NK_IntegerNode || ak == NK_FloatNode || ak == NK_SymbolNode || ak == NK_ConstantReadNode; int w = -1, r1, r2; if (simple) { r1 = argv[k]; r2 = nt_clone_subtree(nt, argv[k]); if (r2 < 0) { ok = 0; break; } } else { char tn[64]; snprintf(tn, sizeof tn, "__sbo_%s_%d", comp_node_tag(c, id), k); w = nt_new_node(nt, "LocalVariableWriteNode"); r1 = nt_new_node(nt, "LocalVariableReadNode"); r2 = nt_new_node(nt, "LocalVariableReadNode"); if (w < 0 || r1 < 0 || r2 < 0) { ok = 0; break; } nt_node_set_str(nt, w, "name", tn); nt_node_set_ref(nt, w, "value", argv[k]); nt_node_set_int(nt, w, "depth", 0); nt_node_set_str(nt, r1, "name", tn); nt_node_set_int(nt, r1, "depth", 0); nt_node_set_str(nt, r2, "name", tn); nt_node_set_int(nt, r2, "depth", 0); scope_local_intern(sc, tn); } int nl = nt_new_node(nt, "StringNode"); if (nl < 0) { ok = 0; break; } nt_node_set_str(nt, nl, "content", "\n"); nt_node_set_str(nt, nl, "unescaped", "\n"); int insp = sbo_call(nt, r1, "inspect", NULL, 0, id); int text = insp >= 0 ? sbo_call(nt, insp, "+", &nl, 1, id) : -1; int wr = text >= 0 ? sbo_call(nt, sbo_stdout_read(c, id), "write", &text, 1, id) : -1; if (wr < 0) { ok = 0; break; } if (w >= 0) stm[ns++] = w; writes[k] = wr; reads[k] = r2; } if (!ok) continue; for (int k = 0; k < argc; k++) stm[ns++] = writes[k]; /* and flushes an IO it wrote to, as Kernel#p does */ int isio = nt_new_node(nt, "ConstantReadNode"); if (isio < 0) continue; nt_node_set_str(nt, isio, "name", "IO"); int pred = sbo_call(nt, sbo_stdout_read(c, id), "is_a?", &isio, 1, id); int fl = sbo_call(nt, sbo_stdout_read(c, id), "flush", NULL, 0, id); int ts = nt_new_node(nt, "StatementsNode"), ifn = nt_new_node(nt, "IfNode"); if (pred < 0 || fl < 0 || ts < 0 || ifn < 0) continue; nt_node_set_arr(nt, ts, "body", &fl, 1); nt_node_set_ref(nt, ifn, "predicate", pred); nt_node_set_ref(nt, ifn, "statements", ts); stm[ns++] = ifn; if (argc == 1) stm[ns++] = reads[0]; else { int arr = nt_new_node(nt, "ArrayNode"); if (arr < 0) continue; nt_node_set_arr(nt, arr, "elements", reads, argc); stm[ns++] = arr; } } else { int fmt = sbo_call(nt, -1, "format", argv, argc, id); int wr = fmt >= 0 ? sbo_call(nt, sbo_stdout_read(c, id), "write", &fmt, 1, id) : -1; int nil = nt_new_node(nt, "NilNode"); if (wr < 0 || nil < 0) continue; stm[ns++] = wr; stm[ns++] = nil; } int stmts = nt_new_node(nt, "StatementsNode"); if (stmts < 0) continue; nt_node_set_arr(nt, stmts, "body", stm, ns); /* the node keeps its id, line and file: the parentheses */ nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); } comp_grow_node_arrays(c); int encl = c->nscope[id]; for (int j = first; j < nt->count; j++) { c->nscope[j] = encl; if (c->node_cbody) c->node_cbody[j] = c->node_cbody[id]; } changed = 1; } return changed; } /* A bare warn while the program assigns $stderr and it can hold a program object with its own write (std_stream_foreign_writer): Kernel#warn renders its messages as puts would, into one String, and hands it to `$stderr.write`, so the object's own write receives it. The emitter writes only to a stream or a StringIO, and refused the call; it becomes that write: warn a, b -> (t = ; $stderr.write(t) if t; nil) The rendered text is the warn call itself, marked warn_text: typed a String, emitted as its messages' lines joined, and nil while $VERBOSE is nil. The `if t` is there only when the program assigns $VERBOSE. A warn with keywords (uplevel:, category:), a splat or a block argument is left to the emitter, which still refuses it here; so is one the program defines for itself. */ static int desugar_std_stream_bare_warn(Compiler *c) { if (!gvar_has_write(c, "stderr") || !std_stream_foreign_writer(c, NULL)) return 0; NodeTable *nt = (NodeTable *)c->nt; int verb_nil = gvar_has_write(c, "VERBOSE"); int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) continue; if (nt_ref(nt, id, "block") >= 0 || nt_int(nt, id, "warn_text", 0)) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "warn")) continue; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; /* no message writes nothing, which the emitter already does */ if (argc == 0) continue; if (nt_argv_has(nt, argv, argc, NT_ARG_SPLAT | NT_ARG_BLOCK | NT_ARG_KEYWORDS) || id >= c->node_cap) continue; if (std_stream_bare_call_own(c, id, nm)) continue; int first = nt->count; long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); Scope *sc = comp_scope_of(c, id); if (!sc) continue; /* the messages are rendered, and so evaluated, before $stderr is read and before $VERBOSE is tested, as Kernel#warn's arguments are; a nil $VERBOSE makes the rendering answer nil (warn_text 2), tested by truth, which dispatches nothing */ int text = sbo_call(nt, -1, "warn", argv, argc, id); if (text < 0) continue; nt_node_set_int(nt, text, "warn_text", verb_nil ? 2 : 1); char tn[64]; snprintf(tn, sizeof tn, "__sbw_%s", comp_node_tag(c, id)); int w = nt_new_node(nt, "LocalVariableWriteNode"); int r1 = nt_new_node(nt, "LocalVariableReadNode"), r2 = nt_new_node(nt, "LocalVariableReadNode"); if (w < 0 || r1 < 0 || r2 < 0) continue; nt_node_set_str(nt, w, "name", tn); nt_node_set_ref(nt, w, "value", text); nt_node_set_int(nt, w, "depth", 0); nt_node_set_str(nt, r1, "name", tn); nt_node_set_int(nt, r1, "depth", 0); nt_node_set_str(nt, r2, "name", tn); nt_node_set_int(nt, r2, "depth", 0); scope_local_intern(sc, tn); int wr = sbo_call(nt, sbo_gvar_read(c, id, "$stderr"), "write", &r1, 1, id); if (wr < 0) continue; if (verb_nil) { int ws = nt_new_node(nt, "StatementsNode"), ifn = nt_new_node(nt, "IfNode"); if (ws < 0 || ifn < 0) continue; nt_node_set_arr(nt, ws, "body", &wr, 1); nt_node_set_ref(nt, ifn, "predicate", r2); nt_node_set_ref(nt, ifn, "statements", ws); wr = ifn; } int nil = nt_new_node(nt, "NilNode"), stmts = nt_new_node(nt, "StatementsNode"); if (nil < 0 || stmts < 0) continue; int stm[3] = { w, wr, nil }; nt_node_set_arr(nt, stmts, "body", stm, 3); /* the node keeps its id, line and file: the parentheses */ nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); comp_grow_node_arrays(c); int encl = c->nscope[id]; for (int j = first; j < nt->count; j++) { c->nscope[j] = encl; if (c->node_cbody) c->node_cbody[j] = c->node_cbody[id]; } changed = 1; } return changed; } /* `Array[a, b, c]` and `Range.new(lo, hi)` are the constructor spellings of the `[a, b, c]` and `(lo..hi)` literals, and both raised NoMethodError -- the literal worked and the documented constructor for the same value did not (#3485, #3486). Build the literal and carry it as the receiver of a marker call that inference and emission both see through, the way `Hash[k: v]` already reaches the hash literal. Everything downstream then types and emits these exactly as the literal form, with no second implementation to keep in step. Left alone when the program owns the constructor: reopening Array is fine, defining `Array.[]` means whatever it says. */ static int desugar_class_literal_ctors(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_ConstantReadNode) continue; if (nt_ref(nt, id, "block") >= 0) continue; const char *rn = nt_str(nt, recv, "name"); const char *mn = nt_str(nt, id, "name"); if (!rn || !mn) continue; int ca = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = ca >= 0 ? nt_arr(nt, ca, "arguments", &argc) : NULL; if (nt_argv_has_splat(nt, argv, argc)) continue; int ci = comp_class_index(c, rn); if (sp_streq(rn, "Array") && sp_streq(mn, "[]")) { if (ci >= 0 && comp_cmethod_in_chain(c, ci, "[]", NULL) >= 0) continue; int els[64]; if (argc > (int)(sizeof els / sizeof els[0])) continue; for (int k = 0; k < argc; k++) els[k] = argv[k]; nt_node_reset(nt, id, "ArrayNode"); nt_node_set_arr(nt, id, "elements", els, argc); } else if (sp_streq(rn, "Range") && sp_streq(mn, "new") && (argc == 2 || argc == 3)) { if (ci >= 0 && comp_cmethod_in_chain(c, ci, "new", NULL) >= 0) continue; /* the exclusive flag is part of the literal's shape, so it has to be readable here; a computed third argument keeps today's behaviour */ int excl = 0; if (argc == 3) { NodeKind ek = nt_kind(nt, argv[2]); if (ek == NK_TrueNode) excl = 1; else if (ek != NK_FalseNode && ek != NK_NilNode) continue; } int lo = argv[0], hi = argv[1]; nt_node_reset(nt, id, "RangeNode"); nt_node_set_ref(nt, id, "left", lo); nt_node_set_ref(nt, id, "right", hi); if (excl) nt_range_set_excl(nt, id, 1); } else continue; changed = 1; } return changed; } int desugar_include_math(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; int has_include = 0; for (int id = 0; id < n0 && !has_include; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "include")) continue; int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *aa = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { NodeKind ak = nt_kind(nt, aa[j]); if (ak == NK_ConstantReadNode && nt_str(nt, aa[j], "name") && sp_streq(nt_str(nt, aa[j], "name"), "Math")) { has_include = 1; break; } } } if (!has_include) return 0; c->has_include_math = 1; int changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !name_is_math_fn(nm)) continue; int anode = nt_ref(nt, id, "arguments"); int an = 0; if (anode >= 0) nt_arr(nt, anode, "arguments", &an); if (an < 1) continue; /* a user def of this name (any scope) shadows the Math function */ int shadowed = 0; for (int s = 0; s < c->nscopes && !shadowed; s++) if (c->scopes[s].name && sp_streq(c->scopes[s].name, nm)) shadowed = 1; if (shadowed) continue; int mc = nt_new_node(nt, "ConstantReadNode"); if (mc < 0) continue; nt_node_set_str(nt, mc, "name", "Math"); nt_node_set_ref(nt, id, "receiver", mc); comp_grow_node_arrays(c); c->nscope[mc] = c->nscope[id]; changed = 1; } return changed; } /* An iterator synth_struct_each generated (each, each_pair, each_with_index), not one the program wrote. A Struct inherits those names from Struct and Enumerable rather than defining them itself, and a Data has none. */ int scope_is_struct_synth(Compiler *c, int si) { if (si < 0 || si >= c->nscopes) return 0; int dn = c->scopes[si].def_node; return dn >= 0 && nt_str(c->nt, dn, "synth") != NULL; } /* A CallNode's rewrites keyed by its method name, ahead of the array-enum ones: Enumerator and Hash chains, Struct and Set members, the finders, step and % (desugar_enum_method_recv's rewrites, in their order) */ static int desugar_enum_named_call(Compiler *c, int id, NodeTable *nt, const char *nm, int *changed) { /* n.times.each { } is n.times { }: the blockless times is a Range here, whose each answered 0...n where the Enumerator's each answers n */ if (nm && sp_streq(nm, "each") && nt_kind(nt, nt_ref(nt, id, "block")) == NK_BlockNode && nt_ref(nt, id, "arguments") < 0 && !nt_safe_nav(nt, id)) { int trecv = nt_ref(nt, id, "receiver"); if (trecv >= 0 && nt_kind(nt, trecv) == NK_CallNode && nt_str(nt, trecv, "name") && sp_streq(nt_str(nt, trecv, "name"), "times") && nt_ref(nt, trecv, "block") < 0 && nt_ref(nt, trecv, "arguments") < 0 && !nt_safe_nav(nt, trecv)) { int n = nt_ref(nt, trecv, "receiver"); if (n >= 0 && infer_type(c, n) == TY_INT) { nt_node_set_str(nt, id, "name", "times"); nt_node_set_ref(nt, id, "receiver", n); *changed = 1; return 1; } } } /* hash.map.with_index { } / hash.each.with_index { }: interpose to_a so the pair-array enumerator chain (which the array machinery serves) carries it -- h.to_a.map.with_index. Type-aware, hence here and not in the one-shot pre-pass. */ if (nm && (sp_streq(nm, "with_index") || sp_streq(nm, "with_object"))) { int wrecv = nt_ref(nt, id, "receiver"); if (wrecv >= 0 && nt_kind(nt, wrecv) == NK_CallNode && nt_ref(nt, wrecv, "block") < 0) { const char *wrn = nt_str(nt, wrecv, "name"); if (wrn && (sp_streq(wrn, "map") || sp_streq(wrn, "each") || sp_streq(wrn, "collect"))) { int hrecv = nt_ref(nt, wrecv, "receiver"); if (hrecv >= 0 && ty_is_hash(infer_type(c, hrecv)) && !(nt_kind(nt, hrecv) == NK_CallNode && nt_str(nt, hrecv, "name") && sp_streq(nt_str(nt, hrecv, "name"), "to_a"))) { int toa = nt_new_node(nt, "CallNode"); if (toa >= 0) { nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", hrecv); nt_node_set_ref(nt, wrecv, "receiver", toa); /* `h.each.with_index { }` answers h, not the pairs the walk now goes over: (h.to_a.each.with_index { }; h). A Hash no variable names is held in a temp first, read once. */ if (sp_streq(nm, "with_index") && sp_streq(wrn, "each") && nt_ref(nt, id, "block") >= 0) { NodeKind hk = nt_kind(nt, hrecv); int simple = hk == NK_LocalVariableReadNode || hk == NK_InstanceVariableReadNode || hk == NK_ConstantReadNode; int base = nt->count; int inner = nt_new_node(nt, "CallNode"); int st = nt_new_node(nt, "StatementsNode"); int pre = -1, tail = -1; if (simple) tail = nt_clone_subtree(nt, hrecv); else { char tn[64]; snprintf(tn, sizeof tn, "__hwi_%s", comp_node_tag(c, id)); Scope *hs = comp_scope_of(c, id); pre = nt_new_node(nt, "LocalVariableWriteNode"); int r1 = nt_new_node(nt, "LocalVariableReadNode"); tail = nt_new_node(nt, "LocalVariableReadNode"); if (pre >= 0 && r1 >= 0 && tail >= 0 && hs) { nt_node_set_str(nt, pre, "name", tn); nt_node_set_ref(nt, pre, "value", hrecv); nt_node_set_str(nt, r1, "name", tn); nt_node_set_str(nt, tail, "name", tn); nt_node_set_ref(nt, toa, "receiver", r1); scope_local_intern(hs, tn); } else tail = -1; } if (inner >= 0 && st >= 0 && tail >= 0) { nt_node_set_str(nt, inner, "name", "with_index"); nt_node_set_ref(nt, inner, "receiver", wrecv); nt_node_set_ref(nt, inner, "arguments", nt_ref(nt, id, "arguments")); nt_node_set_ref(nt, inner, "block", nt_ref(nt, id, "block")); int items[3], ni = 0; if (pre >= 0) items[ni++] = pre; items[ni++] = inner; items[ni++] = tail; nt_node_set_arr(nt, st, "body", items, ni); nt_node_set_type(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "body", st); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = c->nscope[id]; } } comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[wrecv]; *changed = 1; } } } } } /* `x.to_h { |..| pair }` on a value only known at run time is `x.map { |..| pair }.to_h`: Array#to_h, Hash#to_h and Enumerable#to_h with a block all build the hash from the block's pairs. The boxed dispatch has the blockless to_h and a boxed map, but no block form of to_h, so the call compiled to an unconditional NoMethodError naming the very class that defines it (#4838). A typed receiver keeps its own emitter; a program class with its own to_h keeps it too, since the boxed value may be one of those. */ if (nm && sp_streq(nm, "to_h") && nt_kind(nt, nt_ref(nt, id, "block")) == NK_BlockNode && nt_ref(nt, id, "arguments") < 0) { int trecv = nt_ref(nt, id, "receiver"); int user_to_h = 0; for (int ci = 0; ci < c->nclasses && !user_to_h; ci++) if (comp_method_in_class(c, ci, "to_h") >= 0) user_to_h = 1; if (trecv >= 0 && !user_to_h && infer_type(c, trecv) == TY_POLY) { int mapc = nt_new_node(nt, "CallNode"); /* a Struct or Data value maps over its [member, value] pairs, not its values: the receiver is read through __to_h_subject first */ int subj = nt_new_node(nt, "CallNode"); if (mapc >= 0 && subj >= 0) { nt_node_set_str(nt, subj, "name", "__to_h_subject"); nt_node_set_ref(nt, subj, "receiver", trecv); nt_node_set_str(nt, mapc, "name", "map"); nt_node_set_ref(nt, mapc, "receiver", subj); nt_node_set_ref(nt, mapc, "block", nt_ref(nt, id, "block")); nt_node_set_ref(nt, id, "receiver", mapc); nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[mapc] = c->nscope[id]; c->nscope[subj] = c->nscope[id]; *changed = 1; return 1; } } } /* Enumerable#each_entry on a receiver whose #each yields ONE value per element is #each: same elements, same receiver as the value. That is every builtin enumerable -- an Array/Range/Enumerator element, a Hash pair (already packed), a Dir entry -- so rename and let the #each emitters serve it. Without this the call had no emitter at all for a Hash or a Range, and answered nil for an Array or a Dir (#3395). A user Enumerable is deliberately NOT rewritten: its #each may `yield a, b`, and there each_entry packs the pair where each spreads it. That path keeps its own machinery. Type-aware, hence here and not in the one-shot pre-pass; while the receiver type is still unsettled g_infer_optimistic holds the rewrite back rather than guessing. */ if (nm && sp_streq(nm, "each_entry") && nt_ref(nt, id, "block") >= 0) { int erecv = nt_ref(nt, id, "receiver"); if (erecv >= 0) { TyKind ert = infer_type(c, erecv); if (ert == TY_UNKNOWN) { } else if (ty_is_array(ert) || ty_is_hash(ert) || ert == TY_RANGE || ert == TY_ENUMERATOR || ert == TY_DIR) { /* #each_entry answers the receiver, which #each does not do for an Enumerator; record it before the name is gone (#3591). A blockless `.each` receiver IS that Enumerator however its own type settled -- over a Range it settles as the materialized element array, and the call then answered those elements (#3857). */ int erecv_enum = ert == TY_ENUMERATOR; if (!erecv_enum && nt_kind(nt, erecv) == NK_CallNode && nt_ref(nt, erecv, "block") < 0) { const char *ernm = nt_str(nt, erecv, "name"); if (ernm && (sp_streq(ernm, "each") || sp_streq(ernm, "each_with_index") || sp_streq(ernm, "reverse_each"))) erecv_enum = 1; } if (erecv_enum) nt_node_set_int(nt, id, "enum_self_result", erecv); nt_node_set_str(nt, id, "name", "each"); *changed = 1; return 1; /* `nm` was the name just replaced (freed): the arms below read it */ } } } /* The block forms of each_slice / each_cons answer the receiver too, and over a blockless `.each` that receiver is the Enumerator (#3857). */ if (nm && (is_each_window(nm)) && nt_ref(nt, id, "block") >= 0 && nt_int(nt, id, "enum_self_result", -1) < 0) { int srecv = nt_ref(nt, id, "receiver"); if (srecv >= 0 && nt_kind(nt, srecv) == NK_CallNode && nt_ref(nt, srecv, "block") < 0) { const char *srnm = nt_str(nt, srecv, "name"); if (srnm && sp_streq(srnm, "each")) { nt_node_set_int(nt, id, "enum_self_result", srecv); *changed = 1; } } } /* Enumerator::Product.new(*enums) is Enumerator.product(*enums) without a block: Product.new takes none, and ignores one given */ if (nm && sp_streq(nm, "new")) { int prv = nt_ref(nt, id, "receiver"); int ppar = prv >= 0 && nt_kind(nt, prv) == NK_ConstantPathNode ? nt_ref(nt, prv, "parent") : -1; if (ppar >= 0 && nt_str(nt, prv, "name") && sp_streq(nt_str(nt, prv, "name"), "Product") && nt_kind(nt, ppar) == NK_ConstantReadNode && nt_str(nt, ppar, "name") && sp_streq(nt_str(nt, ppar, "name"), "Enumerator")) { nt_node_set_ref(nt, id, "receiver", ppar); nt_node_set_str(nt, id, "name", "product"); nt_node_set_ref(nt, id, "block", -1); *changed = 1; return 1; } } /* Enumerator::Chain.new(*enums) -> __enum_chain_of([*enums]): a chain over each source's items in order, a splat contributing its elements as sources (an Array literal spreads it); #class reports Enumerator::Chain, as for Enumerable#chain. No sources give the empty chain. */ if (nm && sp_streq(nm, "new")) { int prv = nt_ref(nt, id, "receiver"); int ppar = prv >= 0 && nt_kind(nt, prv) == NK_ConstantPathNode ? nt_ref(nt, prv, "parent") : -1; if (ppar >= 0 && nt_str(nt, prv, "name") && sp_streq(nt_str(nt, prv, "name"), "Chain") && nt_kind(nt, ppar) == NK_ConstantReadNode && nt_str(nt, ppar, "name") && sp_streq(nt_str(nt, ppar, "name"), "Enumerator") && nt_ref(nt, id, "block") < 0) { int cargs = nt_ref(nt, id, "arguments"); int cn = 0; const int *cv = cargs >= 0 ? nt_arr(nt, cargs, "arguments", &cn) : NULL; int *els = cn > 0 ? malloc(sizeof(int) * cn) : NULL; if (cn > 0 && !els) return 0; /* out of memory: leave the call alone */ for (int k = 0; k < cn; k++) els[k] = cv[k]; /* copy before the node table grows */ int srcs = nt_new_node(nt, "ArrayNode"); int nargs = srcs >= 0 ? nt_new_node(nt, "ArgumentsNode") : -1; if (nargs >= 0) { nt_node_set_arr(nt, srcs, "elements", els, cn); nt_node_set_arr(nt, nargs, "arguments", &srcs, 1); comp_grow_node_arrays(c); c->nscope[srcs] = c->nscope[id]; c->nscope[nargs] = c->nscope[id]; nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_str(nt, id, "name", "__enum_chain_of"); nt_node_set_ref(nt, id, "arguments", nargs); free(els); *changed = 1; return 1; } free(els); } } /* Enumerator.product(*enums) { blk } iterates the tuples and answers nil; the constructor arm builds the Enumerator, so drive it with #each (#3589) */ if (nm && sp_streq(nm, "product") && nt_ref(nt, id, "block") >= 0) { int prv = nt_ref(nt, id, "receiver"); if (prv >= 0 && nt_kind(nt, prv) == NK_ConstantReadNode && nt_str(nt, prv, "name") && sp_streq(nt_str(nt, prv, "name"), "Enumerator")) { int pargs = nt_ref(nt, id, "arguments"); int inner = nt_new_node(nt, "CallNode"); if (inner >= 0) { nt_node_set_str(nt, inner, "name", "product"); nt_node_set_ref(nt, inner, "receiver", prv); nt_node_set_ref(nt, inner, "arguments", pargs); nt_node_set_ref(nt, inner, "block", -1); nt_node_set_ref(nt, id, "receiver", inner); nt_node_set_str(nt, id, "name", "each"); nt_node_set_ref(nt, id, "arguments", -1); /* the block form answers nil, not the enumerator #each hands back */ nt_node_set_int(nt, id, "nil_result", 1); comp_grow_node_arrays(c); c->nscope[inner] = c->nscope[id]; *changed = 1; return 1; } } } /* enum.to_set == Set.new(enum.to_a) whenever the set package's Set class is in the program (an in-place rewrite; Set's initialize adds each element, deduplicating). Type-aware, hence here and not in the one-shot pre-pass: a class that defines its OWN to_set has to keep it. The pre-pass could not tell -- it runs before class collection, so it rewrote every to_set in the program and a `CookieJar#to_set` returning a Hash became a Set, with the error landing on whatever the real return type supported several lines later (#3378). While the receiver's type is still unresolved, wait rather than guess: g_infer_optimistic says the fixpoint has more to say. The argument count, not just the ArgumentsNode's presence, is what matters: a respond_to? probe (desugar_respond_to_probe) always builds an ArgumentsNode, even for its no-argument shapes, so gating on "no ArgumentsNode at all" never matched a probe and respond_to?(:to_set) answered false on a concrete receiver for a method that otherwise works (K-026). The shape is LR_TO_SET's row. */ const LowerRow *tsr = lower_row_nt(nt, id, LR_TO_SET); if (tsr && comp_class_index(c, tsr->target) >= 0) { int recv = nt_ref(nt, id, "receiver"); if (recv >= 0) { TyKind rt = infer_type(c, recv); int decline = 0; if (rt == TY_UNKNOWN && g_infer_optimistic) decline = 1; /* not yet known */ /* Set#to_set is the receiver itself, which is also what keeps a `to_set.to_set` chain from nesting one Set.new inside another (#3623) */ else if (ty_is_object(rt) && ty_object_class(rt) == comp_class_index(c, tsr->target) && nt_ref(nt, id, "block") < 0) { nt_node_set_int(nt, id, "enum_self_result", recv); nt_node_set_str(nt, id, "name", "itself"); nt_node_set_ref(nt, id, "arguments", -1); *changed = 1; return 1; } else if (ty_is_object(rt) && comp_method_in_chain(c, ty_object_class(rt), "to_set", NULL) >= 0) decline = 1; /* the class owns the name */ if (!decline) { int cst = nt_new_node(nt, "ConstantReadNode"); int one = nt_new_node(nt, "ArgumentsNode"); /* Materialize the receiver to a flat array first: Set#initialize iterates its arg via `enum.each`, and a struct/user object passed as a poly value does not dispatch #each through the poly path. to_a rides the struct-native / __enum_to_a machinery, and is a no-op copy for array/range/hash receivers. */ int toa = nt_new_node(nt, "CallNode"); if (cst >= 0 && one >= 0 && toa >= 0) { nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", recv); nt_node_set_int(nt, toa, "to_set", 1); nt_node_set_str(nt, cst, "name", tsr->target); nt_node_set_arr(nt, one, "arguments", &toa, 1); nt_node_set_str(nt, id, "name", "new"); nt_node_set_ref(nt, id, "receiver", cst); nt_node_set_ref(nt, id, "arguments", one); /* a block maps each element on the way in: Set.new's own block parameter does exactly that (#3623) */ comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; c->nscope[cst] = c->nscope[id]; *changed = 1; return 1; } } } } /* Explicit `self.` receiver inside a class-method body: self IS the class there, so a call that resolves to a sibling/inherited class method drops the receiver and rides the bare-call cmethod dispatch (`def self.go; self.maker; end`). A writer goes the same way (`def self.reset; self.table_name = "t"; end`): kept, its self was a class no arm took a setter on, and the call was refused. Names with no matching class method (self.class, self.name, ...) keep their receiver. */ { int srecv = nt_ref(nt, id, "receiver"); if (nm && srecv >= 0 && nt_kind(nt, srecv) == NK_SelfNode && nm[0] && !nt_int(nt, srecv, "ie_rself", 0)) { /* a rebindable block's self is whatever instance_exec binds */ Scope *ssc = comp_scope_of(c, id); if (ssc && ssc->is_cmethod && ssc->class_id >= 0 && call_target(c, id, ssc->class_id, nm, CT_CMETHOD, NULL) >= 0) { nt_node_set_ref(nt, id, "receiver", -1); *changed = 1; return 1; } } } /* poly-array sum { blk } == map { blk }.sum (the typed-array redispatch serves int/float receivers natively) */ if (nm && sp_streq(nm, "sum") && nt_ref(nt, id, "block") >= 0) { int srecv = nt_ref(nt, id, "receiver"); int sa = nt_ref(nt, id, "arguments"); int sac = 0; if (sa >= 0) nt_arr(nt, sa, "arguments", &sac); /* a block that breaks out of the sum answers the break value: the mapped copy would break out of the map and sum what it gave (#4918) */ int sblk = nt_ref(nt, id, "block"); int sbreaks = sblk >= 0 && nt_kind(nt, sblk) == NK_BlockNode && block_has_top_break(c, nt_ref(nt, sblk, "body")); if (srecv >= 0 && sac == 0 && !sbreaks && infer_type(c, srecv) == TY_POLY_ARRAY) { int mapc = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, mapc, "name", "map"); nt_node_set_ref(nt, mapc, "receiver", srecv); nt_node_set_ref(nt, mapc, "block", nt_ref(nt, id, "block")); nt_node_set_str(nt, id, "name", "sum"); nt_node_set_ref(nt, id, "receiver", mapc); nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[mapc] = c->nscope[id]; *changed = 1; return 1; } } /* `arr.each_index.reverse_each { }` / `.each_with_index.reverse_each { }`: the blockless inner call answers an Enumerator, and reverse_each has no arm for one, so the whole chain was refused -- though `.each` on the same Enumerator works and `.to_a` on it answers the right elements. Interpose to_a, exactly as the Range chains below do, and the array machinery serves it (#4302). */ if (nm && sp_streq(nm, "reverse_each")) { int rr = nt_ref(nt, id, "receiver"); if (rr >= 0 && nt_kind(nt, rr) == NK_CallNode && nt_ref(nt, rr, "block") < 0 && infer_type(c, rr) == TY_ENUMERATOR) { const char *rrn = nt_str(nt, rr, "name"); int rra = nt_ref(nt, rr, "arguments"); int rrac = 0; if (rra >= 0) nt_arr(nt, rra, "arguments", &rrac); /* `each` belongs here with the index enumerators: it reaches the array machinery on its own, but then answers the array it walked rather than the Enumerator, and the marked hop below is what carries the receiver through (#4325). */ if (rrn && rrac == 0 && (sp_streq(rrn, "each_index") || sp_streq(rrn, "each_with_index") || sp_streq(rrn, "each") || sp_streq(rrn, "each_entry"))) { int toa2 = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, toa2, "name", "to_a"); nt_node_set_int(nt, toa2, "made_to_a", 1); nt_node_set_ref(nt, toa2, "receiver", rr); /* The block form of reverse_each answers the RECEIVER, and after this hop that is the interposed array rather than the Enumerator the program wrote (#4325). `enum_recv` is the marker the value emitter already reads for exactly this: it yields the marked hop's own receiver instead of the hop. */ nt_node_set_str(nt, toa2, "enum_recv", "1"); nt_node_set_ref(nt, id, "receiver", toa2); comp_grow_node_arrays(c); c->nscope[toa2] = c->nscope[id]; *changed = 1; return 1; } } } /* `recv.(args).each { blk }` IS `recv.(args) { blk }`: Enumerator#each runs the method the Enumerator came from with that block, and answers what THAT method answers -- the receiver for each_with_index, the memo for each_with_object, the mapped array for map. Read instead as an iteration over the values the Enumerator yields, it answered those values (#4332), and over an each_with_index Enumerator it did not compile at all (#4331). `lazy` is left alone: its block form is not its each form. */ if (nm && sp_streq(nm, "each") && nt_ref(nt, id, "block") >= 0 && /* ...but never a synthesized one. `Enumerator.product(a, b) { blk }` is lowered to `Enumerator.product(a, b).each { blk }` above (#3589), and rewriting that back left the two rules undoing each other until the fixpoint gave up, with the block never run. The nil_result marker is what that lowering leaves behind, and `lowered_each` the string range's block-driven step (#4962). */ !nt_int(nt, id, "nil_result", 0) && !nt_int(nt, id, "lowered_each", 0)) { int ea = nt_ref(nt, id, "arguments"); int eac = 0; if (ea >= 0) nt_arr(nt, ea, "arguments", &eac); int er = nt_ref(nt, id, "receiver"); /* ...also `v.with_object(memo).each { }` over a value known only at run time: with_object's own dispatch (desugar_builtin_enum_calls) answers it for an Enumerator and raises for the rest, as the inner call does */ const char *wrn = er >= 0 && nt_kind(nt, er) == NK_CallNode ? nt_str(nt, er, "name") : NULL; int wo_poly = wrn && sp_streq(wrn, "with_object") && nt_ref(nt, er, "receiver") >= 0 && infer_type(c, nt_ref(nt, er, "receiver")) == TY_POLY; if (eac == 0 && er >= 0 && nt_kind(nt, er) == NK_CallNode && nt_ref(nt, er, "block") < 0 && nt_ref(nt, er, "receiver") >= 0 && (wo_poly || infer_type(c, er) == TY_ENUMERATOR)) { const char *ern = nt_str(nt, er, "name"); if (ern && !sp_streq(ern, "lazy")) { /* a boxed receiver's to_enum (desugar_to_enum) builds the Enumerator and takes no block: with one it is the receiver's own each, which answers what each answers */ if (sp_streq(ern, "__poly_enum_for") || sp_streq(ern, "__to_enum_each")) ern = "each"; nt_node_set_str(nt, id, "name", ern); nt_node_set_ref(nt, id, "receiver", nt_ref(nt, er, "receiver")); int ira = nt_ref(nt, er, "arguments"); if (ira >= 0) nt_node_set_ref(nt, id, "arguments", ira); *changed = 1; return 1; } } } /* `arr.each_with_index.reduce { }` / `.inject { }`: the blockless inner call answers an Enumerator, and the fold read its elements as scalars -- a seedless fold assigned a [value, index] pair into an sp_int and the C compiler refused it, while a fold answering the accumulator answered 0 (#4321). Interpose to_a, exactly as the reverse_each sibling above does; the fold's answer is its own, so this hop needs no `enum_recv` marker. */ if (nm && (is_reduce_alias(nm)) && nt_ref(nt, id, "block") >= 0) { int fr = nt_ref(nt, id, "receiver"); if (fr >= 0 && nt_kind(nt, fr) == NK_CallNode && nt_ref(nt, fr, "block") < 0 && infer_type(c, fr) == TY_ENUMERATOR) { const char *frn = nt_str(nt, fr, "name"); int fra = nt_ref(nt, fr, "arguments"); int frac = 0; if (fra >= 0) nt_arr(nt, fra, "arguments", &frac); if (frn && frac == 0 && (is_indexed_each(frn))) { int toa3 = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, toa3, "name", "to_a"); nt_node_set_int(nt, toa3, "made_to_a", 1); nt_node_set_ref(nt, toa3, "receiver", fr); nt_node_set_ref(nt, id, "receiver", toa3); comp_grow_node_arrays(c); c->nscope[toa3] = c->nscope[id]; *changed = 1; return 1; } } } /* Range no-block enumerator chains: interpose to_a so the array machinery serves them ((1..5).each_with_index.to_a, (1..3).map.with_index { }, (1..3).cycle.first(7)). Blockless `each` (and each_slice/each_cons, whose runtime ctors take any boxed source) keep their first-class Enumerator arms. A beginless/endless literal has no array to build and falls through to the loud reject. */ if (nm && nt_ref(nt, id, "block") < 0) { static const char *const RENUM0[] = { "each_with_index", "each_index", "map", "collect", "cycle", NULL }; int rargs = nt_ref(nt, id, "arguments"); int rac = 0; if (rargs >= 0) nt_arr(nt, rargs, "arguments", &rac); int hit = 0; if (rac == 0) { for (int k = 0; RENUM0[k]; k++) if (sp_streq(nm, RENUM0[k])) { hit = 1; break; } } if (hit) { int rrecv = nt_ref(nt, id, "receiver"); if (rrecv >= 0 && infer_type(c, rrecv) == TY_RANGE) { int rlit = nt_paren_single(nt, rrecv); int open_ended = rlit >= 0 && nt_kind(nt, rlit) == NK_RangeNode && (nt_ref(nt, rlit, "left") < 0 || nt_ref(nt, rlit, "right") < 0); if (!open_ended) { int toa = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", rrecv); nt_node_set_int(nt, toa, "range_hop", 1); /* srange_member_hop */ nt_node_set_ref(nt, id, "receiver", toa); comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; *changed = 1; return 1; } } } } /* .with_object(memo) { } (LR_WITH_OBJECT's row): drain to an array and ride the array each_with_object machinery (covers both statement and value forms, with the memo's own typing). Type-aware, hence fixpoint. */ const LowerRow *wor = lower_row_nt(nt, id, LR_WITH_OBJECT); if (wor) { int erecv = nt_ref(nt, id, "receiver"); if (infer_type(c, erecv) == TY_ENUMERATOR && !enumerator_own_with_object(c, id)) { int toa = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", erecv); nt_node_set_str(nt, id, "name", wor->target); nt_node_set_ref(nt, id, "receiver", toa); comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; *changed = 1; return 1; } } /* Hash[k: v, ...] with a keyword-hash argument IS the hash literal */ if (nm && sp_streq(nm, "[]")) { int krc = nt_ref(nt, id, "receiver"); NodeKind krt2_k = nt_kind(nt, krc); if (krt2_k == NK_ConstantReadNode && nt_str(nt, krc, "name") && sp_streq(nt_str(nt, krc, "name"), "Hash")) { int ka = nt_ref(nt, id, "arguments"); int kac = 0; const int *kav = ka >= 0 ? nt_arr(nt, ka, "arguments", &kac) : NULL; if (kac == 1 && kav && nt_kind(nt, kav[0]) == NK_KeywordHashNode) { /* rewrite this call node into a plain HashNode with the same element list */ int en2 = 0; const int *els2 = nt_arr(nt, kav[0], "elements", &en2); int hn2 = nt_new_node(nt, "HashNode"); nt_node_set_arr(nt, hn2, "elements", els2, en2); comp_grow_node_arrays(c); c->nscope[hn2] = c->nscope[id]; /* graft: turn the CallNode into itself... easiest is retarget via receiver replacement not possible; instead rewrite in place by changing this node's type is unsupported -- wrap: make the call `(hash_literal).itself`-free by pointing the parent at hn2 is also unavailable here, so emit-side handles it; mark via rename */ nt_node_set_str(nt, id, "name", "__hash_brackets_kw"); nt_node_set_ref(nt, id, "receiver", hn2); nt_node_set_ref(nt, id, "arguments", -1); *changed = 1; } } return 1; } /* Hash#store(k, v) is exactly []= (whose value form already works) */ if (nm && sp_streq(nm, "store")) { int hrc = nt_ref(nt, id, "receiver"); int ha = nt_ref(nt, id, "arguments"); int hac = 0; if (ha >= 0) nt_arr(nt, ha, "arguments", &hac); /* (not where a Hash reopen defines its own store) */ TyKind hrt = hrc >= 0 ? infer_type(c, hrc) : TY_UNKNOWN; /* ...and on a Hash subclass instance whose class chain has no store of its own: Hash's []=, even where the class defines its own []=, which Hash#store does not call. Not where the class undefines store or makes it private (NoMethodError on a receiver other than self), nor where a Hash reopen defines its own []=. */ int scid = ty_is_object(hrt) ? ty_object_class(hrt) : -1; int sub = hrc >= 0 && scid >= 0 && comp_ary_is_hash(c, scid) && (nt_int(nt, id, "builtin_only", 0) || (comp_arysub_name_is_array(c, scid, nm) && !comp_is_undeffed_in_chain(c, scid, nm) && (comp_method_vis_in_chain(c, scid, nm) == SP_VIS_PUBLIC || nt_kind(nt, hrc) == NK_SelfNode))) && comp_builtin_kind_reopen_mi(c, TY_POLY_POLY_HASH, "[]=") < 0; if (hrc >= 0 && hac == 2 && nt_ref(nt, id, "block") < 0 && (ty_is_hash(hrt) || sub) && an_reopen_owner(c, id, CORE_HASH) < 0) { nt_node_set_str(nt, id, "name", "[]="); if (sub) nt_node_set_int(nt, id, "builtin_only", 1); *changed = 1; } return 1; } /* struct[member_literal] = v rewrites to the generated writer, so the member's type unifies with the value like any accessor write */ if (nm && sp_streq(nm, "[]=")) { int wrc = nt_ref(nt, id, "receiver"); int wa = nt_ref(nt, id, "arguments"); int wac = 0; const int *wav = wa >= 0 ? nt_arr(nt, wa, "arguments", &wac) : NULL; TyKind wrt = wrc >= 0 ? infer_type(c, wrc) : TY_UNKNOWN; if (wac == 2 && ty_is_object(wrt) && c->classes[ty_object_class(wrt)].is_struct) { ClassInfo *wsc = &c->classes[ty_object_class(wrt)]; int wmi = struct_member_idx(c, wsc, wav[0]); /* a Float literal offset, unless the class has a `[]=` of its own */ if (wmi < 0 && comp_method_in_chain(c, ty_object_class(wrt), "[]=", NULL) < 0) wmi = struct_member_idx_float(c, wsc, wav[0]); if (wmi >= 0) { char wn[300]; snprintf(wn, sizeof wn, "%s=", wsc->ivars[wmi] + 1); int one = nt_new_node(nt, "ArgumentsNode"); if (one >= 0) { int varg = wav[1]; nt_node_set_arr(nt, one, "arguments", &varg, 1); nt_node_set_str(nt, id, "name", wn); nt_node_set_ref(nt, id, "arguments", one); comp_grow_node_arrays(c); c->nscope[one] = c->nscope[id]; *changed = 1; } } } return 1; } if (nm && (sp_streq(nm, "default=") || sp_streq(nm, "default"))) { /* default access on an un-narrowed empty-hash local: give it the symbol-keyed poly variant so the setter has a slot to store into (`a = {}; a.default = 9; a.default`) */ int drc = nt_ref(nt, id, "receiver"); if (drc >= 0 && nt_kind(nt, drc) == NK_LocalVariableReadNode && infer_type(c, drc) == TY_UNKNOWN) { Scope *dsc = comp_scope_of(c, drc); const char *dvn = nt_str(nt, drc, "name"); LocalVar *dlv = (dsc && dvn) ? scope_local(dsc, dvn) : NULL; if (dlv && dlv->type == TY_UNKNOWN) { lv_set_type(dlv, TY_SYM_POLY_HASH); *changed = 1; } } return 1; } if (nm && (sp_streq(nm, "any?") || sp_streq(nm, "none?") || sp_streq(nm, "all?") || sp_streq(nm, "one?") || sp_streq(nm, "empty?"))) { /* blockless predicate on an un-narrowed empty-hash local: adopt the symbol-keyed poly variant so the hash fold arm serves it (`b = {}; b.none?`). Every write must be an empty {} literal, so an empty-ARRAY local can never be pulled into a hash type here. */ int qa = nt_ref(nt, id, "arguments"); int qac = 0; if (qa >= 0) nt_arr(nt, qa, "arguments", &qac); int qrc = nt_ref(nt, id, "receiver"); if (qac == 0 && nt_ref(nt, id, "block") < 0 && qrc >= 0 && nt_kind(nt, qrc) == NK_LocalVariableReadNode && infer_type(c, qrc) == TY_UNKNOWN) { Scope *qsc = comp_scope_of(c, qrc); const char *qvn = nt_str(nt, qrc, "name"); LocalVar *qlv = (qsc && qvn) ? scope_local(qsc, qvn) : NULL; if (qlv && qlv->type == TY_UNKNOWN && local_all_writes_empty_hash(c, qsc, qvn)) { lv_set_type(qlv, TY_SYM_POLY_HASH); *changed = 1; } } /* no rewrite of this node: fall through to the remaining arms */ } if (nm && sp_streq(nm, "each_with_object")) { /* an empty-hash local passed as the memo becomes a general boxed key/value hash so any key type the block writes fits, matching the inline each_with_object({}) memo (#2969) */ int ea = nt_ref(nt, id, "arguments"); int eac = 0; const int *eav = ea >= 0 ? nt_arr(nt, ea, "arguments", &eac) : NULL; if (eac >= 1 && eav && nt_kind(nt, eav[0]) == NK_LocalVariableReadNode && infer_type(c, eav[0]) == TY_UNKNOWN) { Scope *esc = comp_scope_of(c, eav[0]); const char *evn = nt_str(nt, eav[0], "name"); LocalVar *elv = (esc && evn) ? scope_local(esc, evn) : NULL; if (elv && elv->type == TY_UNKNOWN && local_all_writes_empty_hash(c, esc, evn)) { lv_set_type(elv, TY_POLY_POLY_HASH); *changed = 1; } } /* fall through */ } if (nm && sp_streq(nm, "yield")) { /* Proc#yield is exactly #call */ int yrc = nt_ref(nt, id, "receiver"); if (yrc >= 0 && infer_type(c, yrc) == TY_PROC) { /* the spelling the program wrote, for a NoMethodError on nil */ nt_node_set_str(nt, id, "written_name", "yield"); nt_node_set_str(nt, id, "name", "call"); *changed = 1; } return 1; } if (nm && sp_streq(nm, "each_codepoint") && nt_ref(nt, id, "arguments") < 0 && nt_ref(nt, id, "block") >= 0 && nt_kind(nt, nt_ref(nt, id, "block")) == NK_BlockNode) { /* each_codepoint with a block is the codepoints walk: the same characters, the same ordinals, and the receiver as its value. The blockless form stays, an Enumerator of its own (#7198). */ int crc = nt_ref(nt, id, "receiver"); if (crc >= 0 && infer_type(c, crc) == TY_STRING) { if (!nt_str(nt, id, "written_name")) nt_node_set_str(nt, id, "written_name", "each_codepoint"); nt_node_set_str(nt, id, "name", "codepoints"); *changed = 1; return 1; } } if (nm && sp_streq(nm, "codepoints") && nt_ref(nt, id, "block") >= 0 && nt_str(nt, id, "written_name") && sp_streq(nt_str(nt, id, "written_name"), "each_codepoint")) { /* the receiver widened after the walk above was chosen (a String that also comes out of a mixed Array): each_codepoint again, which the boxed receiver has an arm for */ int crc = nt_ref(nt, id, "receiver"); if (crc >= 0 && infer_type(c, crc) != TY_STRING) { nt_node_set_str(nt, id, "name", "each_codepoint"); *changed = 1; return 1; } } if (nm && (sp_streq(nm, "grapheme_clusters") || sp_streq(nm, "each_grapheme_cluster"))) { /* grapheme clusters == characters over the supported text domain (no combining sequences): alias to the chars/each_char machinery */ int grc = nt_ref(nt, id, "receiver"); TyKind grt = grc >= 0 ? infer_type(c, grc) : TY_UNKNOWN; /* a boxed receiver (a String read out of a container) reaches the poly chars / each_char arms, which run it when it is a String at run time and raise NoMethodError otherwise; a program that defines any of the names itself keeps the call to its own dispatch */ int grpoly = grt == TY_POLY && nt_ref(nt, id, "arguments") < 0 && !an_user_defines_or_reads(c, nm) && !an_user_defines_or_reads(c, sp_streq(nm, "grapheme_clusters") ? "chars" : "each_char"); if (grt == TY_STRING || grpoly) { nt_node_set_str(nt, id, "name", sp_streq(nm, "grapheme_clusters") ? "chars" : "each_char"); *changed = 1; return 1; } } /* Hash#reverse_each { |k, v| }: desugar to to_a.reverse_each so the existing pair-array destructure serves the two-param block (#2372). The to_a hop is marked (enum_recv), so the value emitter answers the Hash itself, as CRuby's reverse_each answers its receiver. */ if (nm && sp_streq(nm, "reverse_each") && nt_ref(nt, id, "block") >= 0) { int hrc = nt_ref(nt, id, "receiver"); if (hrc >= 0 && ty_is_hash(infer_type(c, hrc))) { int toa = nt_new_node(nt, "CallNode"); if (toa >= 0) { nt_node_set_str(nt, toa, "name", "to_a"); nt_node_set_int(nt, toa, "made_to_a", 1); nt_node_set_ref(nt, toa, "receiver", hrc); nt_node_set_str(nt, toa, "enum_recv", "1"); nt_node_set_ref(nt, id, "receiver", toa); comp_grow_node_arrays(c); c->nscope[toa] = c->nscope[id]; *changed = 1; } return 1; } } /* member? on a builtin container is Array/Hash/Range#include? (#2388). User classes keep their own. (entries on an Array keeps its own name: it copies, and it raises on a nil receiver where to_a answers [].) */ if (nm && sp_streq(nm, "member?") && nt_ref(nt, id, "block") < 0) { int mrc = nt_ref(nt, id, "receiver"); int man = 0; { int _a = nt_ref(nt, id, "arguments"); if (_a >= 0) nt_arr(nt, _a, "arguments", &man); } TyKind mrt = mrc >= 0 ? infer_type(c, mrc) : TY_UNKNOWN; /* a not-yet-narrowed local holding an empty [] literal is still an array; restrict the UNKNOWN case to plain variable/literal receivers so a not-yet-typed method-call chain keeps its own path */ int mrt_open = 0; if (mrt == TY_UNKNOWN && mrc >= 0 && nt_kind(nt, mrc) == NK_ArrayNode) { int user_defines = 0; for (int uk = 0; uk < c->nclasses; uk++) if (call_target(c, id, uk, nm, 0, NULL) >= 0) { user_defines = 1; break; } mrt_open = !user_defines; } if (sp_streq(nm, "member?") && man == 1 && (ty_is_array(mrt) || ty_is_hash(mrt) || mrt == TY_RANGE || mrt_open)) { nt_node_set_str(nt, id, "name", "include?"); *changed = 1; return 1; } } /* `a.chain(b, c)` is desugared wholesale by desugar_enumerable_chain into `__enum_chain(a.to_a + b.to_a + c.to_a)` -- a real Enumerator::Chain that serves every terminal, not just a `.to_a` directly on the chain call. */ /* find_all is a full alias of select on the builtin containers; rename so the select/with_index machinery serves both (#2389) */ if (nm && sp_streq(nm, "find_all")) { int frc = nt_ref(nt, id, "receiver"); TyKind frt = frc >= 0 ? infer_type(c, frc) : TY_UNKNOWN; int fa_user = 0; for (int uk = 0; uk < c->nclasses; uk++) if (comp_method_in_chain(c, uk, "find_all", NULL) >= 0) { fa_user = 1; break; } /* NOT hashes: Hash#find_all answers an array of pairs (the Enumerable contract), while Hash#select answers a hash -- the existing hash find_all machinery keeps that shape */ int frt_open = frt == TY_UNKNOWN && !fa_user && frc >= 0 && nt_kind(nt, frc) == NK_ArrayNode; if (ty_is_array(frt) || frt == TY_RANGE || frt_open) { nt_node_set_str(nt, id, "name", "select"); /* a blockless one's Enumerator still says find_all */ if (nt_ref(nt, id, "block") < 0) nt_node_set_str(nt, id, "enum_label", "find_all"); *changed = 1; return 1; } } /* String-endpoint ranges materialize to a StrArray, which has no begin/end of its own: alias them to first/last (#2411). */ if (nm && (is_range_bound_reader(nm)) && nt_ref(nt, id, "block") < 0) { int brc = nt_ref(nt, id, "receiver"); int ban = 0; { int _a = nt_ref(nt, id, "arguments"); if (_a >= 0) nt_arr(nt, _a, "arguments", &ban); } if (brc >= 0 && ban == 0 && infer_type(c, brc) == TY_STR_ARRAY) { nt_node_set_str(nt, id, "name", sp_streq(nm, "begin") ? "first" : "last"); *changed = 1; return 1; } } if (nm && (sp_streq(nm, "find") || sp_streq(nm, "detect") || sp_streq(nm, "rfind"))) { int fr = nt_ref(nt, id, "receiver"), fa = nt_ref(nt, id, "arguments"), fac = 0; const int *fav = fa >= 0 ? nt_arr(nt, fa, "arguments", &fac) : NULL; TyKind frt = fr >= 0 ? infer_type(c, fr) : TY_UNKNOWN; if (fac == 1 && nt_kind(nt, fav[0]) == NK_NilNode && (ty_is_array(frt) || ty_is_hash(frt) || frt == TY_RANGE || frt == TY_ENUMERATOR)) { nt_node_set_ref(nt, id, "arguments", -1); *changed = 1; return 1; } } if (nm && sp_streq(nm, "rfind")) { int rrc = nt_ref(nt, id, "receiver"); /* an empty `[]` literal receiver infers TY_UNKNOWN but is an array all the same -- rfind on it must still desugar (to yield nil) (#2367) */ int rrc_empty_lit = rrc >= 0 && nt_kind(nt, rrc) == NK_ArrayNode && ({ int _n = 0; nt_arr(nt, rrc, "elements", &_n); _n == 0; }); if (rrc >= 0 && (ty_is_array(infer_type(c, rrc)) || rrc_empty_lit)) { int rev = nt_ref(nt, id, "block") >= 0 ? -1 : nt_new_node(nt, "CallNode"); if (rev >= 0) { nt_node_set_str(nt, rev, "name", "reverse"); nt_node_set_ref(nt, rev, "receiver", rrc); nt_node_set_ref(nt, id, "receiver", rev); comp_grow_node_arrays(c); c->nscope[rev] = c->nscope[id]; } else nt_node_set_int(nt, id, "rfind", 1); nt_node_set_str(nt, id, "name", "find"); *changed = 1; return 1; } } if (nm && sp_streq(nm, "step") && nt_ref(nt, id, "receiver") >= 0) { /* Numeric#step keyword forms lower to the positional (limit, step): step(to: T, by: B) / step(by: B, to: T) / step(T, by: B). An endless step (by: only, no to:) stays a loud reject. */ int sargs = nt_ref(nt, id, "arguments"); int sac = 0; const int *sav = sargs >= 0 ? nt_arr(nt, sargs, "arguments", &sac) : NULL; if (sac <= 2 && nt_argv_kwh(nt, sav, sac) >= 0) { int kwh = sav[sac - 1]; int en = 0; const int *els = nt_arr(nt, kwh, "elements", &en); int to_v = -1, by_v = -1, other = 0, by_first = 0; for (int e = 0; e < en; e++) { int kk = nt_ref(nt, els[e], "key"); const char *kn = (kk >= 0 && nt_kind(nt, kk) == NK_SymbolNode) ? nt_str(nt, kk, "value") : NULL; if (kn && sp_streq(kn, "to")) to_v = nt_ref(nt, els[e], "value"); else if (kn && sp_streq(kn, "by")) { by_v = nt_ref(nt, els[e], "value"); by_first = to_v < 0; } else other = 1; } if (sac == 2 && to_v < 0) to_v = sav[0]; /* step(limit, by: B) */ if (other && step_kw_unknown(c, nt, id, sargs)) { *changed = 1; return 1; } if (!other && to_v >= 0 && by_v >= 0) { step_kw_positional(c, nt, id, sargs, to_v, by_v, by_first && sac == 1); *changed = 1; return 1; } } } if (nm && sp_streq(nm, "%")) { /* (range) % n is Range#step(n) (the arithmetic-sequence operator) */ int prc = nt_ref(nt, id, "receiver"); int pa = nt_ref(nt, id, "arguments"); int pac = 0; if (pa >= 0) nt_arr(nt, pa, "arguments", &pac); if (prc >= 0 && pac == 1 && nt_ref(nt, id, "block") < 0 && (infer_type(c, prc) == TY_RANGE || infer_type(c, prc) == TY_FLOAT_RANGE)) { nt_node_set_str(nt, id, "name", "step"); *changed = 1; } return 1; } /* Blockless each_pair on a Struct answers an Enumerator over the [name, value] pairs, which is what the member hash's own blockless #each answers; the synthesized yielding each_pair raised LocalJumpError. Ahead of the Enumerable-name gate, which does not list each_pair (and must not: its block form yields pairs, not members). The synthesized each_with_index is the same case over the member array, whose own blockless each_with_index answers the [member, index] Enumerator. */ int wix = nm && sp_streq(nm, "each_with_index"); if (nm && (sp_streq(nm, "each_pair") || wix) && nt_ref(nt, id, "block") < 0 && !(wix && nt_ref(nt, id, "arguments") >= 0)) { int prv = nt_ref(nt, id, "receiver"); TyKind prt = prv >= 0 ? infer_type(c, prv) : TY_UNKNOWN; int pcid = ty_is_object(prt) ? ty_object_class(prt) : -1; /* each_with_index only over the generated iterator and the member array: a Struct that defines its own each_with_index or to_a answers through its own methods */ int okc = pcid >= 0 && pcid < c->nclasses; int smi = wix && okc ? call_target(c, id, pcid, nm, 0, NULL) : -1; int cmi = wix && okc ? comp_method_in_chain(c, pcid, "to_a", NULL) : -1; int own = (smi >= 0 && !scope_is_struct_synth(c, smi)) || cmi >= 0 || (okc && c->classes[pcid].is_struct && struct_has_member(&c->classes[pcid], nm)); if (okc && c->classes[pcid].is_struct && !c->classes[pcid].is_data && !own) { int wrap = nt_new_node(nt, "CallNode"); if (wrap >= 0) { nt_node_set_str(nt, wrap, "name", wix ? "to_a" : "to_h"); nt_node_set_ref(nt, wrap, "receiver", prv); nt_set_ignore_visibility(nt, wrap, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, wrap, "arguments", -1); nt_node_set_ref(nt, wrap, "block", -1); nt_node_set_ref(nt, id, "receiver", wrap); nt_set_ignore_visibility(nt, id, 0); if (!wix) nt_node_set_str(nt, id, "name", "each"); comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; *changed = 1; return 1; } } } return 0; } int desugar_enum_method_recv(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); /* a method the program defines on Enumerator answers the call itself: the hops below would hand it to the element array's builtin */ { int r0 = nt_ref(nt, id, "receiver"); if (nm && r0 >= 0 && infer_type(c, r0) == TY_ENUMERATOR && cplan_user_fresh(c, id)->mi >= 0) continue; } if (desugar_enum_named_call(c, id, nt, nm, &changed)) continue; if (!nm || !is_array_enum_method(nm)) continue; /* the name the call was written with, where the pre-pass renamed it (detect to find): the one a class's own reader or method answers */ const char *qn = nt_str(nt, id, "orig_name") ? nt_str(nt, id, "orig_name") : nm; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) { /* `count` with no receiver inside another method of the same class: the redirect below keys on the receiver's type, and an implicit self has no receiver node at all, so an Enumerable call on self was left with nothing to resolve to. Give it one. */ Scope *osc = comp_scope_of(c, id); int ocid = osc ? osc->class_id : -1; if (ocid < 0 || osc->is_cmethod) continue; /* a Data class includes no Enumerable in CRuby -- `to_a` inside one is a NameError, not its members */ if (ocid < c->nclasses && c->classes[ocid].is_data) continue; /* a class without an #each that answers the name itself has the name it was called by back (detect, renamed to find by the pre-pass) */ if (qn != nm && !comp_enum_answers_first(c, ocid, qn)) { changed |= enum_unrename(nt, id); continue; } if (comp_method_in_chain(c, ocid, "__enum_to_a", NULL) < 0) continue; if (!comp_enum_answers_first(c, ocid, qn)) continue; /* the class's own */ int selfn = nt_new_node(nt, "SelfNode"); int wrap = nt_new_node(nt, "CallNode"); if (selfn < 0 || wrap < 0) continue; nt_node_set_str(nt, wrap, "name", "__enum_to_a"); nt_set_ignore_visibility(nt, wrap, 1); nt_set_self_receiver(nt, wrap, selfn); nt_node_set_ref(nt, wrap, "arguments", -1); nt_node_set_ref(nt, wrap, "block", -1); nt_node_set_ref(nt, id, "receiver", wrap); nt_set_ignore_visibility(nt, id, 0); comp_grow_node_arrays(c); c->nscope[selfn] = c->nscope[id]; c->nscope[wrap] = c->nscope[id]; changed = 1; continue; } /* find_all == select once the receiver is ARRAY-shaped (a hash receiver reaches here as the redispatched pair array, so the result is the Enumerable pair list, not a hash) */ if (sp_streq(nm, "find_all") && nt_ref(nt, id, "block") >= 0 && (ty_is_array(comp_ntype(c, recv)) || ty_is_array(infer_type(c, recv)))) { nt_node_set_str(nt, id, "name", "select"); changed = 1; continue; } /* already redirected through __enum_to_a -> leave it (idempotent) */ if (nt_kind(nt, recv) == NK_CallNode) { const char *rn = nt_str(nt, recv, "name"); if (rn && sp_streq(rn, "__enum_to_a")) continue; } TyKind rt = infer_type(c, recv); if (rt == TY_POLY && sym_proc_poly_pair_view(c, id)) changed = 1; /* A materialized Enumerator delegates its block-driven Enumerable methods to its element array: `enum.map { }` -> `enum.to_a.map { }`. Only block forms need this; the blockless terminals (to_a, next, size, first) are emitted against the enumerator directly. to_a/entries ARE the materializer, so never wrap them (would recurse). An index-producing receiver (each_with_index / each_index / with_index) has its own dedicated chain codegen -- leave its shape intact rather than materializing early. The same holds for each_slice/each_cons: their .map/.collect chains are fold-unrolled with the receiver's concrete element type (a float slice's .sum came out 0 through the materialized poly detour). */ int recv_is_index_enum = 0; if (nt_kind(nt, recv) == NK_CallNode) { const char *rn = nt_str(nt, recv, "name"); recv_is_index_enum = rn && (sp_streq(rn, "each_with_index") || sp_streq(rn, "each_index") || sp_streq(rn, "with_index") || sp_streq(rn, "each_slice") || sp_streq(rn, "each_cons")); } /* Blockless Enumerable terminals a materialized enumerator does not lower natively (unlike to_a/next/peek/size/first/take/with_index) also delegate through the element array: `enum.count` -> `enum.to_a.count`. */ int enum_terminal = 0; if (nt_ref(nt, id, "block") < 0) { static const char *const term[] = { "count", "include?", "member?", "sum", "min", "max", "minmax", "sort", "reduce", "inject", "tally", "to_h", "uniq", "reverse", "find_index", "index", "join", "compact", "zip", NULL }; for (int t = 0; term[t]; t++) if (sp_streq(nm, term[t])) { enum_terminal = 1; break; } /* bare `first` returns the single first element, which the materialized enumerator does not lower natively (only the counted first(n) does), so it delegates through the element array too (#2994) */ /* bare `first` is `take(1)[0]`: the counted take lowers natively and stops after one element, so an infinite enumerator answers (#3756) */ if (sp_streq(nm, "first")) { int fa = nt_ref(nt, id, "arguments"); int fac = 0; if (fa >= 0) nt_arr(nt, fa, "arguments", &fac); if (fac == 0 && rt == TY_ENUMERATOR) { int one = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, one, "value", 1); int targs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, targs, "arguments", &one, 1); int tk = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, tk, "name", "take"); nt_node_set_ref(nt, tk, "receiver", recv); nt_set_ignore_visibility(nt, tk, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, tk, "arguments", targs); nt_node_set_ref(nt, tk, "block", -1); int zero = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, zero, "value", 0); int zargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, zargs, "arguments", &zero, 1); nt_node_set_str(nt, id, "name", "[]"); nt_node_set_ref(nt, id, "receiver", tk); nt_set_ignore_visibility(nt, id, 0); nt_node_set_ref(nt, id, "arguments", zargs); comp_grow_node_arrays(c); c->nscope[one] = c->nscope[id]; c->nscope[targs] = c->nscope[id]; c->nscope[tk] = c->nscope[id]; c->nscope[zero] = c->nscope[id]; c->nscope[zargs] = c->nscope[id]; changed = 1; continue; } } } /* The block of sort / min / max / minmax compares two of an index enumerator's [element, index] pairs, not an element and its index, so the two-param chain codegen has no arm for it: it reads the pairs through to_a as the blockless forms do (`a.each.with_index.sort { }`). */ if (nt_ref(nt, id, "block") >= 0 && recv_is_index_enum && (sp_streq(nm, "sort") || sp_streq(nm, "min") || sp_streq(nm, "max") || sp_streq(nm, "minmax"))) enum_terminal = 1; /* A blockless TERMINAL on an index/slice enumerator delegates through to_a too (each_slice(2).count / .to_h): only the block-driving chains need the raw shape for their fold unrolls. */ /* A BLOCK-form Enumerable on a SLICE/CONS enumerator delegates through to_a -- except map/collect, whose dedicated fold arms need the raw shape. The index enums (each_with_index / with_index) keep ALL their block chains on the dedicated two-param codegen. */ /* find/detect/take_while are driven lazily through #next by their own emitter, so they terminate over an INFINITE enumerator; materializing the elements first would loop forever (#3590) */ int enum_lazy_driven = nt_ref(nt, id, "block") >= 0 && (sp_streq(nm, "find") || sp_streq(nm, "detect") || sp_streq(nm, "take_while") || sp_streq(nm, "find_index")); /* include?/member? stop at the first hit through the same driver (#3756) */ if (!enum_lazy_driven && nt_ref(nt, id, "block") < 0 && (sp_streq(nm, "include?") || sp_streq(nm, "member?") || sp_streq(nm, "find_index"))) { int ia = nt_ref(nt, id, "arguments"); int iac = 0; if (ia >= 0) nt_arr(nt, ia, "arguments", &iac); if (iac == 1) enum_lazy_driven = 1; } int recv_is_slice_enum = 0; if (nt_kind(nt, recv) == NK_CallNode) { const char *rn2 = nt_str(nt, recv, "name"); recv_is_slice_enum = rn2 && (is_each_window(rn2)); } if (rt == TY_ENUMERATOR && recv_is_slice_enum && nt_ref(nt, id, "block") >= 0 && !enum_lazy_driven && !sp_streq(nm, "map") && !sp_streq(nm, "collect") && !sp_streq(nm, "with_index") && !sp_streq(nm, "each_with_index") && !sp_streq(nm, "to_a") && !sp_streq(nm, "entries")) { int wrap3 = nt_new_node(nt, "CallNode"); if (wrap3 >= 0) { nt_node_set_str(nt, wrap3, "name", "to_a"); nt_node_set_int(nt, wrap3, "made_to_a", 1); nt_node_set_ref(nt, wrap3, "receiver", recv); nt_set_ignore_visibility(nt, wrap3, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, id, "receiver", wrap3); nt_set_ignore_visibility(nt, id, 0); comp_grow_node_arrays(c); c->nscope[wrap3] = c->nscope[id]; changed = 1; continue; } } if (rt == TY_ENUMERATOR && recv_is_index_enum && enum_terminal && !enum_lazy_driven && !sp_streq(nm, "size")) { int wrap2 = nt_new_node(nt, "CallNode"); if (wrap2 >= 0) { nt_node_set_str(nt, wrap2, "name", "to_a"); nt_node_set_int(nt, wrap2, "made_to_a", 1); nt_node_set_ref(nt, wrap2, "receiver", recv); nt_set_ignore_visibility(nt, wrap2, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, id, "receiver", wrap2); nt_set_ignore_visibility(nt, id, 0); comp_grow_node_arrays(c); c->nscope[wrap2] = c->nscope[id]; changed = 1; continue; } } /* the blockless grouping/cycling Enumerables answer an Enumerator of their own; delegating through to_a gives the Array forms, which are wired (#3604) */ int enum_regroup = nt_ref(nt, id, "block") < 0 && (sp_streq(nm, "each_slice") || sp_streq(nm, "each_cons") || sp_streq(nm, "each_entry") || sp_streq(nm, "cycle") || sp_streq(nm, "reverse_each")); /* (a blockless window of a window, `a.each_slice(2).each_cons(2)`, regroups the windows the receiver yields: no chain arm reads it) */ if (rt == TY_ENUMERATOR && (!recv_is_index_enum || (enum_regroup && recv_is_slice_enum)) && !enum_lazy_driven && !sp_streq(nm, "to_a") && !sp_streq(nm, "entries") && (nt_ref(nt, id, "block") >= 0 || enum_terminal || enum_regroup)) { /* the block forms of these answer the RECEIVING Enumerator, but the redirect makes the call read the materialized array, whose own each_cons/each_slice/each_entry answers that array (#3591) */ if (nt_ref(nt, id, "block") >= 0 && (sp_streq(nm, "each_slice") || sp_streq(nm, "each_cons") || sp_streq(nm, "each_entry"))) nt_node_set_int(nt, id, "enum_self_result", recv); int wrap = nt_new_node(nt, "CallNode"); /* When the call answers its receiving Enumerator, that receiver has to STAY one: a blockless `.each` over a Range otherwise reads the to_a hop as its consumer and settles as the materialized element array, which the call then answered (#3857). */ if (nt_int(nt, id, "enum_self_result", -1) >= 0) nt_node_set_str(nt, wrap, "enum_recv", "1"); /* a statement `e.each { }` pulls the elements one at a time instead (codegen): an endless Enumerator has no array to read */ if ((is_each_or_index(nm)) && nt_ref(nt, id, "block") >= 0) nt_node_set_str(nt, wrap, "enum_each_wrap", "1"); /* the call still yields what the Enumerator yields (enum_pair_source_call) */ nt_node_set_str(nt, wrap, "enum_hop", "1"); nt_node_set_str(nt, wrap, "name", "to_a"); nt_node_set_int(nt, wrap, "made_to_a", 1); nt_node_set_ref(nt, wrap, "receiver", recv); nt_set_ignore_visibility(nt, wrap, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, id, "receiver", wrap); nt_set_ignore_visibility(nt, id, 0); comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; enum_hop_yield_view(c, id, wrap); changed = 1; continue; } /* An enumerable ARGUMENT gets the same treatment as the receiver: `a.zip(b)` where b is a user Enumerable handed the array path a raw instance pointer. Done before the receiver check so an Array receiver with such an argument is covered too. */ if (sp_streq(nm, "zip") || sp_streq(nm, "product")) { int zargs = nt_ref(nt, id, "arguments"); int zn = 0; const int *zav = zargs >= 0 ? nt_arr(nt, zargs, "arguments", &zn) : NULL; for (int zi = 0; zi < zn && zav; zi++) { int za = zav[zi]; if (nt_kind(nt, za) == NK_CallNode) { const char *zn2 = nt_str(nt, za, "name"); if (zn2 && sp_streq(zn2, "__enum_to_a")) continue; /* idempotent */ } TyKind zat = infer_type(c, za); if (!ty_is_object(zat)) continue; int zcid = ty_object_class(zat); if (zcid < 0 || comp_method_in_chain(c, zcid, "__enum_to_a", NULL) < 0) continue; int zwrap = nt_new_node(nt, "CallNode"); if (zwrap < 0) continue; nt_node_set_str(nt, zwrap, "name", "__enum_to_a"); nt_node_set_ref(nt, zwrap, "receiver", za); nt_set_ignore_visibility(nt, zwrap, nt_kind(nt, za) == NK_SelfNode); nt_node_set_ref(nt, zwrap, "arguments", -1); nt_node_set_ref(nt, zwrap, "block", -1); int newargs[8]; if (zn > 8) break; for (int k = 0; k < zn; k++) newargs[k] = (k == zi) ? zwrap : zav[k]; nt_node_set_arr(nt, zargs, "arguments", newargs, zn); comp_grow_node_arrays(c); c->nscope[zwrap] = c->nscope[id]; changed = 1; zav = nt_arr(nt, zargs, "arguments", &zn); } } if (!ty_is_object(rt)) continue; /* each_with_index keeps its own object-receiver clone (builtins/ enumerable.rb, via desugar_builtin_enum_calls's ty_is_object arm), which walks the TRUE receiver's own #each directly: this bridge's to_a materialization answered the MATERIALIZED ARRAY as the block form's `self` tail instead of the receiver itself (Nums.new(1,2,3) .each_with_index{}.class -> Array, not Nums). The TY_ENUMERATOR arm above this one still needs the name (`arr.each.each_with_index{}`, Enumerator's own native override), so it stays in is_array_enum_method; only THIS object-receiver wrap declines it. */ if (sp_streq(nm, "each_with_index")) continue; int cid = ty_object_class(rt); if (comp_method_in_chain(c, cid, "__enum_to_a", NULL) < 0) continue; /* not an #each class */ /* A Struct's blockless #each returns an Enumerator over its members (CRuby), not the LocalJumpError the synthesized yielding #each would raise. Route it through the member array's blockless #each, which materializes the enumerator. (each_entry was already renamed to each upstream.) */ if (c->classes[cid].is_struct && nt_ref(nt, id, "block") < 0 && (sp_streq(nm, "each") || sp_streq(nm, "each_entry"))) { int wrap = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, wrap, "name", "__enum_to_a"); nt_node_set_ref(nt, wrap, "receiver", recv); nt_set_ignore_visibility(nt, wrap, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, id, "receiver", wrap); nt_set_ignore_visibility(nt, id, 0); /* the array's blockless #each yields the Enumerator; each_entry keeps its own name (array #each_entry with no block also materializes one) */ if (sp_streq(nm, "each_entry")) nt_node_set_str(nt, id, "name", "each"); comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; changed = 1; continue; } if (!comp_enum_answers_first(c, cid, qn)) { changed |= enum_unrename(nt, id); continue; } /* class defines it */ /* minmax only where Enumerable really is: a Struct, or a class that mixes it in, itself, through a superclass or through an included module (whose own minmax builtins/enumerable.rb also answers); a class with a bare #each keeps its NoMethodError */ if (sp_streq(nm, "minmax") && !c->classes[cid].is_struct && !an_enum_reaches(c, cid)) continue; /* a Struct/Data class serves these natively in the struct emit section (member-pair to_h, ordered to_a/values, size, dig, ...); the flat element array would change their semantics */ if (c->classes[cid].is_struct) { /* A member accessor always beats the inherited Enumerable surface: CRuby defines accessors directly on the Struct/Data subclass, so Data.define(:count).new(count: 7).count reads the member (7), never Enumerable#count (the member total). Members are not methods in the chain (codegen serves them natively), so check them explicitly. */ char mn[272]; snprintf(mn, sizeof mn, "@%s", nm); if (comp_ivar_index(&c->classes[cid], mn) >= 0) continue; static const char *const snative[] = { "to_a", "values", "to_h", "members", "size", "length", "dig", "deconstruct", "deconstruct_keys", "with", "inspect", "to_s", NULL }; int nat = 0; for (int t = 0; snative[t]; t++) if (sp_streq(nm, snative[t])) { nat = 1; break; } if (nat) continue; } /* The block forms of each_slice/each_cons return the RECEIVER in Ruby, but the redirect makes the call read the flat element array, whose each_slice returns that array. Record the original receiver so the emit yields it instead (#2981). */ if (nt_ref(nt, id, "block") >= 0 && (is_each_window(nm))) nt_node_set_int(nt, id, "enum_self_result", recv); /* The short-circuiting Enumerables stop before the source runs out, so they ride the fiber-backed generator rather than the eager element array: a class whose #each never ends materializes forever (#3756). */ const char *matn = "__enum_to_a"; if (comp_method_in_chain(c, cid, "__to_enum_each", NULL) >= 0 && (sp_streq(nm, "first") || sp_streq(nm, "take") || sp_streq(nm, "lazy") || ((is_find_or_take_while(nm)) && nt_ref(nt, id, "block") >= 0) || ((is_membership_alias(nm)) && nt_ref(nt, id, "block") < 0))) matn = "__to_enum_each"; int wrap = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, wrap, "name", matn); nt_node_set_ref(nt, wrap, "receiver", recv); nt_set_ignore_visibility(nt, wrap, nt_ignore_visibility(nt, id)); nt_node_set_ref(nt, id, "receiver", wrap); nt_set_ignore_visibility(nt, id, 0); comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; changed = 1; } return changed; } /* `for x in obj` over a user Enumerable (a class with an #each, hence a synthesized __enum_to_a): materialize the receiver to a flat element array so the for-loop iterates it via the existing array path. Without this the loop collection is unrecognized and silently iterates nothing. Idempotent; struct classes serve to_a natively and are left alone. */ /* A terminal that the lazy pipeline does not fuse -- sum, count, min, include? and friends -- runs on the materialized result instead: `lz.sum` becomes `lz.to_a.sum`. Without this the call had no lazy arm at all and answered nil or refused to compile (#3585). */ /* `a.upto(b)` over Strings is the String range `(a..b)`: the succ-based walk the range's own iteration already does. There was no arm for it at all (#3600). The Integer form has its own emitter and is left alone. */ /* `Enumerator.new { |y| src.each(&y) }`: the yielder passed as a block. It has no proc object -- `y << v` lowers to a Fiber.yield -- so drive the iterator with a literal block that pushes each element instead (#3587). */ static int desugar_yielder_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; /* The two walks below ran over every node once per generator, every round. Their candidates -- a call named to_proc, a call passed a block argument -- are collected once, ascending; each is still tested in full where it is used, since a rewrite for one generator (`&y.to_proc` -> `&y`) can make a node a candidate's match for the next. */ int *tp_ids = NULL, ntp = 0, *ba_ids = NULL, nba = 0, has_gen = 0; for (int id = 0; id < n0 && !has_gen; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, "new") && nt_ref(nt, id, "block") >= 0) has_gen = 1; } if (!has_gen) return 0; tp_ids = malloc(sizeof(int) * (size_t)(n0 > 0 ? n0 : 1)); ba_ids = malloc(sizeof(int) * (size_t)(n0 > 0 ? n0 : 1)); for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (nm && sp_streq(nm, "to_proc")) tp_ids[ntp++] = id; int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode) ba_ids[nba++] = id; } for (int en = 0; en < n0; en++) { if (nt_kind(nt, en) != NK_CallNode) continue; const char *enm = nt_str(nt, en, "name"); int erecv = nt_ref(nt, en, "receiver"); if (!enm || !sp_streq(enm, "new") || erecv < 0) continue; if (nt_kind(nt, erecv) != NK_ConstantReadNode && nt_kind(nt, erecv) != NK_ConstantPathNode) continue; const char *ecn = nt_str(nt, erecv, "name"); if (!ecn || !sp_streq(ecn, "Enumerator")) continue; int eblk = nt_ref(nt, en, "block"); if (eblk < 0 || nt_kind(nt, eblk) != NK_BlockNode) continue; const char *yname = block_param_name(c, eblk, 0); int ebody = nt_ref(nt, eblk, "body"); if (!yname || ebody < 0) continue; /* The yielder is a value of its own (the fiber under the Yielder id when a proc inside the body captures it), never a container: its `<<` is a Fiber.yield. Pin it boxed so the push evidence on `y << [x, i]` in such a proc cannot make it an array. */ { Scope *ysc = comp_scope_of(c, eblk); LocalVar *ylv = ysc ? scope_local_intern(ysc, yname) : NULL; if (ylv && !ylv->rbs_seeded) { lv_set_type(ylv, TY_POLY); ylv->rbs_seeded = 1; ylv->is_block_param = 1; } } /* `y.to_proc` names the same channel the yielder is: a call through it pushes, and `&y.to_proc` is `&y`. Neither had an arm, so the explicit spelling raised NoMethodError (#3844). Rewritten before the `&y` loop below so the block-argument form rides it. */ for (int ti = 0; ti < ntp; ti++) { int id = tp_ids[ti]; if (nt_kind(nt, id) != NK_CallNode) continue; const char *tnm = nt_str(nt, id, "name"); if (!tnm || !sp_streq(tnm, "to_proc")) continue; int trecv = nt_ref(nt, id, "receiver"); if (trecv < 0 || nt_kind(nt, trecv) != NK_LocalVariableReadNode) continue; const char *tvn = nt_str(nt, trecv, "name"); if (!tvn || !sp_streq(tvn, yname)) continue; if (!a_subtree_contains(nt, ebody, id, 0)) continue; /* `&y.to_proc` -> `&y` */ for (int ba = 0; ba < n0; ba++) if (nt_kind(nt, ba) == NK_BlockArgumentNode && nt_ref(nt, ba, "expression") == id) { nt_node_set_ref(nt, ba, "expression", trecv); changed = 1; } /* `y.to_proc.call(v)` / `.()` / `[v]` -> `y << v` */ for (int cl = 0; cl < n0; cl++) { if (nt_kind(nt, cl) != NK_CallNode || nt_ref(nt, cl, "receiver") != id) continue; const char *cnm = nt_str(nt, cl, "name"); if (!cnm || !is_call_or_yield(cnm)) continue; nt_node_set_str(nt, cl, "name", "<<"); nt_node_set_ref(nt, cl, "receiver", trecv); nt_node_set_int(nt, cl, "yielder_push", 1); changed = 1; } } /* every `&y` inside this generator body */ for (int bi = 0; bi < nba; bi++) { int id = ba_ids[bi]; if (nt_kind(nt, id) != NK_CallNode) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockArgumentNode) continue; int e = nt_ref(nt, blk, "expression"); if (e < 0 || nt_kind(nt, e) != NK_LocalVariableReadNode) continue; const char *vn = nt_str(nt, e, "name"); if (!vn || !sp_streq(vn, yname)) continue; if (!a_subtree_contains(nt, ebody, id, 0)) continue; char pnm[48]; snprintf(pnm, sizeof pnm, "__yb_e_%s", comp_node_tag(c, id)); int preq = nt_new_node(nt, "RequiredParameterNode"); if (preq < 0) continue; nt_node_set_str(nt, preq, "name", pnm); int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", &preq, 1); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int yread = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, yread, "name", yname); int pread = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, pread, "name", pnm); int pargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, pargs, "arguments", &pread, 1); int push = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, push, "name", "<<"); nt_node_set_ref(nt, push, "receiver", yread); nt_node_set_ref(nt, push, "arguments", pargs); nt_node_set_ref(nt, push, "block", -1); /* the push is a Fiber.yield, not an array append: its value is whatever the consumer feeds back, so type it boxed rather than as a container */ nt_node_set_int(nt, push, "yielder_push", 1); int pbody = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, pbody, "body", &push, 1); int nb = nt_new_node(nt, "BlockNode"); if (nb < 0) continue; nt_node_set_ref(nt, nb, "parameters", bparams); nt_node_set_ref(nt, nb, "body", pbody); nt_node_set_ref(nt, id, "block", nb); comp_grow_node_arrays(c); for (int j = preq; j <= nb && j < nt->count; j++) c->nscope[j] = c->nscope[id]; Scope *sc = comp_scope_of(c, id); if (sc) { LocalVar *lv = scope_local_intern(sc, pnm); if (lv) lv->is_block_param = 1; } changed = 1; } } free(tp_ids); free(ba_ids); return changed; } /* `iter(&curried)`: a curried Proc has no proc object to hand a block slot, so drive it through a literal block that applies one element -- exactly what CRuby's Proc#curry answers to `&` (#3654). */ static int desugar_curry_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; int *calls, ncalls = an_calls_with_block_arg(c, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; if (nt_kind(nt, id) != NK_CallNode) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockArgumentNode) continue; int e = nt_ref(nt, blk, "expression"); if (e < 0 || infer_type(c, e) != TY_CURRY) continue; char pnm[48]; snprintf(pnm, sizeof pnm, "__curry_e_%s", comp_node_tag(c, id)); int preq = nt_new_node(nt, "RequiredParameterNode"); if (preq < 0) continue; nt_node_set_str(nt, preq, "name", pnm); int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", &preq, 1); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); int pread = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, pread, "name", pnm); int cargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, cargs, "arguments", &pread, 1); int capply = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, capply, "name", "[]"); nt_node_set_ref(nt, capply, "receiver", e); nt_node_set_ref(nt, capply, "arguments", cargs); nt_node_set_ref(nt, capply, "block", -1); int body = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, body, "body", &capply, 1); int bn = nt_new_node(nt, "BlockNode"); if (bn < 0) continue; nt_node_set_ref(nt, bn, "parameters", bparams); nt_node_set_ref(nt, bn, "body", body); nt_node_set_ref(nt, id, "block", bn); comp_grow_node_arrays(c); for (int j = preq; j <= bn && j < nt->count; j++) c->nscope[j] = c->nscope[id]; Scope *sc = comp_scope_of(c, id); if (sc) { LocalVar *lv = scope_local_intern(sc, pnm); if (lv) lv->is_block_param = 1; } changed = 1; } free(calls); return changed; } /* `m(&method(:x))`: a Method passed as a block is its #to_proc, which spinel already synthesizes -- interposing it lets the callable reach the block slot instead of failing the conversion at run time (#3688). */ static int desugar_method_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; int *calls, ncalls = an_calls_with_block_arg(c, n0, &calls); for (int q = 0; q < ncalls; q++) { int cid = calls[q]; if (nt_kind(nt, cid) != NK_CallNode) continue; int id = nt_ref(nt, cid, "block"); if (id < 0 || nt_kind(nt, id) != NK_BlockArgumentNode) continue; /* Only where the callee is a USER method that keeps a real &block: the builtin iterators have their own arm for a Method operand, and a local that merely types TY_METHOD may be a forwarded block param. */ { const char *cn = nt_str(nt, cid, "name"); if (!cn || nt_ref(nt, cid, "receiver") >= 0) continue; int mi = call_target(c, cid, -1, cn, CT_TOP, NULL); if (mi < 0) { Scope *sc = comp_scope_of(c, cid); if (sc && sc->class_id >= 0) mi = call_target(c, cid, sc->class_id, cn, 0, NULL); } if (mi < 0 || !c->scopes[mi].blk_param || !c->scopes[mi].blk_param[0]) continue; } int e = nt_ref(nt, id, "expression"); if (e < 0 || nt_kind(nt, e) != NK_CallNode) continue; { const char *en = nt_str(nt, e, "name"); if (!en || !sp_streq(en, "method")) continue; } if (infer_type(c, e) != TY_METHOD) continue; int tp = nt_new_node(nt, "CallNode"); if (tp < 0) continue; nt_node_set_str(nt, tp, "name", "to_proc"); nt_node_set_ref(nt, tp, "receiver", e); nt_node_set_ref(nt, tp, "arguments", -1); nt_node_set_ref(nt, tp, "block", -1); nt_node_set_ref(nt, id, "expression", tp); comp_grow_node_arrays(c); c->nscope[tp] = c->nscope[id]; changed = 1; } free(calls); return changed; } /* `File::Stat.new(path)` is CRuby's direct constructor for what `File.stat` answers, and spinel carries a stat as the File handle itself: rewrite the constructor to the class method rather than resolving a Stat class that has no definition (#3766). */ /* `module_function` makes each method BOTH a module method and a private instance method of every includer. Spinel models it as the module method alone wherever it can, and rewrites a receiverless call inside an includer onto the module call the emitter already serves rather than cloning the method in (#3734). That holds only for a body which does not depend on its receiver. This comment used to claim no such body can -- "its self is the module in one spelling and the instance in the other" -- which is the reason the two spellings differ, not a reason they agree: through the includer CRuby runs the method on the INSTANCE, so `@x` is the instance's ivar, `self` is the instance, and a receiverless sibling call dispatches on the instance's class (#4603). A body that does any of those is cloned into the includer by the include transplant (module_function_self_dependent), which gives it a real entry in the includer's chain -- and the comp_method_in_chain test at the top of the loop below then declines to rewrite its call. The rewrite still serves every body that genuinely does not care. */ /* The module_function scopes by name, for one run of the pass below: per name (its index in mfn_names) the chain of the modules that have a module_function of it. The pass asked every receiverless call's class chain for its name, and each module on the way scanned every scope of the program for one; a large program spent minutes on the one call it rewrote. With the names in hand a call named as no module_function is passed over before any lookup. Built at each run: the pass adds no scope. */ static ANameHash mfn_names; static int *mfn_first, *mfn_next, *mfn_mod, mfn_n, mfn_cap; static void mfn_build(Compiler *c) { anh_free(&mfn_names); memset(&mfn_names, 0, sizeof mfn_names); free(mfn_first); mfn_first = NULL; mfn_n = 0; for (int si = 1; si < c->nscopes; si++) { const Scope *ms = &c->scopes[si]; if (ms->class_id < 0 || !ms->is_module_function || !ms->name) continue; int i = anh_find(&mfn_names, ms->name); if (i < 0) { anh_add(&mfn_names, ms->name); i = mfn_names.n - 1; mfn_first = realloc(mfn_first, sizeof *mfn_first * (size_t)mfn_names.n); mfn_first[i] = -1; } if (mfn_n == mfn_cap) { mfn_cap = mfn_cap ? mfn_cap * 2 : 64; mfn_next = realloc(mfn_next, sizeof *mfn_next * (size_t)mfn_cap); mfn_mod = realloc(mfn_mod, sizeof *mfn_mod * (size_t)mfn_cap); } mfn_mod[mfn_n] = ms->class_id; mfn_next[mfn_n] = mfn_first[i]; mfn_first[i] = mfn_n++; } } /* does module m have a module_function named nm (mfn_build ran)? */ static int mfn_module_has(int m, const char *nm) { int i = anh_find(&mfn_names, nm); for (int e = i >= 0 ? mfn_first[i] : -1; e >= 0; e = mfn_next[e]) if (mfn_mod[e] == m) return 1; return 0; } /* The first module, in the ancestor order module m and the modules it includes take (m, then its includes from the last made), that has a module_function named nm; -1 for none. depth only stops an include cycle: no chain of distinct modules is longer than the class count. */ static int module_function_owner(Compiler *c, int m, const char *nm, int depth) { if (m < 0 || m >= c->nclasses || depth > c->nclasses) return -1; if (mfn_module_has(m, nm)) return m; const ClassInfo *mi = &c->classes[m]; for (int j = mi->nincluded_mods - 1; j >= 0; j--) { int r = module_function_owner(c, mi->included_mods[j], nm, depth + 1); if (r >= 0) return r; } return -1; } static int desugar_module_function_call(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; mfn_build(c); if (mfn_names.n == 0) return 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; if (nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || anh_find(&mfn_names, nm) < 0) continue; Scope *sc = comp_scope_of(c, id); if (!sc || sc->class_id < 0 || sc->class_id >= c->nclasses) continue; if (call_target(c, id, sc->class_id, nm, CT_CMETHOD, NULL) >= 0) continue; /* a local of this name is a variable read, not a call */ if (scope_local(sc, nm)) continue; /* the module whose module_function it is, included by the class or a superclass directly or through a module it includes (`module B; include A; end`, then `include B`), the first in the ancestor order (a class, then its includes from the last made, then its superclass). A class on the way that has the name itself -- its own method or reader, or a copy an include put in its table -- answers the call. */ int mod = -1, own = 0; for (int k = sc->class_id; k >= 0 && mod < 0 && !own; k = c->classes[k].parent) { ClassInfo *cl = &c->classes[k]; if (call_target(c, id, k, nm, CT_OWN, NULL) >= 0 || comp_is_reader(cl, nm)) { own = 1; break; } for (int j = cl->nincluded_mods - 1; j >= 0 && mod < 0; j--) mod = module_function_owner(c, cl->included_mods[j], nm, 0); } if (mod < 0) continue; int mc = nt_new_node(nt, "ConstantReadNode"); if (mc < 0) continue; nt_node_set_str(nt, mc, "name", c->classes[mod].name); nt_node_set_ref(nt, id, "receiver", mc); comp_grow_node_arrays(c); c->nscope[mc] = c->nscope[id]; changed = 1; } return changed; } /* Marshal.restore -> Marshal.load: the same method by another name, read in one spelling by every pass that recognizes a load (an object made without its initialize, any class minted). A program that opens a Marshal of its own keeps its call. */ /* the name of the constant n spells at the top level (`Marshal`, `::Marshal`), or NULL */ static const char *top_level_const_name(const NodeTable *nt, int n) { if (n < 0) return NULL; NodeKind k = nt_kind(nt, n); return k == NK_ConstantReadNode || (k == NK_ConstantPathNode && nt_ref(nt, n, "parent") < 0) ? nt_str(nt, n, "name") : NULL; } int desugar_marshal_restore_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; for (int id = 0; id < lc->n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_ModuleNode && k != NK_ClassNode) continue; int cp = nt_ref(nt, id, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (cn && sp_streq(cn, "Marshal")) return 0; } return 1; } int desugar_marshal_restore_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "restore")) return 0; const char *rn = top_level_const_name(nt, nt_ref(nt, id, "receiver")); return rn && sp_streq(rn, "Marshal"); } int desugar_marshal_restore_at(Compiler *c, LowerCtx *lc, int id) { if (!desugar_marshal_restore_left(c, lc, id)) return 0; nt_node_set_str((NodeTable *)c->nt, id, "name", "load"); return 1; } /* Thread.start / Thread.fork -> Thread.new: the same constructor by other names (the arguments go to the block; neither runs the subclass's initialize, which a program's Thread subclass is refused for anyway). Done first, so every pass that recognizes a thread's block reads one spelling. A program defining a class method of either name on a class of its own named Thread keeps its call. */ int desugar_thread_start_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; int own = 0; for (int id = 0; id < lc->n0 && !own; id++) { if (nt_kind(nt, id) != NK_DefNode) continue; const char *dn = nt_str(nt, id, "name"); int dr = nt_ref(nt, id, "receiver"); if (dn && dr >= 0 && (sp_streq(dn, "start") || sp_streq(dn, "fork"))) own = 1; } if (!own) return 1; for (int id = 0; id < lc->n0; id++) { if (nt_kind(nt, id) != NK_ClassNode) continue; int cp = nt_ref(nt, id, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (cn && sp_streq(cn, "Thread")) return 0; } return 1; } int desugar_thread_start_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "start") && !sp_streq(nm, "fork"))) return 0; const char *rn = top_level_const_name(nt, nt_ref(nt, id, "receiver")); return rn && sp_streq(rn, "Thread"); } int desugar_thread_start_at(Compiler *c, LowerCtx *lc, int id) { if (!desugar_thread_start_left(c, lc, id)) return 0; nt_node_set_str((NodeTable *)c->nt, id, "name", "new"); return 1; } static int is_socket_constructor_class(const char *n) { return n && (sp_streq(n, "TCPSocket") || sp_streq(n, "TCPServer") || sp_streq(n, "UDPSocket") || sp_streq(n, "UNIXSocket") || sp_streq(n, "UNIXServer") || sp_streq(n, "Socket")); } /* TCPSocket.open(host, port) and the other socket classes' open without a block -> .new: the same constructor by another name, which otherwise reached the private Kernel#open. The block form, which also closes the socket when the block returns, keeps its call. A program defining a class method named open on a class of its own with one of these names keeps its call too. */ int desugar_socket_open_gate(Compiler *c, LowerCtx *lc) { if (!sp_feature_required("socket")) return 0; const NodeTable *nt = c->nt; int own = 0; for (int id = 0; id < lc->n0 && !own; id++) { if (nt_kind(nt, id) != NK_DefNode) continue; const char *dn = nt_str(nt, id, "name"); if (dn && nt_ref(nt, id, "receiver") >= 0 && sp_streq(dn, "open")) own = 1; } if (!own) return 1; for (int id = 0; id < lc->n0; id++) { if (nt_kind(nt, id) != NK_ClassNode) continue; int cp = nt_ref(nt, id, "constant_path"); if (cp >= 0 && is_socket_constructor_class(nt_str(nt, cp, "name"))) return 0; } return 1; } int desugar_socket_open_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "open") || nt_ref(nt, id, "block") >= 0) return 0; return is_socket_constructor_class(top_level_const_name(nt, nt_ref(nt, id, "receiver"))); } int desugar_socket_open_at(Compiler *c, LowerCtx *lc, int id) { if (!desugar_socket_open_left(c, lc, id)) return 0; nt_node_set_str((NodeTable *)c->nt, id, "name", "new"); return 1; } /* system(cmd, ..., out: File::NULL) -> __sp_system_spawn(cmd, ..., out: ...): CRuby's system is a spawn and a wait, and Process.spawn already takes the options, so a call with keyword arguments goes to builtins/system_spawn.rb, which the parser spliced for it (the shape is LR_SYSTEM_OPTIONS's row). Only when that method is there and the program defines no `system` of its own; any other system call keeps its direct path (and its refusals). */ int desugar_system_options_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; const LowerRow *row = lower_row_of(LR_SYSTEM_OPTIONS); int helper = 0; for (int id = 0; id < lc->n0; id++) { if (nt_kind(nt, id) != NK_DefNode || nt_ref(nt, id, "receiver") >= 0) continue; const char *dn = nt_str(nt, id, "name"); if (!dn) continue; if (sp_streq(dn, row->name)) return 0; if (sp_streq(dn, row->target)) helper = 1; } return helper; } int desugar_system_options_left(Compiler *c, LowerCtx *lc, int id) { return lower_row_nt(c->nt, id, LR_SYSTEM_OPTIONS) != NULL; } int desugar_system_options_at(Compiler *c, LowerCtx *lc, int id) { const LowerRow *r = lower_row_nt(c->nt, id, LR_SYSTEM_OPTIONS); if (!r) return 0; nt_node_set_str((NodeTable *)c->nt, id, "name", r->target); return 1; } /* `cmd` / %x(cmd #{x}) -> `("cmd"), when the program defines a method named ` itself: CRuby runs a backtick literal as a receiverless call of that name, so a user's `def `(cmd)` answers it wherever self reaches the def. Where self does not reach it, the call still runs the command (the bare ` call, emit_kernel_call). Without such a def every literal stays as it is. */ int desugar_user_backtick_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; /* a def of the name, or an alias, alias_method or define_method that names it (by a Symbol or a String), or a method named by a value the program computes */ int own = an_prog_names_method("`"); if (!own) NT_FOREACH_KIND(nt, NK_DefNode, d) own |= sp_streq(nt_str(nt, d, "name"), "`"); return own; } int desugar_user_backtick_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; NodeKind k = nt_kind(nt, id); long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); int arg, args = nt_new_node(nt, "ArgumentsNode"); if (args < 0) return 0; if (k == NK_XStringNode) { /* the command's bytes move to a String literal of their own, frozen, as CRuby hands it */ arg = nt_clone_subtree(nt, id); if (arg < 0) return 0; nt_node_set_type(nt, arg, "StringNode"); nt_node_set_int(nt, arg, "fzl", 1); } else { int pn = 0; const int *pv = nt_arr(nt, id, "parts", &pn); int *parts = malloc(sizeof(int) * (size_t)(pn > 0 ? pn : 1)); if (!parts) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (pn > 0) memcpy(parts, pv, sizeof(int) * (size_t)pn); arg = nt_new_node(nt, "InterpolatedStringNode"); if (arg >= 0) nt_node_set_arr(nt, arg, "parts", parts, pn); free(parts); if (arg < 0) return 0; } nt_node_set_int(nt, arg, "node_line", line); nt_node_set_int(nt, arg, "node_file", file); nt_node_set_int(nt, arg, "node_col", col); nt_node_set_arr(nt, args, "arguments", &arg, 1); nt_node_reset(nt, id, "CallNode"); nt_node_set_str(nt, id, "name", "`"); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", args); nt_node_set_ref(nt, id, "block", -1); nt_set_ignore_visibility(nt, id, 1); nt_node_set_int(nt, id, "xstr_call", 1); /* defined? answers "expression" */ nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); return 1; } /* IO.popen(cmd, mode) { |io| ... } / IO.popen(cmd, &:read) -> IO.__sp_popen(...): the block form reads the command's output through a pipe and reaps the child (builtins/io_popen.rb, spliced by the parser). Only when that method is there and the call has a block (LR_POPEN_BLOCK's row); the blockless form keeps its refusal. */ int desugar_io_popen_block_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; const LowerRow *row = lower_row_of(LR_POPEN_BLOCK); for (int id = 0; id < lc->n0; id++) if (nt_kind(nt, id) == NK_DefNode && nt_ref(nt, id, "receiver") >= 0 && nt_str(nt, id, "name") && sp_streq(nt_str(nt, id, "name"), row->target)) return 1; return 0; } /* the row of the IO.popen call at id the block form serves, or NULL */ static const LowerRow *popen_block_site(const NodeTable *nt, int id) { const LowerRow *r = lower_row_nt(nt, id, LR_POPEN_BLOCK); if (!r) return NULL; /* only a read: the mode, when given, is a literal "r", "rb", "rt", ... A write mode or one known only at run time keeps the compile-time refusal (#7199) rather than a NotImplementedError when it runs. */ int args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an >= 2 && nt_kind(nt, av[1]) != NK_KeywordHashNode) { const char *mode = nt_kind(nt, av[1]) == NK_StringNode ? nt_str(nt, av[1], "content") : NULL; if (!mode || mode[0] != 'r' || strchr(mode, '+')) return NULL; } return r; } int desugar_io_popen_block_left(Compiler *c, LowerCtx *lc, int id) { return popen_block_site(c->nt, id) != NULL; } int desugar_io_popen_block_at(Compiler *c, LowerCtx *lc, int id) { const LowerRow *r = popen_block_site(c->nt, id); if (!r) return 0; nt_node_set_str((NodeTable *)c->nt, id, "name", r->target); return 1; } static int desugar_file_stat_new(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "new")) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_ConstantPathNode) continue; const char *cn = nt_str(nt, recv, "name"); if (!cn || !sp_streq(cn, "Stat")) continue; int par = nt_ref(nt, recv, "parent"); const char *pn = par >= 0 ? nt_str(nt, par, "name") : NULL; if (!pn || !sp_streq(pn, "File")) continue; if (cplan_const_user(c, id, recv, 1)) continue; int args = nt_ref(nt, id, "arguments"); int an = 0; if (args >= 0) nt_arr(nt, args, "arguments", &an); if (an != 1) continue; int fc = nt_new_node(nt, "ConstantReadNode"); if (fc < 0) continue; nt_node_set_str(nt, fc, "name", "File"); nt_node_set_ref(nt, id, "receiver", fc); nt_node_set_str(nt, id, "name", "stat"); nt_node_set_int(nt, id, "builtin_only", 1); comp_grow_node_arrays(c); c->nscope[fc] = c->nscope[id]; changed = 1; } return changed; } static int desugar_string_upto(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const calls_of[] = { "upto", NULL }; int *calls, ncalls = an_calls_named_any(c, calls_of, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "upto")) continue; int recv = nt_ref(nt, id, "receiver"); int args = nt_ref(nt, id, "arguments"); int ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; if (recv < 0 || (ac != 1 && ac != 2)) continue; if (infer_type(c, recv) != TY_STRING || infer_type(c, av[0]) != TY_STRING) continue; /* the second argument is the exclusive flag, which is the range's own */ int excl = 0; if (ac == 2) { NodeKind ek = nt_kind(nt, av[1]); if (ek == NK_TrueNode) excl = 1; else if (!(ek == NK_FalseNode)) continue; } int rng = nt_new_node(nt, "RangeNode"); if (rng < 0) continue; nt_node_set_ref(nt, rng, "left", recv); nt_node_set_ref(nt, rng, "right", av[0]); nt_range_set_excl(nt, rng, excl); /* nil has no upto: the Range tests a begin that can be nil (emit_range_expr) */ nt_node_set_int(nt, rng, "made_upto", 1); nt_node_set_ref(nt, id, "receiver", rng); nt_node_set_str(nt, id, "name", "each"); nt_node_set_ref(nt, id, "arguments", -1); /* upto answers the RECEIVER, not the range it walks */ if (nt_ref(nt, id, "block") >= 0) nt_node_set_int(nt, id, "enum_self_result", recv); comp_grow_node_arrays(c); c->nscope[rng] = c->nscope[id]; changed = 1; } free(calls); return changed; } static int desugar_lazy_terminal(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; static const char *const TERMS[] = { "sum", "count", "min", "max", "minmax", "min_by", "max_by", "tally", "include?", "member?", "reduce", "inject", "group_by", "partition", "sort", "sort_by", "each_with_object", "find_index", "each_entry", NULL }; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int hit = 0; for (int t = 0; TERMS[t] && !hit; t++) if (sp_streq(nm, TERMS[t])) hit = 1; if (!hit) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || !chain_is_lazy_valued(c, recv)) continue; int wrap = nt_new_node(nt, "CallNode"); if (wrap < 0) continue; nt_node_set_str(nt, wrap, "name", "to_a"); nt_node_set_int(nt, wrap, "made_to_a", 1); nt_node_set_ref(nt, wrap, "receiver", recv); nt_node_set_ref(nt, wrap, "arguments", -1); nt_node_set_ref(nt, wrap, "block", -1); nt_node_set_ref(nt, id, "receiver", wrap); comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; changed = 1; } return changed; } static int desugar_for_enumerable(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_ForNode) continue; int coll = nt_ref(nt, id, "collection"); if (coll < 0) continue; NodeKind ck = nt_kind(nt, coll); if (ck == NK_CallNode && nt_str(nt, coll, "name") && (sp_streq(nt_str(nt, coll, "name"), "__enum_to_a") || sp_streq(nt_str(nt, coll, "name"), "__enum_to_a_raw"))) continue; /* idempotent */ TyKind rt = infer_type(c, coll); if (!ty_is_object(rt)) continue; int cid = ty_object_class(rt); if (cid < 0 || comp_method_in_chain(c, cid, "__enum_to_a", NULL) < 0) continue; if (c->classes[cid].is_struct) continue; /* struct to_a is native */ int idxn = nt_ref(nt, id, "index"); const char *ixt = idxn >= 0 ? nt_type(nt, idxn) : NULL; NodeKind ixk = nt_kind(nt, idxn); /* One index variable over a class whose #each yields one value at some sites and several at others: the packed element cannot tell `yield 2, 3` from `yield [2, 3]`, so the loop runs over the values as yielded and the variable takes the first, `x = x[0]`. */ int stmts = nt_ref(nt, id, "statements"); if (c->classes[cid].enum_yield_packed && ixk == NK_LocalVariableTargetNode && comp_method_in_chain(c, cid, "__enum_to_a_raw", NULL) >= 0 && (stmts < 0 || nt_kind(nt, stmts) == NK_StatementsNode)) { int n1 = nt->count; const char *xn = nt_str(nt, idxn, "name"); int wr = te_lvwrite(nt, xn, te_call(nt, te_lvread(nt, xn), "[]", te_args1(nt, nt_new_int(nt, 0)), -1)); int bn = 0; const int *bb = stmts >= 0 ? nt_arr(nt, stmts, "body", &bn) : NULL; int *nb = malloc(sizeof(int) * (size_t)(bn + 1)); if (!nb) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nb[0] = wr; for (int q = 0; q < bn; q++) nb[q + 1] = bb[q]; if (stmts < 0) { stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_ref(nt, id, "statements", stmts); } nt_node_set_arr(nt, stmts, "body", nb, bn + 1); free(nb); int raw = te_call(nt, coll, "__enum_to_a_raw", -1, -1); nt_node_set_ref(nt, id, "collection", raw); comp_grow_node_arrays(c); for (int q = n1; q < nt->count; q++) c->nscope[q] = c->nscope[id]; changed = 1; continue; } int wrap = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, wrap, "name", "__enum_to_a"); nt_node_set_ref(nt, wrap, "receiver", coll); nt_node_set_ref(nt, id, "collection", wrap); /* `for` binds only as many values as it has index variables, while the collector packs a multi-value yield into one array element. One index variable therefore destructures that element and keeps its first value, which is what a single-left MultiTarget already means. */ if ((c->classes[cid].enum_yield_arity > 1 || c->classes[cid].enum_yield_packed) && ixt && ixk != NK_MultiTargetNode && !nt_int(nt, id, "for_packed", 0)) { int mt = nt_new_node(nt, "MultiTargetNode"); nt_node_set_arr(nt, mt, "lefts", &idxn, 1); nt_node_set_ref(nt, id, "index", mt); comp_grow_node_arrays(c); c->nscope[mt] = c->nscope[id]; } comp_grow_node_arrays(c); c->nscope[wrap] = c->nscope[id]; changed = 1; } return changed; } /* each_with_index (builtins/enumerable.rb) walks the receiver's #each with `each do |x|`, which takes only the first of several yielded values, where CRuby's packs them into one element as the collector does. In the copy for a call on a class whose #each yields several values, the walk's block takes them all and packs them: `|*x|`, then `x = x.length <= 1 ? x[0] : x`. */ static int enum_copy_site(Compiler *c, const Scope *sc); static int desugar_ewi_pack_values(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const calls_of[] = { "each", NULL }; int *calls, ncalls = an_calls_named_any(c, calls_of, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "each")) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || nt_int(nt, blk, "ewi_packed", 0)) continue; Scope *sc = comp_scope_of(c, id); if (!sc || !sc->name || strncmp(sc->name, "__enum_each_with_index__", 24) != 0) continue; int site = enum_copy_site(c, sc); if (site < 0 || site >= n0 || nt_kind(nt, site) != NK_CallNode) continue; int srecv = nt_ref(nt, site, "receiver"); TyKind rt = srecv >= 0 ? infer_type(c, srecv) : TY_UNKNOWN; if (!ty_is_object(rt)) continue; int cid = ty_object_class(rt); if (cid < 0 || cid >= c->nclasses || (c->classes[cid].enum_yield_arity <= 1 && !c->classes[cid].enum_yield_packed)) continue; int bp = nt_ref(nt, blk, "parameters"); int pn = bp >= 0 && nt_kind(nt, bp) == NK_BlockParametersNode ? nt_ref(nt, bp, "parameters") : -1; if (pn < 0 || nt_kind(nt, pn) != NK_ParametersNode) continue; int nreq = 0; const int *reqs = nt_arr(nt, pn, "requireds", &nreq); if (nreq != 1 || nt_kind(nt, reqs[0]) != NK_RequiredParameterNode || nt_ref(nt, pn, "rest") >= 0) continue; const char *pnm = nt_str(nt, reqs[0], "name"); int body = nt_ref(nt, blk, "body"); if (!pnm || body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); if (bn + 1 > 64) continue; int n1 = nt->count; const char *vn = "__ewi_v"; /* the values, kept apart from the element */ int rest = nt_new_node(nt, "RestParameterNode"); nt_node_set_str(nt, rest, "name", vn); nt_node_set_arr(nt, pn, "requireds", NULL, 0); nt_node_set_ref(nt, pn, "rest", rest); int len = te_call(nt, te_lvread(nt, vn), "length", -1, -1); int cmp = te_call(nt, len, "<=", te_args1(nt, nt_new_int(nt, 1)), -1); int first = te_call(nt, te_lvread(nt, vn), "[]", te_args1(nt, nt_new_int(nt, 0)), -1); int els = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, els, "statements", te_stmts1(nt, te_lvread(nt, vn))); int iff = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, iff, "predicate", cmp); nt_node_set_ref(nt, iff, "statements", te_stmts1(nt, first)); nt_node_set_ref(nt, iff, "subsequent", els); int nb[64]; nb[0] = te_lvwrite(nt, pnm, iff); for (int q = 0; q < bn; q++) nb[q + 1] = bb[q]; nt_node_set_arr(nt, body, "body", nb, bn + 1); nt_node_set_int(nt, blk, "ewi_packed", 1); comp_grow_node_arrays(c); for (int q = n1; q < nt->count; q++) c->nscope[q] = c->nscope[blk]; changed = 1; } free(calls); return changed; } /* `obj.map { |x| }` on a user Enumerable whose #each yields SEVERAL values: CRuby hands them to the block, so a one-parameter block binds the first value -- while #select / #find / #sort_by answer the packed element. The same holds for flat_map. The collector packs every yield into one array element, so the map block's sole parameter takes its first value, exactly as a `for` index does (#3879); a lone `*a` takes the values, and a lone `x = d` the first or, when nothing was yielded, its default. When #each yields one value at some sites and several at others, the packed element cannot tell `yield 2, 3` from `yield [2, 3]`, so the block runs over the values as yielded (__enum_to_a_raw) instead. */ static int desugar_multi_yield_map_param(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; static const char *const calls_of[] = { "map", "collect", "flat_map", "collect_concat", NULL }; int *calls, ncalls = an_calls_named_any(c, calls_of, n0, &calls); for (int q = 0; q < ncalls; q++) { int id = calls[q]; if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "map") && !sp_streq(nm, "collect") && !sp_streq(nm, "flat_map") && !sp_streq(nm, "collect_concat"))) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) continue; int cid = ty_object_class(rt); if (cid < 0 || cid >= c->nclasses || (c->classes[cid].enum_yield_arity <= 1 && !c->classes[cid].enum_yield_packed)) continue; int packed = c->classes[cid].enum_yield_packed; if (packed && comp_method_in_chain(c, cid, "__enum_to_a_raw", NULL) < 0) continue; int bp = nt_ref(nt, blk, "parameters"); if (bp < 0) continue; NodeKind bpk = nt_kind(nt, bp); int pn = (bpk == NK_BlockParametersNode) ? nt_ref(nt, bp, "parameters") : bp; if (pn < 0 || nt_kind(nt, pn) != NK_ParametersNode) continue; int nreq = 0; const int *reqs = nt_arr(nt, pn, "requireds", &nreq); int nopt = 0; const int *opts = nt_arr(nt, pn, "optionals", &nopt); int npost = 0; nt_arr(nt, pn, "posts", &npost); int nkw = 0; nt_arr(nt, pn, "keywords", &nkw); int rest = nt_ref(nt, pn, "rest"); if (npost || nkw || nt_ref(nt, pn, "keyword_rest") >= 0 || nt_ref(nt, pn, "block") >= 0) continue; /* the lone parameter: several take the packed element apart as an Array's map does already */ int p0 = -1; if (nreq == 1 && !nopt && rest < 0 && nt_kind(nt, reqs[0]) == NK_RequiredParameterNode) p0 = reqs[0]; else if (!nreq && nopt == 1 && rest < 0 && nt_kind(nt, opts[0]) == NK_OptionalParameterNode) p0 = opts[0]; else if (!nreq && !nopt && rest >= 0 && nt_kind(nt, rest) == NK_RestParameterNode) p0 = rest; if (p0 < 0) continue; const char *pnm = nt_str(nt, p0, "name"); int body = nt_ref(nt, blk, "body"); if (!pnm || body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); /* idempotent: the rewritten body starts with the very assignment below */ if (bn > 0 && nt_kind(nt, bb[0]) == NK_LocalVariableWriteNode && nt_int(nt, bb[0], "map_first", 0)) continue; if (bn + 1 > 64) continue; int n1 = nt->count; /* `x = x[0]`, `a = a[0]`, or `x = x.empty? ? d : x[0]` */ int val = te_call(nt, te_lvread(nt, pnm), "[]", te_args1(nt, nt_new_int(nt, 0)), -1); if (nt_kind(nt, p0) == NK_OptionalParameterNode) { int dflt = nt_ref(nt, p0, "value"); if (dflt < 0) continue; int els = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, els, "statements", te_stmts1(nt, val)); int iff = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, iff, "predicate", te_call(nt, te_lvread(nt, pnm), "empty?", -1, -1)); nt_node_set_ref(nt, iff, "statements", te_stmts1(nt, dflt)); nt_node_set_ref(nt, iff, "subsequent", els); val = iff; /* the element is always there: the default moves into the body */ int req = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, req, "name", pnm); nt_node_set_arr(nt, pn, "requireds", &req, 1); nt_node_set_arr(nt, pn, "optionals", NULL, 0); } int wr = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, wr, "name", pnm); nt_node_set_ref(nt, wr, "value", val); nt_node_set_int(nt, wr, "map_first", 1); { int nb[64]; nb[0] = wr; for (int q = 0; q < bn; q++) nb[q + 1] = bb[q]; nt_node_set_arr(nt, body, "body", nb, bn + 1); } if (packed) { int raw = te_call(nt, recv, "__enum_to_a_raw", -1, -1); nt_node_set_ref(nt, id, "receiver", raw); } comp_grow_node_arrays(c); for (int q = n1; q < nt->count; q++) c->nscope[q] = c->nscope[blk]; if (packed) c->nscope[nt_ref(nt, id, "receiver")] = c->nscope[id]; changed = 1; } free(calls); return changed; } /* What the search reads: the compiler, and the class of the method's `self` (-1 where there is none). A call whose type is that class answers the receiver, as `self` does, and is no value of the block form. */ typedef struct { Compiler *c; int cls; } TeCtx; static int te_tail_is_value(const TeCtx *x, int n); /* Does a def's body answer a VALUE on some path -- a local or ivar read, a literal, an assignment -- as the last expression or through a `return`? The `each`-like idiom answers self, nil or an iterator call, whose value nobody keeps; index_by answers the Hash it built. The tail is read through the iterator (every shape: if, unless, case, case/in, begin, the rescue modifier, `||` and `&&`, parentheses and a `return`'s value); a `return` that hands out several values is judged by its first. A return before the tail is not a value position: te_guard_value reads those. */ static int te_value_leaf(const TeCtx *x, int n) { const NodeTable *nt = x->c->nt; switch (nt_kind(nt, n)) { case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: case NK_LocalVariableWriteNode: case NK_InstanceVariableWriteNode: case NK_HashNode: case NK_ArrayNode: case NK_StringNode: case NK_InterpolatedStringNode: case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_TrueNode: case NK_FalseNode: return 1; /* a call answers its value -- `@a.select { |x| yield x }` an Array -- unless it is an each-like iteration, whose value (the receiver) nobody keeps */ case NK_CallNode: { const char *cn = nt_str(nt, n, "name"); if (!cn) return 0; if (strncmp(cn, "each", 4) == 0 || sp_streq(cn, "times") || sp_streq(cn, "upto") || sp_streq(cn, "downto") || sp_streq(cn, "step") || sp_streq(cn, "loop")) return 0; /* nor is the guard's own Enumerator a value of the block form */ if (sp_streq(cn, "to_enum") || sp_streq(cn, "enum_for") || strncmp(cn, "__to_enum_", 10) == 0) return 0; /* `return self.dup`, `return clone`: the receiver's class is the each-like idiom's self */ if (x->cls >= 0 && infer_type(x->c, n) == ty_object(x->cls)) return 0; return 1; } default: return 0; } } /* One value position of the tail. A walk that gave up, which a tree cannot cause, is a value: the method is left unpinned. */ static int te_tail_leaf(const VpLeaf *leaf, void *ud) { const TeCtx *x = ud; const NodeTable *nt = x->c->nt; if (leaf->kind == VPK_GAVE_UP) return 1; if (leaf->kind == VPK_JUMP && nt_kind(nt, leaf->node) == NK_ReturnNode) { int a = nt_ref(nt, leaf->node, "arguments"); int ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; return ac > 0 && te_tail_is_value(x, av[0]); } return leaf->kind == VPK_EXPR && te_value_leaf(x, leaf->node); } /* A `return v`, `if` or `unless` among the statements before the last of a list answers a value too (the guard `return @cache if @cache`), and so does a return inside a case, a case/in or a begin among them. The lists read are those the tail reaches: through if, unless, parentheses, the clauses of a case or a case/in and its else, the body, the rescue clauses and the else of a begin, and a `return`'s value. A return in a block is not read. */ static int te_guard_value(const TeCtx *x, int n) { const NodeTable *nt = x->c->nt; for (;;) { switch (nt_kind(nt, n)) { case NK_StatementsNode: { int bn = 0; const int *bb = nt_arr(nt, n, "body", &bn); if (bn <= 0) return 0; for (int i = 0; i < bn - 1; i++) { NodeKind k = nt_kind(nt, bb[i]); if (k == NK_ReturnNode || k == NK_IfNode || k == NK_UnlessNode) { if (te_tail_is_value(x, bb[i])) return 1; } else if ((k == NK_CaseNode || k == NK_CaseMatchNode || k == NK_BeginNode) && te_guard_value(x, bb[i])) return 1; } n = bb[bn - 1]; continue; } case NK_IfNode: case NK_UnlessNode: if (te_guard_value(x, nt_ref(nt, n, "statements"))) return 1; n = nt_ref(nt, n, nt_kind(nt, n) == NK_IfNode ? "subsequent" : "else_clause"); continue; case NK_CaseNode: case NK_CaseMatchNode: { int cn = 0; const int *cv = nt_arr(nt, n, "conditions", &cn); for (int i = 0; i < cn; i++) if (te_guard_value(x, nt_ref(nt, cv[i], "statements"))) return 1; n = nt_ref(nt, n, "else_clause"); continue; } case NK_BeginNode: if (te_guard_value(x, nt_ref(nt, n, "statements"))) return 1; for (int r = nt_ref(nt, n, "rescue_clause"); r >= 0; r = nt_ref(nt, r, "subsequent")) if (te_guard_value(x, nt_ref(nt, r, "statements"))) return 1; n = nt_ref(nt, n, "else_clause"); continue; case NK_ElseNode: n = nt_ref(nt, n, "statements"); continue; case NK_ParenthesesNode: n = nt_ref(nt, n, "body"); continue; case NK_ReturnNode: { int a = nt_ref(nt, n, "arguments"); int ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; return ac > 0 && te_tail_is_value(x, av[0]); } default: return 0; } } } static int te_tail_is_value(const TeCtx *x, int n) { if (n < 0) return 0; return for_each_value_position(x->c->nt, n, VP_FOLLOW_RETURN | VP_AND_LEFT | VPS_ALL, te_tail_leaf, (void *)x) || te_guard_value(x, n); } /* Is `call` the value of a `return` guarded by block_given? alone at the top level of `body`: `return to_enum(:m) unless block_given?` or `if !block_given?`? Only then does every blockless call answer the Enumerator. A guard that also tests an argument (`if mapping.nil? && !block_given?`, activesupport's transform_keys) lets a blockless call with the argument run on to the body, so both values come back. */ static int te_pure_block_guard_return(const NodeTable *nt, int body, int call) { int n = 0; const int *b = body >= 0 ? nt_arr(nt, body, "body", &n) : NULL; for (int k = 0; k < n; k++) { NodeKind gk = nt_kind(nt, b[k]); if (gk != NK_IfNode && gk != NK_UnlessNode) continue; if (nt_ref(nt, b[k], "subsequent") >= 0) continue; int stm = nt_ref(nt, b[k], "statements"); int sn = 0; const int *sb = stm >= 0 ? nt_arr(nt, stm, "body", &sn) : NULL; int hit = 0; for (int j = 0; j < sn && !hit; j++) { if (nt_kind(nt, sb[j]) != NK_ReturnNode) continue; int ra = nt_ref(nt, sb[j], "arguments"); int rc = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rc) : NULL; hit = rc == 1 && rv[0] == call; } if (!hit) continue; int bg = nt_ref(nt, b[k], "predicate"); if (gk == NK_IfNode) { if (bg < 0 || nt_kind(nt, bg) != NK_CallNode || !nt_str(nt, bg, "name") || !sp_streq(nt_str(nt, bg, "name"), "!")) return 0; bg = nt_ref(nt, bg, "receiver"); } return bg >= 0 && nt_kind(nt, bg) == NK_CallNode && nt_ref(nt, bg, "receiver") < 0 && nt_str(nt, bg, "name") && sp_streq(nt_str(nt, bg, "name"), "block_given?"); } return 0; } /* Does `node` hold a next, break or redo that leaves the block it is in (none under a nested block, lambda or def counts)? */ static int se_leaves_block(const NodeTable *nt, int node) { if (node < 0) return 0; NodeKind k = nt_kind(nt, node); if (k == NK_NextNode || k == NK_BreakNode || k == NK_RedoNode) return 1; if (k == NK_BlockNode || k == NK_LambdaNode || k == NK_DefNode) return 0; int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (se_leaves_block(nt, nt_ref_at(nt, node, i))) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) if (se_leaves_block(nt, ids[j])) return 1; } return 0; } /* Are the parameters of block `blk` none, or plain requireds alone? */ static int se_plain_block_params(const NodeTable *nt, int blk) { int bp = nt_ref(nt, blk, "parameters"); if (bp < 0) return 1; if (nt_kind(nt, bp) != NK_BlockParametersNode) return 0; int pp = nt_ref(nt, bp, "parameters"); if (pp < 0) return 1; static const char *const arrs[] = { "optionals", "posts", "keywords", NULL }; for (int a = 0; arrs[a]; a++) { int n = 0; nt_arr(nt, pp, arrs[a], &n); if (n) return 0; } return nt_ref(nt, pp, "rest") < 0 && nt_ref(nt, pp, "keyword_rest") < 0 && nt_ref(nt, pp, "block") < 0; } /* `str.to_enum(:scan, re).map { |m| ... }`, and `.each { }`: the Enumerator runs scan, which sets $~ for each match its block sees (`Regexp.last_match` reads it). The blockless scan answers the matches with $~ left at the last one, so the consumer's block becomes the block of the scan itself: .each { body } -> str.scan(re) { body } .map { body } -> (__sm_N = []; str.scan(re) { __sm_N << begin body end }; __sm_N) Only for a block of plain parameters whose body leaves it by none of next, break or redo (a `next v` would push nothing). 1 when rewritten. */ static int desugar_scan_enum_consumer(Compiler *c, int te) { NodeTable *nt = (NodeTable *)c->nt; int par = -1; for (int p = 0; p < nt->count && par < 0; p++) if (nt_kind(nt, p) == NK_CallNode && nt_ref(nt, p, "receiver") == te) par = p; const char *pn = par >= 0 ? nt_str(nt, par, "name") : NULL; int is_map = pn && (sp_streq(pn, "map") || sp_streq(pn, "collect")); if (!pn || (!is_map && !sp_streq(pn, "each"))) return 0; int pa = 0; if (nt_ref(nt, par, "arguments") >= 0) nt_arr(nt, nt_ref(nt, par, "arguments"), "arguments", &pa); int blk = nt_ref(nt, par, "block"); if (pa || blk < 0 || nt_kind(nt, blk) != NK_BlockNode || !se_plain_block_params(nt, blk)) return 0; int body = nt_ref(nt, blk, "body"); if (se_leaves_block(nt, body)) return 0; int recv = nt_ref(nt, te, "receiver"); int ac = 0; const int *av = nt_arr(nt, nt_ref(nt, te, "arguments"), "arguments", &ac); if (recv < 0 || ac != 2) return 0; int re = av[1]; int base = nt->count; int inner = body >= 0 ? body : blk; int sargs = te_args1(nt, re); if (!is_map) { nt_node_set_ref(nt, par, "receiver", recv); nt_node_set_str(nt, par, "name", "scan"); nt_node_set_ref(nt, par, "arguments", sargs); } else { char an[48]; snprintf(an, sizeof an, "__sm_%s", comp_node_tag(c, par)); int empty = nt_new_node(nt, "ArrayNode"); int w = te_lvwrite(nt, an, empty); int beg = nt_new_node(nt, "BeginNode"); if (body >= 0) nt_node_set_ref(nt, beg, "statements", body); int push = te_call(nt, te_lvread(nt, an), "<<", te_args1(nt, beg), -1); int pbody = te_stmts1(nt, push); int inner_n0 = beg; /* the nodes from here to pbody sit in the block */ nt_node_set_ref(nt, blk, "body", pbody); int scan = te_call(nt, recv, "scan", sargs, blk); int seq[3] = { w, scan, te_lvread(nt, an) }; int stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, stmts, "body", seq, 3); nt_node_reset(nt, par, "ParenthesesNode"); nt_node_set_ref(nt, par, "body", stmts); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = j >= inner_n0 && j <= pbody ? c->nscope[inner] : c->nscope[par]; scope_local_intern(comp_scope_of(c, par), an); } nt_node_reset(nt, te, "NilNode"); comp_grow_node_arrays(c); for (int j = base; j < nt->count && !is_map; j++) c->nscope[j] = c->nscope[par]; return 1; } /* A `return v` written inside a block of the method (`@a.each { |x| return x if yield(x) > 1 }`) leaves the method with a value, which neither the tail nor the statements before it show (te_guard_value reads no block): the block form answers it as much as a value tail does. */ static int te_block_return_value(const TeCtx *x, int si) { Compiler *c = x->c; const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_ReturnNode, rid) { if (c->nscope[rid] != si || return_in_lambda(c, rid)) continue; int a = nt_ref(nt, rid, "arguments"); int ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac > 0 && te_tail_is_value(x, av[0])) return 1; } return 0; } /* Rewrite `recv.to_enum(:m, *a)` / `enum_for(:m, *a)` into a real Enumerator, dispatched on the receiver kind (needs the receiver type, so it runs in the fixpoint). A user-class receiver whose class defines a yielding `m` becomes `recv.__to_enum_m` (the synthesized generator helper); a builtin receiver becomes the plain blockless `recv.m(*a)`, which already lowers to a materialized Enumerator. Un-resolved or unsupported receivers are left untouched -- a leftover to_enum/enum_for is loudly rejected downstream (no silent-wrong). Returns 1 if anything changed. */ /* The `return` that hands out the guard's `call`, or -1. */ static int guard_return_of(const NodeTable *nt, int call) { NT_FOREACH_KIND(nt, NK_ReturnNode, rid) { int a = nt_ref(nt, rid, "arguments"); int ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac == 1 && av[0] == call) return rid; } return -1; } static int desugar_to_enum(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { char m[128]; int extra = 0, has_block = 0; if (!to_enum_target(c, id, m, sizeof m, &extra, &has_block)) continue; /* A size block -- `to_enum(:m) { size }`, the blockless-branch idiom of activesupport's index_by / index_with / Array#extract! -- names what Enumerator#size would answer, which is not modelled: the block is dropped once the call is rewritten below (left on the builtin form it would run as the iteration's block). A call the rewrite leaves is refused downstream as before. */ int recv = nt_ref(nt, id, "receiver"); Scope *es = comp_scope_of(c, id); /* receiver type: an explicit receiver's inferred type, else the enclosing self (implicit-self `enum_for(:m)` inside an instance method). */ TyKind rt; if (recv >= 0) rt = infer_type(c, recv); else rt = (es && es->class_id >= 0 && !es->is_cmethod) ? ty_object(es->class_id) : TY_UNKNOWN; int self_recv = recv < 0 || (nt_kind(nt, recv) == NK_SelfNode); /* self is main in a top-level method or the program body: m and its generator are top-level methods */ int toplevel = self_recv && es->class_id < 0 && !es->is_cmethod; /* A boxed receiver -- an Array, or an object whose class iterates by a yielding each (Rack::Lint's `@body.enum_for.to_a`) -- dispatches to the generator helper the classes that have one carry, and the dispatch's default arm builds the builtin's Enumerator; with no such class, __poly_enum_for is that arm alone (emit_poly_enum_for). */ if (rt == TY_POLY && recv >= 0 && !self_recv && extra == 0 && sp_streq(m, "each")) { int any_helper = 0; for (int k = 0; k < c->nclasses && !any_helper; k++) if (comp_method_in_class(c, k, "__to_enum_each") >= 0) any_helper = 1; nt_node_set_str(nt, id, "name", any_helper ? "__to_enum_each" : "__poly_enum_for"); int empty = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, empty, "arguments", NULL, 0); nt_node_set_ref(nt, id, "arguments", empty); if (has_block) nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[empty] = c->nscope[id]; changed = 1; continue; } /* self in a reopened builtin (Array#extract!'s `to_enum(:extract!)`) is boxed, but the helper is the reopening's own, as for a program class */ int reopen_ci = -1; if (self_recv && !ty_is_object(rt) && es && es->class_id >= 0 && !es->is_cmethod && is_builtin_reopen(c->classes[es->class_id].name)) reopen_ci = es->class_id; if (ty_is_object(rt) || toplevel || reopen_ci >= 0) { char hname[160]; snprintf(hname, sizeof hname, "__to_enum_%s", m); int helper = toplevel ? comp_method_index(c, hname) : comp_method_in_chain(c, reopen_ci >= 0 ? reopen_ci : ty_object_class(rt), hname, NULL); if (helper < 0) continue; /* no yielding m: leave */ if (extra > 0) continue; /* user-class to_enum with args: PR follow-up (loud downstream) */ /* `return enum_for(:m) unless block_given?` inside method m: a blockless call to m returns the Enumerator (with a block m runs the body and its return is ignored). Pin m's return type so the blockless call site does not inherit the polluted union of the enumerator and the yield path -- for an `each`-like m, whose block form answers self or nothing. A method whose block form answers a VALUE (activesupport's index_by builds a Hash) is left to the union: pinned, it returned that value through an Enumerator-typed C signature, a type error; widened, a blockless call site reads a boxed Enumerator its consumers dispatch on. */ TeCtx tx = { c, es && es->class_id >= 0 && !es->is_cmethod ? es->class_id : -1 }; int value_form = es && es->body >= 0 && (te_tail_is_value(&tx, es->body) || te_block_return_value(&tx, (int)(es - c->scopes))); /* not in a reopened builtin, whose self tail is the boxed receiver: the blockless call already reads the Enumerator (ret_noblock) */ if (self_recv && es && es->name && sp_streq(es->name, m) && !value_form && reopen_ci < 0 && es->yields && es->ret != TY_ENUMERATOR && te_pure_block_guard_return(nt, es->body, id) && !return_before(c, (int)(es - c->scopes), guard_return_of(nt, id))) { scope_set_ret(es, TY_ENUMERATOR); es->ret_specialized = 1; changed = 1; /* The pin is the blockless call's value. A call with a block runs the body and answers what it answers, as in CRuby (a `self` tail the receiver, an iteration's tail its receiver, a `return self` in a block the receiver): method_call_ret reads the values of the body other than the guard's. */ scope_set_ret_pin_guard(es, id); } nt_node_set_str(nt, id, "name", hname); int empty = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, empty, "arguments", NULL, 0); nt_node_set_ref(nt, id, "arguments", empty); if (has_block) nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[empty] = c->nscope[id]; changed = 1; continue; } if (recv >= 0 && sp_streq(m, "scan") && (rt == TY_STRING || rt == TY_POLY) && desugar_scan_enum_consumer(c, id)) { changed = 1; continue; } /* Builtin receiver: retarget to the blockless iterator (drops the leading method symbol, keeps any trailing args). */ int is_builtin = ty_is_array(rt) || ty_is_hash(rt) || rt == TY_STRING || rt == TY_RANGE || rt == TY_INT; /* A boxed receiver (an Array read out of a container) takes the same blockless form, which its own arms answer an Enumerator for, unless the program defines the method itself. Left alone, `h[:k].to_enum` typed nothing and its `.each { }` raised NoMethodError. */ if (rt == TY_POLY && !an_user_defines_or_reads(c, m)) is_builtin = 1; if (is_builtin && recv >= 0 && to_enum_builtin_method(m)) { int args = nt_ref(nt, id, "arguments"); int ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; int *rest = NULL; if (ac > 1) { rest = malloc(sizeof(int) * (size_t)(ac - 1)); if (!rest) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int j = 1; j < ac; j++) rest[j - 1] = av[j]; /* snapshot before realloc */ } nt_node_set_str(nt, id, "name", m); int newargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, newargs, "arguments", rest, ac > 1 ? ac - 1 : 0); nt_node_set_ref(nt, id, "arguments", newargs); if (has_block) nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[newargs] = c->nscope[id]; free(rest); changed = 1; continue; } /* else: unresolved/unsupported receiver -- leave for a later iteration or a downstream loud reject. */ } return changed; } /* ===== Post-fixpoint: narrow monomorphic object arrays to TY_OBJ_ARRAY ===== A POLY_ARRAY local/param whose every element is an instance of one user class X, and whose every use is in a small supported op set (index, push, length, ...), is narrowed to ty_obj_array(X) -- the runtime sp_PtrArray of unboxed sp_X*, dropping the per-element boxing and cls-id dispatch that a poly array pays on every `arr[i]` / `arr[i].field`. Interprocedural soundness is by an optimistic-then-revoke union-find: a slot flowing (as a positional arg, or an alias `b = a`) into another slot shares one C container type, so the two are unioned and the whole component narrows together or not at all. Strictly conservative: any unmodeled use, class conflict, unresolved flow, or absent object evidence kills the component, leaving it TY_POLY_ARRAY. Runs ONCE, after the fixpoint, so the new type never feeds forward inference. */ /* A slot is a local (lv), a method's value (lv NULL, sidx the method), an @ivar of one class (ici/iiv, sidx -1; #4444), or a constant (lv its entry, sidx OA_CONST). row_cls / row_escape / odd_elem: the evidence the rows a slot's element reads answer give (oa_row_use), for a table whose rows are empty literals */ typedef struct { int sidx; LocalVar *lv; int cls; int alive; int uf; int needs_cmp; int removes; int poly_elem; int row_iter; int saw_call; TyKind old_pin; int ici, iiv; int row_cls, row_escape, odd_elem; } OAS; #define OA_CONST (-2) /* The (sidx, lv) -> first slot index map, so a lookup is not a scan of every slot: the pass asks once per node, and the slot list grows with the program. Built on first use for a given (sl, n) and dropped by oa_index_reset() when the pass starts over; an index is the FIRST matching slot, which is what the scan answered. */ static struct { const OAS *sl; int n, cap; int *tab; } g_oa_ix; static void oa_index_reset(void) { g_oa_ix.sl = NULL; g_oa_ix.n = -1; } static unsigned oa_hash(int sidx, const LocalVar *lv) { uint64_t h = (uint64_t)(uintptr_t)lv * 0x9E3779B97F4A7C15ull ^ (uint64_t)(uint32_t)sidx * 0xC2B2AE3D27D4EB4Full; return (unsigned)(h ^ (h >> 29)); } static int oa_find(OAS *sl, int n, int sidx, LocalVar *lv) { if (g_oa_ix.sl != sl || g_oa_ix.n != n) { int cap = 16; while (cap < 2 * n) cap <<= 1; if (cap > g_oa_ix.cap) { free(g_oa_ix.tab); g_oa_ix.tab = (int *)malloc(sizeof(int) * cap); g_oa_ix.cap = cap; } for (int i = 0; i < g_oa_ix.cap; i++) g_oa_ix.tab[i] = -1; unsigned mask = (unsigned)g_oa_ix.cap - 1; for (int i = 0; i < n; i++) { if (sl[i].ici >= 0) continue; unsigned h = oa_hash(sl[i].sidx, sl[i].lv) & mask; while (g_oa_ix.tab[h] >= 0 && !(sl[g_oa_ix.tab[h]].sidx == sl[i].sidx && sl[g_oa_ix.tab[h]].lv == sl[i].lv)) h = (h + 1) & mask; if (g_oa_ix.tab[h] < 0) g_oa_ix.tab[h] = i; /* keep the first */ } g_oa_ix.sl = sl; g_oa_ix.n = n; } unsigned mask = (unsigned)g_oa_ix.cap - 1; for (unsigned h = oa_hash(sidx, lv) & mask; g_oa_ix.tab[h] >= 0; h = (h + 1) & mask) { const OAS *e = &sl[g_oa_ix.tab[h]]; if (e->sidx == sidx && e->lv == lv) return g_oa_ix.tab[h]; } return -1; } static int oa_uf_find(OAS *sl, int i) { while (sl[i].uf != i) { sl[i].uf = sl[sl[i].uf].uf; i = sl[i].uf; } return i; } static void oa_uf_union(OAS *sl, int a, int b) { int ra = oa_uf_find(sl, a), rb = oa_uf_find(sl, b); if (ra != rb) sl[ra].uf = rb; } /* Element-class sentinels: the array's elements are themselves int-arrays (ExtField-style int[4]) or float-arrays (a numeric table), narrowing the container to TY_INT_ARRAY_ARRAY / TY_FLOAT_ARRAY_ARRAY rather than an object-pointer array. Distinct from -1 (none) / -2 (conflict) and from any user class id (>= 0), so oa_cls_join composes them unchanged -- and distinct from EACH OTHER, so a component mixing int rows and float rows joins to -2 and stays boxed, as two user classes would. */ #define OA_CLS_IA (-1000) #define OA_CLS_FA (-1001) #define OA_CLS_IS_NESTED(x) ((x) == OA_CLS_IA || (x) == OA_CLS_FA) /* --int-overflow=promote: a row of Integers some of which are boxed (`[a, a + b]` once the widening made them poly). It is an Integer row as the program and an `Array[Array[Integer]]` seed mean it, but no unboxed table holds it: an int row joins it to this, and the component stays the boxed table. */ #define OA_CLS_PIA (-1002) #define OA_CLS_IS_ROW(x) (OA_CLS_IS_NESTED(x) || (x) == OA_CLS_PIA) /* the single user-object class of a node's value, a nested scalar-array sentinel, or -1 if neither a lone object nor a lone scalar array. */ static int oa_obj_class_of(Compiler *c, int node) { TyKind t = infer_type(c, node); /* an Array subclass instance is no pointer array's element: its box is its Array's, and the pointer array boxes by the class id it reads first in the object (#7449) */ if (comp_ty_ary_root(c, t) >= 0) return -2; if (ty_is_object(t)) return ty_object_class(t); if (t == TY_INT_ARRAY || t == TY_FLOAT_ARRAY) { /* A literal whose elements are not all settled reads as a scalar array while the fixpoint runs -- an UNKNOWN element unifies away -- and `[obj, -1]` looked like one until `obj` acquired its class. Narrowing on that evidence pinned the container to a table of int arrays, and the pin survived the rounds that knew better (#3781). Take no evidence until every element has a type. */ const NodeTable *nt = c->nt; NodeKind nk = nt_kind(nt, node); if (nk == NK_ArrayNode) { int en = 0; const int *els = nt_arr(nt, node, "elements", &en); for (int k = 0; k < en; k++) if (infer_type(c, els[k]) == TY_UNKNOWN) return -1; } return t == TY_INT_ARRAY ? OA_CLS_IA : OA_CLS_FA; } if (g_promote_mode && t == TY_POLY_ARRAY && nt_kind(c->nt, node) == NK_ArrayNode) { int en = 0; const int *els = nt_arr(c->nt, node, "elements", &en); int ints = en > 0; for (int k = 0; k < en && ints; k++) { TyKind et = nt_kind(c->nt, els[k]) == NK_SplatNode ? TY_UNKNOWN : infer_type(c, els[k]); ints = et == TY_INT || et == TY_POLY || et == TY_BIGINT; } if (ints) return OA_CLS_PIA; } /* A value of another CONCRETE type is evidence against one class: a String or an Integer pushed or `[]=`-stored into the array puts something in it the pointer array cannot hold (`a[0] = "x"` was joined as "nothing seen" and the store then initialised an sp_Vec * from a string, #4444). A type not known yet, nil (a NULL element) and a boxed poly value stay neutral: the poly may well be the class (a widened return, #4293), and the push emitter unboxes it with the class check at run time. */ if (t == TY_UNKNOWN || t == TY_NIL || t == TY_POLY) return -1; return -2; } /* class evidence join: -1 = none seen, -2 = conflicting classes. */ static int oa_cls_join(int a, int b) { if (b == -1) return a; if (a == -1) return b; if (a == b) return a; if ((a == OA_CLS_PIA && b == OA_CLS_IA) || (a == OA_CLS_IA && b == OA_CLS_PIA)) return OA_CLS_PIA; return -2; /* two classes, or a foreign element (-2) from either side */ } /* The empty `[]` literals that flowed into a slot as ELEMENTS this round: a row of `Array.new(n) { [] }`, an element of `[[], []]`, the value of a push or a `[]=`. An empty literal is no evidence about the element kind (it infers UNKNOWN until a use fixes it) and is neutral in the join; when the component does narrow to a table, from another row or from an `Array[Array[Float]]` seed, the literal has to be BUILT as a row of that kind, or the table would hold a poly array where its reader casts an sp_FloatArray *. arr_want is the durable pin an empty literal takes its kind from (mark_empty_array_operands uses the same one), so a stamp here is seen by the next round's inference and by codegen (#4484). */ static int *g_oa_empt_slot, *g_oa_empt_node, g_oa_empt_n, g_oa_empt_cap; /* The empty literals this pass itself stamped a row kind on, kept across rounds: each round clears them on the way in, beside its map sources, and stamps again only the rows of a component that still narrows. Only its own -- other producers stamp empty `[]` literals too. */ static unsigned char *g_oa_empt_mine; static int g_oa_empt_mine_cap; /* its bits: this pass stamped the row; a seed built it (oa_mark_seed_row) */ #define OA_MINE 1 #define OA_SEED_ROW 2 static void oa_mark_empty_mine(int lit) { if (lit >= g_oa_empt_mine_cap) { int nc = g_oa_empt_mine_cap ? g_oa_empt_mine_cap : 256; while (nc <= lit) nc *= 2; g_oa_empt_mine = (unsigned char *)realloc(g_oa_empt_mine, (size_t)nc); if (!g_oa_empt_mine) { fprintf(stderr, "oom\n"); exit(1); } memset(g_oa_empt_mine + g_oa_empt_mine_cap, 0, (size_t)(nc - g_oa_empt_mine_cap)); g_oa_empt_mine_cap = nc; } g_oa_empt_mine[lit] |= OA_MINE; } /* An empty row a seed built at its kind (oa_seeded_literal_rows) carries a second mark: the seed is trusted, so widen_nested_literals leaves it to the seed's kind as it leaves the rows of `Array.new(n) { [] }`, and the push into it converts its value or is refused, as a push into those does */ static void oa_mark_seed_row(int lit) { oa_mark_empty_mine(lit); g_oa_empt_mine[lit] |= OA_SEED_ROW; } int oa_seed_row_literal(int lit) { return lit >= 0 && lit < g_oa_empt_mine_cap && (g_oa_empt_mine[lit] & OA_SEED_ROW); } /* Nodes whose own emitted type has to follow the slot they feed: a `idx.map { |k| cols[k] }` builds the table in place, so narrowing the slot without retyping the map leaves the emitter building a poly array and assigning it to a narrowed one -- which does not compile. The empty-literal list beside this one does the same job for `[]`. */ static int *g_oa_src_slot, *g_oa_src_node, g_oa_src_n, g_oa_src_cap; static void oa_note_src(int S, int node) { if (node < 0) return; if (g_oa_src_n >= g_oa_src_cap) { g_oa_src_cap = g_oa_src_cap ? g_oa_src_cap * 2 : 64; g_oa_src_slot = (int *)realloc(g_oa_src_slot, sizeof(int) * (size_t)g_oa_src_cap); g_oa_src_node = (int *)realloc(g_oa_src_node, sizeof(int) * (size_t)g_oa_src_cap); if (!g_oa_src_slot || !g_oa_src_node) { fprintf(stderr, "oom\n"); exit(1); } } g_oa_src_slot[g_oa_src_n] = S; g_oa_src_node[g_oa_src_n] = node; g_oa_src_n++; } static void oa_note_empty(Compiler *c, int S, int node) { if (!is_empty_array_literal(c->nt, node, c->node_cap)) return; if (g_oa_empt_n >= g_oa_empt_cap) { g_oa_empt_cap = g_oa_empt_cap ? g_oa_empt_cap * 2 : 64; g_oa_empt_slot = (int *)realloc(g_oa_empt_slot, sizeof(int) * (size_t)g_oa_empt_cap); g_oa_empt_node = (int *)realloc(g_oa_empt_node, sizeof(int) * (size_t)g_oa_empt_cap); if (!g_oa_empt_slot || !g_oa_empt_node) { fprintf(stderr, "oom\n"); exit(1); } } g_oa_empt_slot[g_oa_empt_n] = S; g_oa_empt_node[g_oa_empt_n] = node; g_oa_empt_n++; } /* The class evidence of a boxed local's writes. A boxed value is neutral evidence (oa_obj_class_of), but a local is boxed because its writes disagree: `t = buf.last` and `t = Text.new` both reach `buf << t`, and the Text is an element of another class that the pointer array would hold under the wrong struct. Its writes are the evidence it has: a plain `=`, `||=` or `&&=` gives its value's class. A value that comes in another way (a parameter's or a block parameter's, a multiple-assignment or rescue target, an op-assign) is not seen here, so the local is no evidence of one class (-2): taken as its writes alone, `t = A.new if t.nil?; a << t` narrowed `a` to A while t could hold the B a caller passed. */ static int oa_local_write_evidence(Compiler *c, int node) { const NodeTable *nt = c->nt; int s = c->nscope[node]; const char *nm = nt_str(nt, node, "name"); if (!nm || s < 0 || s >= c->nscopes) return -2; LocalVar *lv = scope_local(&c->scopes[s], nm); if (!lv || lv->is_param || lv->is_block_param) return -2; int cls = -1; for (int w = comp_lvw_first_sc(c, s, nm); w >= 0 && cls != -2; w = comp_lvw_next_sc(c, w)) { const char *wn = nt_str(nt, w, "name"); if (c->nscope[w] != s || !wn || !sp_streq(wn, nm)) continue; NodeKind wk = nt_kind(nt, w); if (wk == NK_LocalVariableWriteNode || wk == NK_LocalVariableOrWriteNode || wk == NK_LocalVariableAndWriteNode) cls = oa_cls_join(cls, oa_obj_class_of(c, nt_ref(nt, w, "value"))); else cls = -2; } return cls; } /* element evidence from one value flowing into slot S, noting an empty literal for the stamp above and a boxed element for the row check at resolution */ static int oa_elem_evidence(Compiler *c, OAS *sl, int S, int node) { oa_note_empty(c, S, node); int ec; if (node >= 0 && infer_type(c, node) == TY_POLY) { sl[S].poly_elem = 1; ec = nt_kind(c->nt, node) == NK_LocalVariableReadNode ? oa_local_write_evidence(c, node) : oa_obj_class_of(c, node); } else ec = oa_obj_class_of(c, node); /* a neutral element that is no empty row (a nil, a value with no type yet): the rows' own pushes cannot decide such a table (oa_row_use) */ if (ec == -1 && !is_empty_array_literal(c->nt, node, c->node_cap)) sl[S].odd_elem = 1; return ec; } /* `[A.new, A.new]` passed straight to a method: a literal of objects only, built at the parameter's kind when that narrows (oa_note_src) */ static int oa_obj_literal(Compiler *c, int node) { const NodeTable *nt = c->nt; if (node < 0 || nt_kind(nt, node) != NK_ArrayNode) return 0; int en = 0; const int *el = nt_arr(nt, node, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_SplatNode || !ty_is_object(infer_type(c, el[e]))) return 0; return en > 0; } /* The filters: 1 for reject!, select! and filter!, which answer the array or nil, and 2 for select, filter and reject, which answer a new array of the same elements. The block takes the element as one plain parameter, or none, and is not a block whose parameters desugar_builtin_iter_block_shapes lowered; 0 for any other call. */ static int oa_filter_call(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); int blk = nt_ref(nt, id, "block"); int args = nt_ref(nt, id, "arguments"), argc = 0; if (args >= 0) nt_arr(nt, args, "arguments", &argc); if (!nm || argc != 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; if (nt_int(nt, blk, "bs_lowered", 0)) return 0; if (block_param_name(c, blk, 1) || block_param_is_multi(c, blk, 0) || block_opt_name(c, blk, 0) || block_post_name(c, blk, 0) || block_keyword_name(c, blk, 0) || block_rest_marker(c, blk)) return 0; if (is_select_reject_bang(nm)) return 1; if (is_select_alias(nm) || sp_streq(nm, "reject")) return 2; return 0; } /* narrow_object_arrays' last arm for a call on slot S: a filter claims its receiver recv, and any other call answers 0, which kills the slot. The filters bind an sp_X * element, as the walks do (bit 1), and have emitters for an object array only (the removes bit). A filter with a bang answers the array or nil, which a consumer would read as the boxed array: it narrows only where its value is thrown away (walk_disc). A filter without a bang answers a new array of the same class, an edge to the slot that takes it (call_ret), or a value thrown away: value_ok also passes the last statement of a block, whose value the block's caller reads, so the copy needs walk_disc too. */ static int oa_filter_arm(Compiler *c, int id, int recv, OAS *sl, int S, char *claimed, int *call_ret, char *value_ok, const char *walk_disc, int nc) { int fk = oa_filter_call(c, id); if (!fk) return 0; claimed[recv] = 1; sl[S].removes = 1; sl[S].row_iter |= 1; int disc = id < nc && walk_disc[id]; if (fk == 1 && !disc) sl[S].alive = 0; if (fk == 2 && id < nc) { call_ret[id] = S; value_ok[id] &= disc; } return 1; } /* narrow_object_arrays' arm for a walk on slot S that yields the row: each, each_with_index, zip, map and reduce (nested_row_iter_call). Admitted so the slot can still narrow. A nested numeric table takes all of them, and a walk whose block takes the row apart (ROW_WALK_SPLITS, bit 16). An object array takes the walks -- each, reverse_each, each_entry, each_with_index, map, collect -- whose emitters bind an sp_X * element (#4846); a reduce, an inject or a zip (bit 2) keeps it boxed, those emitters walking only numeric rows. An each-family walk answers its receiver: one whose value is used hands the array on (bit 8). A map answers a new array. Answers 0 for any other call. */ static int oa_walk_arm(Compiler *c, int id, int recv, OAS *sl, int S, char *claimed, const char *walk_disc, int nc) { const char *name = nt_str(c->nt, id, "name"); int walk = nested_row_iter_call(c, id), splits = walk == ROW_WALK_SPLITS; if (walk == ROW_WALK_NONE) return 0; claimed[recv] = 1; int fold = name && (sp_streq(name, "reduce") || sp_streq(name, "inject") || sp_streq(name, "zip")); if (splits) sl[S].row_iter |= 16; else { sl[S].row_escape = 1; /* the block binds each row */ sl[S].row_iter |= fold ? 2 : 1; } if (!fold && !(name && is_map_alias(name)) && !(id < nc && walk_disc[id])) sl[S].row_iter |= 8; return 1; } static int oa_recv_op_ok(const char *nm, int argc, int has_block) { if (!nm || has_block) return 0; if ((is_element_at_alias(nm)) && argc == 1) return 1; if (sp_streq(nm, "[]=") && argc == 2) return 1; if (is_push_alias(nm) && argc >= 1) return 1; if (is_len_alias(nm) && argc == 0) return 1; if (sp_streq(nm, "empty?") && argc == 0) return 1; if ((is_endpoint_query(nm)) && argc == 0) return 1; /* the removals: pop and shift answer an element, delete_at the element at an Integer index (step 4 checks the type), and clear the array itself */ if ((sp_streq(nm, "pop") || sp_streq(nm, "shift") || sp_streq(nm, "clear")) && argc == 0) return 1; if (sp_streq(nm, "delete_at") && argc == 1) return 1; /* no-block comparisons: usable when the element class has `<=>` (the needs_cmp bit, checked at component resolution). sort's RESULT must also land in a modeled place (slot alias or statement position) -- step 4 kills the component otherwise, so an escaping `arr.sort.map` keeps today's boxed poly path instead of becoming a reject. */ if ((is_minmax_query(nm)) && argc == 0) return 1; if ((is_sort_family(nm)) && argc == 0) return 1; return 0; } /* A row an element read `rd` of slot S answers (`t[i]`, `t.first`). The rows of `Array.new(n) { [] }` are empty literals, which say nothing of their kind; the pushes into them do, and a table of them narrows on that alone (step 7) when the pass sees every use of every row. So a row may be: - pushed Integers or Floats (`t[i] << v`): evidence, a row kind per value, and the push answers the row, so its value must be thrown away; - read in a way its typed row answers (an element, a length, a walk); - thrown away. Anything else hands the row on, where a push the pass does not see could store a value of another kind into a typed row: row_escape. */ static void oa_row_use(Compiler *c, OAS *sl, int S, int rd, const int *par, const char *walk_disc, int nc) { const NodeTable *nt = c->nt; int p = par[rd]; if (p < 0 || nt_kind(nt, p) != NK_CallNode || nt_ref(nt, p, "receiver") != rd) { if (!(rd < nc && walk_disc[rd])) sl[S].row_escape = 1; return; } const char *nm = nt_str(nt, p, "name"); int args = nt_ref(nt, p, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; int blk = nt_ref(nt, p, "block"); if (!nm || (blk >= 0 && nt_kind(nt, blk) != NK_BlockNode)) { sl[S].row_escape = 1; return; } if (is_push_alias(nm) && argc >= 1 && blk < 0) { for (int a = 0; a < argc; a++) { TyKind at = nt_kind(nt, argv[a]) == NK_SplatNode ? TY_UNKNOWN : infer_type(c, argv[a]); sl[S].row_cls = oa_cls_join(sl[S].row_cls, at == TY_INT ? OA_CLS_IA : at == TY_FLOAT ? OA_CLS_FA : -2); } if (!(p < nc && walk_disc[p])) sl[S].row_escape = 1; return; } if (blk < 0 && ((is_element_at_alias(nm) && argc == 1) || ((is_len_alias(nm) || is_endpoint_query(nm) || sp_streq(nm, "empty?") || is_minmax_query(nm) || sp_streq(nm, "sum")) && argc == 0) || (sp_streq(nm, "include?") && argc == 1))) return; /* a walk answers the row, so its value too must be thrown away */ if (blk >= 0 && argc == 0 && (sp_streq(nm, "each") || sp_streq(nm, "each_with_index")) && p < nc && walk_disc[p]) return; sl[S].row_escape = 1; } /* An @ivar (or a bare attr_reader that returns one) passed to a helper whose parameter was push-widened (a `<<` inside pushed an element the bound type could not hold, e.g. an object into an empty-`[]` int array) shares its backing storage with that parameter, so the ivar must widen to the poly array too -- otherwise the call copy-converts the narrow int array and the helper's push lands on the throwaway copy. Runs post-fixpoint, when push_widened is final (during the fixpoint it is set only after bind_call_params has already observed the param). Monotone: typed array -> poly array only. (#3154) */ /* The sibling of the pass below, for the other way an ivar's storage escapes: read out through a READER into a slot that is a poly array. `e = m.a` over `def initialize; @a = ["seed"]; end` binds a poly local, because `e << 7` puts an Integer in it. The ivar stayed a str array, so the emitter widened at the assignment -- and widening an array is a COPY, so the push landed on the copy and `equal?` was false where CRuby says true (#4412). Widening the ivar removes the conversion instead of copying through it, which is what the sibling pass already does for the argument shape (#3154). Monotone and narrow: only a typed array becomes a poly array, only when the slot it is read into is already one. A reader is a method whose body is a bare ivar read, which is what both `attr_reader :a` and `def a; @a; end` come to. */ static const char *an_reader_ivar_name(Compiler *c, int call, const char *mname, int cls) { if (cls < 0 || !mname) return NULL; /* attr_reader is synthesized, so it has no scope to read a body out of: comp_resolve_member is what knows the name is an attribute, and its backing ivar is the name with an @ in front (see the attr_reader backing-ivar rule). */ { int def_cls = -1, mix = -1; if (comp_resolve_member(c, cls, mname, 0, &def_cls, &mix) == SP_MEMBER_ATTR) { static char ivb[128]; if (strlen(mname) < sizeof ivb - 2) { ivb[0] = '@'; strcpy(ivb + 1, mname); return ivb; } return NULL; } } int mi = call_target(c, call, cls, mname, 0, NULL); if (mi < 0) return NULL; Scope *m = &c->scopes[mi]; if (m->body < 0) return NULL; int n = 0; const int *st = nt_arr(c->nt, m->body, "body", &n); if (n != 1 || !st) return NULL; NodeKind bk = nt_kind(c->nt, st[0]); if (bk != NK_InstanceVariableReadNode) return NULL; return nt_str(c->nt, st[0], "name"); } static void widen_ivars_read_into_poly(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, id) { int v = nt_ref(nt, id, "value"); if (v < 0 || nt_kind(nt, v) != NK_CallNode) continue; { int ca = nt_ref(nt, v, "arguments"); int an = 0; if (ca >= 0) nt_arr(nt, ca, "arguments", &an); if (an > 0 || nt_ref(nt, v, "block") >= 0) continue; } int recv = nt_ref(nt, v, "receiver"); if (recv < 0) continue; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) continue; const char *mname = nt_str(nt, v, "name"); if (!mname) continue; int rcls = ty_object_class(rt); const char *ivn = an_reader_ivar_name(c, v, mname, rcls); if (!ivn) continue; /* the slot being written must already be a poly array: that is what says the program puts something in it the ivar's element type cannot hold */ Scope *ws = comp_scope_of(c, id); const char *wn = nt_str(nt, id, "name"); LocalVar *wl = (ws && wn) ? scope_local(ws, wn) : NULL; if (!wl || wl->type != TY_POLY_ARRAY) continue; int ivi = comp_ivar_index(&c->classes[rcls], ivn); if (ivi < 0) continue; TyKind ivt = c->classes[rcls].ivar_types[ivi]; if (!ty_is_array(ivt) || ivt == TY_POLY_ARRAY) continue; ivar_set_type(&c->classes[rcls], ivi, TY_POLY_ARRAY); } /* And the shape with no local at all: a push straight through the reader, `self.errors << "blank"` over `@errors = []`. The empty literal defaults to an int array, so the String met sp_IntArray_push and the C compiler refused it -- the same lock seen from the side where no conversion exists to insert. Widen when the ivar's element type cannot hold what is pushed; an int array pushed an int stays an int array. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "<<") && !sp_streq(nm, "push"))) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_CallNode) continue; { int ca = nt_ref(nt, recv, "arguments"); int an = 0; if (ca >= 0) nt_arr(nt, ca, "arguments", &an); if (an > 0 || nt_ref(nt, recv, "block") >= 0) continue; } int rr = nt_ref(nt, recv, "receiver"); if (rr < 0) continue; TyKind rt = infer_type(c, rr); if (!ty_is_object(rt)) continue; int rcls = ty_object_class(rt); const char *rn = nt_str(nt, recv, "name"); const char *ivn = rn ? an_reader_ivar_name(c, recv, rn, rcls) : NULL; if (!ivn) continue; int ivi = comp_ivar_index(&c->classes[rcls], ivn); if (ivi < 0) continue; TyKind ivt = c->classes[rcls].ivar_types[ivi]; if (!ty_is_array(ivt) || ivt == TY_POLY_ARRAY) continue; int ca2 = nt_ref(nt, id, "arguments"); int an2 = 0; const int *av2 = ca2 >= 0 ? nt_arr(nt, ca2, "arguments", &an2) : NULL; if (!av2 || an2 != 1) continue; TyKind at = infer_type(c, av2[0]); if (at == TY_UNKNOWN) continue; if (at == ty_array_elem(ivt)) continue; ivar_set_type(&c->classes[rcls], ivi, TY_POLY_ARRAY); } } static void widen_ivars_from_pushed_params(Compiler *c) { const NodeTable *nt = c->nt; /* An ivar a caller's array comes from through locals and method values (`x = box.items; fill(x)`, `def get(i) = @a`): the binding marked it and pinned the slots on the way, and the ivar widens here with them. */ for (int id = 0; c->ivar_widen_src && id < nt->count && id < c->node_cap; id++) { if (!c->ivar_widen_src[id]) continue; const char *ivn = NULL; int cls = ivar_src_slot(c, id, &ivn); int ivi = cls >= 0 ? comp_ivar_index(&c->classes[cls], ivn) : -1; if (ivi < 0) continue; TyKind ivt = c->classes[cls].ivar_types[ivi]; if (ty_is_array(ivt) && !ty_is_ptr_array(ivt)) ivar_set_type(&c->classes[cls], ivi, TY_POLY_ARRAY); } NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *name = nt_str(nt, id, "name"); if (!name) continue; int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (!av || an == 0) continue; /* resolve the callee scope */ int recv = nt_ref(nt, id, "receiver"); int mi = -1; if (recv < 0) mi = comp_self_call_mi(c, id, name); else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) mi = call_target(c, id, ty_object_class(rt), name, 0, NULL); /* `Held.add(@found)` / `W.new(@found)`: a singleton method (or the constructor) shares the ivar's backing storage with its parameter exactly like an instance method does, and was the one caller shape this resolution missed -- the pushed element was stored as the int array's own kind, or dropped (#4213; the same arm #3505 added to the nil-marking loop). */ else if (nt_kind(nt, recv) == NK_ConstantReadNode) { int rci = const_node_class_index(c, recv); if (rci >= 0) mi = sp_streq(name, "new") ? comp_method_in_chain(c, rci, "initialize", NULL) : call_target(c, id, rci, name, CT_CMETHOD, NULL); } } if (mi < 0) continue; Scope *m = &c->scopes[mi]; Scope *asc = comp_scope_of(c, id); if (!asc || asc->class_id < 0) continue; ClassInfo *acls = &c->classes[asc->class_id]; /* each parameter against the argument the call's layout hands it */ ArgLayout L; call_layout(c, m, av, an, &L); for (int k = 0; k < m->nparams; k++) { int a = layout_plain_arg(c, m, av, &L, k); LocalVar *p = a >= 0 && m->pnames[k] ? scope_local(m, m->pnames[k]) : NULL; if (!p) continue; /* Two ways in. A typed parameter that was push-widened says so directly. A BOXED one hid its container from the call site, so it carries the element it pushes instead and the comparison happens here, where the ivar's own element type is in hand -- widen only when the ivar cannot hold what the callee pushes, so an int array stays an int array. */ int boxed_hazard = 0; if (!p->push_widened) { if (p->type != TY_POLY || (p->boxed_push_elem == TY_UNKNOWN && !p->store_val_src && !p->store_elems_src && !p->store_rest_src)) continue; boxed_hazard = 1; } const char *aty = nt_type(nt, a); NodeKind ak = nt_kind(nt, a); if (!aty) continue; char ivbuf[128]; const char *ivn = NULL; if (ak == NK_InstanceVariableReadNode) { ivn = nt_str(nt, a, "name"); /* already "@name" */ } else if (ak == NK_CallNode && nt_ref(nt, a, "receiver") < 0) { const char *cn = nt_str(nt, a, "name"); /* a bare attr_reader call */ if (cn && cn[0] != '@' && strlen(cn) < sizeof ivbuf - 1) { ivbuf[0] = '@'; strcpy(ivbuf + 1, cn); ivn = ivbuf; } } if (!ivn) continue; int ivi = comp_ivar_index(acls, ivn); if (ivi < 0) continue; TyKind ivt = acls->ivar_types[ivi]; if (!ty_is_array(ivt) || ivt == TY_POLY_ARRAY) continue; if (boxed_hazard && (p->boxed_push_elem == TY_UNKNOWN || ty_array_elem(ivt) == p->boxed_push_elem) && !param_src_misfits(c, m, p, ivt, av, &L) && !param_rest_misfits(c, m, p, ivt, av, an)) continue; ivar_set_type(acls, ivi, TY_POLY_ARRAY); } arg_layout_free(&L); } } /* Narrow an @ivar holding a table of int arrays. An ivar is harder than a local in one way that matters: its references are spread over every method of the class and can escape through a reader, a bare return or an argument, and a missed one here is not a lost optimization but an sp_PtrArray * meeting sp_RbVal in the emitted C. So the vetting is deliberately absolute -- EVERY reference to the ivar, anywhere in the program, must be the receiver of an op this pass models, from a method of the owning class itself; anything else leaves the slot alone. An attr reader/writer is an escape by definition and disqualifies the ivar outright. Placement matters as much as the vetting: this runs inside narrow_object_arrays, after every pass that re-derives an ivar type by unifying it with its writes. Earlier, that re-derivation would unify TY_INT_ARRAY_ARRAY with the write's TY_POLY_ARRAY -- two array kinds, which unify to the plain poly scalar -- and the slot would come out WORSE than it went in. */ static int narrow_int_table_ivars(Compiler *c, int in_round) { int narrowed = 0; /* Re-assert first. The slot's own write still reads TY_POLY_ARRAY and ~90 sites derive an ivar type from one, so something re-derives this one on nearly every round -- and TY_INT_ARRAY_ARRAY unified with TY_POLY_ARRAY is the plain poly SCALAR, strictly worse than either. Guarding every writer is not practical; restoring the pinned type on each round is what ivar_str_shared already does for the same reason. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { if (!cl->ivar_int_table[iv]) continue; TyKind want = cl->ivar_oa_type[iv] != TY_UNKNOWN ? cl->ivar_oa_type[iv] : TY_INT_ARRAY_ARRAY; if (cl->ivar_types[iv] != want) ivar_set_type(cl, iv, want); } } const NodeTable *nt = c->nt; /* The ivar reads and writes, in node order, listed once and grouped by name: each candidate slot below walked the whole table for them, and then every ivar node of every name. And for a read, the first call it is the receiver of -- the only one the check below looks at -- which it found by walking the whole table again, per read. */ int *ivl = NULL, nivl = 0, *ivl_next = NULL, *first_call = NULL, *parent_of = NULL; const char **ivl_name = NULL; int ivl_buckets = 0, *ivl_head = NULL; for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { /* A table this pass pinned is vetted again on every round: what it was pinned on can change. `@banks[0] = a1` pinned @banks while a1, through a chain of methods written below their callees, still answered the int array an append on a receiver not typed yet makes; a1 then settled boxed, and the boxed row was read back as a bare sp_IntArray *. narrow_object_arrays' own pin (ivar_oa_type) is decided again by that pass. The call after the fixpoint keeps a pinned table: a local that only ever held nil has been declared boxed by then, and `x = nil; @t[i] = x` would read as a boxed row, with no round left to retype what was read out of the table. */ int pinned = cl->ivar_int_table[iv]; if (pinned && (!in_round || cl->ivar_oa_type[iv] != TY_UNKNOWN)) continue; if (!ivl) { ivl = malloc(sizeof(int) * (size_t)(nt->count + 1)); if (!ivl) return narrowed; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_InstanceVariableReadNode || k == NK_InstanceVariableWriteNode || k == NK_InstanceVariableOperatorWriteNode || k == NK_InstanceVariableOrWriteNode || k == NK_InstanceVariableAndWriteNode) ivl[nivl++] = id; } /* name -> its ivar nodes, ascending (chained in reverse, walked forward) */ ivl_buckets = nivl > 0 ? nivl : 1; ivl_head = malloc(sizeof(int) * (size_t)ivl_buckets); ivl_next = malloc(sizeof(int) * (size_t)(nivl + 1)); ivl_name = malloc(sizeof(char *) * (size_t)(nivl + 1)); first_call = malloc(sizeof(int) * (size_t)(nt->count + 1)); if (!ivl_head || !ivl_next || !ivl_name || !first_call) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int b = 0; b < ivl_buckets; b++) ivl_head[b] = -1; for (int li = nivl - 1; li >= 0; li--) { const char *nm = nt_str(nt, ivl[li], "name"); ivl_name[li] = nm; unsigned h = 2166136261u; for (const char *q = nm ? nm : ""; *q; q++) { h ^= (unsigned char)*q; h *= 16777619u; } int b = (int)(h % (unsigned)ivl_buckets); ivl_next[li] = ivl_head[b]; ivl_head[b] = li; } for (int u = 0; u < nt->count; u++) first_call[u] = -1; for (int u = nt->count - 1; u >= 0; u--) { /* descending: the lowest call wins */ if (nt_kind(nt, u) != NK_CallNode) continue; int r = nt_ref(nt, u, "receiver"); if (r >= 0 && r < nt->count) first_call[r] = u; } } if (!pinned && cl->ivar_types[iv] != TY_POLY_ARRAY) continue; const char *ivn = cl->ivars[iv]; if (!ivn || !ivn[0]) continue; if (class_ivar_pinned(cl, ivn)) continue; /* an --rbs seed owns it */ /* this pass makes int tables only; a slot declared `Array[Array[Float]]` is narrow_object_arrays' to decide, where the seed is evidence and an int row against it is the contradiction reported after the fixpoint rather than a table of the other kind (#4484) */ if (cl->ivar_oa_seed[iv] == SEED_OA_FLT_TABLE) continue; const char *bare = ivn[0] == '@' ? ivn + 1 : ivn; if (comp_is_reader(cl, bare) || comp_is_writer(cl, bare)) continue; if (comp_is_sg_reader(cl, bare) || comp_is_sg_writer(cl, bare)) continue; int ok = 1, saw_table = 0; unsigned hb = 2166136261u; for (const char *q = ivn; *q; q++) { hb ^= (unsigned char)*q; hb *= 16777619u; } for (int li = ivl_head[hb % (unsigned)ivl_buckets]; li >= 0 && ok; li = ivl_next[li]) { if (!ivl_name[li] || !sp_streq(ivl_name[li], ivn)) continue; int id = ivl[li]; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; int is_read = ty_k == NK_InstanceVariableReadNode; int is_write = ty_k == NK_InstanceVariableWriteNode; int is_owrite = ty_k == NK_InstanceVariableOperatorWriteNode || ty_k == NK_InstanceVariableOrWriteNode || ty_k == NK_InstanceVariableAndWriteNode; if (!is_read && !is_write && !is_owrite) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, ivn)) continue; Scope *sc = comp_scope_of(c, id); if (!sc) { ok = 0; break; } if (sc->class_id != ci) { /* An unrelated class with an ivar of the same name is a different slot -- ignore it. A subclass (or a module transplanted in) reads THIS slot from code this pass does not vet, so that disqualifies; so does an ancestor, whose methods the class inherits and runs on its own copy of the slot (a subclass rewriting an ancestor's Float table as an Integer one was pinned while the ancestor stayed boxed: the two layouts disagreed). */ int related = 0; for (int k = sc->class_id; k >= 0; k = c->classes[k].parent) if (k == ci) { related = 1; break; } for (int k = c->classes[ci].parent; k >= 0 && !related; k = c->classes[k].parent) if (k == sc->class_id) related = 1; if (!related) continue; ok = 0; break; } if (is_owrite) { ok = 0; break; } /* `@t ||= ...`: unmodeled */ if (is_write) { int v = nt_ref(nt, id, "value"); const char *vty = v >= 0 ? nt_type(nt, v) : NULL; NodeKind vk = nt_kind(nt, v); if (!vty) { ok = 0; break; } if (vk == NK_NilNode) continue; /* neutral */ if (vk == NK_ArrayNode) { int en = 0; const int *el = nt_arr(nt, v, "elements", &en); if (en == 0) continue; /* `@t = []` */ for (int e = 0; e < en && ok; e++) if (infer_type(c, el[e]) != TY_INT_ARRAY) ok = 0; if (ok) saw_table = 1; continue; } /* `@t = Array.new(n) { }` */ if (vk != NK_CallNode) { ok = 0; break; } const char *cn = nt_str(nt, v, "name"); int crecv = nt_ref(nt, v, "receiver"); int gb = nt_ref(nt, v, "block"); if (!cn || !sp_streq(cn, "new") || crecv < 0 || gb < 0 || nt_kind(nt, crecv) != NK_ConstantReadNode || !nt_str(nt, crecv, "name") || !sp_streq(nt_str(nt, crecv, "name"), "Array")) { ok = 0; break; } int gbody = nt_ref(nt, gb, "body"); int gn = 0; const int *gs = gbody >= 0 ? nt_arr(nt, gbody, "body", &gn) : NULL; if (!gs || gn <= 0 || infer_type(c, gs[gn - 1]) != TY_INT_ARRAY) { ok = 0; break; } saw_table = 1; continue; } /* a read: it must BE the receiver of a modeled op, nothing else */ int used = 0; for (int u = first_call[id]; u >= 0; u = -1) { const char *un = nt_str(nt, u, "name"); int ua = nt_ref(nt, u, "arguments"); int uan = 0; if (ua >= 0) nt_arr(nt, ua, "arguments", &uan); /* sort/min/max need the boxed comparator, which this element kind has no arm for, and the removals are typed only for an object array: leave those tables alone */ if (un && (sp_streq(un, "sort") || sp_streq(un, "sort!") || sp_streq(un, "min") || sp_streq(un, "max") || sp_streq(un, "pop") || sp_streq(un, "shift") || sp_streq(un, "clear") || sp_streq(un, "delete_at"))) { used = 0; break; } if (!oa_recv_op_ok(un, uan, nt_ref(nt, u, "block") >= 0)) { used = 0; break; } /* what a store puts in the table is a row: a boxed value or another kind would be taken as a bare sp_IntArray *. A value not typed yet waits for a later round, since a pinned table is not vetted again; an empty `[]` is typed by the table it lands in. */ if (un && (sp_streq(un, "[]=") || sp_streq(un, "push") || sp_streq(un, "<<") || sp_streq(un, "append"))) { const int *uav = nt_arr(nt, ua, "arguments", &uan); int rows_ok = 1; for (int a = sp_streq(un, "[]=") ? 1 : 0; a < uan && rows_ok; a++) { TyKind at = infer_type(c, uav[a]); if (at == TY_UNKNOWN && is_empty_array_literal(nt, uav[a], c->node_cap)) continue; if (at != TY_INT_ARRAY && at != TY_NIL) rows_ok = 0; } if (!rows_ok) break; /* an append answers the table: dropped, or it is another reference to it that this scan does not follow */ if (!sp_streq(un, "[]=")) { if (!parent_of) parent_of = an_parent_map(nt); if (!parent_of || !an_value_dropped(nt, parent_of, u)) break; } } used = 1; break; } if (!used) { ok = 0; break; } } if (ok && saw_table) { ivar_set_type(cl, iv, TY_INT_ARRAY_ARRAY); { cl->ivar_int_table[iv] = 1; comp_touch_class(cl->idx); } if (!pinned) narrowed = 1; } else if (pinned) { /* back on the poly array its write reads, for the evidence of the rounds to come */ ivar_set_type(cl, iv, TY_POLY_ARRAY); { cl->ivar_int_table[iv] = 0; comp_touch_class(cl->idx); } narrowed = 1; } } } free(ivl); free(ivl_head); free(ivl_next); free(ivl_name); free(first_call); free(parent_of); return narrowed; } /* Does every write of local `nm` in scope s other than a plain `=` leave it holding the element type `elem` the plain writes narrow it to? `b ||= v` and `b &&= v` store v, an op-assign stores what its operator answers, and a multiple-assignment target an element this pass does not see. */ static int narrowed_local_other_writes_fit(Compiler *c, int s, const char *nm, TyKind elem) { const NodeTable *nt = c->nt; for (int id = comp_lvw_first_sc(c, s, nm); id >= 0; id = comp_lvw_next_sc(c, id)) { NodeKind k = nt_kind(nt, id); const char *wn = nt_str(nt, id, "name"); if (k == NK_LocalVariableWriteNode || c->nscope[id] != s || !wn || !sp_streq(wn, nm)) continue; if (k == NK_LocalVariableTargetNode) return 0; TyKind vt = infer_type(c, nt_ref(nt, id, "value")); if (k == NK_LocalVariableOperatorWriteNode) { const char *op = nt_str(nt, id, "binary_operator"); int fits = elem == TY_FLOAT ? (vt == TY_FLOAT || vt == TY_INT) : elem == TY_INT ? vt == TY_INT : elem == TY_STRING ? vt == TY_STRING && op && sp_streq(op, "+") : 0; if (!fits) return 0; } else if (vt != elem && vt != TY_NIL) return 0; } return 1; } /* The parameter of a walk over a narrowed object array -- `xs.each { |x| }`, reverse_each, each_entry, each_with_index's first, map, collect and the filters -- binds an element, as `x = xs[i]` does in narrow_locals_from_arrays: it takes the element class, so its reads unbox. A block parameter is a local of the enclosing scope, so every block or lambda that binds it must be such a walk of the same class, and nothing else may write it. */ static int narrow_walk_block_params(Compiler *c) { const NodeTable *nt = c->nt; int ns = c->nscopes, total = 0; int *off = (int *)malloc(sizeof(int) * (size_t)(ns + 1)); if (!off) return 0; for (int s = 0; s < ns; s++) { off[s] = total; total += c->scopes[s].nlocals; } off[ns] = total; TyKind *elem = (TyKind *)malloc(sizeof(TyKind) * (size_t)(total ? total : 1)); char *bad = (char *)calloc((size_t)(total ? total : 1), 1); if (!elem || !bad) { free(off); free(elem); free(bad); return 0; } for (int i = 0; i < total; i++) elem[i] = TY_UNKNOWN; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); int blk = k == NK_LambdaNode ? id : nt_ref(nt, id, "block"); if (blk < 0 || (nt_kind(nt, blk) != NK_BlockNode && nt_kind(nt, blk) != NK_LambdaNode)) continue; Scope *bs = comp_scope_of(c, blk); if (!bs) continue; int s = (int)(bs - c->scopes); /* a walk of an object array, with one plain parameter (each_with_index a second, the index) and no other kind of parameter */ const char *cn = k == NK_CallNode ? nt_str(nt, id, "name") : NULL; int ca = k == NK_CallNode ? nt_ref(nt, id, "arguments") : -1; int recv = k == NK_CallNode ? nt_ref(nt, id, "receiver") : -1; TyKind rt = recv >= 0 ? infer_type(c, recv) : TY_UNKNOWN; int np = 0; while (block_param_name(c, blk, np)) np++; int bp = nt_ref(nt, blk, "parameters"), pn = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int extra = 0; if (pn >= 0) { static const char *const more[] = { "optionals", "posts", "keywords", NULL }; for (int m = 0; more[m]; m++) { int mn = 0; nt_arr(nt, pn, more[m], &mn); extra |= mn > 0; } extra |= nt_ref(nt, pn, "rest") >= 0 || nt_ref(nt, pn, "keyword_rest") >= 0 || nt_ref(nt, pn, "block") >= 0; } int walk = cn && (is_each_walk_or_with_index(cn) || is_map_alias(cn) || oa_filter_call(c, id)) && ca < 0 && blk != id && ty_is_obj_array(rt) && !extra && (np == 1 || (np == 2 && sp_streq(cn, "each_with_index"))) && !block_param_is_multi(c, blk, 0); for (int pi = 0; pi < np; pi++) { const char *nm = block_param_name(c, blk, pi); LocalVar *lv = nm ? scope_local(bs, nm) : NULL; if (!lv) continue; int gi = off[s] + (int)(lv - bs->locals); if (gi < off[s] || gi >= off[s + 1]) continue; if (!walk || pi != 0) { bad[gi] = 1; continue; } TyKind ec = ty_array_elem(rt); if (elem[gi] == TY_UNKNOWN) elem[gi] = ec; else if (elem[gi] != ec) bad[gi] = 1; } } int any = 0; for (int s = 0; s < ns; s++) { Scope *sc = &c->scopes[s]; for (int li = 0; li < sc->nlocals; li++) { LocalVar *lv = &sc->locals[li]; if (!lv->is_block_param || lv->rbs_seeded) continue; int gi = off[s] + li; int fits = !bad[gi] && ty_is_object(elem[gi]) && !comp_lvw_any_sc(c, s, lv->name); /* not written in the body */ /* a pin whose walk no longer qualifies (the array widened, another block binds the name) goes, and the parameter is the walk's boxed element again */ if (ty_is_object(lv->oa_pin) && (!fits || elem[gi] != lv->oa_pin)) { lv->oa_pin = TY_UNKNOWN; lv_set_type(lv, TY_POLY); any = 1; } if (!fits || lv->type != TY_POLY) continue; lv_set_type(lv, elem[gi]); lv->oa_pin = elem[gi]; any = 1; } } free(off); free(elem); free(bad); return any; } /* Locals read out of an already-narrowed array: `b = arr[i]` makes b the element type, so its own reads unbox. Split out of narrow_object_arrays so it still runs when that pass has no local candidate slot and returns early -- an ivar table narrowed by narrow_int_table_ivars has exactly that shape, and without this its rows stayed boxed and the narrowing bought nothing. */ static int narrow_locals_from_arrays(Compiler *c) { const NodeTable *nt = c->nt; /* dependent locals: `b = arr[i]` (arr now a narrowed obj array) makes `b` the element object type, so its field accesses unbox. Every write of the local must be such an index read of one class (nil writes excepted). Iterated, because the narrowing chains: `a = rows[0]` narrows a to an int array only in this step, and only then can `m = a[1]` see an int (#3354). */ int any = 0; for (int prop = 0; prop < 8; prop++) { int prop_ch = narrow_walk_block_params(c); any |= prop_ch; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int li = 0; li < sc->nlocals; li++) { LocalVar *lv = &sc->locals[li]; if (lv->type != TY_POLY || lv->is_param || lv->is_block_param || lv->rbs_seeded) continue; TyKind elem = TY_UNKNOWN; int ok = 1, saw = 0, other = 0; /* the (scope, name) write chain replaces a full node-table walk that ran once per candidate local per propagation round -- quadratic on machine-generated programs, where one 52k-line input spent 58% of its analyze in this loop. The chain carries hash collisions, so the kind/scope/name filters stay. */ for (int id = comp_lvw_first_sc(c, s, lv->name); id >= 0 && ok; id = comp_lvw_next_sc(c, id)) { const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty || c->nscope[id] != s) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, lv->name)) continue; /* the local's other writes are checked against the element type once it is known; passed over, the pin below held `b ||= 7` in a Float slot as 7.0, and a String `||=` into an object one did not build */ if (k != NK_LocalVariableWriteNode) { other = 1; continue; } int v = nt_ref(nt, id, "value"); NodeKind vk = nt_kind(nt, v); if (vk == NK_NilNode) continue; if (vk != NK_CallNode) { ok = 0; break; } const char *cn = nt_str(nt, v, "name"); int crecv = nt_ref(nt, v, "receiver"); int can = 0; { int ca = nt_ref(nt, v, "arguments"); if (ca >= 0) nt_arr(nt, ca, "arguments", &can); } int idx_op = cn && (is_element_at_alias(cn)) && can == 1; int end_op = cn && (is_endpoint_query(cn)) && can == 0; if ((!idx_op && !end_op) || crecv < 0) { ok = 0; break; } /* an index that may be a Range reads a subarray too, boxed (infer_array_call) */ if (idx_op && infer_type(c, v) == TY_POLY) { ok = 0; break; } TyKind rt = infer_type(c, crecv); /* element type of a narrowed obj-array OR the new int-array-array */ /* a scalar-element array yields its element type; an out-of-range read is that type's nil (the sp_oint's flag / NULL), which it models */ TyKind ec = ty_is_obj_array(rt) ? ty_object(ty_obj_array_class(rt)) : (rt == TY_INT_ARRAY_ARRAY) ? TY_INT_ARRAY : (rt == TY_FLOAT_ARRAY_ARRAY) ? TY_FLOAT_ARRAY : (rt == TY_INT_ARRAY || rt == TY_FLOAT_ARRAY || rt == TY_STR_ARRAY) ? ty_array_elem(rt) : TY_UNKNOWN; if (ec == TY_UNKNOWN) { ok = 0; break; } if (elem == TY_UNKNOWN) elem = ec; else if (elem != ec) { ok = 0; break; } saw = 1; } if (ok && saw && other && elem != TY_UNKNOWN) ok = narrowed_local_other_writes_fit(c, s, lv->name, elem); if (ok && saw && elem != TY_UNKNOWN) { /* Pin it: the next write pass resets the slot and re-derives the plain poly the container read hands back, and this pass would narrow it again -- an oscillation that never converged, so the fixpoint ran its full round budget and left whatever types the cap happened to catch (#3781). Every write of the local is an index read of the same element type (the loop above insists), so the pin cannot mask a genuine widening. */ lv_set_type(lv, elem); lv->oa_pin = elem; prop_ch = 1; any = 1; } } } if (!prop_ch) break; } return any; } /* A value position that is no value: a missing or empty arm, a nil written as nil or as `(nil)`, a walk that gave up. A return is not one: it hands out no nil of its own. */ static int oa_unvalued_leaf(const VpLeaf *leaf, void *ud) { const NodeTable *nt = ud; return leaf->kind == VPK_NIL || leaf->kind == VPK_GAVE_UP || (leaf->kind == VPK_EXPR && nt_kind(nt, leaf->node) == NK_NilNode); } /* Does every branch of `if` value v end in a value other than nil? (A nil one leaves the slot boxed, taking the branch by reference, #4486.) The branches are the value positions of an if, an unless, an elsif and an else. */ static int oa_branches_all_valued(const NodeTable *nt, int v) { return v >= 0 && !for_each_value_position(nt, v, VPS_IF | VPS_UNLESS, oa_unvalued_leaf, (void *)nt); } /* Classify one value flowing into slot S: an object or int-array literal is element evidence, another slot (or a narrowable call's value) is an alias edge, nil is neutral, and anything the pass does not model kills the slot. Shared by a local's write sources and by a method's return expressions -- `t = build_table(n)` names one storage location in two scopes, so the caller's local and the callee's value have to agree on one container type or the call would hand an unboxed sp_PtrArray* to a boxed reader. */ /* Is `node` a read of a parameter of the method it sits in? */ static int oa_read_is_param(Compiler *c, int node) { const NodeTable *nt = c->nt; if (nt_kind(nt, node) != NK_LocalVariableReadNode) return 0; const char *rn = nt_str(nt, node, "name"); Scope *sc = comp_scope_of(c, node); if (!rn || !sc || sc->def_node < 0) return 0; int pn = nt_ref(nt, sc->def_node, "parameters"); if (pn < 0) return 0; static const char *const lists[] = { "requireds", "optionals", "posts", "keywords", NULL }; for (int l = 0; lists[l]; l++) { int k = 0; const int *ps = nt_arr(nt, pn, lists[l], &k); for (int i = 0; ps && i < k; i++) { const char *nm = nt_str(nt, ps[i], "name"); if (nm && sp_streq(nm, rn)) return 1; } } int rest = nt_ref(nt, pn, "rest"); const char *rnm = rest >= 0 ? nt_str(nt, rest, "name") : NULL; return rnm && sp_streq(rnm, rn); } /* The row kind a map's block answers (OA_CLS_IA / OA_CLS_FA), or -1: its tail and every valued next or break are one kind of scalar row, and a literal row has every element typed (oa_obj_class_of). An object, a boxed value or an empty `[]` is no evidence: the emitters that read an object table built by a map still want the boxed element. Nor is a block that breaks: the map then answers the break's value, not a table. */ static int oa_map_row_evidence(Compiler *c, int block, int tail) { if (tail < 0 || block_body_breaks(c->nt, nt_ref(c->nt, block, "body"))) return -1; TyKind et = infer_map_block_elem_ty(c, block); int ec = oa_obj_class_of(c, tail); if (!OA_CLS_IS_NESTED(ec) || et != (ec == OA_CLS_IA ? TY_INT_ARRAY : TY_FLOAT_ARRAY)) return -1; return ec; } /* Is s `dup` or `clone`, whose answer is a copy of the receiver's own class: no class of the program defines either itself? */ static int oa_copy_name(Compiler *c, const char *s) { if (!s || !(sp_streq(s, "dup") || sp_streq(s, "clone"))) return 0; for (int k = 0; k < c->nclasses; k++) if (comp_method_in_chain(c, k, s, NULL) >= 0) return 0; return 1; } static void oa_classify_value(Compiler *c, OAS *sl, int n, const int *read_slot, const int *call_ret, char *claimed, int S, int v) { const NodeTable *nt = c->nt; const char *vty = v >= 0 ? nt_type(nt, v) : NULL; NodeKind vk = nt_kind(nt, v); if (!vty) { sl[S].alive = 0; return; } /* not a kill for a local whose branches all have a value: it died while the branches narrowed, and inference then gave it a pointer array no pass had vetted */ if (sl[S].lv && (vk == NK_IfNode || vk == NK_UnlessNode || vk == NK_ElseNode || vk == NK_StatementsNode) && oa_branches_all_valued(nt, v)) { if (vk == NK_StatementsNode) { int bn = 0; const int *bb = nt_arr(nt, v, "body", &bn); if (bn > 0) oa_classify_value(c, sl, n, read_slot, call_ret, claimed, S, bb[bn - 1]); return; } int st = nt_ref(nt, v, "statements"); if (st >= 0) oa_classify_value(c, sl, n, read_slot, call_ret, claimed, S, st); int alt = vk == NK_IfNode ? nt_ref(nt, v, "subsequent") : vk == NK_UnlessNode ? nt_ref(nt, v, "else_clause") : -1; if (alt >= 0) oa_classify_value(c, sl, n, read_slot, call_ret, claimed, S, alt); return; } if (vk == NK_ArrayNode) { int en = 0; const int *el = nt_arr(nt, v, "elements", &en); for (int e = 0; e < en; e++) { NodeKind ek = nt_kind(nt, el[e]); int ec = (ek == NK_SplatNode) ? -2 : oa_elem_evidence(c, sl, S, el[e]); /* OA_CLS_IA / OA_CLS_FA (a nested scalar array) are negative SENTINELS, not an "invalid" -1/-2: they are real evidence, so they must not kill the slot (an `[[a,b],[c,d]]` array-of-int-array literal narrows like a pushed one already does -- the push arm never had this < 0 guard). An empty `[]` element is a row with no kind of its own yet: neutral, and built at the component's kind if it narrows (oa_note_empty). */ if (is_empty_array_literal(nt, el[e], c->node_cap) && !OA_CLS_IS_NESTED(ec)) continue; /* a foreign row is the conflict the seed check reports, whatever rows came before it */ if (ec < 0 && !OA_CLS_IS_ROW(ec)) { if (ec == -2 && ek != NK_SplatNode) sl[S].cls = -2; sl[S].alive = 0; break; } sl[S].cls = oa_cls_join(sl[S].cls, ec); } return; } if (read_slot[v] >= 0) { /* a local, an @ivar, or its attr_reader's value */ claimed[v] = 1; oa_uf_union(sl, S, read_slot[v]); return; } if (vk == NK_NilNode) return; /* nullable, neutral */ if (vk == NK_CallNode) { /* `b = arr.sort` / `b = arr.sort!`: the sorted array shares arr's element class -- an alias edge, like a plain slot-to-slot copy. `arr.clear` answers arr itself, the same edge. */ const char *cn = nt_str(nt, v, "name"); int crecv = nt_ref(nt, v, "receiver"); int cargs = nt_ref(nt, v, "arguments"); int can = 0; if (cargs >= 0) nt_arr(nt, cargs, "arguments", &can); if (cn && (is_sort_family(cn) || sp_streq(cn, "clear")) && can == 0 && nt_ref(nt, v, "block") < 0 && crecv >= 0 && read_slot[crecv] >= 0) { claimed[crecv] = 1; oa_uf_union(sl, S, read_slot[crecv]); } /* `slot << x` / `slot.push(x)` answer the receiver itself: a method whose body is `collection << Flag.new` has that array as its value, and without this edge its return slot died while its parameter narrowed, leaving an sp_PtrArray body under an sp_PolyArray return. The push argument is element evidence exactly as in the receiver-op arm. */ else if (cn && is_push_alias(cn) && can >= 1 && nt_ref(nt, v, "block") < 0 && crecv >= 0 && read_slot[crecv] >= 0) { int cargv_n = 0; const int *cargv = nt_arr(nt, cargs, "arguments", &cargv_n); for (int a = 0; a < cargv_n; a++) sl[S].cls = oa_cls_join(sl[S].cls, oa_elem_evidence(c, sl, S, cargv[a])); claimed[crecv] = 1; oa_uf_union(sl, S, read_slot[crecv]); } /* `t = Array.new(n, v)`: every slot holds the one fill value v, so v is the same evidence a pushed element gives, as the generator block's value is below. */ else if (crecv >= 0 && nt_kind(nt, crecv) == NK_ConstantReadNode && is_array_new(nt_str(nt, crecv, "name"), cn) && nt_ref(nt, v, "block") < 0 && can == 2) { int ec = oa_elem_evidence(c, sl, S, nt_arr(nt, cargs, "arguments", &can)[1]); if (ec < 0 && !OA_CLS_IS_ROW(ec)) sl[S].alive = 0; else sl[S].cls = oa_cls_join(sl[S].cls, ec); } /* `t = Array.new(n) { }`: the generator block's value is the element, so its type is the same evidence a pushed element gives. The nested int table this builds is the one shape the pass still left on the boxed poly path, so every read of it went through sp_poly_arr_get and its arithmetic boxed. */ else if (crecv >= 0 && nt_kind(nt, crecv) == NK_ConstantReadNode && is_array_new(nt_str(nt, crecv, "name"), cn) && nt_ref(nt, v, "block") >= 0) { int gb = nt_ref(nt, v, "block"); int gbody = gb >= 0 ? nt_ref(nt, gb, "body") : -1; int gn = 0; const int *gs = gbody >= 0 ? nt_arr(nt, gbody, "body", &gn) : NULL; int ec = (gs && gn > 0) ? oa_elem_evidence(c, sl, S, gs[gn - 1]) : -1; /* `Array.new(n) { [] }`: the row is an empty literal, neutral here and built at the table's kind once something else (a pushed row, an `Array[Array[Float]]` seed) decides it (#4484) */ if (gs && gn > 0 && is_empty_array_literal(nt, gs[gn - 1], c->node_cap) && !ty_is_ptr_array(infer_type(c, gs[gn - 1])) && infer_type(c, gs[gn - 1]) != TY_INT_ARRAY && infer_type(c, gs[gn - 1]) != TY_FLOAT_ARRAY) { } else if (ec < 0 && !OA_CLS_IS_ROW(ec)) sl[S].alive = 0; else sl[S].cls = oa_cls_join(sl[S].cls, ec); } /* `raw = idx.map { |k| cols[k] }`: the mapped table's rows ARE the other table's rows -- the same objects, not copies -- so the two must agree on a row kind. That is an EDGE, not element evidence: the source table's kind is still being derived this round (every slot is reset at the top of the pass), so reading it as evidence answers "no kind yet" and the slot dies. The while-loop spelling never needed this because its rows come from its own `Array.new(0, 0.0)` generator. Deliberately only this shape. Keeping every map source alive also narrows `[...].map { Obj.new(s) }` to an object table, and the places that consume one -- `min`, `join`, interpolation -- still want the boxed element, so the C stopped compiling. The rows here are already unboxed pointers read out of a narrowed table, which is what makes this shape the one that pays. The receiver must be an ARRAY. `h.map { |k, v| cols[v] }` answers the same rows, but its emitter has no pointer-array container to collect them into: the hash-collect path bails on the kind and the fallback it drops through does not reach Hash#map at all, so the program stopped running. Narrowing a slot whose builder cannot build it at that kind is worse than leaving it boxed. */ else if (cn && (is_map_alias(cn)) && can == 0 && nt_ref(nt, v, "block") >= 0 && nt_kind(nt, nt_ref(nt, v, "block")) == NK_BlockNode && crecv >= 0 && (ty_is_array(infer_type(c, crecv)) || infer_type(c, crecv) == TY_RANGE)) { int mb = nt_ref(nt, v, "block"); int mbody = nt_ref(nt, mb, "body"); int mn = 0; const int *ms = mbody >= 0 ? nt_arr(nt, mbody, "body", &mn) : NULL; int mtail = (ms && mn > 0) ? ms[mn - 1] : -1; int joined = 0; if (mtail >= 0 && nt_kind(nt, mtail) == NK_CallNode) { const char *mtn = nt_str(nt, mtail, "name"); int mtr = nt_ref(nt, mtail, "receiver"); int mta = nt_ref(nt, mtail, "arguments"); int mtan = 0; if (mta >= 0) nt_arr(nt, mta, "arguments", &mtan); if (mtn && (is_element_at_alias(mtn)) && mtan == 1 && mtr >= 0 && read_slot[mtr] >= 0 && ty_is_array(infer_type(c, crecv))) { claimed[mtr] = 1; oa_uf_union(sl, S, read_slot[mtr]); /* the map BUILDS the table in place, so the emitter has to build it at the kind this component settles on -- recorded as a want, which later inference passes re-read (a c->ntype stamp would be recomputed away before emission) */ oa_note_src(S, v); joined = 1; } } /* `copies = as.map(&:dup)`, which arrives here as `as.map { |a| a.dup }`: each element's copy is of the element's class, so the copy table is the slot's kind, built in place by the map as above. Not where the program defines its own dup or clone, which may answer anything. */ if (!joined && mn == 1 && read_slot[crecv] >= 0 && mtail >= 0 && nt_kind(nt, mtail) == NK_CallNode && nt_ref(nt, mtail, "block") < 0 && nt_ref(nt, mtail, "arguments") < 0 && oa_copy_name(c, nt_str(nt, mtail, "name"))) { int pr = nt_ref(nt, mtail, "receiver"); const char *pn = block_param_name(c, mb, 0); if (pr >= 0 && nt_kind(nt, pr) == NK_LocalVariableReadNode && pn && !block_param_name(c, mb, 1) && sp_streq(nt_str(nt, pr, "name"), pn)) { claimed[crecv] = 1; oa_uf_union(sl, S, read_slot[crecv]); oa_note_src(S, v); joined = 1; } } /* `(0...n).map { |k| [k, k + 1] }`: a block that answers an Integer or a Float row builds a table of such rows, the evidence a pushed row gives, and the map builds it in place as above */ int ec = joined ? -1 : oa_map_row_evidence(c, mb, mtail); if (OA_CLS_IS_NESTED(ec)) { sl[S].cls = oa_cls_join(sl[S].cls, ec); oa_note_src(S, v); } else if (!joined) sl[S].alive = 0; /* every other map source, as before */ } /* a call whose own value is a tracked slot: join the two. */ else if (call_ret[v] >= 0) { claimed[v] = 1; oa_uf_union(sl, S, call_ret[v]); } else sl[S].alive = 0; return; } sl[S].alive = 0; } /* A want narrow_object_arrays cleared at the top of a round and did NOT re-stamp is a decision that went away. Reporting it re-runs write inference without it; see the call at the end of the pass for why that matters. Only a genuinely dropped one: the ordinary round clears and re-stamps, and a node still carrying a stamp is not a drop. */ static int oa_want_dropped(Compiler *c, const int *cleared, int n_cleared) { if (!c->arr_want) return 0; for (int e = 0; e < n_cleared; e++) { int cid = cleared[e]; if (cid >= 0 && cid < c->node_cap && c->arr_want[cid] == TY_UNKNOWN) return 1; } return 0; } /* The call a per-site copy desugar_builtins made (`__enum___`) was specialized for, which it recorded on the copy's def. */ static int enum_copy_site(Compiler *c, const Scope *sc) { if (!sc || !sc->name || strncmp(sc->name, "__enum_", 7) != 0 || sc->def_node < 0) return -1; return (int)nt_int(c->nt, sc->def_node, "enum_site", -1); } static void oa_mark_subtree(const NodeTable *nt, int id, unsigned char *dead) { if (id < 0 || id >= nt->count || dead[id]) return; dead[id] = 1; const SpNode *nd = &nt->nodes[id]; for (int j = 0; j < nd->nr; j++) oa_mark_subtree(nt, nd->r[j].ref, dead); for (int j = 0; j < nd->na; j++) for (int k = 0; k < nd->a[j].n; k++) oa_mark_subtree(nt, nd->a[j].ids[k], dead); } /* Put back the pin this pass cleared on its way in, for a slot it did not narrow this round. A pin the element-narrowing wrote goes back as it was (#3781). This pass's OWN narrowing (a pointer-array kind) gets one round of grace -- a slot can miss its decision for a round while the slots around it catch up -- and is then dropped: the slot was reset to the poly array at the top, and handing the withdrawn decision back as a pin had infer_write_types re-assert it as the narrowed kind next round, only for this pass to reset it again, every round to the cap (#4962). That holds for a slot the evidence rejected (a boxed row, a comparator, an iterator) as for one it reached no decision on. */ static void oa_restore_pin(Compiler *c, OAS *sl) { TyKind back = sl->old_pin; unsigned char *grace = sl->lv ? &sl->lv->oa_grace : &c->scopes[sl->sidx].ret_oa_grace; if (back == TY_INT_ARRAY_ARRAY || back == TY_FLOAT_ARRAY_ARRAY || ty_is_obj_array(back)) { if (*grace) { back = TY_UNKNOWN; *grace = 0; } else *grace = 1; } if (sl->lv) sl->lv->oa_pin = back; else c->scopes[sl->sidx].ret_oa_pin = back; } int is_descendant(Compiler *c, int k, int anc); /* Whether an instance of class ci, or of a subclass of it, can answer a call named `name`: a reader, a method or an alias by the name in its chain or a reopened Object's, or a method_missing. An ivar slot is listed under its reader's name whether or not the class has that reader, so a `.b` on any dynamic receiver, or a Symbol :b (a Struct with a member :b anywhere in the program spells one), killed every class's @b. The slot covers the ivar in every subclass, so a subclass's own `b` reaches it too. */ static int oa_one_class_may_answer(Compiler *c, int ci, const char *name) { if (comp_resolve_member(c, ci, name, 0, NULL, NULL) != SP_MEMBER_NONE) return 1; const char *rn = comp_resolve_alias(c, ci, name); if (rn && !sp_streq(rn, name) && comp_resolve_member(c, ci, rn, 0, NULL, NULL) != SP_MEMBER_NONE) return 1; return comp_method_in_chain(c, ci, "method_missing", NULL) >= 0; } static int oa_class_may_answer(Compiler *c, int ci, const char *name) { for (int k = 0; k < c->nclasses; k++) if ((k == ci || is_descendant(c, k, ci)) && oa_one_class_may_answer(c, k, name)) return 1; int oc = comp_core_class(c, CORE_OBJECT); return oc >= 0 && oc != ci && comp_resolve_member(c, oc, name, 0, NULL, NULL) != SP_MEMBER_NONE; } /* The slots by the name a call on a dynamic receiver reaches them under: an ivar slot by its reader's name, a method's value and parameter slots by the method's. The kill such a call makes read every slot, at every such call; on a Rails program the lowered sends are such calls, and the scan was most of the pass (two fifths of a sampled window). Listed once per pass, after the slots are collected; a name's slots chain from first through next, newest first. */ typedef struct { ANameHash names; int *first, *next; int cap; } OaByName; static OaByName oa_byname_build(Compiler *c, const OAS *sl, int n) { OaByName b; memset(&b, 0, sizeof b); b.next = malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); if (!b.next) { fprintf(stderr, "oom\n"); exit(1); } for (int pi = 0; pi < n; pi++) { const char *key = sl[pi].ici >= 0 ? c->classes[sl[pi].ici].ivars[sl[pi].iiv] + 1 : sl[pi].sidx >= 0 ? c->scopes[sl[pi].sidx].name : NULL; b.next[pi] = -1; if (!key) continue; int e = anh_find(&b.names, key); if (e < 0) { anh_add(&b.names, key); e = anh_find(&b.names, key); if (e >= b.cap) { b.cap = e + 64; b.first = realloc(b.first, sizeof(int) * (size_t)b.cap); if (!b.first) { fprintf(stderr, "oom\n"); exit(1); } } b.first[e] = -1; } b.next[pi] = b.first[e]; b.first[e] = pi; } return b; } static int oa_byname_first(const OaByName *b, const char *name) { int e = name ? anh_find(&b->names, name) : -1; return e >= 0 ? b->first[e] : -1; } static void oa_byname_free(OaByName *b) { anh_free(&b->names); free(b->first); free(b->next); } /* A method that the runtime can call with no call node of its own: a protocol, pattern matching, method_missing, an operator or a setter (`sum` calls `+`, `case` calls `===`), an extension entry, a proc form and a lowered yield. sh_settle_reads counts the same methods as read. */ static int oa_entered_unseen(const Scope *sc) { const char *nm = sc->name; if (sc->is_ext_entry || sc->is_proc_form || sc->is_lowered_yield) return 1; if (method_name_implicitly_invoked(nm) || sp_streq(nm, "method_missing") || sp_streq(nm, "deconstruct") || sp_streq(nm, "deconstruct_keys")) return 1; if (!(nm[0] == '_' || (nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z'))) return 1; return name_is_plain_setter(nm); } /* (name, method) pairs, sorted by name: each name a method answers to */ typedef struct { const char *nm; int s; } OaNamed; static int oa_named_cmp(const void *a, const void *b) { return strcmp(((const OaNamed *)a)->nm, ((const OaNamed *)b)->nm); } /* Does the call run its block through a builtin walk that ignores the block's value? each & co. on an Array, a Hash, a Range or an Integer, unless the program redefines that walk on the receiver's class. */ static int oa_ignores_block_value(Compiler *c, int call) { const NodeTable *nt = c->nt; static const char *const walks[] = { "each", "each_with_index", "reverse_each", "each_entry", "times", "upto", "downto", "each_key", "each_value", "each_pair", NULL }; const char *nm = nt_str(nt, call, "name"); int recv = nt_ref(nt, call, "receiver"); if (!nm || recv < 0 || !str_in(nm, walks)) return 0; TyKind rt = infer_type(c, recv); const char *cls = ty_is_array(rt) || ty_is_ptr_array(rt) ? "Array" : ty_is_hash(rt) ? "Hash" : rt == TY_RANGE ? "Range" : rt == TY_INT ? "Integer" : NULL; return cls && !share_builtin_overridden(c, cls, nm); } /* an_value_dropped's answer, with the block of an iterator taken to throw its value away only when oa_ignores_block_value says the receiver's builtin walk does: a program's own `each` may read what its block answers (`x = yield; x << "s"`), which an_value_dropped, knowing the walk by its name alone, takes as thrown away */ static int oa_value_dropped(Compiler *c, int id) { const NodeTable *nt = c->nt; const int *par = comp_parent_map(c); VpUp up; VpUpKind ended = vp_climb(nt, par, id, AN_DROP_CLIMB, &up); if (up.reach) return 0; if (ended == VPU_DROPPED || vp_up_loop(nt, &up) >= 0) return 1; if (vp_up_effect_clause(nt, &up)) return 1; int call = vp_up_block_call(nt, par, &up); return call >= 0 && oa_ignores_block_value(c, call); } /* walk_disc[id]: a walk's value is thrown away. An object array walk narrows only there: `y = a.each { }` or `a.each { break r }` hands the array (or the break value) to a consumer typed for the boxed array, which the narrowed sp_PtrArray is not (#4879). These statements throw their value away: one that another statement follows, a top-level statement, the last statement of a loop body, and the last statement of the block of a builtin walk that ignores its block's value (oa_ignores_block_value): `def walk; @kids.each { |k| k.walk }; end` reads no walk's value. The last statement of a method is the method's value. This pass finds the method names whose value something can read. A call of a name reads the value, except a call that walk_disc marks, or a call at the end of a method. A call at the end of a method reads the value only when that method's own value is read. Calls are known only by name, so a call of a name on any receiver counts. A Symbol, a String given to `send`, a `super`, and the getter of `x.m ||= v` also read the value of their name. A computed name given to `send` or `method` can read any value. A method entered with no call node (oa_entered_unseen) is read too. The last statement of each method whose value nothing reads goes into walk_disc. */ static char *oa_walk_disc(Compiler *c, int nc) { const NodeTable *nt = c->nt; char *walk_disc = (char *)calloc(nc, 1); for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_StatementsNode) continue; int bn = 0; const int *bl = nt_arr(nt, id, "body", &bn); for (int i = 0; i + 1 < bn; i++) if (bl[i] >= 0 && bl[i] < nc) walk_disc[bl[i]] = 1; } { int root = nt->count > 0 ? nt->count - 1 : -1; for (int id = 0; id < nt->count; id++) if (nt_kind(nt, id) == NK_ProgramNode) { root = id; break; } int ps = root >= 0 ? nt_ref(nt, root, "statements") : -1; int bn = 0; const int *bl = ps >= 0 ? nt_arr(nt, ps, "body", &bn) : NULL; for (int i = 0; i < bn; i++) if (bl[i] >= 0 && bl[i] < nc) walk_disc[bl[i]] = 1; } for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); int body = -1; if (k == NK_WhileNode || k == NK_UntilNode) body = nt_ref(nt, id, "statements"); else if (k == NK_CallNode) { int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && oa_ignores_block_value(c, id)) body = nt_ref(nt, blk, "body"); } if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bl = nt_arr(nt, body, "body", &bn); if (bn > 0 && bl[bn - 1] >= 0 && bl[bn - 1] < nc) walk_disc[bl[bn - 1]] = 1; } /* tail[s]: the last statement of method s, or -1 */ int ns = c->nscopes ? c->nscopes : 1; int *tail = (int *)malloc(sizeof(int) * (size_t)ns); char *is_tail = (char *)calloc((size_t)nc, 1); for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; tail[s] = -1; if (sc->def_node < 0 || !sc->name || sc->body < 0) continue; int bn = 0; const int *bl = nt_arr(nt, sc->body, "body", &bn); if (bn > 0 && bl[bn - 1] >= 0 && bl[bn - 1] < nc) { tail[s] = bl[bn - 1]; is_tail[tail[s]] = 1; } } ANameHash read; memset(&read, 0, sizeof read); int all = 0; for (int id = 0; id < nt->count && id < nc && !all; id++) { NodeKind k = nt_kind(nt, id); const char *mn = NULL; if (k == NK_SymbolNode) mn = nt_str(nt, id, "value"); else if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { Scope *ssc = comp_scope_of(c, id); mn = ssc ? ssc->name : NULL; } else if (k == NK_CallOrWriteNode || k == NK_CallAndWriteNode || (nt_kind(nt, id) == NK_CallOperatorWriteNode)) mn = nt_str(nt, id, "read_name") ? nt_str(nt, id, "read_name") : nt_str(nt, id, "name"); else if (k == NK_CallNode) { const char *cn = nt_str(nt, id, "name"); if (cn && (sp_streq(cn, "send") || sp_streq(cn, "__send__") || sp_streq(cn, "public_send") || sp_streq(cn, "method") || sp_streq(cn, "public_method") || sp_streq(cn, "instance_method"))) { int args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; NodeKind ak = an > 0 ? nt_kind(nt, av[0]) : NK_NilNode; if (ak == NK_StringNode && nt_str(nt, av[0], "content")) { const char *sn = nt_str(nt, av[0], "content"); if (!anh_has(&read, sn)) anh_add(&read, sn); } else if (ak != NK_SymbolNode) all = 1; } /* (nor a call whose value its own place throws away, as the last statement of an if's arm in a builtin walk's block is: oa_value_dropped) */ if (!walk_disc[id] && !is_tail[id] && !oa_value_dropped(c, id)) mn = cn; } if (mn && !anh_has(&read, mn)) anh_add(&read, mn); } /* a method answers to its own name, and to the name of its def: a copy (an include's shadow, a proc-form clone) has a name no call spells */ OaNamed *named = (OaNamed *)malloc(sizeof(OaNamed) * (size_t)ns * 2); int nnamed = 0; for (int s = 0; s < c->nscopes; s++) { if (tail[s] < 0) continue; named[nnamed++] = (OaNamed){ c->scopes[s].name, s }; const char *dn = nt_str(nt, c->scopes[s].def_node, "name"); if (dn && !sp_streq(dn, c->scopes[s].name)) named[nnamed++] = (OaNamed){ dn, s }; } qsort(named, (size_t)nnamed, sizeof *named, oa_named_cmp); /* m_read[s]: something reads method s's value. read.key is the worklist: a method read through a name adds its tail call's name at the end. */ char *m_read = (char *)calloc((size_t)ns, 1); #define OA_READ_METHOD(s) do { int _s = (s); if (!m_read[_s]) { m_read[_s] = 1; int _t = tail[_s]; \ const char *_cn = !walk_disc[_t] && nt_kind(nt, _t) == NK_CallNode ? nt_str(nt, _t, "name") : NULL; \ if (_cn && !anh_has(&read, _cn)) anh_add(&read, _cn); } } while (0) for (int s = 0; s < c->nscopes; s++) if (tail[s] >= 0 && (all || oa_entered_unseen(&c->scopes[s]))) OA_READ_METHOD(s); for (int i = 0; i < read.n; i++) { int lo = 0, hi = nnamed; while (lo < hi) { int mid = (lo + hi) / 2; if (strcmp(named[mid].nm, read.key[i]) < 0) lo = mid + 1; else hi = mid; } for (int j = lo; j < nnamed && sp_streq(named[j].nm, read.key[i]); j++) OA_READ_METHOD(named[j].s); } #undef OA_READ_METHOD /* a tail node two methods share is read when either method is read */ char *tail_read = (char *)calloc((size_t)nc, 1); for (int s = 0; s < c->nscopes; s++) if (tail[s] >= 0 && m_read[s]) tail_read[tail[s]] = 1; for (int s = 0; s < c->nscopes; s++) if (tail[s] >= 0 && !tail_read[tail[s]]) walk_disc[tail[s]] = 1; anh_free(&read); free(named); free(m_read); free(tail_read); free(is_tail); free(tail); return walk_disc; } /* A table an `Array[Array[Integer]]` / `Array[Array[Float]]` seed decided builds its empty rows at the seed's kind (narrow_object_arrays, #4484), but an element of a `[[], []]` literal may already want a poly array: widen_nested_literals takes it boxed when a push into the row comes before the row has a kind. The seed is that kind, and the literal is built at it (oa_mark_seed_row), as the rows of `Array.new(n) { [] }` are: a push into it converts its value (an Integer into a Float row) or is refused, and a row held in a local or passed on is the seed's to type. A row another producer gave a typed kind of its own keeps the table the poly array its rows are, before any row is built, instead of reading a row of one kind back as the other. */ /* Is component r an ivar's an `Array[Array[Integer]]` / `Array[Array[Float]]` seed decided? */ static int oa_component_seeded(Compiler *c, OAS *sl, int n, int r) { for (int j = 0; j < n; j++) if (oa_uf_find(sl, j) == r && sl[j].ici >= 0) { int sd = c->classes[sl[j].ici].ivar_oa_seed[sl[j].iiv]; if (sd == SEED_OA_INT_TABLE || sd == SEED_OA_FLT_TABLE) return 1; } return 0; } static void oa_seeded_literal_rows(Compiler *c, OAS *sl, int n) { for (int i = 0; i < n; i++) { if (oa_uf_find(sl, i) != i || !OA_CLS_IS_NESTED(sl[i].cls)) continue; if (!c->arr_want || !oa_component_seeded(c, sl, n, i)) continue; /* only a table that narrows builds rows: one the pass keeps boxed (dead, or a remove, a comparison or a boxed row, as narrow_object_arrays checks) keeps its rows boxed */ if (!sl[i].alive || sl[i].needs_cmp || sl[i].removes || sl[i].poly_elem) continue; TyKind rk = sl[i].cls == OA_CLS_IA ? TY_INT_ARRAY : TY_FLOAT_ARRAY; for (int e = 0; e < g_oa_empt_n && sl[i].cls != -1; e++) { if (oa_uf_find(sl, g_oa_empt_slot[e]) != i) continue; int lit = g_oa_empt_node[e]; if (lit >= c->node_cap) continue; TyKind w = c->arr_want[lit]; if (w != TY_UNKNOWN && w != rk && w != TY_POLY_ARRAY) sl[i].cls = -1; } for (int e = 0; e < g_oa_empt_n && sl[i].cls != -1; e++) { if (oa_uf_find(sl, g_oa_empt_slot[e]) != i) continue; int lit = g_oa_empt_node[e]; if (lit < c->node_cap && c->arr_want[lit] == TY_POLY_ARRAY) { comp_set_arr_want(c, lit, rk); oa_mark_seed_row(lit); } } } } /* The kind of rows of a table that the receiver `recv` of a walk taking each row apart builds on the spot: a literal (`[[1, 2], [3, 4]].each { |x, y| }`) or a map (`(0...n).map { |k| [k, k] }.each { |x, y| }`) of Integer or Float rows. Answers OA_CLS_IA / OA_CLS_FA, or -1 for anything else. */ static int oa_walked_table_kind(Compiler *c, int recv) { const NodeTable *nt = c->nt; if (nt_kind(nt, recv) == NK_ArrayNode) { int en = 0; const int *el = nt_arr(nt, recv, "elements", &en); int cls = -1; /* an empty row (`[]`, `Array.new`) has no kind of its own and is built boxed, whatever kind it reads as */ for (int e = 0; e < en; e++) cls = oa_cls_join(cls, nt_kind(nt, el[e]) == NK_SplatNode || node_is_empty_container(c, el[e]) ? -2 : oa_obj_class_of(c, el[e])); return OA_CLS_IS_NESTED(cls) ? cls : -1; } const char *nm = nt_kind(nt, recv) == NK_CallNode ? nt_str(nt, recv, "name") : NULL; int blk = nm ? nt_ref(nt, recv, "block") : -1, mrecv = nm ? nt_ref(nt, recv, "receiver") : -1; if (!nm || !is_map_alias(nm) || nt_ref(nt, recv, "arguments") >= 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode || mrecv < 0) return -1; TyKind mt = infer_type(c, mrecv); if (!ty_is_array(mt) && mt != TY_RANGE) return -1; int body = nt_ref(nt, blk, "body"), bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; return oa_map_row_evidence(c, blk, bn > 0 ? bb[bn - 1] : -1); } /* Such a table is held by no slot, so the slots never see it. Its one use is the walk, which reads each row's elements and hands no row on, so it is built in place as the table (arr_want), as the map a narrowed slot takes is. An each answers its receiver, so it qualifies only where its value is thrown away. The tables stamped last round are kept, so a round that stamps the same ones again reports no change; one that stops qualifying gets its want back. Answers whether the set changed. */ static int oa_stamp_walked_tables(Compiler *c) { const NodeTable *nt = c->nt; if (!c->arr_want) return 0; int n = 0, cap = 0, changed = 0; int *node = NULL; TyKind *kind = NULL; NT_FOREACH_KIND(nt, NK_CallNode, id) { int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || recv >= c->node_cap || nested_row_iter_call(c, id) != ROW_WALK_SPLITS) continue; if (!is_map_alias(nt_str(nt, id, "name")) && !oa_value_dropped(c, id)) continue; int rows = oa_walked_table_kind(c, recv); if (!OA_CLS_IS_NESTED(rows)) continue; TyKind k = rows == OA_CLS_IA ? TY_INT_ARRAY_ARRAY : TY_FLOAT_ARRAY_ARRAY; if (c->arr_want[recv] != TY_UNKNOWN && c->arr_want[recv] != k) continue; /* another producer's */ if (n == cap) { cap = cap ? cap * 2 : 16; node = (int *)realloc(node, sizeof(int) * (size_t)cap); kind = (TyKind *)realloc(kind, sizeof(TyKind) * (size_t)cap); if (!node || !kind) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } node[n] = recv; kind[n++] = k; } for (int i = 0; i < c->oa_walked_n; i++) { int kept = 0; for (int j = 0; j < n && !kept; j++) kept = node[j] == c->oa_walked_node[i] && kind[j] == c->oa_walked_kind[i]; if (kept) continue; changed = 1; int old = c->oa_walked_node[i]; if (old < c->node_cap && c->arr_want[old] == c->oa_walked_kind[i]) comp_set_arr_want(c, old, TY_UNKNOWN); } if (n != c->oa_walked_n) changed = 1; for (int j = 0; j < n; j++) if (c->arr_want[node[j]] != kind[j]) comp_set_arr_want(c, node[j], kind[j]); free(c->oa_walked_node); free(c->oa_walked_kind); c->oa_walked_node = node; c->oa_walked_kind = kind; c->oa_walked_n = n; return changed; } /* The copy of Enumerable#each_with_index (and the other yielding builtins) keeps both arms of `if block_given?` in the AST. Codegen folds the test, so exactly one arm runs for this site. The other still reads the table (`recv = self` in the Enumerator arm) and, unclaimed, killed the pin. Mark that arm dead for the rest of this pass. The cell the else arm allocates stays in the AST; this only stops it counting as a use. Answers the mark per node, or NULL when it cannot be allocated. */ static unsigned char *oa_dead_block_given_arms(Compiler *c, int nc) { const NodeTable *nt = c->nt; unsigned char *dead = (unsigned char *)calloc((size_t)nc, 1); if (!dead) return NULL; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_IfNode) continue; int pred = nt_ref(nt, id, "predicate"); if (pred < 0 || nt_kind(nt, pred) != NK_CallNode || nt_ref(nt, pred, "receiver") >= 0) continue; const char *pn = nt_str(nt, pred, "name"); if (!pn || !sp_streq(pn, "block_given?")) continue; Scope *sc = comp_scope_of(c, id); int site = enum_copy_site(c, sc); if (site < 0 || site >= nt->count || nt_kind(nt, site) != NK_CallNode) continue; int has_block = nt_ref(nt, site, "block") >= 0; int arm = -1; if (has_block) { int sub = nt_ref(nt, id, "subsequent"); if (sub >= 0 && nt_kind(nt, sub) == NK_ElseNode) arm = sub; } else { arm = nt_ref(nt, id, "statements"); } if (arm >= 0) oa_mark_subtree(nt, arm, dead); } return dead; } /* Does component root s narrow (narrow_object_arrays step 7)? Alive, with one class known, and no use its narrowed kind has no emitter for. */ static int oa_component_narrows(Compiler *c, const OAS *s) { if (!s->alive || s->cls == -1 || s->cls == -2 || s->cls == OA_CLS_PIA) return 0; /* A block iterator is kept for a numeric row, and for an object element only when it is one of the walks (bit 1): a reduce, an inject or a zip (bit 2), or a block that takes the row apart (bit 16), on an object array would narrow a table those emitters do not walk. */ /* ... and only while nothing yet unsupported comes with it: a walk whose value is used (bit 8), or an element class that is itself Enumerable, whose Enumerable methods have no emitter on a typed receiver. An element class with its own `each` but no Enumerable has no such methods, and its `each` is an ordinary method call on the element. */ if (!OA_CLS_IS_NESTED(s->cls) && ((s->row_iter & 26) || (s->row_iter && s->cls >= 0 && an_class_includes_enumerable(c, s->cls)))) return 0; /* a nested scalar array: the codegen supports index/push/[]=/length/ first/last but not the boxed sort/min/max comparators yet, so a component that used those (needs_cmp) stays on the poly path for now, and so does one that used pop, shift, delete_at, clear or a filter (removes). A boxed element stored as a row stays there too: an object element is unboxed with a class check, but a row is taken as a bare pointer, and a boxed poly array read back through the table as an sp_IntArray * printed garbage and crashed. */ if (OA_CLS_IS_NESTED(s->cls)) return !(s->needs_cmp || s->removes || s->poly_elem); /* a component using no-block sort/min/max narrows only when the element class can actually compare (has `<=>` in its chain); otherwise it stays poly, where the boxed comparator raises the CRuby ArgumentError. */ return !(s->needs_cmp && comp_method_in_chain(c, s->cls, "<=>", NULL) < 0); } /* Interprocedural edges of call id, whose target narrow_object_arrays resolved to method oa_tmi (-1: unattributable). A PARAM slot may only narrow when EVERY argument reaching it is a same-component candidate slot: the callee's C signature and the caller's C locals must agree on one container type. A call that resolves to the method but passes a non-slot value for that position, or a call to the method we cannot attribute at all, kills the param slot (the caller would keep passing a boxed poly array into an unboxed sp_PtrArray* -- the #1867 mis-box). Instance methods resolve through the class chain now; previously only free functions made edges, so an instance method's param narrowed with its callers' locals left poly. */ static void oa_call_edges(Compiler *c, OAS *sl, int n, int id, int oa_tmi, const int *argv, int argc, const int *read_slot, char *claimed, int *call_ret, int nc) { const char *name = nt_str(c->nt, id, "name"); ArgLayout L; memset(&L, 0, sizeof L); if (oa_tmi >= 0) { int R = oa_find(sl, n, oa_tmi, NULL); if (R >= 0 && id < nc) { call_ret[id] = R; sl[R].saw_call = 1; } Scope *M = &c->scopes[oa_tmi]; call_layout(c, M, argv, argc, &L); for (int k = 0; k < M->nparams; k++) { LocalVar *plv = M->pnames[k] ? scope_local(M, M->pnames[k]) : NULL; int T = plv ? oa_find(sl, n, oa_tmi, plv) : -1; if (T < 0) continue; int a = layout_plain_arg(c, M, argv, &L, k); if (a >= 0 && read_slot[a] >= 0) { claimed[a] = 1; oa_uf_union(sl, read_slot[a], T); } else if (oa_obj_literal(c, a)) { oa_classify_value(c, sl, n, read_slot, call_ret, claimed, T, a); oa_note_src(T, a); } else { sl[T].alive = 0; /* unmodeled incoming value for this position */ } /* An element the method stores through this parameter from another of its own parameters (store_val_src) is this call's argument for that one, and its class is evidence as a push of it here would be. Read through the parameter it is boxed, which is neutral, so a helper pushing objects of two classes narrowed every array it was handed to the first class, and an element of the other was read as that class. */ for (int j = 0; plv->store_val_src && j < M->nparams && j < 64; j++) { if (!((plv->store_val_src >> j) & 1ULL)) continue; int aj = layout_plain_arg(c, M, argv, &L, j); if (aj >= 0) sl[T].cls = oa_cls_join(sl[T].cls, oa_elem_evidence(c, sl, T, aj)); } } } for (int k = 0; argv && k < argc; k++) { int a = argv[k]; if (read_slot[a] < 0) continue; int S = read_slot[a]; /* zip's other operand is a peer array. A nested table passed there is still the table: claiming it lets the literal `[[10],[20]]` narrow instead of dying as an unmodeled builtin argument. */ if (oa_tmi < 0) { if (k == 0 && name && sp_streq(name, "zip") && nested_row_iter_call(c, id)) { claimed[a] = 1; sl[S].row_iter |= 2; /* a zip's argument: numeric rows only (#4846) */ continue; } sl[S].alive = 0; continue; } /* the parameter the layout funds from argument k; none (the rest collects it) kills */ Scope *M = &c->scopes[oa_tmi]; int pk = 0; while (pk < M->nparams && layout_plain_arg(c, M, argv, &L, pk) != a) pk++; LocalVar *plv = pk < M->nparams && M->pnames[pk] ? scope_local(M, M->pnames[pk]) : NULL; int T = plv ? oa_find(sl, n, oa_tmi, plv) : -1; if (T < 0) { sl[S].alive = 0; continue; } claimed[a] = 1; oa_uf_union(sl, S, T); } arg_layout_free(&L); } /* narrow_object_arrays' calls on an element of a slot, decided in step 5d: the call node, the slot, and the class it was decided on (-1: dynamic) */ typedef struct { int id, slot, cls; } OaPend; static int oa_lv_cmp(const void *a, const void *b) { uintptr_t x = (uintptr_t)*(LocalVar *const *)a, y = (uintptr_t)*(LocalVar *const *)b; return x < y ? -1 : x > y; } /* The locals a write node assigns, sorted (bsearch with oa_lv_cmp). */ static LocalVar **oa_written_locals(Compiler *c, int *nout) { const NodeTable *nt = c->nt; int n = 0, cap = 0; LocalVar **out = NULL; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_LocalVariableWriteNode && k != NK_LocalVariableTargetNode && k != NK_LocalVariableOperatorWriteNode && k != NK_LocalVariableAndWriteNode && k != NK_LocalVariableOrWriteNode) continue; const char *nm = nt_str(nt, id, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[c->nscope[id]], nm) : NULL; if (!lv) continue; if (n == cap) { cap = cap ? cap * 2 : 64; out = (LocalVar **)realloc(out, sizeof *out * (size_t)cap); if (!out) { fprintf(stderr, "oom\n"); exit(1); } } out[n++] = lv; } if (n) qsort(out, (size_t)n, sizeof *out, oa_lv_cmp); *nout = n; return out; } /* The constant slot read id (a ConstantReadNode or ConstantPathNode) names, or -1, as infer_type resolves the read: by its leaf, unless the name is a class's or the path cannot reach the entry. A path through a class VALUE (`klass::X`) reads whichever class's X the value is, so it answers that slot with *dyn set: a use nothing vets. */ static int oa_const_slot(Compiler *c, OAS *sl, int n, int id, int *dyn) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); *dyn = 0; if (!nm || comp_class_index(c, nm) >= 0) return -1; if (nt_kind(nt, id) == NK_ConstantPathNode) { if (const_path_in_closed_builtin(c, id) || const_path_misses_toplevel(c, id)) return -1; int p = nt_ref(nt, id, "parent"); NodeKind pk = p >= 0 ? nt_kind(nt, p) : NK_NONE; const char *pn = p >= 0 ? nt_str(nt, p, "name") : NULL; if (p >= 0 && (!(pk == NK_ConstantReadNode || pk == NK_ConstantPathNode) || !pn || comp_class_index(c, pn) < 0)) *dyn = 1; } LocalVar *cv = comp_const(c, nm); return cv ? oa_find(sl, n, OA_CONST, cv) : -1; } static int strbuf_elem_first_iterator(const char *n); /* The slot whose element node recv is, or -1: an element read of a slot (`as[i]`), or the element parameter of a walk over one (`as.each { |a| }`, each_with_index, map) that nothing in the block reassigns. Once the slot narrows, either is typed as the slot's element class. */ static int oa_elem_recv_slot(Compiler *c, int recv, const int *read_slot, const int *par, int nc, LocalVar **wlv, int nwlv) { const NodeTable *nt = c->nt; if (recv < 0 || recv >= nc) return -1; NodeKind k = nt_kind(nt, recv); if (k == NK_CallNode) { const char *nm = nt_str(nt, recv, "name"); int r = nt_ref(nt, recv, "receiver"), args = nt_ref(nt, recv, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (!nm || !is_element_at_alias(nm) || argc != 1 || nt_ref(nt, recv, "block") >= 0 || r < 0 || r >= nc || nt_kind(nt, argv[0]) == NK_RangeNode) return -1; return read_slot[r]; } if (k != NK_LocalVariableReadNode) return -1; const char *nm = nt_str(nt, recv, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[c->nscope[recv]], nm) : NULL; if (!lv || !lv->is_block_param || (nwlv && bsearch(&lv, wlv, (size_t)nwlv, sizeof *wlv, oa_lv_cmp))) return -1; /* the block that binds the name: the nearest enclosing one declaring it */ for (int b = par[recv]; b >= 0; b = par[b]) { if (nt_kind(nt, b) != NK_BlockNode) continue; int idx = -1; for (int j = 0; idx < 0 && block_param_name(c, b, j); j++) if (sp_streq(block_param_name(c, b, j), nm)) idx = j; if (idx < 0) continue; int w = par[b]; if (idx != 0 || w < 0 || nt_kind(nt, w) != NK_CallNode || nt_ref(nt, w, "block") != b) return -1; const char *wn = nt_str(nt, w, "name"); int wr = nt_ref(nt, w, "receiver"); if (!wn) return -1; /* a Ruby-defined builtin's call, rewritten receiverless with the receiver as its first argument (desugar_builtin_enum_calls): its copy yields the receiver's elements as the builtin does */ char bn[64]; if (wr < 0 && pivs_enum_iter(wn, bn, sizeof bn)) { int wa = nt_ref(nt, w, "arguments"), wan = 0; const int *wav = wa >= 0 ? nt_arr(nt, wa, "arguments", &wan) : NULL; return wan >= 1 && strbuf_elem_first_iterator(bn) && wav[0] < nc ? read_slot[wav[0]] : -1; } if (nested_row_iter_call(c, w) != ROW_WALK_BINDS || !(is_each_walk_or_with_index(wn) || is_map_alias(wn))) return -1; return wr >= 0 && wr < nc ? read_slot[wr] : -1; } return -1; } static int narrow_object_arrays(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; /* Start every round from the same shape. A slot narrowed last round holds its pointer-array type, which would keep it out of the candidate list below and split its component; put it back on the poly array and let the evidence decide again. Doing it this way rather than pinning-and-skipping keeps the pass idempotent, so a slot that stops qualifying (a new escape appeared as the fixpoint desugared the program) un-narrows instead of keeping a type its uses no longer support. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int li = 0; li < sc->nlocals; li++) { TyKind pn = sc->locals[li].oa_pin; /* only THIS pass's own narrowings go back on the poly array: the pin field also carries the element-narrowing's decision (a local bound from a container read), and resetting one of those to a poly ARRAY made the two passes trade the slot every round (#3781) */ if (pn == TY_INT_ARRAY_ARRAY || pn == TY_FLOAT_ARRAY_ARRAY || ty_is_obj_array(pn)) lv_set_type(&sc->locals[li], TY_POLY_ARRAY); /* Enumerable's per-site copy types `__self` from its argument, so a nested table arrives here already narrowed and would miss the candidate list. The argument loop then reads "param not a candidate" as a kill and the caller's table oscillates with the pin reassert. Put the copy's receiver back on the poly array and let this round's evidence decide it again, the same as a pin this pass wrote itself. */ if (sc->name && strncmp(sc->name, "__enum_", 7) == 0 && sc->locals[li].name && sp_streq(sc->locals[li].name, "__self") && (sc->locals[li].type == TY_INT_ARRAY_ARRAY || sc->locals[li].type == TY_FLOAT_ARRAY_ARRAY)) lv_set_type(&sc->locals[li], TY_POLY_ARRAY); } } for (int i = 0; i < c->nconsts; i++) if (ty_is_obj_array(c->consts[i].oa_pin)) lv_set_type(&c->consts[i], TY_POLY_ARRAY); /* 1. candidate slots: POLY_ARRAY locals/params (skip block params + rbs). */ int cap = 16, n = 0; OAS *sl = (OAS *)malloc(sizeof(OAS) * cap); oa_index_reset(); g_oa_empt_n = 0; g_oa_src_n = 0; /* Drop every want this pass stamped on a map source in an earlier round, before the round re-derives them. OA_DROP_SRC_STAMP below only reaches the sources RECORDED this round, and a node stops being recorded for reasons that have nothing to do with it still carrying a stamp -- its slot is no longer a candidate, the receiver's type no longer reads as an array, the row it aliases is no longer a slot. Such a node kept a pointer-array want while its destination went back to the poly array, and infer_type hands that want back ahead of the poly fallback. A map/collect CallNode is the exact discriminator: this pass is the only producer that stamps one. The other producers stamp empty `[]` literals, which are ArrayNodes, so their wants are untouched -- except the empty rows this pass stamped itself, which g_oa_empt_mine names. Those fire: `@row = []` reads as an int array for a round before `@row = pattern` makes it boxed, and `@banks[0] = @row` narrowed @banks on that round. The drop a round later left the `[]` of `@banks = [[]]` an int-array row, and a boxed row stored over it was read back as an sp_IntArray *. */ int n_cleared = 0, cap_cleared = 0; int *cleared = NULL; if (c->arr_want) { for (int id = 0; id < c->nt->count && id < c->node_cap; id++) { if (id < g_oa_empt_mine_cap && g_oa_empt_mine[id]) { g_oa_empt_mine[id] = 0; if (c->arr_want[id] != TY_INT_ARRAY && c->arr_want[id] != TY_FLOAT_ARRAY) continue; } else { if (!ty_is_ptr_array(c->arr_want[id])) continue; if (nt_kind(nt, id) == NK_ArrayNode) { if (!ty_is_obj_array(c->arr_want[id]) || is_empty_array_literal(nt, id, c->node_cap)) continue; } else { if (nt_kind(nt, id) != NK_CallNode) continue; const char *rn = nt_str(nt, id, "name"); if (!rn || !(is_map_alias(rn))) continue; } } comp_set_arr_want(c, id, TY_UNKNOWN); if (n_cleared == cap_cleared) { cap_cleared = cap_cleared ? cap_cleared * 2 : 16; int *nc = (int *)realloc(cleared, sizeof(int) * (size_t)cap_cleared); if (!nc) { free(cleared); cleared = NULL; n_cleared = 0; cap_cleared = 0; break; } cleared = nc; } cleared[n_cleared++] = id; } } for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int li = 0; li < sc->nlocals; li++) { LocalVar *lv = &sc->locals[li]; if (lv->type != TY_POLY_ARRAY || lv->is_block_param || lv->rbs_seeded || cext_scope(c, s)) continue; if (n >= cap) { cap *= 2; sl = (OAS *)realloc(sl, sizeof(OAS) * cap); if (!sl) { fprintf(stderr, "oom\n"); exit(1); } } sl[n].sidx = s; sl[n].lv = lv; sl[n].cls = -1; sl[n].alive = 1; sl[n].uf = n; sl[n].needs_cmp = 0; sl[n].removes = 0; sl[n].poly_elem = 0; sl[n].row_iter = 0; sl[n].saw_call = 0; sl[n].ici = -1; sl[n].iiv = -1; sl[n].row_cls = -1; sl[n].row_escape = 0; sl[n].odd_elem = 0; sl[n].old_pin = lv->oa_pin; lv->oa_pin = TY_UNKNOWN; n++; } } /* 1b. one more slot per method whose VALUE is a poly array. Factoring a table out into `build_table(n)` puts the same storage in two scopes, and with no slot for the callee's value the caller's `t = build_table(n)` was an unmodeled write source that killed the caller's slot -- the narrowing stopped at the first method boundary. A return slot carries a NULL local; its scope index names the method. Bodies with no single C return to retype (inlined yielders, proc-form clones, synthesized and transplanted ones) and returns pinned by a seed stay out. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->ret_oa_pin == TY_INT_ARRAY_ARRAY || sc->ret_oa_pin == TY_FLOAT_ARRAY_ARRAY || ty_is_obj_array(sc->ret_oa_pin)) scope_set_ret(sc, TY_POLY_ARRAY); /* same reset */ if (sc->ret != TY_POLY_ARRAY || !sc->name || sc->def_node < 0) continue; if (sc->ret_rbs_seeded || sc->ret_specialized) continue; /* `new` answers the object, never initialize's value: a constructor ending in `@items << x` has no return slot to keep alive, and one that joined the ivar's component through that push would only sink it */ if (sp_streq(sc->name, "initialize")) continue; if (sc->yields || sc->is_proc_form || sc->is_lowered_yield || sc->cs_synth || sc->is_transplanted_source) continue; /* a runtime protocol enters the method with no call node to vet, and the emitted protocol call reads the boxed array back (`5 + money` asks Money#coerce for a pair) */ if (method_name_implicitly_invoked(sc->name)) continue; if (n >= cap) { cap *= 2; sl = (OAS *)realloc(sl, sizeof(OAS) * cap); if (!sl) { fprintf(stderr, "oom\n"); exit(1); } } sl[n].sidx = s; sl[n].lv = NULL; sl[n].cls = -1; sl[n].alive = 1; sl[n].uf = n; sl[n].needs_cmp = 0; sl[n].removes = 0; sl[n].poly_elem = 0; sl[n].row_iter = 0; sl[n].saw_call = 0; sl[n].ici = -1; sl[n].iiv = -1; sl[n].row_cls = -1; sl[n].row_escape = 0; sl[n].odd_elem = 0; sl[n].old_pin = sc->ret_oa_pin; sc->ret_oa_pin = TY_UNKNOWN; n++; } /* 1c. one slot per @ivar holding a poly array (#4444). An ivar's references are spread over every method of its class and can leave through an attr_reader, so the slot is vetted the way narrow_int_table_ivars vets a table: every read is the receiver of a modeled op or an alias edge this pass follows, every write a modeled source, from methods of the owning class or a subclass (whose copy of the ivar is this slot: its struct begins with the same fields, and inherited methods read them through a cast). A reader is not an escape by itself -- each of its call sites on a receiver statically of this class or a subclass is one more read of the slot -- but a dynamic receiver that could dispatch to it, a symbol naming it, a writer, or a class-level (singleton) read all keep the slot boxed. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; if (cl->def_node < 0 || nt_kind(nt, cl->def_node) == NK_ModuleNode) continue; if (cl->is_struct || cl->is_data || cl->is_native_class || cl->is_value_type) continue; for (int iv = 0; iv < cl->nivars; iv++) { const char *ivn = cl->ivars[iv]; if (!ivn || ivn[0] != '@') continue; if (cl->ivar_oa_type[iv] != TY_UNKNOWN) { /* this pass's own narrowing from the last round: back on the poly array, the evidence decides again (the same reset the locals get) */ ivar_set_type(cl, iv, TY_POLY_ARRAY); { cl->ivar_int_table[iv] = 0; comp_touch_class(cl->idx); } } if (cl->ivar_types[iv] != TY_POLY_ARRAY || cl->ivar_int_table[iv]) continue; if (class_ivar_pinned(cl, ivn)) continue; const char *bare = ivn + 1; if (comp_is_sg_writer(cl, bare) || comp_is_sg_reader(cl, bare)) continue; int inherited = 0; for (int k = cl->parent; k >= 0; k = c->classes[k].parent) if (comp_ivar_index(&c->classes[k], ivn) >= 0) { inherited = 1; break; } /* a subclass's copy of an ancestor's ivar is that ancestor's slot: the decision is applied to every copy, and reset with it above */ if (inherited) { cl->ivar_oa_type[iv] = TY_UNKNOWN; continue; } if (n >= cap) { cap *= 2; sl = (OAS *)realloc(sl, sizeof(OAS) * cap); if (!sl) { fprintf(stderr, "oom\n"); exit(1); } } sl[n].sidx = -1; sl[n].lv = NULL; sl[n].cls = -1; sl[n].alive = 1; sl[n].uf = n; sl[n].needs_cmp = 0; sl[n].removes = 0; sl[n].poly_elem = 0; sl[n].row_iter = 0; sl[n].saw_call = 0; sl[n].ici = ci; sl[n].iiv = iv; sl[n].row_cls = -1; sl[n].row_escape = 0; sl[n].odd_elem = 0; /* An `Array[Array[Integer]]` / `Array[Array[Float]]` seed is element evidence, the same evidence a pushed row gives: it decides the kind when the program's own rows are silent (`Array.new(n) { [] }`), and a row of the other kind joins against it into a conflict the report after the fixpoint names (#4484). The seed is evidence and not a type pin because a pinned ivar is skipped by this very pass. */ if (cl->ivar_oa_seed[iv] == SEED_OA_INT_TABLE) sl[n].cls = OA_CLS_IA; else if (cl->ivar_oa_seed[iv] == SEED_OA_FLT_TABLE) sl[n].cls = OA_CLS_FA; sl[n].old_pin = cl->ivar_oa_type[iv]; cl->ivar_oa_type[iv] = TY_UNKNOWN; n++; } } /* 1d. one slot per constant holding a poly array of objects (scope index OA_CONST): a table built once (`BODIES = [Body.new(...), ...]`) and read from the methods. A constant is kept by its leaf name, as every read and write of the name reaches the one entry. Its reads are vetted as a local's are, its writes are its sources, and a route by name (const_get, a Symbol) keeps it boxed, and so does a C extension, which reaches any constant by name (rb_const_get). */ for (int i = 0; i < c->nconsts && !c->n_cext_extensions; i++) { LocalVar *cv = &c->consts[i]; if (cv->type != TY_POLY_ARRAY || cv->rbs_seeded) continue; if (n >= cap) { cap *= 2; sl = (OAS *)realloc(sl, sizeof(OAS) * cap); if (!sl) { fprintf(stderr, "oom\n"); exit(1); } } sl[n].sidx = OA_CONST; sl[n].lv = cv; sl[n].cls = -1; sl[n].alive = 1; sl[n].uf = n; sl[n].needs_cmp = 0; sl[n].removes = 0; sl[n].poly_elem = 0; sl[n].row_iter = 0; sl[n].saw_call = 0; sl[n].ici = -1; sl[n].iiv = -1; sl[n].row_cls = -1; sl[n].row_escape = 0; sl[n].odd_elem = 0; sl[n].old_pin = cv->oa_pin; cv->oa_pin = TY_UNKNOWN; n++; } /* the drop still has to be reported (and `cleared` freed) on the way out: a round with no candidate slot at all still cleared this pass's wants. */ int walked = oa_stamp_walked_tables(c); if (n == 0) { int dropped = oa_want_dropped(c, cleared, n_cleared) | walked; free(cleared); free(sl); return dropped; } /* (class, ivar) -> slot + 1, so a read costs one ivar-name lookup in its own class rather than a scan of every slot: the scan was O(nodes x slots) per round and took a 100k-line program from seconds to minutes. */ int **ivslot = (int **)calloc((size_t)(c->nclasses ? c->nclasses : 1), sizeof(int *)); for (int i = 0; i < n; i++) { if (sl[i].ici < 0) continue; if (!ivslot[sl[i].ici]) ivslot[sl[i].ici] = (int *)calloc((size_t)c->classes[sl[i].ici].nivars, sizeof(int)); ivslot[sl[i].ici][sl[i].iiv] = i + 1; } /* An ivar slot's answer to oa_class_may_answer, asked by its reader's name -- the name both rules below ask it for -- once per pass: -1 until asked. It scans the class table for subclasses, and a slot it answers no for stays alive, so every dynamic call of the name asked again. */ signed char *oa_ans = (signed char *)malloc((size_t)n); if (!oa_ans) { fprintf(stderr, "oom\n"); exit(1); } memset(oa_ans, -1, (size_t)n); #define OA_MAY_ANSWER(i) (oa_ans[(i)] < 0 ? (oa_ans[(i)] = (signed char)oa_class_may_answer(c, sl[(i)].ici, \ c->classes[sl[(i)].ici].ivars[sl[(i)].iiv] + 1)) : oa_ans[(i)]) /* (a subclass's use is a use of the slot its ancestor declares) */ #define OA_IVSLOT(ci, ivn) ({ int _r = -1; \ for (int _k = (ci); _r < 0 && _k >= 0 && _k < c->nclasses; _k = c->classes[_k].parent) \ if (ivslot[_k]) { int _iv = comp_ivar_index(&c->classes[_k], (ivn)); if (_iv >= 0) _r = ivslot[_k][_iv] - 1; } \ _r; }) /* A reference inside an instance_eval/exec block is to the RECEIVER's ivar (ie_class_of), not the enclosing method's class's: a use of the slot from another class's code, unvetted. Kills the receiver's slot (each class a poly receiver can be; every slot of the name when those are not known, or fill the list and may have been cut short) and answers 1; 0 for a node outside such a block, or in one whose receiver is of the enclosing instance method's own class (the same slot, vetted as any of the method's own uses). */ #define OA_IE_KILL(node, ivn) ({ int _h = 0, _iec = ie_class_of(c, (node)); \ Scope *_isc = comp_scope_of(c, (node)); \ if (_iec >= 0 && _isc && !_isc->is_cmethod && _isc->class_id == _iec) { } \ else if (_iec >= 0) { int _s = OA_IVSLOT(_iec, (ivn)); if (_s >= 0) sl[_s].alive = 0; _h = 1; } \ else if (_iec < -1) { \ int _pk[64], _np = ie_poly_classes_at(c, (node), _pk, 64); \ for (int _q = 0; _q < _np; _q++) { int _s = OA_IVSLOT(_pk[_q], (ivn)); if (_s >= 0) sl[_s].alive = 0; } \ if (_np == 0 || _np == 64) for (int _q = 0; _q < n; _q++) \ if (sl[_q].ici >= 0 && sp_streq(c->classes[sl[_q].ici].ivars[sl[_q].iiv], (ivn))) sl[_q].alive = 0; \ _h = 1; } \ _h; }) int nc = nt->count ? nt->count : 1; unsigned char *dead = oa_dead_block_given_arms(c, nc); int *read_slot = (int *)malloc(sizeof(int) * nc); /* call_ret[id]: the return slot this CallNode's value comes from, or -1. */ int *call_ret = (int *)malloc(sizeof(int) * nc); for (int id = 0; id < nc; id++) call_ret[id] = -1; char *claimed = (char *)calloc(nc, 1); /* value_ok[id]: node id sits in statement position (a StatementsNode body entry) -- one modeled consumer for a `sort`/`sort!` result. The other (slot-alias RHS) is recognized in step 5. */ char *value_ok = (char *)calloc(nc, 1); for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_StatementsNode) continue; int bn = 0; const int *bl = nt_arr(nt, id, "body", &bn); for (int i = 0; i < bn; i++) if (bl[i] >= 0 && bl[i] < nc) value_ok[bl[i]] = 1; } /* The tail of a method body is the method's value, not a discarded statement. When the method has a return slot, step 5b classifies the tail; when it has none (its return was still unknown this round because the callee is defined further down the file, or is not an array at all) the value leaves through a return this pass does not model, so the callee's slot must die here rather than narrow under a caller whose own uses were never vetted (#4490). */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->def_node < 0 || !sc->name || sc->body < 0) continue; if (oa_find(sl, n, s, NULL) >= 0) continue; int bn = 0; const int *bl = nt_arr(nt, sc->body, "body", &bn); if (bn > 0 && bl[bn - 1] >= 0 && bl[bn - 1] < nc) value_ok[bl[bn - 1]] = 0; } /* walk_disc[id]: a walk's value is thrown away (oa_walk_disc) */ char *walk_disc = oa_walk_disc(c, nc); for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_LocalVariableWriteNode) continue; int sidx = c->nscope[id]; const char *nm = nt_str(nt, id, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[sidx], nm) : NULL; if (!lv || oa_find(sl, n, sidx, lv) < 0) continue; /* target must be a slot */ int v = nt_ref(nt, id, "value"); if (v >= 0 && v < nc) value_ok[v] = 1; /* alias edge made in step 5 */ } /* 2. map each LocalVariableReadNode of a slot to its slot index. An ivar slot is read by `@x` inside an instance method of its class, and by a call of its attr_reader on a receiver statically of that class (or a bare reader call from inside the class); a read from any other class (a subclass, a module method, a singleton method) is a use this pass does not vet and kills the slot. */ for (int id = 0; id < nt->count; id++) { read_slot[id] = -1; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; if (ty_k == NK_LocalVariableReadNode) { if (dead && dead[id]) continue; int sidx = c->nscope[id]; const char *nm = nt_str(nt, id, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[sidx], nm) : NULL; if (lv) read_slot[id] = oa_find(sl, n, sidx, lv); continue; } if (ty_k == NK_ConstantReadNode || ty_k == NK_ConstantPathNode) { if (dead && dead[id]) continue; int dyn = 0, i = oa_const_slot(c, sl, n, id, &dyn); if (i >= 0) { if (dyn) sl[i].alive = 0; else read_slot[id] = i; } continue; } if (ty_k == NK_InstanceVariableReadNode) { /* the slot's class and its subclasses read it from their instance methods; the only other reader is a singleton method of one of them (a class-level ivar of the same name) */ const char *nm = nt_str(nt, id, "name"); Scope *sc = comp_scope_of(c, id); if (!nm || !sc || OA_IE_KILL(id, nm)) continue; int i = OA_IVSLOT(sc->class_id, nm); if (i < 0) continue; if (sc->is_cmethod) sl[i].alive = 0; else read_slot[id] = i; continue; } if (ty_k == NK_CallNode) { const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int args = nt_ref(nt, id, "arguments"); int an = 0; if (args >= 0) nt_arr(nt, args, "arguments", &an); if (!nm || an != 0 || nt_ref(nt, id, "block") >= 0) continue; char ivn[300]; if (strlen(nm) >= sizeof ivn - 1) continue; ivn[0] = '@'; strcpy(ivn + 1, nm); /* a bare call in an instance_eval/exec block is the receiver's reader */ if (recv < 0 && OA_IE_KILL(id, ivn)) continue; int rci = -1; if (recv >= 0) { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) rci = ty_object_class(rt); } else { Scope *sc = comp_scope_of(c, id); if (sc && !sc->is_cmethod) rci = sc->class_id; } if (rci < 0 || !comp_reader_in_chain(c, rci, nm, NULL)) continue; int i = OA_IVSLOT(rci, ivn); /* the synthesized attr_reader only: an explicit `def x; @x; end` is a method with a return slot, reached through call_ret in step 4 */ if (i >= 0 && call_target(c, id, rci, nm, 0, NULL) < 0) read_slot[id] = i; } } /* 3. op-assign / multi-target write forms on a slot are unmodeled -> kill. An ivar form directly in an instance method of a class writes that class's ivar, so it kills that slot alone: a memoizing `@t ||= x` in one class is no write of an unrelated class's @t. Anywhere else -- a block (instance_eval, a trampoline), a module, a singleton method, the top level -- self is not settled here, so it kills every class's slot of the name. */ unsigned char *in_blk = (unsigned char *)calloc((size_t)nc, 1); for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k == NK_BlockNode || ty_k == NK_LambdaNode) oa_mark_subtree(nt, id, in_blk); } /* (the ivar names such a write names outside its own class, as a set: matched against the slots once below, not per write) */ ANameHash ivw; memset(&ivw, 0, sizeof ivw); for (int id = 0; id < nt->count; id++) { const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; if (ty_k == NK_InstanceVariableOperatorWriteNode || ty_k == NK_InstanceVariableTargetNode || ty_k == NK_InstanceVariableAndWriteNode || ty_k == NK_InstanceVariableOrWriteNode) { const char *nm = nt_str(nt, id, "name"); if (!nm) continue; Scope *sc = comp_scope_of(c, id); int own = (sc && !sc->is_cmethod && !in_blk[id] && sc->class_id >= 0 && sc->class_id < c->nclasses && c->classes[sc->class_id].def_node >= 0 && nt_kind(nt, c->classes[sc->class_id].def_node) != NK_ModuleNode) ? sc->class_id : -1; if (own >= 0) { int i = OA_IVSLOT(own, nm); if (i >= 0) sl[i].alive = 0; continue; } if (!anh_has(&ivw, nm)) anh_add(&ivw, nm); continue; } if (ty_k != NK_LocalVariableOperatorWriteNode && ty_k != NK_LocalVariableTargetNode && ty_k != NK_LocalVariableAndWriteNode && ty_k != NK_LocalVariableOrWriteNode) continue; int sidx = c->nscope[id]; const char *nm = nt_str(nt, id, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[sidx], nm) : NULL; int si = lv ? oa_find(sl, n, sidx, lv) : -1; if (si >= 0) sl[si].alive = 0; } if (ivw.n) for (int i = 0; i < n; i++) if (sl[i].ici >= 0 && anh_has(&ivw, c->classes[sl[i].ici].ivars[sl[i].iiv])) sl[i].alive = 0; anh_free(&ivw); free(in_blk); /* 3a. a route that INVOKES an ivar's writer. A writer stores whatever it is handed, and a synthesized `attr_accessor`/`attr_writer` one does so with no node in the tree -- so unlike every other write source, an unvetted one leaves nothing behind for step 5a to classify and kill the slot with. Every route is named here instead: `o.t = v` and the multi-target form carry the setter name, the op-assign forms carry the BARE name (they read and write through both), and a `:t=` symbol anywhere is `send`/`method`/`define_method`/`alias`. A route whose receiver is statically an instance of a class reaches only that class's writer or a subclass's, so it kills only those slots: `o.t = v` on a Ctx is no write of an unrelated State's @t. A symbol, or a receiver that is not statically a class instance, kills the slot of every class with the name -- the conservatism step 3 uses too. This replaces a blanket bail on any class DECLARING a writer (#4444), which cost the narrowing to writers that are never called -- ones spinel does not even emit. A method absent from the output should not change the output's types. Collected as DISTINCT names first, then matched against the slots once: a scan of every slot per setter call is the O(nodes x slots) shape that took a 100k-line program from seconds to minutes (the same trap ivslot above was introduced for). Distinct setter names are tens, not thousands. Each name is kept with its receiver's class (-1 when unresolved), so a pair is distinct by both. */ int nwn = 0, cwn = 0; char **wn = NULL; int *wcls = NULL; for (int id = 0; id < nt->count; id++) { const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; int trim; /* the node names the SETTER (trim the `=`) or the attribute */ if (ty_k == NK_CallNode || ty_k == NK_CallTargetNode || ty_k == NK_SymbolNode) trim = 1; else if (ty_k == NK_CallOperatorWriteNode || ty_k == NK_CallAndWriteNode || ty_k == NK_CallOrWriteNode) trim = 0; else continue; const char *nm = ty_k == NK_SymbolNode ? nt_str(nt, id, "value") : nt_str(nt, id, "name"); if (!nm) continue; char attr[300]; if (trim) { if (!setter_base_name(nm, attr, sizeof attr)) continue; } else { if (strlen(nm) >= sizeof attr) continue; strcpy(attr, nm); } int rcls = -1; if (ty_k != NK_SymbolNode) { int recv = nt_ref(nt, id, "receiver"); TyKind rt = recv >= 0 ? infer_type(c, recv) : TY_UNKNOWN; if (ty_is_object(rt)) rcls = ty_object_class(rt); if (rcls >= c->nclasses) rcls = -1; } int seen = 0; for (int k = 0; k < nwn && !seen; k++) if (wcls[k] == rcls && sp_streq(wn[k], attr)) seen = 1; if (seen) continue; if (nwn >= cwn) { cwn = cwn ? cwn * 2 : 16; wn = (char **)realloc(wn, sizeof(char *) * (size_t)cwn); wcls = (int *)realloc(wcls, sizeof(int) * (size_t)cwn); if (!wn || !wcls) { fprintf(stderr, "oom\n"); exit(1); } } wcls[nwn] = rcls; wn[nwn++] = strdup(attr); } for (int i = 0; nwn && i < n; i++) { if (sl[i].ici < 0 || !sl[i].alive) continue; const char *bare = c->classes[sl[i].ici].ivars[sl[i].iiv] + 1; for (int k = 0; k < nwn && sl[i].alive; k++) { if (!sp_streq(wn[k], bare)) continue; /* the slot's class is the receiver's, a subclass of it, or (the slot covering its subclasses' copies) an ancestor of it */ if (wcls[k] >= 0 && is_descendant(c, wcls[k], sl[i].ici)) { sl[i].alive = 0; break; } for (int a = sl[i].ici; a >= 0; a = c->classes[a].parent) if (wcls[k] < 0 || a == wcls[k]) { sl[i].alive = 0; break; } } } for (int k = 0; k < nwn; k++) free(wn[k]); free(wn); free(wcls); OaByName oa_bn = oa_byname_build(c, sl, n); const int *par = comp_parent_map(c); /* A dynamic receiver may dispatch to ANY same-named instance method at runtime (the poly dispatch arms), passing boxed values and expecting a boxed one back; neither a param slot nor a return slot reachable that way must narrow. */ #define OA_DYN_KILL(nm) do { \ for (int pi = oa_byname_first(&oa_bn, (nm)); pi >= 0; pi = oa_bn.next[pi]) { \ if (!sl[pi].alive) continue; \ /* a dynamic receiver may be this class: its reader would hand the \ unboxed array to a caller expecting the boxed one -- when the \ class has a reader, or a method, by the name at all */ \ if (sl[pi].ici >= 0) { if (OA_MAY_ANSWER(pi)) sl[pi].alive = 0; continue; } \ if (!sl[pi].lv) { sl[pi].alive = 0; continue; } \ Scope *PS = &c->scopes[sl[pi].sidx]; \ for (int pk = 0; pk < PS->nparams; pk++) \ if (PS->pnames[pk] && scope_local(PS, PS->pnames[pk]) == sl[pi].lv) \ sl[pi].alive = 0; \ } \ } while (0) /* the locals some write node assigns, so a block parameter one of them reassigns is no element of the walk that bound it (oa_elem_recv_slot) */ int nwlv = 0; LocalVar **wlv = oa_written_locals(c, &nwlv); /* the calls on an element of a slot, decided in step 5d */ OaPend *pend = NULL; int npend = 0, cpend = 0; /* 4. CallNodes: classify slot-as-receiver (supported op + element evidence) and slot-as-arg (positional into a resolvable free method -> edge). */ for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_CallNode) continue; if (dead && dead[id]) continue; const char *name = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int has_block = nt_ref(nt, id, "block") >= 0; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (recv >= 0 && read_slot[recv] >= 0) { int S = read_slot[recv]; if (oa_recv_op_ok(name, argc, has_block)) { claimed[recv] = 1; /* an element read answers a row: what is done with it (oa_row_use) */ if (name && ((is_element_at_alias(name) && argc == 1) || (is_endpoint_query(name) && argc == 0))) oa_row_use(c, sl, S, id, par, walk_disc, nc); if (name && is_push_alias(name)) { for (int a = 0; a < argc; a++) sl[S].cls = oa_cls_join(sl[S].cls, oa_elem_evidence(c, sl, S, argv[a])); /* an append answers the array: like a sort result it must land in a modeled consumer, or a chained `a.push(x).push(y)` pushes an element no slot sees */ if (!(id < nc && value_ok[id])) sl[S].alive = 0; } /* the removals have emitters for an object array only (the removes bit, checked at component resolution). clear answers the array, so like an append it must land in a modeled consumer, and the emitter reads delete_at's index as a C integer. */ else if (name && (sp_streq(name, "pop") || sp_streq(name, "shift") || sp_streq(name, "clear") || sp_streq(name, "delete_at"))) { sl[S].removes = 1; if (sp_streq(name, "clear") && !(id < nc && value_ok[id])) sl[S].alive = 0; /* (a boxed index too, which the emitter takes as an Integer the way every index is taken: --int-overflow=promote boxes the Integer locals after the fixpoint and runs the pass again, and a slot that fell back to the box there left the removals already typed as an element, whose nil then had no check) */ if (sp_streq(name, "delete_at") && infer_type(c, argv[0]) != TY_INT && infer_type(c, argv[0]) != TY_POLY) sl[S].alive = 0; } /* ... and a Range-keyed read is a slice, which the element read took as an Integer index and raised TypeError for (#9932): the same slot keeps the poly path */ else if (name && is_element_at_alias(name) && argc == 1 && (nt_kind(nt, argv[0]) == NK_RangeNode || infer_type(c, argv[0]) == TY_RANGE)) sl[S].alive = 0; else if (name && sp_streq(name, "[]=") && argc == 2) { /* a range-keyed []= is a splice, which the obj-array representation has no emitter for: keep the slot on the poly path */ if (infer_type(c, argv[0]) == TY_RANGE) sl[S].alive = 0; else sl[S].cls = oa_cls_join(sl[S].cls, oa_elem_evidence(c, sl, S, argv[1])); } else if (name && (sp_streq(name, "min") || sp_streq(name, "max") || sp_streq(name, "sort") || sp_streq(name, "sort!"))) { sl[S].needs_cmp = 1; /* a sort/sort! RESULT must land in a modeled consumer (statement position, or a slot-alias write handled in step 5); an escaping result keeps the component on the boxed poly path. */ if ((is_sort_family(name)) && !(id < nc && value_ok[id])) sl[S].alive = 0; } } else if (!oa_walk_arm(c, id, recv, sl, S, claimed, walk_disc, nc) && !oa_filter_arm(c, id, recv, sl, S, claimed, call_ret, value_ok, walk_disc, nc)) sl[S].alive = 0; } /* Resolve the call target the way emission will: the enclosing self's ancestry first and a free function only as the fallback, then an explicit receiver -- a constant names a class, so the target is that class's chain of class methods; anything else goes by the receiver's type. -1 = unattributable here. A module of `self.` methods calling its own helpers is the whole shape of a Ruby module used as a namespace, and without the class-method arms every slot passed between two of them died here. This used to ask for the free functions FIRST, which is not what emission does: a same-named top-level def then took the call, so the real callee's parameter was never joined to the caller's slot and the two were narrowed apart -- an sp_PolyArray argument passed to an sp_PtrArray parameter, and the C build stopped (#4130). */ int oa_tmi = -1; if (name) { if (recv < 0) { oa_tmi = comp_self_call_mi(c, id, name); /* Inside a class method, a bare `new(...)` constructs this class -- the `def self.load; ...; new(labels, columns, ...); end` factory shape, which is how the form below is actually written. There is no class method named `new` for the lookup to find, so the arguments' route into initialize's parameters was invisible here too. */ if (oa_tmi < 0 && sp_streq(name, "new")) { Scope *ncs = comp_scope_of(c, id); if (ncs && ncs->is_cmethod && ncs->class_id >= 0) oa_tmi = comp_method_in_chain(c, ncs->class_id, "initialize", NULL); } } else if (nt_type(nt, recv) && (nt_kind(nt, recv) == NK_ConstantReadNode || nt_kind(nt, recv) == NK_ConstantPathNode)) { int rci = const_node_class_index(c, recv); oa_tmi = rci >= 0 ? call_target(c, id, rci, name, CT_CMETHOD, NULL) : -1; /* `T.new(cols)` names no class method -- `new` is implicit -- so the lookup above answered -1 and the argument loop below read that as "callee unattributable" and killed the caller's slot. The arguments really do reach T#initialize's parameters, and a table built as a local in a `self.load` factory and handed to the constructor is ordinary Ruby: it stayed boxed while the identical table assigned to the ivar inside `initialize` narrowed. A class that defines its own `self.new` is found above and keeps it. */ if (oa_tmi < 0 && rci >= 0 && sp_streq(name, "new")) oa_tmi = comp_method_in_chain(c, rci, "initialize", NULL); } else { TyKind ort = infer_type(c, recv); if (ty_is_object(ort)) oa_tmi = call_target(c, id, ty_object_class(ort), name, 0, NULL); else if (ort == TY_POLY || ort == TY_UNKNOWN || ort == TY_NIL) { /* An element of a slot is boxed only while the slot is: the call is decided once the slot's class is known (step 5d) */ int es = ort != TY_NIL ? oa_elem_recv_slot(c, recv, read_slot, par, nc, wlv, nwlv) : -1; if (es >= 0) { if (npend == cpend) { cpend = cpend ? cpend * 2 : 16; pend = (OaPend *)realloc(pend, sizeof *pend * (size_t)cpend); if (!pend) { fprintf(stderr, "oom\n"); exit(1); } } pend[npend].id = id; pend[npend].slot = es; pend[npend].cls = -1; npend++; continue; } OA_DYN_KILL(name); } } } oa_call_edges(c, sl, n, id, oa_tmi, argv, argc, read_slot, claimed, call_ret, nc); } /* 5. LocalVariableWriteNode sources: object literal -> evidence; alias to another slot -> edge; empty/nil -> neutral; anything else -> kill. */ for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_LocalVariableWriteNode) continue; if (dead && dead[id]) continue; int sidx = c->nscope[id]; const char *nm = nt_str(nt, id, "name"); LocalVar *lv = nm ? scope_local(&c->scopes[sidx], nm) : NULL; int S = lv ? oa_find(sl, n, sidx, lv) : -1; if (S < 0) continue; oa_classify_value(c, sl, n, read_slot, call_ret, claimed, S, nt_ref(nt, id, "value")); } /* 5a. InstanceVariableWriteNode sources, the same way; a write from a method of another class (or a singleton method) is unvetted. */ for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_InstanceVariableWriteNode) continue; const char *nm = nt_str(nt, id, "name"); Scope *sc = comp_scope_of(c, id); if (!nm || !sc || OA_IE_KILL(id, nm)) continue; int i = OA_IVSLOT(sc->class_id, nm); if (i < 0) continue; if (sc->is_cmethod) sl[i].alive = 0; else oa_classify_value(c, sl, n, read_slot, call_ret, claimed, i, nt_ref(nt, id, "value")); } /* 5a'. a constant's writes: `X = v` (or `M::X = v`) in statement position is its source; any other write form, or one whose value is used too, is unvetted. */ for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); int tgt = ty_k == NK_ConstantPathWriteNode || ty_k == NK_ConstantPathOrWriteNode || ty_k == NK_ConstantPathOperatorWriteNode || ty_k == NK_ConstantPathAndWriteNode ? nt_ref(nt, id, "target") : -1; const char *nm = tgt >= 0 ? nt_str(nt, tgt, "name") : ty_k == NK_ConstantWriteNode || ty_k == NK_ConstantOrWriteNode || ty_k == NK_ConstantOperatorWriteNode || ty_k == NK_ConstantAndWriteNode || ty_k == NK_ConstantTargetNode || ty_k == NK_ConstantPathTargetNode ? nt_str(nt, id, "name") : NULL; LocalVar *cv = nm ? comp_const(c, nm) : NULL; int S = cv ? oa_find(sl, n, OA_CONST, cv) : -1; if (S < 0) continue; int v = nt_ref(nt, id, "value"); /* (an empty `[]` filled later is built at the kind the passes that stamp empty literals give it, which no write inference of a constant brings into line with this slot) */ if (!((ty_k == NK_ConstantWriteNode || ty_k == NK_ConstantPathWriteNode) && id < nc && value_ok[id]) || is_empty_array_literal(nt, v, c->node_cap)) { sl[S].alive = 0; continue; } oa_classify_value(c, sl, n, read_slot, call_ret, claimed, S, v); if (tgt >= 0) claimed[tgt] = 1; /* `M::X =`'s target path is the write, no read */ /* nothing re-derives a constant's literal from the constant: built at the narrowed kind in place, or the write would hand it a poly array */ if (nt_kind(nt, v) == NK_ArrayNode) oa_note_src(S, v); } /* 5b. a return slot's own value: the method's tail expression and every explicit `return`, classified exactly like a local's write sources. A slot already dead is classified too, as the explicit returns below are: the tail call's value then joins it and dies with it. Skipped, the tail kept its statement position as a modeled consumer, so the callee narrowed under a caller that had refused to; the caller read that narrowed value back and stopped being a slot, and the two traded the verdict every round until the cap (#7993). */ for (int i = 0; i < n; i++) { if (sl[i].lv || sl[i].ici >= 0) continue; Scope *M = &c->scopes[sl[i].sidx]; int bn = 0; const int *bl = M->body >= 0 ? nt_arr(nt, M->body, "body", &bn) : NULL; if (!bl || bn == 0) { sl[i].alive = 0; continue; } oa_classify_value(c, sl, n, read_slot, call_ret, claimed, i, bl[bn - 1]); } for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_ReturnNode) continue; if (dead && dead[id]) continue; int R = oa_find(sl, n, c->nscope[id], NULL); if (R < 0) continue; int ra = nt_ref(nt, id, "arguments"); int rn = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rn) : NULL; if (!rv || rn != 1) { sl[R].alive = 0; continue; } oa_classify_value(c, sl, n, read_slot, call_ret, claimed, R, rv[0]); } /* 5c. a method reachable other than through a call site this pass resolved can be entered by a path that still expects the boxed value: a symbol anywhere names it for `send`/`method`/`define_method`/`&:m`, and `super` reaches it with no call node at all. Retyping its C return under either would hand back something the caller cannot read. */ /* the `attr_reader :items` declaration itself names the reader with a symbol; that one is the reader's definition, not a dynamic route to it. `attr_accessor`/`attr_writer` name it the same way -- their WRITER is what has to be vetted, and step 3a does that by its own `:items=` name, so treating the declaration here as a dynamic route would kill every accessor slot for the bare symbol it is spelled with. */ char *attr_sym = (char *)calloc(nc, 1); for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) continue; const char *cn = nt_str(nt, id, "name"); if (!cn || !(sp_streq(cn, "attr_reader") || sp_streq(cn, "attr") || sp_streq(cn, "attr_accessor") || sp_streq(cn, "attr_writer"))) continue; int aa = nt_ref(nt, id, "arguments"); int aan = 0; const int *aav = aa >= 0 ? nt_arr(nt, aa, "arguments", &aan) : NULL; for (int k = 0; k < aan; k++) if (aav[k] >= 0 && aav[k] < nc) attr_sym[aav[k]] = 1; } /* The names the program's Symbols and supers spell, as a set: each Symbol was compared with every slot (tens of thousands of Symbols by thousands of slots on a Rails program, every round), and a slot dies on any match, so asking the set per slot answers the same. */ ANameHash spelled; memset(&spelled, 0, sizeof spelled); for (int id = 0; id < nt->count; id++) { const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; const char *mn = NULL; if (ty_k == NK_SymbolNode) { if (attr_sym[id]) continue; mn = nt_str(nt, id, "value"); } else if (ty_k == NK_SuperNode || ty_k == NK_ForwardingSuperNode) { Scope *ssc = comp_scope_of(c, id); mn = ssc ? ssc->name : NULL; } if (!mn || anh_has(&spelled, mn)) continue; anh_add(&spelled, mn); } if (spelled.n) for (int i = 0; i < n; i++) { if (sl[i].ici >= 0) { /* `send(:items)`, `instance_variable_get(:@items)`, `method(:items)`, a re-declared accessor: any symbol naming the ivar, or naming its reader when the class can answer it (a `:b` that only names a Struct member elsewhere reaches no `b` of this class) */ const char *ivn = c->classes[sl[i].ici].ivars[sl[i].iiv]; if (anh_has(&spelled, ivn) || (anh_has(&spelled, ivn + 1) && OA_MAY_ANSWER(i))) sl[i].alive = 0; continue; } if (sl[i].sidx == OA_CONST) { if (anh_has(&spelled, sl[i].lv->name)) sl[i].alive = 0; continue; } if (!sl[i].lv && c->scopes[sl[i].sidx].name && anh_has(&spelled, c->scopes[sl[i].sidx].name)) sl[i].alive = 0; } anh_free(&spelled); /* a constant reached by a name the program computes or spells as a String: `const_get("X")` keeps X boxed, `const_get(name)` every one */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *cn = nt_str(nt, id, "name"); if (!cn || !(sp_streq(cn, "const_get") || sp_streq(cn, "const_set") || sp_streq(cn, "remove_const") || sp_streq(cn, "const_source_location"))) continue; int args = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; const char *lit = NULL; if (argc >= 1 && nt_kind(nt, argv[0]) == NK_SymbolNode) continue; /* (spelled above) */ if (argc >= 1 && nt_kind(nt, argv[0]) == NK_StringNode) { lit = nt_str(nt, argv[0], "content"); const char *sep = lit ? strrchr(lit, ':') : NULL; if (sep) lit = sep + 1; } for (int i = 0; i < n; i++) if (sl[i].sidx == OA_CONST && (!lit || sp_streq(lit, sl[i].lv->name))) sl[i].alive = 0; } /* Enumerable#each_with_index answers `self` from the block arm of `if block_given?`. That read is the method's value, the same table, not an escape. Unclaimed, it killed the table the arm walks, so a nested table rewritten onto the Ruby definition went back to a poly array. The dead arm is the same shape. Only a read of a PARAMETER is claimed so: that storage came from the caller, whose own slot vets what the caller does with the answer. A local the method built (filter_map's `out`) leaves through the inlined call's value, which no slot models, so it stays unclaimed and its component dies -- claimed, `a = xs.filter_map { }` narrowed under an `a.reject { }` no slot had vetted, and the call was refused. */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_IfNode) continue; int pred = nt_ref(nt, id, "predicate"); if (pred < 0 || nt_kind(nt, pred) != NK_CallNode || nt_ref(nt, pred, "receiver") >= 0) continue; const char *pn = nt_str(nt, pred, "name"); if (!pn || !sp_streq(pn, "block_given?")) continue; int arms[2]; arms[0] = nt_ref(nt, id, "statements"); int sub = nt_ref(nt, id, "subsequent"); arms[1] = (sub >= 0 && nt_kind(nt, sub) == NK_ElseNode) ? nt_ref(nt, sub, "statements") : -1; for (int a = 0; a < 2; a++) { int bn = 0; const int *bb = arms[a] >= 0 ? nt_arr(nt, arms[a], "body", &bn) : NULL; if (!bb || bn == 0 || bb[bn - 1] < 0 || bb[bn - 1] >= nc) continue; if (read_slot[bb[bn - 1]] >= 0 && oa_read_is_param(c, bb[bn - 1])) claimed[bb[bn - 1]] = 1; } } /* 5d. a call on an element of a slot (`as.each { |a| a.step(as) }`): the receiver is boxed only because the slot still is, and a slot handed to a method of its own element class could never narrow while the call killed it as a dynamic one. With every source and use of the slot seen, a component that may still narrow to one class decides the call as a call on that class, and step 7 checks that it did narrow so; any other is the dynamic call it is today. */ if (npend) { int *rcls = (int *)malloc(sizeof(int) * (size_t)n); char *rdead = (char *)calloc((size_t)n, 1); if (!rcls || !rdead) { fprintf(stderr, "oom\n"); exit(1); } for (int i = 0; i < n; i++) rcls[i] = -1; for (int i = 0; i < n; i++) { int r = oa_uf_find(sl, i); rcls[r] = oa_cls_join(rcls[r], sl[i].cls); if (!sl[i].alive) rdead[r] = 1; } /* (each decided before any is applied: an edge joins components, whose roots then have no class in rcls) */ int *tmis = (int *)malloc(sizeof(int) * (size_t)npend); if (!tmis) { fprintf(stderr, "oom\n"); exit(1); } for (int p = 0; p < npend; p++) { int r = oa_uf_find(sl, pend[p].slot), k = rcls[r]; tmis[p] = !rdead[r] && k >= 0 && k < c->nclasses ? call_target(c, pend[p].id, k, nt_str(nt, pend[p].id, "name"), 0, NULL) : -1; if (tmis[p] >= 0) pend[p].cls = k; } for (int p = 0; p < npend; p++) { int id = pend[p].id, args = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (tmis[p] < 0) OA_DYN_KILL(nt_str(nt, id, "name")); oa_call_edges(c, sl, n, id, tmis[p], argv, argc, read_slot, claimed, call_ret, nc); } free(tmis); free(rcls); free(rdead); } /* 6. a slot read left unclaimed escaped into an unmodeled context -> kill. A call whose value is a return slot is the same story: unless a slot write claimed it above, the value went somewhere unmodeled. And a return slot no call site reached at all is a method entered by some route this pass never saw, so its C signature must not move. */ for (int id = 0; id < nt->count; id++) if (call_ret[id] >= 0 && !claimed[id] && !value_ok[id]) sl[call_ret[id]].alive = 0; for (int i = 0; i < n; i++) if (!sl[i].lv && sl[i].ici < 0 && !sl[i].saw_call) sl[i].alive = 0; for (int id = 0; id < nt->count; id++) if (read_slot[id] >= 0 && !claimed[id]) sl[read_slot[id]].alive = 0; /* 7. resolve components: roll member alive/cls up to the root, then a component narrows only if every member is alive and one class is known. */ for (int i = 0; i < n; i++) { int r = oa_uf_find(sl, i); if (r == i) continue; sl[r].cls = oa_cls_join(sl[r].cls, sl[i].cls); if (sl[i].needs_cmp) sl[r].needs_cmp = 1; if (sl[i].removes) sl[r].removes = 1; if (sl[i].poly_elem) sl[r].poly_elem = 1; sl[r].row_iter |= sl[i].row_iter; if (!sl[i].alive) sl[r].alive = 0; sl[r].row_cls = oa_cls_join(sl[r].row_cls, sl[i].row_cls); sl[r].row_escape |= sl[i].row_escape; sl[r].odd_elem |= sl[i].odd_elem; } /* A table whose every element is an empty row (`Array.new(n) { [] }`) has no element evidence of its own. When every row it hands out is pushed only Integers, or only Floats, and goes nowhere else (oa_row_use), the pushes are its evidence, as an `Array[Array[Integer]]` seed is (#4484): it narrows, and its empty rows are built at that kind below. */ for (int i = 0; i < n; i++) { if (oa_uf_find(sl, i) != i || sl[i].cls != -1 || sl[i].row_escape || sl[i].odd_elem || !OA_CLS_IS_NESTED(sl[i].row_cls)) continue; /* ... and when every empty row can be built at that kind: a row whose want another producer already gave (the elements of a `[[], []]` literal, which widen_nested_literals takes boxed while their kind is still open) would be built at that one, a poly array the table reads back as an sp_IntArray * */ int rows = 0, unbuilt = 0; TyKind rk = sl[i].row_cls == OA_CLS_IA ? TY_INT_ARRAY : TY_FLOAT_ARRAY; for (int e = 0; e < g_oa_empt_n; e++) { if (oa_uf_find(sl, g_oa_empt_slot[e]) != i) continue; int lit = g_oa_empt_node[e]; rows = 1; if (c->arr_want && lit < c->node_cap && c->arr_want[lit] != TY_UNKNOWN && c->arr_want[lit] != rk) unbuilt = 1; } if (rows && !unbuilt) sl[i].cls = sl[i].row_cls; } oa_seeded_literal_rows(c, sl, n); /* a constant holding a table of Integer or Float rows keeps the row kinds its literal gives it (a const table): this pass narrows only a constant's objects */ for (int i = 0; i < n; i++) if (sl[i].sidx == OA_CONST && OA_CLS_IS_NESTED(sl[oa_uf_find(sl, i)].cls)) sl[oa_uf_find(sl, i)].alive = 0; /* A call step 5d decided on the class of a slot's element holds only if the slot narrows to that class; one whose slot does not was a dynamic call after all, and makes its kills. A kill can undo another such decision, so until none is undone. */ for (int undone = 1; undone; ) { undone = 0; for (int p = 0; p < npend; p++) { if (pend[p].cls < 0) continue; int r = oa_uf_find(sl, pend[p].slot); if (sl[r].cls == pend[p].cls && oa_component_narrows(c, &sl[r])) continue; int id = pend[p].id, args = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; OA_DYN_KILL(nt_str(nt, id, "name")); for (int k = 0; k < argc; k++) if (read_slot[argv[k]] >= 0) sl[read_slot[argv[k]]].alive = 0; pend[p].cls = -1; undone = 1; } if (undone) for (int i = 0; i < n; i++) if (!sl[i].alive) sl[oa_uf_find(sl, i)].alive = 0; } for (int i = 0; i < n; i++) { int r = oa_uf_find(sl, i); /* A component that reaches no decision this round drops any stamp an earlier round left on its builder nodes. The pass is written to un-narrow a slot that stops qualifying (a new escape appearing as the fixpoint desugars), and the slot then goes back to the poly array while a stale want would still have the emitter build an sp_PtrArray into it. Used at each of the three bails that do so: no decision, and the two comparator fallbacks (the nested one and the object-array one). Not observed to fire -- across the suite, the packages and a large application the drop site is reached constantly but the want is always already UNKNOWN -- so this states the invariant rather than fixing a reproduced failure. Only THIS pass's own source nodes, which are `map` calls. The empty-row literals of #4484 it stamped are cleared on the way into the round instead (see g_oa_empt_mine). */ #define OA_DROP_SRC_STAMP() do { \ for (int _e = 0; _e < g_oa_src_n; _e++) { \ if (oa_uf_find(sl, g_oa_src_slot[_e]) != oa_uf_find(sl, i)) continue; \ int _sn = g_oa_src_node[_e]; \ if (c->arr_want && _sn >= 0 && _sn < c->node_cap) comp_set_arr_want(c, _sn, TY_UNKNOWN); \ } \ } while (0) /* This pass cleared every candidate's pin on the way in, but the pin field is also written by the element-narrowing below it. Put a pin back when this pass reaches no decision, or the two passes alternate forever and the fixpoint burns its whole round budget (#3781). */ if (sl[i].ici >= 0 && c->classes[sl[i].ici].ivar_oa_seed[sl[i].iiv] < 0) c->classes[sl[i].ici].ivar_oa_conflict[sl[i].iiv] = (unsigned char)(sl[r].cls == -2 ? 1 : sl[r].cls == OA_CLS_PIA ? 2 : 0); if (!oa_component_narrows(c, &sl[r])) { OA_DROP_SRC_STAMP(); if (sl[i].ici >= 0) continue; /* an ivar with no decision stays the poly array it was reset to */ oa_restore_pin(c, &sl[i]); continue; } TyKind nty; if (OA_CLS_IS_NESTED(sl[r].cls)) { nty = sl[r].cls == OA_CLS_IA ? TY_INT_ARRAY_ARRAY : TY_FLOAT_ARRAY_ARRAY; /* every empty `[]` that flowed into this component is a row: build it at the row kind (see oa_note_empty), a seeded table's as the seed's */ int seeded = oa_component_seeded(c, sl, n, r); for (int e = 0; e < g_oa_empt_n; e++) { if (oa_uf_find(sl, g_oa_empt_slot[e]) != r) continue; int lit = g_oa_empt_node[e]; if (c->arr_want && lit < c->node_cap && c->arr_want[lit] == TY_UNKNOWN) { comp_set_arr_want(c, lit, sl[r].cls == OA_CLS_IA ? TY_INT_ARRAY : TY_FLOAT_ARRAY); if (seeded) oa_mark_seed_row(lit); else oa_mark_empty_mine(lit); } } } else nty = ty_obj_array(sl[r].cls); if (sl[i].ici >= 0) { ClassInfo *cl = &c->classes[sl[i].ici]; ivar_set_type(cl, sl[i].iiv, nty); cl->ivar_oa_type[sl[i].iiv] = nty; cl->ivar_int_table[sl[i].iiv] = 1; /* a subclass's struct begins with its ancestors' fields, and inherited methods read them through a cast: every copy takes the one type */ for (int k = 0; k < c->nclasses; k++) { if (k == sl[i].ici || !is_descendant(c, k, sl[i].ici)) continue; ClassInfo *kc = &c->classes[k]; int kiv = comp_ivar_index(kc, cl->ivars[sl[i].iiv]); if (kiv < 0) continue; ivar_set_type(kc, kiv, nty); kc->ivar_oa_type[kiv] = nty; kc->ivar_int_table[kiv] = 1; } } else if (sl[i].lv) { lv_set_type(sl[i].lv, nty); sl[i].lv->oa_pin = nty; sl[i].lv->oa_grace = 0; } else { scope_set_ret(&c->scopes[sl[i].sidx], nty); c->scopes[sl[i].sidx].ret_oa_pin = nty; c->scopes[sl[i].sidx].ret_oa_grace = 0; } /* and the sources that BUILD the table in place, so the emitter builds it at the narrowed kind rather than building a poly array and assigning it into a narrowed slot */ for (int e = 0; e < g_oa_src_n; e++) { if (oa_uf_find(sl, g_oa_src_slot[e]) != oa_uf_find(sl, i)) continue; int src = g_oa_src_node[e]; /* a WANT, not c->ntype: the type cache is recomputed by every later inference pass, so a stamp there is gone by emission. infer_type re-reads arr_want on each pass, which is exactly how the empty-row literal of an `Array.new(n) { [] }` table keeps the kind this pass gives it (#4484). */ if (c->arr_want && src >= 0 && src < c->node_cap) comp_set_arr_want(c, src, nty); } } /* the round changed something exactly when some slot's decision differs from the one it carried in -- the fixpoint's convergence test depends on this being false once the evidence settles. */ for (int i = 0; i < n; i++) { TyKind now = sl[i].ici >= 0 ? c->classes[sl[i].ici].ivar_oa_type[sl[i].iiv] : sl[i].lv ? sl[i].lv->oa_pin : c->scopes[sl[i].sidx].ret_oa_pin; if (now != sl[i].old_pin) { changed = 1; break; } } /* A want that was cleared above and NOT re-stamped is a decision that went away this round. infer_write_types runs before this pass in the fixpoint body, so it had already read that want into a destination local, and the `ch |= infer_write_types(c)` after this pass only runs when this pass reports a change -- which the pin comparison below does not notice, since the slot's pin is simply restored. Report it, and the write types are re-derived without the want. Only a genuinely dropped one: the normal round clears and re-stamps, and reporting that would never converge. */ if (oa_want_dropped(c, cleared, n_cleared) || walked) changed = 1; free(cleared); free(oa_ans); free(pend); free(wlv); oa_byname_free(&oa_bn); #undef OA_DYN_KILL #undef OA_MAY_ANSWER for (int k = 0; k < c->nclasses; k++) free(ivslot[k]); free(ivslot); #undef OA_IE_KILL #undef OA_IVSLOT free(sl); free(read_slot); free(call_ret); free(claimed); free(value_ok); free(walk_disc); free(attr_sym); free(dead); return changed; } /* ===== Post-fixpoint: narrow a poly local that is only ever used as an int ===== A method-local typed TY_POLY whose every read feeds an int context (an arithmetic/comparison operator, or an array/hash index) and whose every write is an int or a poly value is narrowed to TY_INT. The poly writes are then coerced by emit_assign's existing int-slot-poly-rhs path (sp_poly_to_i), and the arithmetic emits natively instead of through the boxed sp_poly_* helpers. This removes per-iteration boxing from hot loops where a value picks up the poly type from a single poly source (e.g. optcarrot's render_pixel `pixel`, which is `sprite[2]` -- a poly-array element -- on one branch but is only ever used `% 4`, `==`, and as `@output_color[pixel]`). Requiring at least one index use proves the value is genuinely an integer, so coercing a poly source is sound -- unless a Range can reach the slot (range_held), which indexes too and takes the slice. Strictly conservative: any non-int use, op-assign, or non-int/poly write source leaves it TY_POLY. */ /* Arithmetic/bitwise operators only -- NOT comparisons (`==`/`<`/`<=>`...), which are polymorphic (a string compares with `==` too), so a comparison operand is not evidence the value is an int. These arithmetic ops are also overloaded on strings/arrays, but the required array-index use proves the value is genuinely an integer, so an int reading of these is then sound. */ static int npi_is_int_op(const char *nm) { if (!nm) return 0; static const char *const ops[] = { "+", "-", "*", "/", "%", "&", "|", "^", "<<", ">>", "**", NULL }; return str_in(nm, ops); } static void narrow_poly_int_locals(Compiler *c) { const NodeTable *nt = c->nt; int nc = nt->count ? nt->count : 1; char *is_read = (char *)malloc(nc); char *claimed = (char *)malloc(nc); /* Every check below only cares about nodes in the candidate local's own scope (a read's consumer in receiver/argument position shares the read's scope -- only a block opens a new one, and blocks are separate refs). Bucket node ids by scope once, so each candidate walks its scope's nodes instead of the whole table: the old shape was O(poly-locals * nodes) and a dominant post-fixpoint cost on large trees. */ int *sc_cnt = (int *)calloc((size_t)(c->nscopes + 1), sizeof(int)); int *sc_off = (int *)malloc(sizeof(int) * (size_t)(c->nscopes + 1)); int *sc_nodes = (int *)malloc(sizeof(int) * (size_t)nc); if (!is_read || !claimed || !sc_cnt || !sc_off || !sc_nodes) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memset(is_read, 0, (size_t)nc); memset(claimed, 0, (size_t)nc); for (int id = 0; id < nt->count; id++) { int s2 = c->nscope[id]; if (s2 >= 0 && s2 < c->nscopes) sc_cnt[s2]++; } sc_off[0] = 0; for (int s = 0; s < c->nscopes; s++) sc_off[s + 1] = sc_off[s] + sc_cnt[s]; for (int s = 0; s <= c->nscopes; s++) sc_cnt[s] = 0; for (int id = 0; id < nt->count; id++) { int s2 = c->nscope[id]; if (s2 >= 0 && s2 < c->nscopes) sc_nodes[sc_off[s2] + sc_cnt[s2]++] = id; } for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int li = 0; li < sc->nlocals; li++) { LocalVar *lv = &sc->locals[li]; if (lv->type != TY_POLY) continue; if (lv->is_param || lv->is_block_param || lv->is_cell || lv->rbs_seeded || lv->range_held) continue; const char *nm = lv->name; int ok = 1, saw_write = 0; /* 1. writes: plain LocalVariableWriteNode with an int/poly value; any op/and/or-write or multi-target leaves it poly. */ for (int k = sc_off[s]; k < sc_off[s + 1] && ok; k++) { int id = sc_nodes[k]; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; if (ty_k == NK_LocalVariableOperatorWriteNode || ty_k == NK_LocalVariableAndWriteNode || ty_k == NK_LocalVariableOrWriteNode || ty_k == NK_LocalVariableTargetNode) { const char *wn = nt_str(nt, id, "name"); if (wn && sp_streq(wn, nm)) ok = 0; } else if (ty_k == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, nm)) continue; saw_write = 1; int v = nt_ref(nt, id, "value"); TyKind vt = v >= 0 ? infer_type(c, v) : TY_UNKNOWN; if (vt != TY_INT && vt != TY_POLY) ok = 0; } } if (!ok || !saw_write) continue; /* 2. map this local's reads, then claim those in an int context. (is_read/claimed are re-cleared per candidate over this scope's nodes only -- nothing outside the scope is ever set.) */ for (int k = sc_off[s]; k < sc_off[s + 1]; k++) { int id = sc_nodes[k]; is_read[id] = 0; claimed[id] = 0; NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_LocalVariableReadNode) continue; const char *rn = nt_str(nt, id, "name"); if (rn && sp_streq(rn, nm)) is_read[id] = 1; } int has_index = 0; for (int k = sc_off[s]; k < sc_off[s + 1]; k++) { int id = sc_nodes[k]; const char *ty = nt_type(nt, id); NodeKind ty_k = nt_kind(nt, id); if (!ty) continue; if (ty_k == NK_CallNode) { const char *cn = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; int is_idx = cn && (sp_streq(cn, "[]") || sp_streq(cn, "at") || sp_streq(cn, "[]=")); if (npi_is_int_op(cn)) { if (recv >= 0 && is_read[recv]) claimed[recv] = 1; for (int k2 = 0; k2 < an; k2++) if (is_read[av[k2]]) claimed[av[k2]] = 1; } else if (is_idx && an >= 1 && is_read[av[0]]) { /* the index is arg 0, and only an ARRAY index proves an int (a hash key can be any type). A `[]=` value (arg 1) is not an index. */ TyKind rt = recv >= 0 ? infer_type(c, recv) : TY_UNKNOWN; if (ty_is_array(rt) || ty_is_obj_array(rt)) { claimed[av[0]] = 1; has_index = 1; } } } } /* every read must be claimed, and at least one must be an index (which proves the value is an integer, making a poly-source coercion sound). */ for (int k = sc_off[s]; k < sc_off[s + 1] && ok; k++) { int id = sc_nodes[k]; if (is_read[id] && !claimed[id]) ok = 0; } if (ok && has_index) lv_set_type(lv, TY_INT); } } free(sc_cnt); free(sc_off); free(sc_nodes); free(is_read); free(claimed); } /* ---- `rescue K => e`: an arm's reads take that arm's class (#4343) -------- A name bound by two rescue arms interns to ONE LocalVar, so the slot cannot take either arm's class and lands on plain TY_EXCEPTION -- and a call naming a method only the user class owns then had no route at all and stopped the build. A read inside an arm still knows which class it is: the arm names it. Every user exception subclass's struct opens with the sp_Exception header, so reading the binding as that class there is a pointer cast, not a guess. Marked in c->nilnarrow, the same read-site table the nil and is_a? guards use, and only for the names the arm's class chain actually owns: everything the builtin exception surface answers (#message, #backtrace) keeps the arms it has today. A slot already specialized to one class needs nothing. */ static int nng_has_write(Compiler *c, int root, const char *pn); static void nrar_mark_calls(Compiler *c, int root, Scope *s, const char *nm, int cid, TyKind t) { if (root < 0) return; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, root); NodeKind ty_k = nt_kind(nt, root); if (!ty) return; /* a nested def has its own scope and its own `e`; the walk stops there */ if (ty_k == NK_DefNode || ty_k == NK_ClassNode || ty_k == NK_ModuleNode) return; if (ty_k == NK_CallNode) { int recv = nt_ref(nt, root, "receiver"); NodeKind rk = nt_kind(nt, recv); const char *mn = nt_str(nt, root, "name"); if (rk == NK_LocalVariableReadNode && mn) { const char *rn = nt_str(nt, recv, "name"); if (rn && sp_streq(rn, nm) && comp_scope_of(c, recv) == s && (call_target(c, root, cid, mn, 0, NULL) >= 0 || comp_reader_in_chain(c, cid, mn, NULL) >= 0)) comp_set_nilnarrow(c, recv, t); } } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) nrar_mark_calls(c, nt_ref_at(nt, root, i), s, nm, cid, t); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, root, i, &n); for (int k = 0; k < n; k++) nrar_mark_calls(c, ids[k], s, nm, cid, t); } } static void narrow_rescue_arm_reads(Compiler *c) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_RescueNode) continue; int ref = nt_ref(nt, id, "reference"); if (ref < 0 || nt_kind(nt, ref) != NK_LocalVariableTargetNode) continue; const char *nm = nt_str(nt, ref, "name"); if (!nm) continue; /* exactly one named class, and a user exception subclass */ int nexc = 0; const int *exc = nt_arr(nt, id, "exceptions", &nexc); if (nexc != 1) continue; NodeKind en_k = nt_kind(nt, exc[0]); if (en_k != NK_ConstantReadNode && en_k != NK_ConstantPathNode) continue; const char *enm = nt_str(nt, exc[0], "name"); int cid = enm ? comp_class_index(c, enm) : -1; if (cid < 0 || !class_is_exc_subclass(c, cid)) continue; Scope *vsc = comp_scope_of(c, ref); LocalVar *lv = vsc ? scope_local(vsc, nm) : NULL; /* only the slot the arms could not agree on: a specialized one already reads as its class, and a builtin-typed one is what this repairs */ if (!lv || lv->type != TY_EXCEPTION) continue; int body = nt_ref(nt, id, "statements"); /* an arm that reassigns the name is no longer holding what it caught: the cast would be reading a class the value never was */ if (nng_has_write(c, body, nm)) continue; nrar_mark_calls(c, body, vsc, nm, cid, ty_object(cid)); } } /* `Cls.new(...)` / `Cls.exception(...)` of an exception class */ static int value_is_new_exception(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0 || nt_kind(nt, v) != NK_CallNode) return 0; const char *m = nt_str(nt, v, "name"); if (!m || (!sp_streq(m, "new") && !sp_streq(m, "exception"))) return 0; int r = nt_ref(nt, v, "receiver"); if (r < 0 || (nt_kind(nt, r) != NK_ConstantReadNode && nt_kind(nt, r) != NK_ConstantPathNode)) return 0; const char *cn = nt_str(nt, r, "name"); if (!cn) return 0; int cid = comp_class_index(c, cn); return cid >= 0 ? class_is_exc_subclass(c, cid) : is_builtin_exception_name(cn); } /* Whether a local that a `rescue => name` arm binds is also written by an ordinary assignment in the same scope; a target is ordinary unless it is a rescue arm's own reference. */ static int rescue_name_written_elsewhere(Compiler *c, Scope *vsc, const char *nm, const int *rescues, int nrescues) { const NodeTable *nt = c->nt; static const NodeKind kinds[] = { NK_LocalVariableWriteNode, NK_LocalVariableOperatorWriteNode, NK_LocalVariableOrWriteNode, NK_LocalVariableAndWriteNode, NK_LocalVariableTargetNode, }; for (size_t k = 0; k < sizeof kinds / sizeof kinds[0]; k++) { NT_FOREACH_KIND(nt, kinds[k], id) { const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, nm) || comp_scope_of(c, id) != vsc) continue; if (kinds[k] == NK_LocalVariableTargetNode) { int own = 0; for (int r = 0; r < nrescues && !own; r++) own = nt_ref(nt, rescues[r], "reference") == id; if (own) continue; } /* an exception built in place (`e = MyErr.new(...)`) is what the exception slot holds anyway */ if (kinds[k] == NK_LocalVariableWriteNode && value_is_new_exception(c, nt_ref(nt, id, "value"))) continue; return 1; } } return 0; } /* ---- `return if p.nil?` guard narrowing (#1661) -------------------- A method whose call sites pass `T | nil` types its parameter poly (T has no first-class nullable slot: String, Time, ...). When the body OPENS with early-return nil guards on that parameter and never reassigns it, every read after the guard is provably non-nil: mark those read nodes with the non-nil type in c->nilnarrow. infer_type returns the narrowed type for the marked reads and codegen unboxes the poly slot at each read site, so the rest of the body (and the method's return) type as T. */ static void nng_mark_reads(Compiler *c, int root, Scope *s, const char *pn, TyKind t) { if (root < 0) return; const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, root); if (k == NK_LocalVariableReadNode) { const char *nm = nt_str(nt, root, "name"); /* only reads owned by the method scope itself: a block-interior read goes through capture plumbing and keeps the poly slot (sound, just unnarrowed) */ if (nm && sp_streq(nm, pn) && comp_scope_of(c, root) == s) comp_set_nilnarrow(c, root, t); } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) nng_mark_reads(c, nt_ref_at(nt, root, i), s, pn, t); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *a = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) nng_mark_reads(c, a[j], s, pn, t); } } /* Any write/target of `pn` anywhere in the method disables narrowing (it could re-store nil, or another type, after the guard). */ static int nng_has_write(Compiler *c, int root, const char *pn) { if (root < 0) return 0; const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, root); if ((k == NK_LocalVariableWriteNode || k == NK_LocalVariableTargetNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode)) { const char *nm = nt_str(nt, root, "name"); if (nm && sp_streq(nm, pn)) return 1; } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) if (nng_has_write(c, nt_ref_at(nt, root, i), pn)) return 1; int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *a = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) if (nng_has_write(c, a[j], pn)) return 1; } return 0; } /* `return if p.nil?` (modifier or block form, no else) on a poly param of scope `s`: returns the param name, else NULL. */ static const char *nng_guard_param(Compiler *c, Scope *s, int st) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, st); if (k != NK_IfNode) return NULL; if (nt_ref(nt, st, "subsequent") >= 0) return NULL; int pred = nt_ref(nt, st, "predicate"); if (pred < 0 || nt_kind(nt, pred) != NK_CallNode) return NULL; const char *cn = nt_str(nt, pred, "name"); if (!cn || !sp_streq(cn, "nil?")) return NULL; if (nt_ref(nt, pred, "arguments") >= 0) return NULL; int recv = nt_ref(nt, pred, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_LocalVariableReadNode) return NULL; const char *pn = nt_str(nt, recv, "name"); if (!pn) return NULL; LocalVar *lv = scope_local(s, pn); if (!lv || !lv->is_param || lv->is_block_param || lv->type != TY_POLY) return NULL; int thb = nt_ref(nt, st, "statements"); int n = 0; const int *a = thb >= 0 ? nt_arr(nt, thb, "body", &n) : NULL; if (!a || n != 1) return NULL; NodeKind rt2_k = nt_kind(nt, a[0]); if (rt2_k != NK_ReturnNode) return NULL; return pn; } /* ---- `if v.is_a?(K)` occurrence typing ------------------------------------ A poly local/param unioned across several concrete types cannot dispatch a method that only SOME members define -- `v.to_a` where v is Array | String aborts at runtime "undefined method 'to_a' for poly". An `is_a?(K)` guard proves the runtime type inside its then-branch, so reads of v there can be narrowed to K's concrete type (unboxed at the read site, the same machinery `return .. if p.nil?` uses). The runtime is_a? check makes the unbox sound. */ /* Map a guard class name to the concrete narrowed type, or TY_UNKNOWN. is_a?(Integer) holds for a Bignum too, which TY_INT (one machine word) cannot carry: a narrowed read unboxed a Bignum's pointer for its value. An Integer method's body (builtins/integer.rb, desugared under the "__int_" prefix) is written for both representations and narrows to TY_BIGINT, which holds every Integer, so gcd's and lcm's arithmetic stays exact; elsewhere the reads stay boxed, where the boxed dispatch answers for both. */ int class_has_subclass(Compiler *c, int ocid); static TyKind isa_narrow_type(Compiler *c, const char *cn, const Scope *s) { if (!cn) return TY_UNKNOWN; /* A user class with no subclass: is_a?(K) proves the exact class, so the read unboxes to K and its calls bind statically. A subclass instance would also pass, and K's static dispatch would skip its overrides. */ int ci = is_builtin_class_name(cn) || is_builtin_module_name(cn) ? -1 : comp_class_index(c, cn); if (ci >= 0) { const ClassInfo *k = &c->classes[ci]; if (!(k->def_node >= 0 && nt_kind(c->nt, k->def_node) == NK_ClassNode && !k->is_builtin_const && !k->is_native_class && k->ary_root <= 0 && !class_has_subclass(c, ci))) return TY_UNKNOWN; /* Not when a user is_a?/kind_of? can answer true for another class. */ if (an_user_defines_or_reads(c, "is_a?") || an_user_defines_or_reads(c, "kind_of?")) return TY_UNKNOWN; /* Not one with a mixin on its chain: narrowing runs after the fixpoint, too late to bring in a module's methods (Enumerable#min) for the narrowed call, which then has no method to bind. */ for (int a = ci; a >= 0; a = c->classes[a].parent) { const ClassInfo *ak = &c->classes[a]; if (ak->nincluded_mods || ak->nincluded_mod_names || ak->enum_inc_at || ak->cmp_inc_at || ak->cmp_prepended || ak->enum_prepended) return TY_UNKNOWN; } return ty_object(ci); } if (sp_streq(cn, "Array")) return TY_POLY_ARRAY; if (sp_streq(cn, "String")) return TY_STRING; if (is_integer_class_name(cn)) return s->name && strncmp(s->name, "__int_", 6) == 0 ? TY_BIGINT : TY_UNKNOWN; if (sp_streq(cn, "Float")) return TY_FLOAT; if (sp_streq(cn, "Symbol")) return TY_SYMBOL; return TY_UNKNOWN; } /* predicate `v.is_a?(K)` / `v.kind_of?(K)` on a poly local v of scope s: returns v's name and sets *out_t to the narrowed type, else NULL. */ static const char *isa_guard_local(Compiler *c, int pred, Scope *s, TyKind *out_t) { const NodeTable *nt = c->nt; if (pred < 0 || nt_kind(nt, pred) != NK_CallNode) return NULL; const char *cn = nt_str(nt, pred, "name"); if (!cn || (!sp_streq(cn, "is_a?") && !sp_streq(cn, "kind_of?"))) return NULL; int recv = nt_ref(nt, pred, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_LocalVariableReadNode) return NULL; const char *pn = nt_str(nt, recv, "name"); if (!pn || comp_scope_of(c, recv) != s) return NULL; LocalVar *lv = scope_local(s, pn); if (!lv || lv->type != TY_POLY) return NULL; /* only a poly local narrows */ int args = nt_ref(nt, pred, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an != 1 || !av || nt_kind(nt, av[0]) != NK_ConstantReadNode) return NULL; TyKind t = isa_narrow_type(c, nt_str(nt, av[0], "name"), s); if (t == TY_UNKNOWN) return NULL; *out_t = t; return pn; } /* Array methods that neither change the receiver nor hand it on: the result is a new object or a plain value, and a block sees elements, never the receiver. (each, each_with_index, each_slice, each_cons, product, combination and permutation return the receiver when given a block.) */ static int isa_array_read_only(const char *nm) { static const char *const R[] = { "length", "size", "count", "empty?", "any?", "all?", "none?", "one?", "first", "last", "[]", "at", "dig", "fetch", "slice", "take", "drop", "take_while", "drop_while", "include?", "member?", "index", "find_index", "rindex", "sum", "min", "max", "minmax", "min_by", "max_by", "sort", "sort_by", "map", "collect", "flat_map", "collect_concat", "select", "filter", "find_all", "reject", "filter_map", "find", "detect", "partition", "group_by", "chunk_while", "slice_when", "zip", "join", "inspect", "to_s", "hash", "==", "!=", "eql?", "<=>", "+", "-", "*", "&", "|", "uniq", "compact", "flatten", "reverse", "rotate", "values_at", "assoc", "rassoc", "pack", "tally", "inject", "reduce", "each_with_object", "to_h", "transpose", "grep", "grep_v", "bsearch", "sample", "shuffle", NULL }; if (!nm) return 0; return str_in(nm, R); } /* Like nng_mark_reads, but a bare read that is a direct ELEMENT of an array or hash literal stays unnarrowed: narrowing it retypes the container literal (`[v]` becomes a typed array), which cascades into the container's consumers and is layout-hostile on hot programs (optcarrot's add_mappings ternary cost ~2-3%% fps) -- the array-typing lesson. Dispatch on v itself still narrows. An Array-narrowed read is a poly-array COPY of whatever typed array the box holds, so it narrows only as the receiver of a method that reads the array without handing the receiver on (isa_array_read_only). A mutator (`v.push(x)`, `v[i] += 1`) grew the copy and left the array the box points at unchanged; a read handed on (an argument, an assignment, a return, a `tap` / `then` block, `itself`, `to_a`) or asked for its identity carried the copy away. Those keep the poly box, which dispatches on the original. */ static void isa_mark_reads(Compiler *c, int root, Scope *s, const char *pn, TyKind t) { if (root < 0) return; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, root); NodeKind k = nt_kind(nt, root); if (k == NK_LocalVariableReadNode) { const char *nm = nt_str(nt, root, "name"); if (nm && sp_streq(nm, pn) && comp_scope_of(c, root) == s && t != TY_POLY_ARRAY) comp_set_nilnarrow(c, root, t); return; } if (t == TY_POLY_ARRAY && ty && nt_kind(nt, root) == NK_CallNode) { int recv = nt_ref(nt, root, "receiver"); const char *cn = nt_str(nt, root, "name"); const char *rn = recv >= 0 && nt_kind(nt, recv) == NK_LocalVariableReadNode ? nt_str(nt, recv, "name") : NULL; if (rn && sp_streq(rn, pn) && comp_scope_of(c, recv) == s && isa_array_read_only(cn)) comp_set_nilnarrow(c, recv, t); } int is_container = (k == NK_ArrayNode || k == NK_HashNode); int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) isa_mark_reads(c, nt_ref_at(nt, root, i), s, pn, t); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *a = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) { if (is_container && a[j] >= 0 && nt_kind(nt, a[j]) == NK_LocalVariableReadNode) { const char *enm = nt_str(nt, a[j], "name"); if (enm && sp_streq(enm, pn)) continue; /* bare element read: keep poly */ } isa_mark_reads(c, a[j], s, pn, t); } } } /* A node that always exits the current control flow (block iteration / method): reaching a statement AFTER ` unless cond` proves cond held. */ static int isa_node_diverges(Compiler *c, int node) { if (node < 0) return 0; const NodeTable *nt = c->nt; const char *ty = nt_type(nt, node); NodeKind k = nt_kind(nt, node); if (!ty) return 0; if (k == NK_NextNode || k == NK_BreakNode || k == NK_ReturnNode || k == NK_RedoNode) return 1; /* a bare `raise` / `raise E` (or exit/abort/throw) also diverges, so a `raise unless v.is_a?(K)` guard proves K for the fall-through (#3150). */ if (k == NK_CallNode) { const char *cn = nt_str(nt, node, "name"); if (cn && nt_ref(nt, node, "receiver") < 0 && is_diverging_call(cn)) return 1; } if (k == NK_StatementsNode) { int n = 0; const int *bb = nt_arr(nt, node, "body", &n); return n > 0 && isa_node_diverges(c, bb[n - 1]); } return 0; } /* Walk statements; for each `if v.is_a?(K)` narrow reads of v in the then-arm (v provably IS K there) unless the arm rewrites v. Recurses so nested and `elsif v.is_a?(K2)` chains each narrow their own arm. */ static void isa_mark_guards(Compiler *c, int root, Scope *s) { if (root < 0) return; const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, root); if (k == NK_IfNode) { TyKind t = TY_UNKNOWN; const char *pn = isa_guard_local(c, nt_ref(nt, root, "predicate"), s, &t); if (pn) { int branch = nt_ref(nt, root, "statements"); /* the then-arm: v IS K */ if (branch >= 0 && !nng_has_write(c, branch, pn)) isa_mark_reads(c, branch, s, pn, t); } } /* guard-continue: ` unless v.is_a?(K)` gates the fall-through, so every SUBSEQUENT sibling statement provably sees v as K (#3142). Scan a StatementsNode for such a guard and narrow the reads that follow it, up to a write of v. */ if (k == NK_StatementsNode) { int n = 0; const int *bb = nt_arr(nt, root, "body", &n); for (int i = 0; i < n; i++) { NodeKind ck = nt_kind(nt, bb[i]); if (ck != NK_UnlessNode) continue; if (nt_ref(nt, bb[i], "else_clause") >= 0) continue; /* a plain guard only */ TyKind t = TY_UNKNOWN; const char *pn = isa_guard_local(c, nt_ref(nt, bb[i], "predicate"), s, &t); if (!pn || !isa_node_diverges(c, nt_ref(nt, bb[i], "statements"))) continue; for (int j = i + 1; j < n; j++) { if (nng_has_write(c, bb[j], pn)) break; isa_mark_reads(c, bb[j], s, pn, t); } } } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) isa_mark_guards(c, nt_ref_at(nt, root, i), s); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *a = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) isa_mark_guards(c, a[j], s); } } static void narrow_isa_guards(Compiler *c) { for (int mi = 0; mi < c->nscopes; mi++) { Scope *s = &c->scopes[mi]; if (s->body < 0 || s->cs_synth) continue; isa_mark_guards(c, s->body, s); } } /* ---- caller-side narrowing of a nil-guarded poly LOCAL (#1675) ------------ `a = m()` where m's value is `nil | Time` types `a` poly (Time has no first-class nullable slot). When a branch is guarded by `a.nil?` or `a` truthiness and the local is never reassigned, reads inside the non-nil arm are provably Time: mark them in c->nilnarrow, the same read-site unbox #1661 uses for parameters -- no global type mutates, so nothing cascades. */ /* returns-walk: every explicit return owned by scope mi is either nil or the single base type; blocks' returns belong to their block scope and are NOT collected, which can only lose a nil witness and disable the narrowing. */ static void spnb_ret_walk(Compiler *c, int root, Scope *s, TyKind *base, int *saw_nil, int *ok) { if (root < 0 || !*ok) return; const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, root); if (k == NK_ReturnNode && comp_scope_of(c, root) == s) { int a = nt_ref(nt, root, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an == 0 || (nt_kind(nt, av[0]) == NK_NilNode)) { *saw_nil = 1; } else { TyKind t = infer_type(c, av[0]); if (t == TY_TIME && (*base == TY_UNKNOWN || *base == TY_TIME)) *base = TY_TIME; else { *ok = 0; return; } } } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) spnb_ret_walk(c, nt_ref_at(nt, root, i), s, base, saw_nil, ok); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *arr = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) spnb_ret_walk(c, arr[j], s, base, saw_nil, ok); } } /* tail-value contribution: the body's falling-off value. Each value position of the body, by every shape and with the left side of `&&` (a sequence's value is its last statement), is a nil (a nil literal, or an arm that is missing or empty; not one on the left of an `||`, which is the value only when truthy, and a position below both an `||` and an `&&` is never the value), a return (counted by the returns walk), a Time, or anything else, which ends the walk with ok = 0. */ typedef struct { Compiler *c; TyKind *base; int *saw_nil, *ok; } SpnbTail; static int spnb_tail_leaf(const VpLeaf *leaf, void *ud) { SpnbTail *t = ud; Compiler *c = t->c; /* The left of an `||` is the value only when truthy and the left of an `&&` only when falsy: a nil there, written or not, is never the walk's nil, and below both a position is never the value. */ if ((leaf->reach & (VPR_TRUTHY | VPR_FALSY)) == (VPR_TRUTHY | VPR_FALSY)) return 0; int may_be_nil = !(leaf->reach & VPR_TRUTHY); if (leaf->kind == VPK_NIL) { if (may_be_nil) *t->saw_nil = 1; return 0; } if (leaf->kind == VPK_JUMP && nt_kind(c->nt, leaf->node) == NK_ReturnNode) return 0; if (leaf->kind == VPK_EXPR) { if (nt_kind(c->nt, leaf->node) == NK_NilNode) { if (may_be_nil) *t->saw_nil = 1; return 0; } if (infer_type(c, leaf->node) == TY_TIME && (*t->base == TY_UNKNOWN || *t->base == TY_TIME)) { *t->base = TY_TIME; return 0; } } *t->ok = 0; return 1; } static void spnb_tail(Compiler *c, int node, TyKind *base, int *saw_nil, int *ok) { if (node < 0 || !*ok) return; SpnbTail t = { c, base, saw_nil, ok }; for_each_value_position(c->nt, node, VP_AND_LEFT | VPS_ALL, spnb_tail_leaf, &t); } /* the single non-nil type scope mi's poly return decomposes to, or UNKNOWN */ static TyKind scope_poly_nil_base(Compiler *c, int mi) { Scope *s = &c->scopes[mi]; if (s->ret != TY_POLY || s->body < 0 || s->cs_synth || s->is_lowered_yield) return TY_UNKNOWN; TyKind base = TY_UNKNOWN; int saw_nil = 0, ok = 1; spnb_ret_walk(c, s->body, s, &base, &saw_nil, &ok); spnb_tail(c, s->body, &base, &saw_nil, &ok); return (ok && saw_nil && base == TY_TIME) ? base : TY_UNKNOWN; } /* predicate shape: `A.nil?` (returns 1) or bare `A` truthiness (returns 2) */ static int spnb_pred_shape(Compiler *c, int pred, Scope *s, const char *nm) { const NodeTable *nt = c->nt; const char *ty = pred >= 0 ? nt_type(nt, pred) : NULL; NodeKind k = nt_kind(nt, pred); if (!ty) return 0; if (k == NK_LocalVariableReadNode) { const char *pn = nt_str(nt, pred, "name"); return pn && sp_streq(pn, nm) && comp_scope_of(c, pred) == s ? 2 : 0; } if (k == NK_CallNode) { const char *cn = nt_str(nt, pred, "name"); if (!cn || !sp_streq(cn, "nil?")) return 0; int recv = nt_ref(nt, pred, "receiver"); NodeKind rk = nt_kind(nt, recv); if (rk != NK_LocalVariableReadNode) return 0; const char *pn = nt_str(nt, recv, "name"); return pn && sp_streq(pn, nm) && comp_scope_of(c, recv) == s ? 1 : 0; } return 0; } static void spnb_mark_guards(Compiler *c, int root, Scope *s, const char *nm, TyKind base) { if (root < 0) return; const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, root); if (k == NK_IfNode || k == NK_UnlessNode) { int is_unless = k == NK_UnlessNode; int shape = spnb_pred_shape(c, nt_ref(nt, root, "predicate"), s, nm); int branch = -1; if (shape == 1) /* A.nil? -> the falsy arm is non-nil */ branch = nt_ref(nt, root, is_unless ? "statements" : "subsequent"); else if (shape == 2) /* bare A -> the truthy arm is non-nil */ branch = nt_ref(nt, root, is_unless ? "else_clause" : "statements"); if (branch >= 0 && !nng_has_write(c, branch, nm)) nng_mark_reads(c, branch, s, nm, base); } int nr = nt_num_refs(nt, root); for (int i = 0; i < nr; i++) spnb_mark_guards(c, nt_ref_at(nt, root, i), s, nm, base); int na = nt_num_arrs(nt, root); for (int i = 0; i < na; i++) { int n2 = 0; const int *arr = nt_arr_at(nt, root, i, &n2); for (int j = 0; j < n2; j++) spnb_mark_guards(c, arr[j], s, nm, base); } } static void narrow_nil_guard_locals(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, id) { Scope *s = comp_scope_of(c, id); if (!s) continue; const char *nm = nt_str(nt, id, "name"); int val = nt_ref(nt, id, "value"); if (!nm || val < 0 || nt_kind(nt, val) != NK_CallNode) continue; LocalVar *lv = scope_local(s, nm); if (!lv || lv->type != TY_POLY || lv->is_param || lv->is_block_param || lv->is_cell) continue; int mi = backprop_call_target(c, val); if (mi < 0) continue; TyKind base = scope_poly_nil_base(c, mi); if (base == TY_UNKNOWN) continue; /* the assignment above must be the local's ONLY write in its scope: nng_has_write counts every write form, so probe the scope body with this node's own write discounted by name-match count */ int writes = 0; { for (int w2 = an_local_writes_named_first(c, nm); w2 >= 0; w2 = an_local_writes_named_next(w2)) { const char *n2 = nt_str(nt, w2, "name"); if (n2 && sp_streq(n2, nm) && comp_scope_of(c, w2) == s) writes++; } NT_FOREACH_KIND(nt, NK_LocalVariableTargetNode, w3) { const char *n3 = nt_str(nt, w3, "name"); if (n3 && sp_streq(n3, nm) && comp_scope_of(c, w3) == s) writes += 2; /* massign etc: bail */ } NT_FOREACH_KIND(nt, NK_LocalVariableOperatorWriteNode, w4) { const char *n4 = nt_str(nt, w4, "name"); if (n4 && sp_streq(n4, nm) && comp_scope_of(c, w4) == s) writes += 2; } NT_FOREACH_KIND(nt, NK_LocalVariableOrWriteNode, w5) { const char *n5 = nt_str(nt, w5, "name"); if (n5 && sp_streq(n5, nm) && comp_scope_of(c, w5) == s) writes += 2; } NT_FOREACH_KIND(nt, NK_LocalVariableAndWriteNode, w6) { const char *n6 = nt_str(nt, w6, "name"); if (n6 && sp_streq(n6, nm) && comp_scope_of(c, w6) == s) writes += 2; } } if (writes != 1) continue; int body = s->body >= 0 ? s->body : (s->def_node >= 0 ? nt_ref(nt, s->def_node, "body") : -1); if (body < 0) continue; spnb_mark_guards(c, body, s, nm, base); } } static void narrow_nil_guard_params(Compiler *c) { const NodeTable *nt = c->nt; /* the names more than one definition of the program carries, read once: asked per method by a walk of every method, this was a square of the method count (the post-fixpoint phase of a Rails program, 119k scopes after lowering, spent its minutes here) */ ANameHash seen, dupn; memset(&seen, 0, sizeof seen); memset(&dupn, 0, sizeof dupn); for (int sj = 0; sj < c->nscopes; sj++) { const char *nm = c->scopes[sj].name; if (c->scopes[sj].def_node < 0 || !nm) continue; if (anh_has(&seen, nm)) { if (!anh_has(&dupn, nm)) anh_add(&dupn, nm); } else anh_add(&seen, nm); } anh_free(&seen); for (int si = 0; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (s->def_node < 0 || s->nparams <= 0 || !s->name) continue; /* one definition of this name program-wide: the argument collection below is name-matched (like param inference itself), so a same-named sibling method would blend unrelated call sites */ if (anh_has(&dupn, s->name)) continue; int body = nt_ref(nt, s->def_node, "body"); int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; if (!bb || bn < 2) continue; for (int g = 0; g < bn - 1; g++) { const char *pn = nng_guard_param(c, s, bb[g]); if (!pn) break; /* leading guards only */ int k = -1; for (int i = 0; i < s->nparams; i++) if (sp_streq(s->pnames[i], pn)) { k = i; break; } if (k < 0 || (s->rest_idx >= 0 && k >= s->rest_idx)) continue; if (nng_has_write(c, body, pn)) continue; /* non-nil unify over every name-matched plain call's argument for parameter k, as the call's layout hands it; any caller shape this cannot see through (super, a splat, the gather or a keyword hash funding it, no argument) bails */ TyKind t = TY_UNKNOWN; int ok = 1; for (int id = 0; id < nt->count && ok; id++) { const char *ty2 = nt_type(nt, id); NodeKind ty2_k = nt_kind(nt, id); if (!ty2) continue; if (ty2_k == NK_SuperNode || ty2_k == NK_ForwardingSuperNode) { Scope *cs = comp_scope_of(c, id); if (cs && cs->name && sp_streq(cs->name, s->name)) ok = 0; continue; } if (ty2_k != NK_CallNode) continue; const char *cn2 = nt_str(nt, id, "name"); if (!cn2 || !sp_streq(cn2, s->name)) continue; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; int a = call_param_arg(c, s, av, an, k); if (a < 0) { ok = 0; break; } if (nt_kind(nt, a) == NK_NilNode) continue; TyKind at = infer_type(c, a); if (at == TY_NIL) continue; if (at == TY_UNKNOWN || at == TY_POLY || at == TY_VOID) { ok = 0; break; } t = (t == TY_UNKNOWN) ? at : ty_unify(t, at); } if (!ok || t == TY_UNKNOWN || t == TY_POLY || t == TY_NIL) continue; /* the read-site unbox must be expressible for T */ if (!(t == TY_INT || t == TY_FLOAT || t == TY_BOOL || t == TY_SYMBOL || t == TY_STRING || t == TY_TIME || ty_is_object(t) || ty_is_array(t) || ty_is_hash(t))) continue; for (int j = g + 1; j < bn; j++) nng_mark_reads(c, bb[j], s, pn, t); } } anh_free(&dupn); } /* An empty `[]` infers TY_UNKNOWN so `x = []; x << 1` can back-fill from the writes. A literal consumed on the spot has no writes to wait for, so it takes its kind from the use: a receiver or interpolation is a poly array; an argument to a typed-array method is that array's kind when the method combines arrays (`+`, `concat`, `==`, ...) and a poly array otherwise. Runs after the fixpoint, where receiver kinds are settled. */ static int empty_arr_same_kind_method(const char *nm) { static const char *const ok[] = { "+", "-", "&", "|", "==", "!=", "<=>", "eql?", "concat", "replace", "union", "intersection", "difference", "intersect?", "equal?", NULL }; return str_in(nm, ok); } static int is_empty_array_literal(const NodeTable *nt, int id, int cap) { if (id < 0 || id >= cap || nt_kind(nt, id) != NK_ArrayNode) return 0; int en = 0; nt_arr(nt, id, "elements", &en); return en == 0; } /* The node of an empty `[]`, or of a bare `Array.new`, which carries no element type either and is as much an operand as the literal, or -1. An operand in parentheses is the node inside them: that is what the marking (empty_arr_recv, arr_want) and the inference of it read. */ static int empty_array_operand(Compiler *c, int id) { if (id < 0 || id >= c->node_cap || !node_is_empty_container(c, id) || an_empty_container_kind(c, id) != 1) return -1; return nt_paren_single(c->nt, id); } static void mark_empty_array_operands(Compiler *c) { if (!c->empty_arr_recv || !c->arr_want) return; const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_EmbeddedStatementsNode, es) { int st = nt_ref(nt, es, "statements"); int bn = 0; const int *bb = st >= 0 ? nt_arr(nt, st, "body", &bn) : NULL; int eo = bn == 1 ? empty_array_operand(c, bb[0]) : -1; if (eo >= 0) comp_set_empty_arr_recv(c, eo, 1); } /* `a, b = []` answers its right-hand side, which no write can back-fill */ NT_FOREACH_KIND(nt, NK_MultiWriteNode, mw) { int v = nt_ref(nt, mw, "value"); int eo = empty_array_operand(c, v); if (eo >= 0) comp_set_empty_arr_recv(c, eo, 1); } NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || recv >= c->node_cap) continue; int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *av = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; int recv_n = empty_array_operand(c, recv); int recv_empty = recv_n >= 0; /* an inject / each_with_object seed is written through the block param and back-fills like a local: leave it */ int seed = nt_ref(nt, id, "block") >= 0 && nm && (sp_streq(nm, "each_with_object") || sp_streq(nm, "inject") || sp_streq(nm, "reduce")); /* `f.call([])` on a Proc: the parameter takes its type from the argument, and an empty literal has none of its own, so the parameter defaulted to int and the literal was still built as a container -- an sp_IntArray * into an sp_int slot, which did not compile (#4295). A plain method's parameter already reads it as a container; give the proc parameter the same poly layout. `.()` and `f[x]` parse to the same names. */ if (an > 0 && nm && !recv_empty && is_call_or_yield(nm) && infer_type(c, recv) == TY_PROC) { for (int k = 0; k < an; k++) { int ek = empty_array_operand(c, av[k]); if (ek >= 0 && c->arr_want[ek] == TY_UNKNOWN) comp_set_arr_want(c, ek, TY_POLY_ARRAY); /* the same for `{}`: the proc-call argument emitter asks the node's type to decide whether the value is storable, and an untyped empty literal made it declare an sp_int temp and box nil, while the literal itself still built a hash */ else if (c->hash_want && av[k] < c->node_cap && c->hash_want[av[k]] == TY_UNKNOWN) { NodeKind hk = nt_kind(nt, av[k]); int hen = 0; if (hk == NK_HashNode || hk == NK_KeywordHashNode) { nt_arr(nt, av[k], "elements", &hen); if (hen == 0) comp_set_hash_want(c, av[k], TY_STR_POLY_HASH); } } } } if (an > 0 && nm && !recv_empty) { TyKind rt = infer_type(c, recv); /* an Array or Hash subclass instance's builtin call that reads its arguments as Arrays reads them as its Array's or Hash's own would (#7449); one storing an argument as an element (fill) is left */ { TyKind ak; if (comp_arysub_call(c, id, rt, &ak) && (ty_is_array(ak) || ty_is_hash(ak)) && comp_arysub_args_viewed(c, id, ak)) rt = ak; } if ((ty_is_array(rt) && !seed) || ty_is_hash(rt)) { TyKind want = (ty_is_array(rt) && rt != TY_POLY_ARRAY && empty_arr_same_kind_method(nm)) ? rt : TY_POLY_ARRAY; for (int k = 0; k < an; k++) { int ek = empty_array_operand(c, av[k]); if (ek >= 0 && c->arr_want[ek] == TY_UNKNOWN) comp_set_arr_want(c, ek, want); } } } if (!recv_empty) continue; /* `[] + a` takes a's kind ... */ if (an == 1 && nm && empty_arr_same_kind_method(nm) && c->arr_want[recv_n] == TY_UNKNOWN) { TyKind at = infer_type(c, av[0]); if (ty_is_array(at) && at != TY_POLY_ARRAY) { comp_set_arr_want(c, recv_n, at); continue; } } /* When both operands are bare `[]`, the receiver becomes a poly array but the argument has no independently inferred kind. Give that argument the same poly layout before codegen; otherwise `[] == []` / `[] | []` pass the default sp_IntArray* into a path expecting sp_PolyArray*. Seeds keep their existing back-fill behavior through the block parameter. */ if (!seed) { for (int k = 0; k < an; k++) { int ek = empty_array_operand(c, av[k]); if (ek >= 0 && c->arr_want[ek] == TY_UNKNOWN) comp_set_arr_want(c, ek, TY_POLY_ARRAY); } } /* ... any other bare `[]` receiver is an empty poly array */ comp_set_empty_arr_recv(c, recv_n, 1); /* The marking gives the receiver an element type, but a block parameter is interned only where something reads it. With neither -- `[].map { |x| 1 }` -- no slot exists at all, while the element loop binds `lv_x` anyway and named an undeclared identifier (#3921). The element of a marked empty literal is boxed, so claim the slot here. */ { int bp = nt_ref(nt, id, "block"); int pn = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int pp = pn >= 0 ? nt_ref(nt, pn, "parameters") : -1; int rn = 0; const int *reqs = pp >= 0 ? nt_arr(nt, pp, "requireds", &rn) : NULL; Scope *bs = reqs ? comp_scope_of(c, bp) : NULL; for (int k = 0; bs && k < rn; k++) { const char *pnm = nt_str(nt, reqs[k], "name"); if (!pnm || scope_local(bs, pnm)) continue; LocalVar *plv = scope_local_intern(bs, pnm); if (plv) { lv_set_type(plv, TY_POLY); plv->is_block_param = 1; } } } } } /* A method whose body TAIL is a bare empty `[]` / `{}` literal returns that container, but an unmarked empty literal infers TY_UNKNOWN (its element type normally back-fills from usage), so the method collapsed to a void C function that silently discarded the value. Mark the tail literal so it types as the bare-container default (poly array / STR_POLY hash), the same thing `x = []; x` returns. Skipped when the method has explicit returns: their unification (`return [1]` elsewhere) should keep narrowing. */ static void mark_empty_literal_tails(Compiler *c) { if (!c->empty_arr_recv || !c->empty_hash_recv) return; const NodeTable *nt = c->nt; char *has_ret = (char *)calloc((size_t)(c->nscopes > 0 ? c->nscopes : 1), 1); if (has_ret) { for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_ReturnNode) continue; Scope *rs = comp_scope_of(c, id); if (rs) has_ret[(int)(rs - c->scopes)] = 1; } } /* Mark the empty literal that ends `stmts`, the body of something whose value that literal is. */ #define MARK_EMPTY_TAIL(stmts) do { \ int bn_ = 0; const int *bb_ = nt_arr(nt, (stmts), "body", &bn_); \ if (bn_ > 0) { \ int last_ = bb_[bn_ - 1]; \ const char *lty_ = (last_ >= 0 && last_ < c->node_cap) ? nt_type(nt, last_) : NULL; \ int en_ = 0; \ if (lty_ && sp_streq(lty_, "ArrayNode")) { \ nt_arr(nt, last_, "elements", &en_); \ if (en_ == 0) comp_set_empty_arr_recv(c, last_, 1); \ } \ else if (lty_ && (sp_streq(lty_, "HashNode") || sp_streq(lty_, "KeywordHashNode"))) { \ nt_arr(nt, last_, "elements", &en_); \ if (en_ == 0) comp_set_empty_hash_recv(c, last_, 1); \ } \ } \ } while (0) for (int s = 1; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->body < 0 || (has_ret && has_ret[s])) continue; if (nt_kind(nt, sc->body) != NK_StatementsNode) continue; MARK_EMPTY_TAIL(sc->body); } /* A block's value is its tail the same way a method's is, but only a DEF scope carries a body, so a block was never reached here: `x.then { [] }` takes the block's value as its own and had none to take, which declared the C temp `void` (#3930). A `return` inside leaves the method rather than the block, so those are skipped exactly as a method's are. */ NT_FOREACH_KIND(nt, NK_BlockNode, blk) { int bs = nt_ref(nt, blk, "body"); if (bs < 0 || nt_kind(nt, bs) != NK_StatementsNode) continue; Scope *sc = comp_scope_of(c, blk); if (has_ret && sc && has_ret[(int)(sc - c->scopes)]) continue; MARK_EMPTY_TAIL(bs); } /* A block whose tail is `next v` hands back v as its value, so an empty `[]` / `{}` there is the value just as a tail one is: left unmarked it took another block's type, and `next []` beside `next 1` went into an sp_int. A `next []` before some other tail is left alone: it is built at the kind that tail gives the block's value (#3978). */ NT_FOREACH_KIND(nt, NK_BlockNode, nblk) { int bs = nt_ref(nt, nblk, "body"); int bn = 0; const int *bb = bs >= 0 && nt_kind(nt, bs) == NK_StatementsNode ? nt_arr(nt, bs, "body", &bn) : NULL; if (bn < 1 || nt_kind(nt, bb[bn - 1]) != NK_NextNode) continue; int na = nt_ref(nt, bb[bn - 1], "arguments"); int an = 0; const int *av = na >= 0 ? nt_arr(nt, na, "arguments", &an) : NULL; int v = an == 1 ? nt_paren_single(c->nt, av[0]) : -1; /* `next({})` */ if (v < 0 || v >= c->node_cap) continue; int en = 0; if (nt_kind(nt, v) == NK_ArrayNode) { nt_arr(nt, v, "elements", &en); if (en == 0) comp_set_empty_arr_recv(c, v, 1); } else if (nt_kind(nt, v) == NK_HashNode) { nt_arr(nt, v, "elements", &en); if (en == 0) comp_set_empty_hash_recv(c, v, 1); } } #undef MARK_EMPTY_TAIL free(has_ret); } static void *an_grow(void *p, int n, int *cap, size_t sz) { if (n < *cap) return p; *cap = *cap ? *cap * 2 : 16; p = realloc(p, sz * (size_t)*cap); if (!p) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } return p; } /* An empty `{}` passed as an argument to a USER method (`m({})`) is an accumulator seed: the callee builds it up (a yield-wrapper's inject / each_with_object), so the param and the method's return settle from it. An unmarked empty {} stays UNKNOWN, and if the callee returns it (`acc`) the whole call is UNKNOWN -> a void method (#2860). Type it as the widest hash (POLY_POLY, so any key/value the callee writes is sound). Only user methods (a builtin has its own arg handling). Empty `[]` args are deliberately NOT marked: POLY_ARRAY would pessimize the common typed-array-seed case, and the observed void-return bug is hash-specific. */ /* True if method scope `mi`'s param `p` is index-assigned (`pname[k] = v`) with a non-string key -- an int or symbol key. The precise body-write widening (#397) only reads string/symbol keys and skips int keys (array-ambiguous), so an int-keyed mutation leaves the param poly, and mutating a poly hash through `[]=` widens it -- dropping the change on the caller's empty `{}` (#2871). */ /* Collected once per mark_empty_literal_args run: every `local[k] = v` write whose key type cannot be assumed String (#2894), as (scope, local-name) pairs. The old form rescanned the whole node table per (call, param) -- O(calls * nodes) once call names resolve. */ typedef struct { const Scope *sc; const char *nm; } NonstrIdxWrite; static NonstrIdxWrite *nonstr_idx_writes(Compiler *c, int *out_n) { const NodeTable *nt = c->nt; NonstrIdxWrite *v = NULL; int n = 0, cap = 0; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "[]=")) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_LocalVariableReadNode) continue; const char *rn = nt_str(nt, recv, "name"); if (!rn) continue; int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an < 2) continue; TyKind kt = infer_type(c, av[0]); /* An unresolved key (typically a parameter whose type arrives from the call sites) cannot be assumed String: defaulting the caller's `{}` to the string-keyed variant would drop the callee's write (#2894). */ if (kt != TY_INT && kt != TY_SYMBOL && kt != TY_UNKNOWN && kt != TY_POLY) continue; v = an_grow(v, n, &cap, sizeof(*v)); v[n].sc = comp_scope_of(c, recv); v[n].nm = rn; n++; } *out_n = n; return v; } static int param_nonstr_index_written(Compiler *c, int mi, int p, const NonstrIdxWrite *ix, int ixn) { if (mi < 0 || p < 0 || p >= c->scopes[mi].nparams) return 0; const char *pn = c->scopes[mi].pnames[p]; if (!pn) return 0; for (int i = 0; i < ixn; i++) if (ix[i].sc == &c->scopes[mi] && sp_streq(ix[i].nm, pn)) return 1; return 0; } static void mark_empty_hash_local_writes(Compiler *c, int a) { const NodeTable *nt = c->nt; const char *ln = nt_str(nt, a, "name"); Scope *asc = comp_scope_of(c, a); if (!ln || !asc || !local_all_writes_empty_hash(c, asc, ln)) return; for (int w = 0; w < nt->count; w++) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ln) || comp_scope_of(c, w) != asc) continue; int wv = nt_ref(nt, w, "value"); if (wv >= 0 && wv < c->node_cap && nt_kind(nt, wv) == NK_HashNode) comp_set_empty_hash_arg(c, wv, 1); } } static void mark_empty_literal_args(Compiler *c) { if (!c->empty_hash_arg) return; const NodeTable *nt = c->nt; /* This pass does not create or rename scopes, so the scope-shape index is stable across it: freeze so the per-class method lookups below (called for every CallNode * every class) hit the O(1) hash index instead of the unfrozen O(nscopes) linear reverse scan -- the dominant analyze cost on large apps (lobsters: ~72% of compile time was this triple loop). The state on entry is put back on exit: this also runs inside the fixpoint, after a builtin call is rewritten (b29ec9a7), where the index is frozen already, and leaving it unfrozen there made every lookup of the rest of the analysis a linear scan over the scopes (#4662). */ int was_frozen = comp_scope_index_is_frozen(); comp_scope_index_set_frozen(1); /* The per-call fallback below resolved an unmatched name by probing every class (comp_method_in_class + comp_cmethod_in_class for k in 0..nclasses). Builtin call names (each/map/puts/...) match no class, so most CallNodes paid the full O(nclasses) scan: O(calls * classes) overall. Build a name -> first-defining-scope map once instead -- inserting classes in ascending order keeps the exact first-match semantics of the old loop. */ int nix_n = 0; NonstrIdxWrite *nix = nonstr_idx_writes(c, &nix_n); int mm_cap = 64; while (mm_cap < c->nscopes * 2) mm_cap <<= 1; struct { const char *nm; int mi; } *mm = calloc((size_t)mm_cap, sizeof *mm); if (!mm) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int si = 1; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (s->class_id < 0 || !s->name) continue; unsigned long h = 5381; for (const char *p = s->name; *p; p++) h = h * 33 + (unsigned char)*p; for (int probe = (int)(h & (mm_cap - 1));; probe = (probe + 1) & (mm_cap - 1)) { if (!mm[probe].nm) { mm[probe].nm = s->name; mm[probe].mi = si; break; } if (sp_streq(mm[probe].nm, s->name)) { /* keep the entry the old class-ascending, methods-before-cmethods probe order would have found first */ Scope *o = &c->scopes[mm[probe].mi]; if (s->class_id < o->class_id || (s->class_id == o->class_id && !s->is_cmethod && o->is_cmethod)) mm[probe].mi = si; break; } } } for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; /* Resolve to a user method. Two shapes need the widest hash for an empty `{}` arg, because the precise body-write widening (#397) can't reach the key/value types: (a) a YIELD-wrapper builds the seed through its block (`acc = yield(acc, x)`); (b) the matching param is index-written with a non-string key (`p[i]=v`), which #397 skips as array-ambiguous, leaving the param poly so a `[]=` widen drops the mutation on the caller's `{}`. A string-keyed direct-index wrapper stays with the precise mechanism. */ int mi = call_target(c, id, -1, nm, CT_TOP, NULL); if (mi < 0) { unsigned long h = 5381; for (const char *p = nm; *p; p++) h = h * 33 + (unsigned char)*p; for (int probe = (int)(h & (mm_cap - 1));; probe = (probe + 1) & (mm_cap - 1)) { if (!mm[probe].nm) break; if (sp_streq(mm[probe].nm, nm)) { mi = mm[probe].mi; break; } } } if (mi < 0) continue; int yields = c->scopes[mi].yields; int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *args = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { int a = args[j]; if (a < 0 || a >= c->node_cap) continue; int want = yields || param_nonstr_index_written(c, mi, j, nix, nix_n); if (!want) continue; NodeKind ak = nt_kind(nt, a); if (ak == NK_HashNode || ak == NK_KeywordHashNode) { int en = 0; nt_arr(nt, a, "elements", &en); if (en == 0) comp_set_empty_hash_arg(c, a, 1); } /* `h = {}; m(h)`: the arg is a local whose value is the empty literal. Mark that write's `{}` so the local (and thus the param) is the widest hash -- the mutation then lands in place on the caller's hash. */ else if (ak == NK_LocalVariableReadNode) mark_empty_hash_local_writes(c, a); } } /* `h = {}; yield h` hands the hash to a block the method does not see: the block fills it (`{ |t| t[:a] = 1 }`) with keys this scope has no evidence of, and the String-keyed default then refused the store. Give it the widest hash, as an argument to a yielding method gets. */ NT_FOREACH_KIND(nt, NK_YieldNode, id) { int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *args = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { int a = args[j]; if (a < 0 || a >= c->node_cap) continue; NodeKind ak = nt_kind(nt, a); if (ak == NK_HashNode) { int en = 0; nt_arr(nt, a, "elements", &en); if (en == 0) comp_set_empty_hash_arg(c, a, 1); continue; } if (ak == NK_LocalVariableReadNode) mark_empty_hash_local_writes(c, a); } } free(mm); free(nix); comp_scope_index_set_frozen(was_frozen); } /* An empty `{}` compared against a hash-typed peer takes that peer's variant, so the two sides share a representation and `==` can compare contents instead of collapsing to a constant false (#3040). The literal carries no keys of its own, so adopting the peer's variant loses nothing. */ static void mark_empty_hash_cmp_peers(Compiler *c) { if (!c->hash_want) return; const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "==") && !sp_streq(nm, "!=") && !sp_streq(nm, "eql?"))) continue; int recv = nt_ref(nt, id, "receiver"); int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *av = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; if (recv < 0 || an != 1) continue; int arg = av[0]; for (int side = 0; side < 2; side++) { int lit = side ? arg : recv, peer = side ? recv : arg; if (lit < 0 || lit >= c->node_cap || peer < 0) continue; NodeKind lk = nt_kind(nt, lit); if (lk != NK_HashNode && lk != NK_KeywordHashNode) continue; int en = 0; nt_arr(nt, lit, "elements", &en); if (en != 0) continue; TyKind pt = infer_type(c, peer); if (ty_is_hash(pt)) comp_set_hash_want(c, lit, pt); } } } /* The key class a literal AST node names, as a bit: a hash whose keys are not all one class needs the general boxed variant. 0 for anything else, which makes the caller decline rather than guess. */ static const MpIx *g_hkb_wix; /* (name, scope) -> local writes, while set */ static unsigned hash_key_bit(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *t = id >= 0 ? nt_type(nt, id) : NULL; NodeKind t_k = nt_kind(nt, id); if (!t) return 0; if (t_k == NK_StringNode || t_k == NK_InterpolatedStringNode) return 1u; if (t_k == NK_SymbolNode) return 2u; if (t_k == NK_IntegerNode) return 4u; /* `k = "x"; h[k] = 9`: a local key names its class through its writes, and only when every one of them agrees. */ if (t_k == NK_LocalVariableReadNode) { const char *kn = nt_str(nt, id, "name"); const Scope *ks = comp_scope_of(c, id); unsigned b = 0; if (!kn || !ks) return 0; /* the local's own writes from mark_mixed_key_hash_locals' index when it is set; the answer does not depend on their order */ int nwi = 0; const int *wis = NULL; if (g_hkb_wix) wis = mp_get(g_hkb_wix, kn, (int)(ks - c->scopes), &nwi); for (int wi = 0, w = 0; g_hkb_wix ? wi < nwi : w < nt->count; g_hkb_wix ? wi++ : w++) { if (g_hkb_wix) w = wis[wi]; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, kn) || comp_scope_of(c, w) != ks) continue; int wv = nt_ref(nt, w, "value"); NodeKind wk = nt_kind(nt, wv); unsigned wb = 0; if (wk == NK_StringNode || wk == NK_InterpolatedStringNode) wb = 1u; else if (wk == NK_SymbolNode) wb = 2u; else if (wk == NK_IntegerNode) wb = 4u; if (!wb) return 0; b |= wb; } return (b && !(b & (b - 1))) ? b : 0; } return 0; } /* A hash literal assigned to a local the program later indexes with a key of another class. The variant came from the literal alone, so the write went in under the WRONG key type -- a Symbol stored into a String-keyed hash became a String, into an Integer-keyed one became its symbol id -- or the two sides named different C structs for the same object and the build failed. The mixed-key literal already answers with the general boxed hash; give this literal the same answer, before the fixpoint so every use agrees (#3927). */ typedef struct { const Scope *sc; const char *nm; unsigned bits; } HashKeyUse; /* The key classes a call writes into its local receiver, or 0 when the call is not such a write or its key class is not settled in the AST. */ static unsigned hash_key_write_bits(Compiler *c, int id, const Scope **sc, const char **nm) { const NodeTable *nt = c->nt; /* `h[:x] ||= v` and `h[:x] &&= v` store under the key too (#4531) */ if (nt_kind(nt, id) == NK_IndexOrWriteNode || nt_kind(nt, id) == NK_IndexAndWriteNode || nt_kind(nt, id) == NK_IndexOperatorWriteNode) { int wr = nt_ref(nt, id, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode) return 0; int wa = nt_ref(nt, id, "arguments"); int wc = 0; const int *wv = wa >= 0 ? nt_arr(nt, wa, "arguments", &wc) : NULL; if (!wv || wc != 1) return 0; unsigned b = hash_key_bit(c, wv[0]); if (!b) return 0; *sc = comp_scope_of(c, wr); *nm = nt_str(nt, wr, "name"); return (*sc && *nm) ? b : 0; } if (nt_kind(nt, id) != NK_CallNode) return 0; const char *wn = nt_str(nt, id, "name"); if (!wn) return 0; int set = is_store_alias(wn); int upd = is_hash_merge_bang(wn); if (!set && !upd) return 0; int wr = nt_ref(nt, id, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode) return 0; int wa = nt_ref(nt, id, "arguments"); int wc = 0; const int *wv = wa >= 0 ? nt_arr(nt, wa, "arguments", &wc) : NULL; if (!wv || wc < 1) return 0; unsigned b = 0; if (set) { if (wc != 2) return 0; b = hash_key_bit(c, wv[0]); } else { /* `h.update({ "x" => 9 })`: the merged literal's keys land in h. */ for (int k = 0; k < wc; k++) { if (nt_kind(nt, wv[k]) != NK_HashNode) return 0; int hn = 0; const int *hels = nt_arr(nt, wv[k], "elements", &hn); for (int e = 0; e < hn; e++) { if (nt_kind(nt, hels[e]) != NK_AssocNode) return 0; unsigned eb = hash_key_bit(c, nt_ref(nt, hels[e], "key")); if (!eb) return 0; b |= eb; } } } if (!b) return 0; *sc = comp_scope_of(c, wr); *nm = nt_str(nt, wr, "name"); return (*sc && *nm) ? b : 0; } /* The hash literals a value expression copies its keys from: the literal itself, `lit.dup` / `lit.clone`, a local that a write in the same scope gave such a literal (`g = {...}; h = g.dup`), or a PARAMETER, which stands for the literals passed in its position at every call of the method (`def parse(arg) = parse_hash(arg.dup)`). A copy has its source's variant, so the widening lands on the source literal; without this a Symbol stored into a dup'ed String-keyed hash raised, and into a dup'ed Integer-keyed one landed as INT64_MIN (#4540). Fills `out`, up to `cap`; answers the count. */ /* The walk fans out -- a parameter to every call of its method, whose arguments are parameters in turn -- and meets the same nodes by many paths, from one query and from the next: the key sites ask it for every local each round. What a node at a depth reaches does not depend on the query that got there, so each (node, depth) is answered once while the table stands, as a list of at most 32 sources in walk order; a parent joins its children's lists in order, dropping repeats, which is the order the walk met them in. Taking the first 32 of each list loses nothing a parent keeps: a child whose list is full fills the parent's. */ #define HLS_CAP 32 static const NodeTable *hlm_nt; static unsigned hlm_ver; static int hlm_cnt, hlm_nsc; static int *hlm_key, *hlm_off, hlm_hcap, hlm_used; /* (node * 8 + depth) -> pool offset */ static int *hlm_pool, hlm_pn, hlm_pcap; static int hls_get(Compiler *c, int val, int depth); /* The arguments in position pidx of the calls named `name`, in table order, kept beside the memo while it stands: a parameter is asked at each depth it is reached at, and every parameter of a method of that name asks the same calls, so the calls of a name were walked once per ask -- seven million call visits a round on a Rails application, for two hundred thousand answers. */ typedef struct { char *name; int pidx, off; } HlaSlot; static HlaSlot *hla_slot; static int hla_hcap, hla_used; static int *hla_pool, hla_pn, hla_pcap; /* off: the count, then the argument nodes */ static void hla_reset(void) { for (int i = 0; i < hla_hcap; i++) free(hla_slot[i].name), hla_slot[i].name = NULL; hla_used = 0; hla_pn = 1; } static int hla_args(Compiler *c, const char *name, int pidx) { const NodeTable *nt = c->nt; if (2 * (hla_used + 1) > hla_hcap) { int ocap = hla_hcap; HlaSlot *os = hla_slot; hla_hcap = ocap ? ocap * 2 : 1024; hla_slot = calloc((size_t)hla_hcap, sizeof *hla_slot); if (!hla_slot) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < ocap; i++) { if (!os[i].name) continue; unsigned h = (sp_strhash(os[i].name) ^ (unsigned)os[i].pidx * 2654435761u) & (unsigned)(hla_hcap - 1); while (hla_slot[h].name) h = (h + 1) & (unsigned)(hla_hcap - 1); hla_slot[h] = os[i]; } free(os); } unsigned h = (sp_strhash(name) ^ (unsigned)pidx * 2654435761u) & (unsigned)(hla_hcap - 1); while (hla_slot[h].name && !(hla_slot[h].pidx == pidx && sp_streq(hla_slot[h].name, name))) h = (h + 1) & (unsigned)(hla_hcap - 1); if (hla_slot[h].name) return hla_slot[h].off; if (!hla_pcap) { hla_pcap = 4096; hla_pn = 1; hla_pool = malloc(sizeof(int) * (size_t)hla_pcap); if (!hla_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } int off = hla_pn++, n = 0; for (int id = an_calls_named_first(c, name); id >= 0; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *cn = nt_str(nt, id, "name"); if (!cn || !sp_streq(cn, name)) continue; int ca = nt_ref(nt, id, "arguments"); int cc = 0; const int *cv = ca >= 0 ? nt_arr(nt, ca, "arguments", &cc) : NULL; if (!cv || pidx >= cc) continue; if (hla_pn == hla_pcap) { hla_pcap *= 2; hla_pool = realloc(hla_pool, sizeof(int) * (size_t)hla_pcap); if (!hla_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } hla_pool[hla_pn++] = cv[pidx]; n++; } hla_pool[off] = n; hla_slot[h].name = strdup(name); if (!hla_slot[h].name) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } hla_slot[h].pidx = pidx; hla_slot[h].off = off; hla_used++; return off; } static int hls_join(int *buf, int n, int off) { for (int i = 0; i < hlm_pool[off] && n < HLS_CAP; i++) { int s = hlm_pool[off + 1 + i], dup = 0; for (int j = 0; j < n && !dup; j++) dup = buf[j] == s; if (!dup) buf[n++] = s; } return n; } static int hls_compute(Compiler *c, int val, int depth, int *out) { const NodeTable *nt = c->nt; int n = 0; const char *vt = nt_type(nt, val); NodeKind vk = nt_kind(nt, val); if (!vt) return 0; if (vk == NK_HashNode) { out[0] = val; return 1; } if (vk == NK_CallNode) { const char *cn = nt_str(nt, val, "name"); int recv = nt_ref(nt, val, "receiver"); /* an element of an Array literal, by a constant index (`[x, 1][0]`), that names a variable: a literal written there directly is boxed by its context and takes the general hash already */ if (cn && sp_streq(cn, "[]") && recv >= 0 && nt_kind(nt, recv) == NK_ArrayNode && nt_ref(nt, val, "block") < 0) { int a = nt_ref(nt, val, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int en = 0; const int *el = nt_arr(nt, recv, "elements", &en); if (ac != 1 || nt_kind(nt, av[0]) != NK_IntegerNode) return 0; long long i = nt_int(nt, av[0], "value", -1); if (i < 0) i += en; if (i < 0 || i >= en) return 0; for (int e = 0; e <= i; e++) if (nt_kind(nt, el[e]) == NK_SplatNode) return 0; if (nt_kind(nt, el[i]) != NK_LocalVariableReadNode) return 0; return hls_join(out, 0, hls_get(c, el[i], depth + 1)); } if (!cn || (!sp_streq(cn, "dup") && !sp_streq(cn, "clone"))) return 0; if (nt_ref(nt, val, "arguments") >= 0 || nt_ref(nt, val, "block") >= 0) return 0; return hls_join(out, 0, hls_get(c, recv, depth + 1)); } if (vk == NK_LocalVariableReadNode) { const char *ln = nt_str(nt, val, "name"); const Scope *ls = comp_scope_of(c, val); if (!ln || !ls) return 0; int pidx = -1; for (int p = 0; p < ls->nparams; p++) if (ls->pnames[p] && sp_streq(ls->pnames[p], ln)) { pidx = p; break; } if (pidx >= 0 && ls->name) { /* a parameter: the arguments in its position at every call by name, through the calls-by-name index -- a walk of the whole table for each parameter reached made a program with a key operation on a copy of a parameter in each of its methods quadratic */ int off = hla_args(c, ls->name, pidx), cnt = hla_pool[off]; /* (by index: a child's ask may grow the pool) */ for (int i = 0; i < cnt && n < HLS_CAP; i++) n = hls_join(out, n, hls_get(c, hla_pool[off + 1 + i], depth + 1)); return n; } /* the local's writes through the (scope, name) index, not a walk of the whole table per read */ int lsi = (int)(ls - c->scopes); for (int w = comp_lvw_first_sc(c, lsi, ln); w >= 0 && n < HLS_CAP; w = comp_lvw_next_sc(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ln) || comp_scope_of(c, w) != ls) continue; n = hls_join(out, n, hls_get(c, nt_ref(nt, w, "value"), depth + 1)); } return n; } return 0; } /* the pool offset of (val, depth)'s list; offset 0 is the empty list */ static int hls_get(Compiler *c, int val, int depth) { const NodeTable *nt = c->nt; if (hlm_nt != nt || hlm_ver != nt->version || hlm_cnt != nt->count || hlm_nsc != c->nscopes) { if (hlm_hcap) memset(hlm_key, -1, sizeof(int) * (size_t)hlm_hcap); hlm_used = 0; if (!hlm_pcap) { hlm_pcap = 4096; hlm_pool = malloc(sizeof(int) * (size_t)hlm_pcap); if (!hlm_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } hlm_pool[0] = 0; hlm_pn = 1; hla_reset(); hlm_nt = nt; hlm_ver = nt->version; hlm_cnt = nt->count; hlm_nsc = c->nscopes; } if (val < 0 || depth > 6) return 0; if (2 * (hlm_used + 1) > hlm_hcap) { int ocap = hlm_hcap, *okey = hlm_key, *ooff = hlm_off; hlm_hcap = ocap ? ocap * 2 : 4096; hlm_key = malloc(sizeof(int) * (size_t)hlm_hcap); hlm_off = malloc(sizeof(int) * (size_t)hlm_hcap); if (!hlm_key || !hlm_off) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memset(hlm_key, -1, sizeof(int) * (size_t)hlm_hcap); for (int i = 0; i < ocap; i++) { if (okey[i] < 0) continue; unsigned h = ((unsigned)okey[i] * 2654435761u) & (unsigned)(hlm_hcap - 1); while (hlm_key[h] >= 0) h = (h + 1) & (unsigned)(hlm_hcap - 1); hlm_key[h] = okey[i]; hlm_off[h] = ooff[i]; } free(okey); free(ooff); } int key = val * 8 + depth; unsigned h = ((unsigned)key * 2654435761u) & (unsigned)(hlm_hcap - 1); while (hlm_key[h] >= 0 && hlm_key[h] != key) h = (h + 1) & (unsigned)(hlm_hcap - 1); if (hlm_key[h] == key) return hlm_off[h]; int buf[HLS_CAP], nb = hls_compute(c, val, depth, buf); /* the children may have grown the table: find the slot again */ h = ((unsigned)key * 2654435761u) & (unsigned)(hlm_hcap - 1); while (hlm_key[h] >= 0 && hlm_key[h] != key) h = (h + 1) & (unsigned)(hlm_hcap - 1); int off = 0; if (nb > 0) { if (hlm_pn + nb + 1 > hlm_pcap) { while (hlm_pn + nb + 1 > hlm_pcap) hlm_pcap *= 2; hlm_pool = realloc(hlm_pool, sizeof(int) * (size_t)hlm_pcap); if (!hlm_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } off = hlm_pn; hlm_pool[hlm_pn++] = nb; memcpy(hlm_pool + hlm_pn, buf, sizeof(int) * (size_t)nb); hlm_pn += nb; } hlm_key[h] = key; hlm_off[h] = off; hlm_used++; return off; } static int hash_literal_sources(Compiler *c, int val, int *out, int cap) { int off = hls_get(c, val, 0), n = hlm_pool[off]; if (n > cap) n = cap; memcpy(out, hlm_pool + off + 1, sizeof(int) * (size_t)n); return n; } /* widen `lit` to the boxed variant when the key classes written into it (`bits`) and its own keys together name more than one class */ static void hash_literal_widen_if_mixed(Compiler *c, int lit, unsigned bits) { const NodeTable *nt = c->nt; if (lit < 0 || lit >= c->node_cap || nt_kind(nt, lit) != NK_HashNode) return; unsigned mask = bits; int en = 0; const int *els = nt_arr(nt, lit, "elements", &en); for (int e = 0; e < en; e++) { if (nt_kind(nt, els[e]) != NK_AssocNode) return; unsigned b = hash_key_bit(c, nt_ref(nt, els[e], "key")); if (!b) return; mask |= b; } if (mask && (mask & (mask - 1))) comp_set_hash_want(c, lit, TY_POLY_POLY_HASH); } static void mark_mixed_key_hash_locals(Compiler *c) { if (!c->hash_want) return; const NodeTable *nt = c->nt; HashKeyUse *uses = NULL; int nu = 0, cap = 0; /* (name, scope) -> the use's index, and -> the local's writes: the uses were found by a linear search per node, and a key's writes by a walk of the table per key (rubys in #5035) */ MpIx uix = {0}, wix = {0}; for (int id = comp_kind_first(c, NK_LocalVariableWriteNode); id >= 0; id = comp_kind_next(c, id)) { if (nt_kind(nt, id) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, id, "name"); if (wn) mp_add(&wix, wn, c->nscope[id], id); } g_hkb_wix = &wix; /* hash_key_write_bits reads keys through it too */ for (int id = 0; id < nt->count; id++) { const Scope *sc = NULL; const char *nm = NULL; unsigned b = hash_key_write_bits(c, id, &sc, &nm); if (!b) continue; int f = -1; { int nf = 0; const int *fs = mp_get(&uix, nm, (int)(sc - c->scopes), &nf); if (nf > 0) f = fs[0]; } if (f >= 0) { uses[f].bits |= b; continue; } uses = an_grow(uses, nu, &cap, sizeof(*uses)); uses[nu].sc = sc; uses[nu].nm = nm; uses[nu].bits = b; mp_add(&uix, nm, (int)(sc - c->scopes), nu); nu++; } if (!nu) { g_hkb_wix = NULL; free(uses); mp_free(&uix); mp_free(&wix); return; } for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_LocalVariableWriteNode) continue; int val = nt_ref(nt, id, "value"); { int fb = an_or_empty_hash_fallback(c, val); if (fb >= 0) val = fb; } const char *vt = val >= 0 ? nt_type(nt, val) : NULL; NodeKind vk = nt_kind(nt, val); int hash_new = 0; if (vk == NK_CallNode && nt_str(nt, val, "name") && sp_streq(nt_str(nt, val, "name"), "new")) { int hr = nt_ref(nt, val, "receiver"); hash_new = hr >= 0 && nt_kind(nt, hr) == NK_ConstantReadNode && nt_str(nt, hr, "name") && sp_streq(nt_str(nt, hr, "name"), "Hash"); } const char *lname = nt_str(nt, id, "name"); const Scope *ls = comp_scope_of(c, id); if (!lname || !ls) continue; unsigned mask = 0; { int nf = 0; const int *fs = mp_get(&uix, lname, (int)(ls - c->scopes), &nf); if (nf > 0) mask = uses[fs[0]].bits; } if (!mask) continue; /* a copy of a literal (`{...}.dup`, `g.dup` with g a literal or a parameter) takes the literal's variant: widen the literals (#4540) */ if (!hash_new && !(vk == NK_HashNode)) { int srcs[32]; int ns = hash_literal_sources(c, val, srcs, 32); for (int q = 0; q < ns; q++) hash_literal_widen_if_mixed(c, srcs[q], mask); continue; } if (!vt || (vk != NK_HashNode && !hash_new) || val >= c->node_cap) continue; int en = 0; const int *els = hash_new ? NULL : nt_arr(nt, val, "elements", &en); for (int k = 0; k < en; k++) { if (nt_kind(nt, els[k]) != NK_AssocNode) { mask = 0; break; } unsigned b = hash_key_bit(c, nt_ref(nt, els[k], "key")); if (!b) { mask = 0; break; } mask |= b; } /* more than one key class in play: only the boxed variant holds them all */ if (mask && (mask & (mask - 1))) comp_set_hash_want(c, val, TY_POLY_POLY_HASH); } /* The same through a PARAMETER: `def f(h) = h[:x] = 1` stores into the literal a caller passed, `f({ "x" => 0 })`, and that literal's variant came from its own keys alone, so the Symbol went in as a String or was dropped (#4531). Each use on a parameter reaches the hash literals in that position at every call of a method with that name. */ for (int k = 0; k < nu; k++) { const Scope *ps = uses[k].sc; int pidx = -1; for (int p = 0; ps && p < ps->nparams; p++) if (ps->pnames[p] && sp_streq(ps->pnames[p], uses[k].nm)) { pidx = p; break; } if (pidx < 0 || !ps->name) continue; for (int id = an_calls_named_first(c, ps->name); id >= 0; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *cn = nt_str(nt, id, "name"); if (!cn || !sp_streq(cn, ps->name)) continue; int ca = nt_ref(nt, id, "arguments"); int cc = 0; const int *cv = ca >= 0 ? nt_arr(nt, ca, "arguments", &cc) : NULL; if (!cv || pidx >= cc) continue; /* the literal in that position, or what a dup/clone/local/parameter there copies from (#4540) */ int srcs[32]; int ns = hash_literal_sources(c, cv[pidx], srcs, 32); for (int q = 0; q < ns; q++) hash_literal_widen_if_mixed(c, srcs[q], uses[k].bits); } } g_hkb_wix = NULL; free(uses); mp_free(&uix); mp_free(&wix); } /* A bare `{}` indexed or fetched with a statically-typed key takes the variant that key selects, instead of the StrPolyHash fallback that would coerce a Symbol key to a String (#3028) or emit ill-typed C for an Integer one (#3029). Values stay poly: the empty literal says nothing about them. */ /* The empty hash a slot write stores when its value is `{}` itself, the nil-guard fallback `x || {}` / `x || Hash.new`, or an attribute write of one (`adj = ctx.adjacent = {}`: the write answers its argument), else -1. */ static int empty_hash_write_lit(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0) return -1; if (nt_kind(nt, v) == NK_HashNode) { int en = 0; nt_arr(nt, v, "elements", &en); return en == 0 ? v : -1; } if (nt_kind(nt, v) == NK_CallNode && nt_ref(nt, v, "receiver") >= 0 && nt_ref(nt, v, "block") < 0) { const char *nm = nt_str(nt, v, "name"); int args = nt_ref(nt, v, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (call_is_setter_assign(nt, v) && an == 1 && av) return empty_hash_write_lit(c, av[0]); } return an_or_empty_hash_fallback(c, v); } /* The variant an empty literal takes from one more key context: the first one wins, unless the two are keyed by different classes among Symbol, String and Integer. Then only the poly-keyed variant holds both: keeping the first dropped the other key's stores (`x = {}; y = x; y[:a] = 1; x["b"] = 2`). */ static TyKind hash_want_join(TyKind cur, TyKind want) { if (!ty_is_hash(cur)) return want; TyKind ck = ty_hash_key(cur), wk = ty_hash_key(want); int ckeyed = ck == TY_SYMBOL || ck == TY_STRING || ck == TY_INT; int wkeyed = wk == TY_SYMBOL || wk == TY_STRING || wk == TY_INT; if (cur == want || ck == wk || !ckeyed || !wkeyed) return cur; return TY_POLY_POLY_HASH; } /* The empty Hash literal node `n` can only answer: through parentheses, a `||=` of a class variable, global or local whose value is `{}`, or a zero-argument self call whose method's last statement is such a write. -1 for anything else. An ivar's memo is not one: the ivar's slot takes the getter's writes as evidence and widens its literals itself (#4902). */ static int ekh_memo_literal(Compiler *c, int n, int depth) { const NodeTable *nt = c->nt; if (n < 0 || depth > 4) return -1; NodeKind k = nt_kind(nt, n); if (k == NK_ParenthesesNode || k == NK_StatementsNode) { int body = k == NK_ParenthesesNode ? nt_ref(nt, n, "body") : n; if (body < 0) return -1; if (nt_kind(nt, body) != NK_StatementsNode) return ekh_memo_literal(c, body, depth + 1); int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); return bn >= 1 ? ekh_memo_literal(c, bb[bn - 1], depth + 1) : -1; } if (k == NK_ClassVariableOrWriteNode || k == NK_GlobalVariableOrWriteNode || k == NK_LocalVariableOrWriteNode) { int v = nt_ref(nt, n, "value"); if (v < 0 || nt_kind(nt, v) != NK_HashNode) return -1; int en = 0; nt_arr(nt, v, "elements", &en); return en == 0 ? v : -1; } if (k == NK_CallNode && nt_ref(nt, n, "arguments") < 0 && nt_ref(nt, n, "block") < 0) { int r = nt_ref(nt, n, "receiver"); if (r >= 0 && nt_kind(nt, r) != NK_SelfNode) return -1; const char *nm = nt_str(nt, n, "name"); Scope *s = comp_scope_of(c, n); if (!nm || !s || s->class_id < 0) return -1; int mi = call_target(c, n, s->class_id, nm, s->is_cmethod ? CT_CMETHOD : 0, NULL); if (mi < 0 || mi >= c->nscopes || c->scopes[mi].nparams != 0) return -1; int body = c->scopes[mi].body; return body >= 0 ? ekh_memo_literal(c, body, depth + 1) : -1; } return -1; } /* The empty Hash literals a key operation's receiver takes its value from, through a conditional or parentheses (`(c ? {} : {})[k] = v`): each one is the receiver the way a bare `{}` is. Answers how many went into lits, or 0 when some value comes from anything else. */ typedef struct { const NodeTable *nt; int *lits; int n, cap; } EkhLits; static int ekh_lit_leaf(const VpLeaf *lf, void *ud) { EkhLits *a = ud; int en = -1; if (lf->kind == VPK_EXPR && nt_kind(a->nt, lf->node) == NK_HashNode) nt_arr(a->nt, lf->node, "elements", &en); if (en != 0 || a->n >= a->cap) return 1; a->lits[a->n++] = lf->node; return 0; } static int ekh_value_literals(const NodeTable *nt, int recv, int *lits, int cap) { EkhLits a = { nt, lits, 0, cap }; if (for_each_value_position(nt, recv, VPS_IF | VPS_UNLESS | VPS_CASE, ekh_lit_leaf, &a)) return 0; return a.n; } /* (set while the class structure is built, ahead of any inference: a write of no type yet says nothing then, and the mark it would pin only widens) */ static int g_eh_pre_infer = 0; static int mark_empty_hash_key_ctx(Compiler *c) { int changed = 0; if (!c->hash_want) return 0; const NodeTable *nt = c->nt; /* The `tap` / `then` / `yield_self` block parameters, collected once. The hop loop below follows a key operation written on such a parameter back to the call's own receiver (#4026), and it asked by scanning the whole node table -- per key site, per hop, and this pass runs on every fixpoint iteration. Ascending id order is the order the scan visited in, so the first match here is the match it found. */ int tp_n = 0, ncall = 0; const char **tp_name = NULL; int *tp_recv = NULL; (void)nt_nodes_of_kind(nt, NK_CallNode, &ncall); if (ncall > 0) { /* One CallNode is the most entries this can take, so size to that and fill in a single walk: no growth step to get an allocation failure half way through, which would leave SOME blocks followed and the rest not. */ tp_name = (const char **)malloc(sizeof(*tp_name) * (size_t)ncall); tp_recv = (int *)malloc(sizeof(*tp_recv) * (size_t)ncall); if (!tp_name || !tp_recv) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } NT_FOREACH_KIND(nt, NK_CallNode, tcid) { const char *cn = nt_str(nt, tcid, "name"); if (!cn || !is_tap_alias(cn)) continue; int blk = nt_ref(nt, tcid, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; const char *pn = block_param_name(c, blk, 0); if (!pn) continue; tp_name[tp_n] = pn; tp_recv[tp_n] = nt_ref(nt, tcid, "receiver"); tp_n++; } } for (int id = 0; id < nt->count; id++) { NodeKind idk = nt_kind(nt, id); /* `h[k] ||= v` and friends select a variant by their key exactly like a read does, but they are not CallNodes, so the name gate below never saw them and an Integer key fell back to StrPolyHash (#3353) */ int is_idx_write = (idk == NK_IndexOrWriteNode || idk == NK_IndexAndWriteNode || idk == NK_IndexOperatorWriteNode || idk == NK_IndexTargetNode); if (idk != NK_CallNode && !is_idx_write) continue; const char *nm = is_idx_write ? "[]" : nt_str(nt, id, "name"); /* `[]=` / `store` name the key in arg 0 exactly as the reads do, and say the same thing about which variant the literal has to be (#4026), as do the other key-taking methods. */ static const char *const key_ops[] = { "fetch", "[]", "[]=", "store", "key?", "has_key?", "include?", "member?", "dig", "slice", "except", "values_at", "fetch_values", "assoc", "delete", NULL }; int is_key_op = 0; for (int k = 0; nm && key_ops[k] && !is_key_op; k++) if (sp_streq(nm, key_ops[k])) is_key_op = 1; if (!is_key_op) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0 || recv >= c->node_cap) continue; /* A `tap` / `then` block parameter is another name for the call's own receiver, so a key operation through it says the same thing about the literal that one written directly on it does. Without this the literal kept the String-keyed default and an Array key went into a `const char *` slot (#4026). */ for (int hop = 0; hop < 8 && recv >= 0 && nt_kind(nt, recv) == NK_LocalVariableReadNode; hop++) { const char *rn2 = nt_str(nt, recv, "name"); if (!rn2) break; int outer = -1; for (int t = 0; t < tp_n; t++) if (sp_streq(tp_name[t], rn2)) { outer = tp_recv[t]; break; } if (outer < 0) break; recv = outer; } if (recv < 0 || recv >= c->node_cap) continue; /* the empty literal a memo holds: `(@@h ||= {})[k]`, or a reader that answers one (`def self.h = (@@h ||= {})`, `h[k] = v`) -- a class variable's literal has no slot the key sites reach the way an ivar's does, and kept the String-keyed default (railties' Annotation registers its extensions under Regexp keys) */ { int lit = ekh_memo_literal(c, recv, 0); if (lit >= 0) recv = lit; } NodeKind rk = nt_kind(nt, recv); int direct = (rk == NK_HashNode || rk == NK_KeywordHashNode); /* (the empty literals a conditional receiver's arms are, #9971) */ int lits[8], nlits = 0; if (direct) { int en = 0; nt_arr(nt, recv, "elements", &en); if (en != 0) continue; if (ty_is_hash(c->hash_want[recv])) continue; /* an earlier context won */ lits[nlits++] = recv; } else if (rk != NK_LocalVariableReadNode && rk != NK_InstanceVariableReadNode) { nlits = ekh_value_literals(nt, recv, lits, 8); int open = 0; for (int l = 0; l < nlits; l++) if (lits[l] < c->node_cap && !ty_is_hash(c->hash_want[lits[l]])) open = 1; if (!open) continue; direct = 1; } int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *av = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; if (an < 1) continue; TyKind kt = infer_type(c, av[0]); TyKind want = TY_UNKNOWN; switch (kt) { case TY_SYMBOL: want = TY_SYM_POLY_HASH; break; case TY_STRING: { /* The key operations this pass keys off (`key?`, `[]`, `fetch`, `dig`) say what the KEY is and nothing about the value, so the poly-valued variant was the only answer available here. But the slot's own `h[k] = v` writes do say, and this mark is permanent -- the variant only ever widens afterwards, so pinning poly loses a concrete value type that is right there. Read the writes; if they exist but have no type yet, wait rather than guess (see g_infer_optimistic). */ int nw = 0; TyKind wv = aset_value_type_ex(c, recv, &nw); if (nw > 0 && wv == TY_UNKNOWN && (g_infer_optimistic || g_eh_pre_infer)) continue; /* The default is a value the hash answers, so it is part of the value type: `Hash.new("none")` written with Integers widens to a poly value that carries the default boxed, instead of casting the string into the Integer constructor and stopping the C build. Same rule the global and constant slots apply. */ { int dn = recv_hash_new_default_arg(c, recv); if (dn >= 0) wv = ty_unify(wv, hash_default_value_ty(c, dn)); } if (wv == TY_STRBUF) wv = TY_STRING; /* (one-String mode: the typed Hash holds the handle) */ want = (wv == TY_INT || wv == TY_STRING) ? ty_hash_of(TY_STRING, wv) : TY_STR_POLY_HASH; break; } case TY_INT: { /* The same argument the String branch makes: this mark is permanent and only ever widens, so pinning the boxed variant throws away a value type the slot's own `h[k] = v` writes state right there. `[]=` joined this rule's name list in 9309c09f, which is when an Int-keyed literal first started reaching it, and every hash built only by writes lost its typed representation from then on. There is no Int->poly variant, so a value that is neither Int nor String still takes the boxed one. */ int nw = 0; TyKind wv = aset_value_type_ex(c, recv, &nw); if (nw > 0 && wv == TY_UNKNOWN && (g_infer_optimistic || g_eh_pre_infer)) continue; /* The default is a value the hash answers, so it is part of the value type: `Hash.new("none")` written with Integers widens to a poly value that carries the default boxed, instead of casting the string into the Integer constructor and stopping the C build. Same rule the global and constant slots apply. */ { int dn = recv_hash_new_default_arg(c, recv); if (dn >= 0) wv = ty_unify(wv, hash_default_value_ty(c, dn)); } if (wv == TY_STRBUF) wv = TY_STRING; want = (wv == TY_INT || wv == TY_STRING) ? ty_hash_of(TY_INT, wv) : TY_POLY_POLY_HASH; break; } /* a boxed key (one call site passes a String, another an Integer) is only representable by the poly-keyed variant; the StrPolyHash default would hand the boxed value to a const char * slot and segfault */ case TY_POLY: want = TY_POLY_POLY_HASH; break; /* Every other KNOWN key kind -- an Array, a Float, a user object -- has no keyed variant of its own either, so the poly-keyed one is the only representation that holds it. Falling through here left the literal at the StrPolyHash default and put the key straight into a const char * slot, which the C compiler reported against generated code (#4000). TY_UNKNOWN still falls through: the key is not settled yet. */ default: if (kt != TY_UNKNOWN && kt != TY_VOID) want = TY_POLY_POLY_HASH; /* An empty container LITERAL as the key infers no kind at all, but it is still a pointer at emit time (a bare `[]` lowers to an array), so it needs the same widening -- the unresolved kind is what let `{}.fetch []` through to the C compiler. */ else if (kt == TY_UNKNOWN && (nt_kind(nt, av[0]) == NK_ArrayNode || nt_kind(nt, av[0]) == NK_HashNode)) want = TY_POLY_POLY_HASH; break; } if (!ty_is_hash(want)) continue; if (direct) { for (int l = 0; l < nlits; l++) { int lit = lits[l]; if (lit >= c->node_cap) continue; if (c->hash_want[lit] != want) { comp_set_hash_want(c, lit, want); changed = 1; } /* A KEY operation is stronger evidence than the bare-receiver default: the literal was marked as one when it reached a block-taking method (`{}.tap { |h| h[k] }`), and that mark answers STR_POLY ahead of the want below, so an Array key still went into a const char * slot (#4026). */ if (c->empty_hash_recv && c->empty_hash_recv[lit] && want != TY_STR_POLY_HASH) { comp_set_empty_hash_recv(c, lit, 0); changed = 1; } } continue; } /* `@h = {}` in initialize, indexed with a typed key elsewhere in the same class: carry the context to the ivar's own empty literal, or the slot stays typeless and the index-write has no hash to emit against */ if (rk == NK_InstanceVariableReadNode) { const char *ivn = nt_str(nt, recv, "name"); Scope *rs = comp_scope_of(c, recv); int rcid = rs ? rs->class_id : -1; if (!ivn || rcid < 0) continue; /* Walk only the ivar-write nodes named `ivn` (index bucket) instead of rescanning the whole table per read site -- an O(sites * nodes) quadratic on ivar-heavy model graphs otherwise. */ for (int r = ivw_shared_first(c, ivn); r >= 0; r = ivw_shared_next(r)) { int w = ivw_shared_node(r); if (nt_kind(nt, w) != NK_InstanceVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); Scope *ws = comp_scope_of(c, w); if (!wn || !sp_streq(wn, ivn) || !ws || ws->class_id != rcid) continue; int wv = empty_hash_write_lit(c, nt_ref(nt, w, "value")); if (wv < 0 || wv >= c->node_cap) continue; /* An earlier context won -- unless THIS one is the poly-keyed variant. The key sites can disagree (`@h[k] = v` where k is a block param of a poly iteration reads as poly on one round and as the default literal's own symbol kind on another), and the poly-keyed hash is the only representation that holds both. Taking whichever landed first left `@units = {}` symbol-keyed while the writes carried String keys, and every one of them was dropped (#4041). */ if (ty_is_hash(c->hash_want[wv]) && (want != TY_POLY_POLY_HASH || c->hash_want[wv] == TY_POLY_POLY_HASH)) continue; if (c->hash_want[wv] == want) continue; comp_set_hash_want(c, wv, want); changed = 1; } continue; } /* `h = {}; h.fetch(k)`: carry the key context back to the write's literal, so the local takes that variant instead of the StrPolyHash default */ const char *ln = nt_str(nt, recv, "name"); Scope *sc = comp_scope_of(c, recv); if (!ln || !sc) continue; /* `def m(k, memo = {})`: the default literal is the only thing that says what the parameter holds, and it has no write node to carry the key context back to -- so a key op on the parameter left the default at the StrPolyHash fallback, where an Integer key never matched (#3877). */ for (int pi = 0; pi < sc->nparams; pi++) { if (!sc->pnames[pi] || !sp_streq(sc->pnames[pi], ln)) continue; int dn = sc->pdefault[pi]; if (dn < 0 || dn >= c->node_cap) break; if (nt_kind(nt, dn) != NK_HashNode && nt_kind(nt, dn) != NK_KeywordHashNode) break; int den = 0; nt_arr(nt, dn, "elements", &den); if (den != 0 || ty_is_hash(c->hash_want[dn])) break; comp_set_hash_want(c, dn, want); changed = 1; break; } /* Walk only the writes of local `ln` in this scope (index bucket) rather than rescanning the whole table per read site (see the ivar case). */ int si = (int)(sc - c->scopes); int all_empty = 0; for (int pass = 0; pass < 2; pass++) { for (int r = lw_shared_first(c, ln, si); r >= 0; r = lw_shared_next(r)) { int w = lw_shared_node(r); if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ln) || comp_scope_of(c, w) != sc) continue; int wv = empty_hash_write_lit(c, nt_ref(nt, w, "value")); if (pass == 0) { if (wv < 0) { all_empty = 0; break; } all_empty = 1; } else if (wv < c->node_cap) { TyKind nw = hash_want_join(c->hash_want[wv], want); if (nw != c->hash_want[wv]) { comp_set_hash_want(c, wv, nw); changed = 1; } } } if (!all_empty) break; } /* Another name for the hash (`y = x`, `y = [x, 1][0]`): the key operation says the same about the empty literal it was copied from, which hash_literal_sources follows back. Without it the literal kept the String-keyed default and a Symbol key stored through the alias raised, or went into a `const char *` slot. */ /* A parameter stands for every call's argument, which the binding answers for: following it here walked the calls once per key site. */ int is_param = 0; for (int pi = 0; pi < sc->nparams && !is_param; pi++) is_param = sc->pnames[pi] && sp_streq(sc->pnames[pi], ln); if (!all_empty && !is_param) { int srcs[32], ns = hash_literal_sources(c, recv, srcs, 32); for (int q = 0; q < ns; q++) { int en = 0; nt_arr(nt, srcs[q], "elements", &en); if (en != 0 || srcs[q] >= c->node_cap) continue; TyKind nw = hash_want_join(c->hash_want[srcs[q]], want); if (nw != c->hash_want[srcs[q]]) { comp_set_hash_want(c, srcs[q], nw); changed = 1; } } } } free((void *)tp_name); free(tp_recv); return changed; } /* A container that holds one hash is written with keys of more than one class across its sites: `@c = {1 => 2}` in initialize and `@c["x"] = "y"` in another method, or `@c = {}` with `put(1, 2)` storing through a parameter and `@c[:k] = 2` elsewhere. Each literal took its variant from its own keys or from the first key site the key context reached, so the other class's store went into a hash that cannot hold it: dropped, stored under a mistyped key, or refused at run time. mark_mixed_key_hash_locals answers this for locals whose keys are literals, before the fixpoint; this pass reads the keys' inferred types, so it runs inside it, and covers ivars, class variables and globals too. More than one key class, counting the literal's own keys, takes the poly-keyed variant, the only one that holds them all. So does a literal typed by its values (`{"a" => "b"}` is String-valued) that another site stores a value of another class into. The mark only ever widens. */ typedef struct { int kind; int cls; const Scope *sc; const char *nm; unsigned kbits, vbits; } HashKeySlot; static unsigned hash_key_class_bit(TyKind kt) { if (kt == TY_UNKNOWN || kt == TY_VOID) return 0; if (kt == TY_STRING || kt == TY_STRBUF) return 1u; if (kt == TY_SYMBOL) return 2u; if (kt == TY_INT) return 4u; return 8u; } /* A boxed value is exempt: the typed setter converts it at run time (#651). */ static unsigned hash_value_class_bit(TyKind vt) { if (vt == TY_UNKNOWN || vt == TY_VOID || vt == TY_POLY) return 0; if (vt == TY_STRING || vt == TY_STRBUF) return 1u; if (vt == TY_INT) return 4u; return 8u; } /* The container a receiver or write node names: 0 ivar, 1 class variable (both by class), 2 global, 3 local (by scope). -1 for anything else. */ static int hash_key_slot_of(Compiler *c, int id, HashKeySlot *out) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, id); int kind = -1; switch (k) { case NK_InstanceVariableReadNode: case NK_InstanceVariableWriteNode: case NK_InstanceVariableOrWriteNode: kind = 0; break; case NK_ClassVariableReadNode: case NK_ClassVariableWriteNode: case NK_ClassVariableOrWriteNode: kind = 1; break; case NK_GlobalVariableReadNode: case NK_GlobalVariableWriteNode: case NK_GlobalVariableOrWriteNode: kind = 2; break; case NK_LocalVariableReadNode: case NK_LocalVariableWriteNode: case NK_LocalVariableOrWriteNode: kind = 3; break; default: return -1; } const char *nm = nt_str(nt, id, "name"); Scope *s = comp_scope_of(c, id); if (!nm || !s) return -1; out->kind = kind; out->nm = nm; out->kbits = out->vbits = 0; out->cls = -1; out->sc = NULL; if (kind == 3) out->sc = s; else if (kind == 0) { out->cls = s->class_id >= 0 ? s->class_id : c->toplevel_class; if (out->cls < 0) return -1; if (class_ivar_pinned(&c->classes[out->cls], nm)) return -1; /* --rbs seed */ } else if (kind == 1) { /* A class body's `@@c = {...}` sits in scope 0, outside its class: its owner is the class whose body states it, as infer_cvar_types reads it. */ out->cls = s->class_id; for (int ci = 0; out->cls < 0 && ci < c->nclasses; ci++) { int body = nt_ref(nt, c->classes[ci].def_node, "body"); int n = 0; const int *stmts = body >= 0 ? nt_arr(nt, body, "body", &n) : NULL; for (int q = 0; q < n; q++) if (stmts[q] == id) { out->cls = ci; break; } } if (out->cls < 0) return -1; } return kind; } static int hash_key_class_under(Compiler *c, int sub, int sup) { for (int d = 0; sub >= 0 && d < 64; d++, sub = c->classes[sub].parent) if (sub == sup) return 1; return 0; } /* The same container; an ivar or class variable is also the one a subclass's methods write (`@c = {}` in the parent's initialize, `@c[k] = v` in the child). */ static int hash_key_slot_same(Compiler *c, const HashKeySlot *a, const HashKeySlot *b, int related) { if (a->kind != b->kind || a->sc != b->sc || !sp_streq(a->nm, b->nm)) return 0; if (a->cls == b->cls) return 1; return related && (hash_key_class_under(c, a->cls, b->cls) || hash_key_class_under(c, b->cls, a->cls)); } /* Can the method this call sits in run? Reachability is settled before a `send(:m)` is rewritten to the call it is, so a method it leaves out still runs when its name is written anywhere: called, or a Symbol or a String. One nothing names is not emitted. `written` holds those names, gathered by the first call that asks: one walk a pass, however many calls ask. */ static int hash_key_call_can_run(Compiler *c, int id, ANameHash *written) { const NodeTable *nt = c->nt; const Scope *s = comp_scope_of(c, id); if (s->reachable || !s->name) return 1; if (!written->nb) { NT_FOREACH_KIND(nt, NK_CallNode, q) { const char *nm = nt_str(nt, q, "name"); if (nm && !anh_has(written, nm)) anh_add(written, nm); } NT_FOREACH_KIND(nt, NK_SymbolNode, q) { const char *nm = nt_str(nt, q, "value"); if (nm && !anh_has(written, nm)) anh_add(written, nm); } NT_FOREACH_KIND(nt, NK_StringNode, q) { const char *nm = nt_str(nt, q, "content"); if (!nm) nm = nt_str(nt, q, "unescaped"); if (nm && !anh_has(written, nm)) anh_add(written, nm); } } return anh_has(written, s->name); } static int widen_mixed_key_hash_slots(Compiler *c) { if (!c->hash_want) return 0; const NodeTable *nt = c->nt; HashKeySlot *slots = NULL; int ns = 0, cap = 0; ANameHash written; memset(&written, 0, sizeof written); static const NodeKind wkinds[] = { NK_CallNode, NK_IndexOrWriteNode, NK_IndexAndWriteNode, NK_IndexOperatorWriteNode }; for (size_t wk = 0; wk < sizeof(wkinds) / sizeof(wkinds[0]); wk++) { NT_FOREACH_KIND(nt, wkinds[wk], id) { int is_call = wkinds[wk] == NK_CallNode; int is_upd = 0, is_dflt = 0; if (is_call) { const char *nm = nt_str(nt, id, "name"); if (!nm) continue; is_upd = is_hash_merge_bang(nm); is_dflt = is_hash_default_setter(nm); if (!is_upd && !is_dflt && !is_store_alias(nm)) continue; } int recv = nt_ref(nt, id, "receiver"); int anode = nt_ref(nt, id, "arguments"); int an = 0; const int *av = anode >= 0 ? nt_arr(nt, anode, "arguments", &an) : NULL; if (recv < 0 || !av || (!is_upd && an != (is_call && !is_dflt ? 2 : 1))) continue; HashKeySlot hs; if (hash_key_slot_of(c, recv, &hs) < 0) continue; unsigned kb = 0, vb = 0; if (is_dflt) { /* `h.default = v`: a value the hash answers for a missing key, so the value evidence a store is, under the key class it has. A nil literal is none: a typed hash keeps it in its values' slot, as the nil a missing key already answers. */ if (nt_kind(nt, av[0]) == NK_NilNode) continue; kb = hash_key_class_bit(ty_hash_key(infer_type(c, recv))); vb = hash_value_class_bit(infer_type(c, av[0])); } else if (is_upd) { /* `h.update("e" => 1.5)` stores each pair of the merged literals, as `h["e"] = 1.5` would */ for (int q = 0; q < an; q++) { NodeKind ak = nt_kind(nt, av[q]); if (ak != NK_HashNode && ak != NK_KeywordHashNode) { /* A Hash that is not written out here (`h.update(opts)`) stores its pairs too, and a Symbol-keyed one says so by its variant. The other key classes stay with the usage fold, which widens a local for them and leaves it as it is for a Symbol key (fold_container_evidence). A call in a method that cannot run widens nothing. */ if (ty_hash_key(infer_type(c, av[q])) == TY_SYMBOL && infer_type(c, recv) != TY_POLY_POLY_HASH && hash_key_call_can_run(c, id, &written)) kb |= hash_key_class_bit(TY_SYMBOL); continue; } int en = 0; const int *els = nt_arr(nt, av[q], "elements", &en); for (int e = 0; e < en; e++) { if (nt_kind(nt, els[e]) != NK_AssocNode) continue; kb |= hash_key_class_bit(infer_type(c, nt_ref(nt, els[e], "key"))); vb |= hash_value_class_bit(infer_type(c, nt_ref(nt, els[e], "value"))); } } /* and the conflict block's value under a colliding key */ vb |= hash_value_class_bit(hash_merge_block_value_ty(c, id)); } else { kb = hash_key_class_bit(infer_type(c, av[0])); /* `h[k] op= v` stores the operator's result, which the key's own reads decide; only a plain store and `||=` / `&&=` name the value. */ int vnode = is_call ? av[1] : wkinds[wk] == NK_IndexOperatorWriteNode ? -1 : nt_ref(nt, id, "value"); vb = vnode >= 0 ? hash_value_class_bit(infer_type(c, vnode)) : 0; /* A boxed parameter of the storing method is boxed because its callers pass values of more than one class: the typed setter cannot convert them all. */ if (!vb && vnode >= 0 && infer_type(c, vnode) == TY_POLY && unassigned_param_read(c, comp_scope_of(c, vnode), vnode) >= 0) vb = 8u; } if (!kb) continue; int f = -1; for (int q = 0; q < ns; q++) if (hash_key_slot_same(c, &slots[q], &hs, 0)) { f = q; break; } if (f < 0) { slots = an_grow(slots, ns, &cap, sizeof(*slots)); f = ns++; slots[f] = hs; } slots[f].kbits |= kb; slots[f].vbits |= vb; } } int changed = 0; static const NodeKind vkinds[] = { NK_InstanceVariableWriteNode, NK_InstanceVariableOrWriteNode, NK_ClassVariableWriteNode, NK_ClassVariableOrWriteNode, NK_GlobalVariableWriteNode, NK_GlobalVariableOrWriteNode, NK_LocalVariableWriteNode, NK_LocalVariableOrWriteNode }; /* A slot's keys include those of every literal assigned to it: with `@c = {1 => 2}`, `@c = {}` and `@c["x"] = 3`, the empty literal has to widen too, or the two literals' variants unify to a plain boxed value rather than the poly-keyed hash. */ for (size_t vk = 0; ns > 0 && vk < sizeof(vkinds) / sizeof(vkinds[0]); vk++) { NT_FOREACH_KIND(nt, vkinds[vk], id) { int val = nt_ref(nt, id, "value"); if (val < 0 || nt_kind(nt, val) != NK_HashNode) continue; int en = 0; const int *els = nt_arr(nt, val, "elements", &en); unsigned lb = 0; for (int e = 0; e < en; e++) { if (nt_kind(nt, els[e]) != NK_AssocNode) { lb = 0; break; } unsigned b = hash_key_class_bit(infer_type(c, nt_ref(nt, els[e], "key"))); if (!b) { lb = 0; break; } lb |= b; } if (!lb) continue; HashKeySlot hs; if (hash_key_slot_of(c, id, &hs) < 0) continue; for (int q = 0; q < ns; q++) if (hash_key_slot_same(c, &slots[q], &hs, 1)) slots[q].kbits |= lb; } } for (size_t vk = 0; ns > 0 && vk < sizeof(vkinds) / sizeof(vkinds[0]); vk++) { NT_FOREACH_KIND(nt, vkinds[vk], id) { int val = nt_ref(nt, id, "value"); { int fb = an_or_empty_hash_fallback(c, val); if (fb >= 0) val = fb; } if (val < 0 || val >= c->node_cap || c->hash_want[val] == TY_POLY_POLY_HASH) continue; NodeKind vkd = nt_kind(nt, val); if (vkd == NK_CallNode) { const char *cn = nt_str(nt, val, "name"); int hr = nt_ref(nt, val, "receiver"); if (!cn || !sp_streq(cn, "new") || hr < 0 || nt_kind(nt, hr) != NK_ConstantReadNode || !nt_str(nt, hr, "name") || !sp_streq(nt_str(nt, hr, "name"), "Hash")) continue; } else if (vkd != NK_HashNode) continue; HashKeySlot hs; if (hash_key_slot_of(c, id, &hs) < 0) continue; unsigned kmask = 0, vmask = 0; for (int q = 0; q < ns; q++) if (hash_key_slot_same(c, &slots[q], &hs, 1)) { kmask |= slots[q].kbits; vmask |= slots[q].vbits; } if (!kmask) continue; int en = 0; const int *els = vkd == NK_HashNode ? nt_arr(nt, val, "elements", &en) : NULL; for (int e = 0; e < en && kmask; e++) { if (nt_kind(nt, els[e]) != NK_AssocNode) { kmask = 0; break; } unsigned b = hash_key_class_bit(infer_type(c, nt_ref(nt, els[e], "key"))); if (!b) { kmask = 0; break; } kmask |= b; } if (!kmask) continue; int widen = (kmask & (kmask - 1)) != 0; /* A non-empty literal's value class is its own; an empty one's variant already reads every store's value (aset_value_type_ex), and a local converts to the String-keyed poly variant its stores unify to. */ if (!widen && en > 0 && hs.kind != 3) { TyKind lt = infer_type(c, val); if (lt == TY_INT_INT_HASH || lt == TY_STR_INT_HASH) widen = (vmask & ~4u) != 0; else if (lt == TY_INT_STR_HASH || lt == TY_STR_STR_HASH) widen = (vmask & ~1u) != 0; } if (!widen) continue; comp_set_hash_want(c, val, TY_POLY_POLY_HASH); changed = 1; /* The slot may already hold the literal's old variant, and two hash variants unify to a plain boxed value: move a typed-hash slot along with its literal. A local is re-typed every round. */ TyRef slot = NULL; if (hs.kind == 0) { ClassInfo *ci = &c->classes[hs.cls]; int iv = comp_ivar_index(ci, hs.nm); if (iv >= 0) slot = &ci->ivar_types[iv]; } else if (hs.kind == 1) { ClassInfo *ci = &c->classes[comp_cvar_owner(c, hs.cls, hs.nm)]; int cv = comp_cvar_index(ci, hs.nm); if (cv >= 0) slot = &ci->cvar_types[cv]; } else if (hs.kind == 2) { const char *rn = comp_resolve_gvar(c, hs.nm + 1); LocalVar *gv = rn ? comp_gvar(c, rn) : NULL; if (gv) slot = &gv->type; } if (slot && ty_is_hash(*slot)) ty_ref_set(c, slot, TY_POLY_POLY_HASH); } } free(slots); anh_free(&written); return changed; } /* `TBL = {}` followed by `TBL[k] = v` elsewhere: an empty literal bound to a constant has no type of its own, so the constant got no runtime slot at all and every read raised "uninitialized constant". Derive the variant from the constant's index-writes (#2879). */ /* The key a read or write on `recv` names, when `recv` is the local `lname` of scope `rs` (lname NULL: the constant `cn`): its type unified into *kt (and a write's value into *vt). */ static void hash_key_evidence(Compiler *c, int w, const char *cn, const char *lname, Scope *rs, TyKind *kt, TyKind *vt) { const NodeTable *nt = c->nt; const char *wn = nt_str(nt, w, "name"); if (!wn) return; int write = is_store_alias(wn); if (!write && !sp_streq(wn, "[]") && !sp_streq(wn, "fetch") && !sp_streq(wn, "dig") && !sp_streq(wn, "key?") && !sp_streq(wn, "has_key?") && !sp_streq(wn, "include?") && !sp_streq(wn, "member?")) return; int wr = nt_ref(nt, w, "receiver"); if (wr < 0) return; if (lname) { if (nt_kind(nt, wr) != NK_LocalVariableReadNode) return; const char *rn = nt_str(nt, wr, "name"); if (!rn || !sp_streq(rn, lname) || comp_scope_of(c, wr) != rs) return; } else { if (nt_kind(nt, wr) != NK_ConstantReadNode) return; const char *rn = nt_str(nt, wr, "name"); if (!rn || !sp_streq(rn, cn)) return; } int wa = nt_ref(nt, w, "arguments"); int wan = 0; const int *wav = wa >= 0 ? nt_arr(nt, wa, "arguments", &wan) : NULL; if (wan < 1) return; *kt = ty_unify(*kt, infer_type(c, wav[0])); if (write && wan >= 2) *vt = ty_unify(*vt, infer_type(c, wav[1])); } static void mark_empty_hash_const_writes(Compiler *c) { if (!c->hash_want) return; const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_ConstantWriteNode) continue; const char *cn = nt_str(nt, id, "name"); int v = nt_ref(nt, id, "value"); if (!cn || v < 0 || v >= c->node_cap) continue; /* `EMPTY = {}.freeze` binds the same literal as `EMPTY = {}` (#4510) */ if (nt_kind(nt, v) == NK_CallNode) { const char *fn = nt_str(nt, v, "name"); int fr = nt_ref(nt, v, "receiver"); int fa = nt_ref(nt, v, "arguments"); int fan = 0; if (fa >= 0) nt_arr(nt, fa, "arguments", &fan); if (fn && fr >= 0 && fan == 0 && nt_ref(nt, v, "block") < 0 && is_self_copy(fn)) v = fr; if (v >= c->node_cap) continue; } if (nt_kind(nt, v) != NK_HashNode && nt_kind(nt, v) != NK_KeywordHashNode) continue; int en = 0; nt_arr(nt, v, "elements", &en); if (en != 0 || ty_is_hash(c->hash_want[v])) continue; TyKind kt = TY_UNKNOWN, vt = TY_UNKNOWN; for (int w = 0; w < nt->count; w++) { if (nt_kind(nt, w) != NK_CallNode) continue; hash_key_evidence(c, w, cn, NULL, NULL, &kt, &vt); } /* A parameter whose default is the constant reads it under its own name: `def initialize(attrs = EMPTY)` followed by `attrs[:x]` says the constant's keys are Symbols, and the frozen empty hash shared by every constructor of an application has no other evidence at all (#4510). */ NT_FOREACH_KIND(nt, NK_OptionalParameterNode, op) { int dv = nt_ref(nt, op, "value"); const char *pnm = nt_str(nt, op, "name"); /* the constant by its bare name or by a path (`Base::EMPTY`, whose node carries the leaf name, qualified by qc_rewrite_reads when it has to be), as the write above spells it (#4511) */ if (dv < 0 || !pnm || (nt_kind(nt, dv) != NK_ConstantReadNode && nt_kind(nt, dv) != NK_ConstantPathNode)) continue; const char *dn = nt_str(nt, dv, "name"); if (!dn || !sp_streq(dn, cn)) continue; Scope *rs = comp_scope_of(c, op); for (int w = 0; w < nt->count; w++) { if (nt_kind(nt, w) != NK_CallNode) continue; hash_key_evidence(c, w, cn, pnm, rs, &kt, &vt); } } /* No resolved index-write: the constant still needs a slot, so give it the widest variant rather than leaving it typeless (and unreachable). */ TyKind want = (kt == TY_SYMBOL) ? TY_SYM_POLY_HASH : (kt == TY_UNKNOWN) ? TY_POLY_POLY_HASH : ty_hash_of(kt, vt); if (!ty_is_hash(want)) want = (kt == TY_STRING) ? TY_STR_POLY_HASH : TY_POLY_POLY_HASH; comp_set_hash_want(c, v, want); } } /* An Enumerable method called on a LOCAL that only ever holds a bare `{}`: the literal has no key context to pick a variant from, so the local stayed typeless and the call found no hash arm at all (#3829). A direct `{}.each {}` receiver already takes the bare-hash default; a local holding one is the same hash. Skipped when a key operation on the local exists, since that is what picks a variant and this mark would pre-empt it. */ static void mark_empty_hash_enum_locals(Compiler *c) { if (!c->empty_hash_recv) return; const NodeTable *nt = c->nt; static const char *const enum_names[] = { "each_entry", "each_cons", "each_slice", "cycle", "grep", "grep_v", "take_while", "drop_while", "chunk_while", "slice_when", "slice_before", "slice_after", "minmax", "minmax_by", "zip", "find_index", "flat_map", "collect_concat", "sort", "uniq", NULL }; static const char *const key_names[] = { "[]", "[]=", "fetch", "store", "dig", "key?", "has_key?", "include?", "member?", "merge", "merge!", "update", "transform_keys", "transform_values", "default=", NULL }; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; int hit = 0; for (int k = 0; enum_names[k] && !hit; k++) if (sp_streq(nm, enum_names[k])) hit = 1; if (!hit) continue; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; if (nt_kind(nt, recv) != NK_LocalVariableReadNode) continue; const char *lname = nt_str(nt, recv, "name"); Scope *rs = comp_scope_of(c, recv); if (!lname || !rs) continue; /* every write of the local is an empty hash literal, and no key op on it */ int lit = -1, ok = 1; for (int w = 0; w < nt->count && ok; w++) { NodeKind wk = nt_kind(nt, w); if (wk == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, lname) || comp_scope_of(c, w) != rs) continue; int v = nt_ref(nt, w, "value"); if (v < 0) { ok = 0; break; } NodeKind vk = nt_kind(nt, v); int en = 0; if (vk == NK_HashNode || vk == NK_KeywordHashNode) nt_arr(nt, v, "elements", &en); else { ok = 0; break; } if (en != 0) { ok = 0; break; } lit = v; } else if (wk == NK_CallNode || wk == NK_IndexOrWriteNode || wk == NK_IndexAndWriteNode || wk == NK_IndexOperatorWriteNode) { int r2 = nt_ref(nt, w, "receiver"); if (r2 < 0 || nt_kind(nt, r2) != NK_LocalVariableReadNode) continue; const char *rn = nt_str(nt, r2, "name"); if (!rn || !sp_streq(rn, lname) || comp_scope_of(c, r2) != rs) continue; const char *cn = wk == NK_CallNode ? nt_str(nt, w, "name") : "[]"; if (!cn) continue; for (int k = 0; key_names[k]; k++) if (sp_streq(cn, key_names[k])) { ok = 0; break; } } } if (ok && lit >= 0 && lit < c->node_cap) comp_set_empty_hash_recv(c, lit, 1); } } static int is_empty_hash_literal(const NodeTable *nt, int id, int cap) { if (id < 0 || id >= cap) return 0; NodeKind k = nt_kind(nt, id); if (k != NK_HashNode && k != NK_KeywordHashNode) return 0; int en = 0; nt_arr(nt, id, "elements", &en); return en == 0; } /* A bare `{}` receiver or interpolation takes the bare-hash default (see mark_empty_array_operands); a key operation refines it in mark_empty_hash_key_ctx. */ static void mark_empty_hash_receivers(Compiler *c) { if (!c->empty_hash_recv) return; const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) { int recv = nt_ref(nt, id, "receiver"); if (!is_empty_hash_literal(nt, recv, c->node_cap)) continue; /* `{}.freeze` (dup, clone, itself) is the literal itself, wherever the call stands: its variant is the call's context's, not a receiver's default. Marked here, `EMPTY = {}.freeze` came out String-keyed while `EMPTY = {}` did not, and a constructor defaulting to it dropped out of every Class#new dispatch whose argument was Symbol-keyed (#4510). */ const char *cn = nt_str(nt, id, "name"); int ac = 0; int args = nt_ref(nt, id, "arguments"); if (args >= 0) nt_arr(nt, args, "arguments", &ac); if (cn && ac == 0 && nt_ref(nt, id, "block") < 0 && is_self_copy(cn)) continue; comp_set_empty_hash_recv(c, recv, 1); } NT_FOREACH_KIND(nt, NK_EmbeddedStatementsNode, es) { int st = nt_ref(nt, es, "statements"); int bn = 0; const int *bb = st >= 0 ? nt_arr(nt, st, "body", &bn) : NULL; if (bn == 1 && is_empty_hash_literal(nt, bb[0], c->node_cap)) comp_set_empty_hash_recv(c, bb[0], 1); } } /* --- byref string out-params (see LocalVar.byref_out) --------------------- CRuby strings are shared heap objects: a callee's `s << "x"` mutates the very object the caller passed. Spinel strings are immutable const char* values and `<<` is a local reassignment, so the mutation silently stops at the callee -- the classic pass-an-output-buffer pattern loses every append. Mark the mutated string params of statically-bound methods byref: codegen passes the caller's slot (const char **), the callee reads and reassigns through it (the existing is_cell deref forms), and the mutation lands in the caller's variable like CRuby. Eligibility is deliberately narrow -- byref changes the C ABI, so every call site must know the callee statically and no plain rebind may leak: - toplevel / class (singleton) methods only: an instance method can be reached through poly dispatch stubs and same-name subtree dispatch, which keep the plain ABI; - the name is unique program-wide, never aliased, and never appears as a symbol/string literal (send/method(:x)/define_method material); - not an `initialize`: its callers are the sp_X_new wrappers, which the class values, `raise`, `allocate` and the poly `new` dispatch all call with values; - the param is mutated via a receiver-reassigning string mutator (one sp_str_mutator names, or a bang form) or passed on into another method's byref slot (fixpoint), and is never PLAIN-reassigned: CRuby `s = ...` rebinds the local invisibly to the caller, which a write-through cell would wrongly propagate. */ int an_str_mutator_name(const char *nm) { size_t l = nm ? strlen(nm) : 0; if (!l) return 0; /* the one table of String mutators (sp_str_mutator), and any bang name */ return sp_str_mutator(nm, SP_MUT_LOCAL) || (l > 1 && nm[l - 1] == '!'); } /* ANY method scope with this name, or -1. Byref is decided per NAME GROUP -- every method of a name gets the ABI or none does -- so a caller asking "is parameter j byref here" may ask any member and get the group's answer. Asking for the UNIQUE one instead made a second same-named method anywhere in the program silently answer -1, which reads as "not byref" and is how an unused class dropped a callee's appends (#4390). */ static int an_any_scope_by_name(Compiler *c, const char *nm) { if (!nm) return -1; /* frozen (scope names fixed): the first scope of each name from a table built once per scope-index epoch; asked per call site, the scan was (sites x scopes) (rubys in #5035) */ if (comp_scope_index_is_frozen()) { static ANameHash names; static int *first, nfirst, stamp_n = -1; static unsigned stamp_gen; if (stamp_n != c->nscopes || stamp_gen != comp_scope_index_gen()) { anh_free(&names); memset(&names, 0, sizeof names); free(first); first = NULL; nfirst = 0; int cap = 0; for (int i = 1; i < c->nscopes; i++) { const char *sn = c->scopes[i].name; if (!sn || anh_has(&names, sn)) continue; if (nfirst == cap) { cap = cap ? cap * 2 : 256; first = realloc(first, sizeof(int) * (size_t)cap); } first[nfirst++] = i; anh_add(&names, sn); } stamp_n = c->nscopes; stamp_gen = comp_scope_index_gen(); } int k = anh_find(&names, nm); return k >= 0 ? first[k] : -1; } for (int i = 1; i < c->nscopes; i++) if (c->scopes[i].name && sp_streq(c->scopes[i].name, nm)) return i; return -1; } /* The scopes of a name, ascending: the first, and the next after s (-1 at the end). Frozen, from a table built once per scope-index epoch, as an_any_scope_by_name's; else the scan. The strbuf walks asked every scope for the methods of a call's name at each call they walked, every round: 17k compares per call on a Rails program, most of its fixpoint. */ static ANameHash an_sbn_names; static int *an_sbn_first, an_sbn_nfirst, *an_sbn_next, an_sbn_stamp_n = -1; static unsigned an_sbn_stamp_gen; static void an_sbn_build(Compiler *c) { anh_free(&an_sbn_names); memset(&an_sbn_names, 0, sizeof an_sbn_names); free(an_sbn_first); an_sbn_first = NULL; an_sbn_nfirst = 0; free(an_sbn_next); an_sbn_next = malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); if (!an_sbn_next) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int *last = NULL, cap = 0; for (int i = 1; i < c->nscopes; i++) { const char *sn = c->scopes[i].name; an_sbn_next[i] = -1; if (!sn) continue; int k = anh_find(&an_sbn_names, sn); if (k >= 0) { an_sbn_next[last[k]] = i; last[k] = i; continue; } if (an_sbn_nfirst == cap) { cap = cap ? cap * 2 : 256; an_sbn_first = realloc(an_sbn_first, sizeof(int) * (size_t)cap); last = realloc(last, sizeof(int) * (size_t)cap); if (!an_sbn_first || !last) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } an_sbn_first[an_sbn_nfirst] = i; last[an_sbn_nfirst] = i; an_sbn_nfirst++; anh_add(&an_sbn_names, sn); } free(last); an_sbn_stamp_n = c->nscopes; an_sbn_stamp_gen = comp_scope_index_gen(); } /* (each scope listed is one the asker reads: infer_type's memo notes it) */ static int an_scopes_named_dep(int s) { if (s >= 0) IMEMO_DEP_SCOPE(s); return s; } int an_scopes_named_first(Compiler *c, const char *nm) { if (!nm) return -1; if (comp_scope_index_is_frozen()) { if (an_sbn_stamp_n != c->nscopes || an_sbn_stamp_gen != comp_scope_index_gen()) an_sbn_build(c); int k = anh_find(&an_sbn_names, nm); return an_scopes_named_dep(k >= 0 ? an_sbn_first[k] : -1); } for (int i = 1; i < c->nscopes; i++) if (c->scopes[i].name && sp_streq(c->scopes[i].name, nm)) return an_scopes_named_dep(i); return -1; } int an_scopes_named_next(Compiler *c, const char *nm, int s) { if (!nm || s < 1) return -1; if (comp_scope_index_is_frozen()) { if (an_sbn_stamp_n != c->nscopes || an_sbn_stamp_gen != comp_scope_index_gen()) an_sbn_build(c); return an_scopes_named_dep(s < c->nscopes ? an_sbn_next[s] : -1); } for (int i = s + 1; i < c->nscopes; i++) if (c->scopes[i].name && sp_streq(c->scopes[i].name, nm)) return an_scopes_named_dep(i); return -1; } /* The scopes sharing scope s's def_node (its copies: a transplanted module method's body, once per includer), ascending: the first, and the next after s (-1 at the end). Frozen, from a table built once per scope-index epoch, as the by-name lists are; else the scan. The share facts asked every scope for a copied self call's twins, at every such call, every build. */ static int *an_sdn_next, *an_sdn_first_of, an_sdn_stamp_n = -1; static unsigned an_sdn_stamp_gen; static void an_sdn_build(Compiler *c) { int n = c->nscopes > 0 ? c->nscopes : 1; free(an_sdn_next); free(an_sdn_first_of); an_sdn_next = malloc(sizeof(int) * (size_t)n); an_sdn_first_of = malloc(sizeof(int) * (size_t)n); unsigned cap = 16; while (cap < 2u * (unsigned)n) cap *= 2; int *tab = malloc(sizeof(int) * (size_t)cap); /* the last scope seen of each def_node */ if (!an_sdn_next || !an_sdn_first_of || !tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (unsigned i = 0; i < cap; i++) tab[i] = -1; for (int i = 0; i < c->nscopes; i++) { an_sdn_next[i] = -1; an_sdn_first_of[i] = i; int d = c->scopes[i].def_node; if (d < 0) continue; unsigned h = ((unsigned)d * 2654435761u) & (cap - 1); while (tab[h] >= 0 && c->scopes[tab[h]].def_node != d) h = (h + 1) & (cap - 1); if (tab[h] >= 0) { an_sdn_next[tab[h]] = i; an_sdn_first_of[i] = an_sdn_first_of[tab[h]]; } tab[h] = i; } free(tab); an_sdn_stamp_n = c->nscopes; an_sdn_stamp_gen = comp_scope_index_gen(); } int an_scopes_same_def_first(Compiler *c, int s) { if (s < 0 || s >= c->nscopes || c->scopes[s].def_node < 0) return -1; if (comp_scope_index_is_frozen()) { if (an_sdn_stamp_n != c->nscopes || an_sdn_stamp_gen != comp_scope_index_gen()) an_sdn_build(c); return an_sdn_first_of[s]; } for (int i = 0; i < c->nscopes; i++) if (c->scopes[i].def_node == c->scopes[s].def_node) return i; return -1; } int an_scopes_same_def_next(Compiler *c, int s) { if (s < 0 || s >= c->nscopes || c->scopes[s].def_node < 0) return -1; if (comp_scope_index_is_frozen()) { if (an_sdn_stamp_n != c->nscopes || an_sdn_stamp_gen != comp_scope_index_gen()) an_sdn_build(c); return an_sdn_next[s]; } for (int i = s + 1; i < c->nscopes; i++) if (c->scopes[i].def_node == c->scopes[s].def_node) return i; return -1; } /* Unique method scope index by name across the program, or -1 (absent or defined more than once). Scope 0 is the top level (name NULL). */ static int an_unique_scope_by_name(Compiler *c, const char *nm) { if (!nm) return -1; /* The cached form an_any_scope_by_name already has, on the same epoch and for the same reason: asked per call site, the scan is (sites x scopes). The `any` variant got a table when that showed up in rubys #5035; this one was left a scan, and it is what the call-target resolution behind the parameter and strbuf passes asks for every CallNode. A name seen twice records -1: not unique is the answer, not a miss. */ if (comp_scope_index_is_frozen()) { static ANameHash names; static int *uniq = NULL; static int nuniq, stamp_n = -1; static unsigned stamp_gen; if (stamp_n != c->nscopes || stamp_gen != comp_scope_index_gen()) { anh_free(&names); memset(&names, 0, sizeof names); free(uniq); uniq = NULL; nuniq = 0; int cap = 0; for (int i = 1; i < c->nscopes; i++) { const char *sn = c->scopes[i].name; if (!sn) continue; int k = anh_find(&names, sn); if (k >= 0) { uniq[k] = -1; continue; } if (nuniq == cap) { cap = cap ? cap * 2 : 256; uniq = realloc(uniq, sizeof(int) * (size_t)cap); } uniq[nuniq++] = i; anh_add(&names, sn); } stamp_n = c->nscopes; stamp_gen = comp_scope_index_gen(); } int k = anh_find(&names, nm); return k >= 0 ? uniq[k] : -1; } int found = -1; for (int i = 1; i < c->nscopes; i++) { Scope *s = &c->scopes[i]; if (!s->name || !sp_streq(s->name, nm)) continue; if (found >= 0) return -1; found = i; } return found; } /* Param index of `vn` in s->pnames, or -1. */ static int an_param_idx(Scope *s, const char *vn) { if (!vn) return -1; for (int i = 0; i < s->nparams; i++) if (s->pnames[i] && sp_streq(s->pnames[i], vn)) return i; return -1; } int comp_byref_param(Compiler *c, Scope *m, int idx) { if (!m || idx < 0 || idx >= m->nparams || !m->pnames[idx]) return 0; LocalVar *p = scope_local(m, m->pnames[idx]); return p && p->byref_out; } /* Is local `vn` of scope `vs` handed by a call in that scope to a method parameter the method appends to in place (the byref slot, or the handle, it is lent)? The callee's append is one to `vn`'s String, so `vn` is mutated in place there as by `<<`. Asked by the parameter's mutation, not its ABI, which is settled after the fixpoint. */ static int an_any_scope_by_name(Compiler *c, const char *nm); static int an_param_mutated_in_place(Compiler *c, int mi, int pi); static int an_call_target_mi(Compiler *c, int id); /* A call whose receiver is a builtin value -- a Hash, an Array, a String, a number -- is the builtin's method, or a reopen's, which an_call_target_mi already answers: no method of a class of the program's own of the same name can take it. The lend tests below fall back to any method by that name only for a receiver that may be such an object, as do the byref and block-keeping tests that look a call up by name (#8144: `target[k] = v` on a Hash lent v to an unrelated class's appending `[]=`, and v became a String handle its Hash#each binder could not fill). */ static int an_call_recv_builtin(Compiler *c, int u) { int r = nt_ref(c->nt, u, "receiver"); if (r < 0 || nt_kind(c->nt, r) == NK_SelfNode) return 0; TyKind rt = infer_type(c, r); return rt != TY_UNKNOWN && rt != TY_POLY && !ty_is_object(rt) && rt != TY_NIL && (ty_is_hash(rt) || ty_is_array(rt) || rt == TY_STRING || rt == TY_STRBUF || ty_is_numeric(rt) || rt == TY_SYMBOL || rt == TY_RANGE); } static int an_local_lent(Compiler *c, const char *vn, Scope *vs) { const NodeTable *nt = c->nt; if (!vn || !vs) return 0; int vsi = (int)(vs - c->scopes); for (int u = comp_scall_first(c, vsi); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || comp_scope_of(c, u) != vs) continue; int mi = an_call_target_mi(c, u); if (mi < 0 && !an_call_recv_builtin(c, u)) mi = an_any_scope_by_name(c, nt_str(nt, u, "name")); if (mi < 0) continue; for (int j = 0; j < c->scopes[mi].nparams; j++) { if (!comp_byref_param(c, &c->scopes[mi], j) && !an_param_mutated_in_place(c, mi, j)) continue; int a = arg_layout_param_node(c, &c->scopes[mi], u, j, NULL); if (a >= 0 && nt_kind(nt, a) == NK_LocalVariableReadNode && sp_streq(nt_str(nt, a, "name"), vn)) return 1; } } return 0; } /* Can a call ever arrive at an instance method of this class or module? Only through a value that is one, so a class nobody instantiates -- and that no instantiated class inherits from or includes -- has neither a direct call site nor a poly-dispatch arm. The emitter already knows this shape: it is what marks such a method `__attribute__((unused))`. Such a method cannot disagree with a name group about an ABI, because nothing can call it to find out. So it neither constrains the group nor joins it -- which is what lets an UNUSED class stop taking the ABI away from a used one (#4390). Unsure answers 1: giving the group one member too many costs an optimisation, and one too few would hand a poly dispatch two arms with different C signatures. */ int an_class_can_be_reached(Compiler *c, int ci) { if (ci < 0 || ci >= c->nclasses) return 1; if (c->classes[ci].instantiated) return 1; for (int j = 0; j < c->nclasses; j++) { if (!c->classes[j].instantiated) continue; for (int k = j; k >= 0; k = c->classes[k].parent) if (k == ci) return 1; /* a module reaches instances through every class that includes it */ for (int k = j; k >= 0; k = c->classes[k].parent) for (int m = 0; m < c->classes[k].nincluded_mods; m++) if (c->classes[k].included_mods[m] == ci) return 1; } return 0; } /* Promote parameter `pi` for EVERY method of this name, or for none of them. A call site resolves by name, so every arm a poly dispatch can reach has to agree on the ABI. The old rule stood in for that by refusing any name defined twice -- right about the danger, wrong about the remedy: it also refused two methods that agree perfectly, so adding an UNUSED class with a same-named method took the ABI away from a method that had it and the caller's buffer came back empty (#4390). Agreeing is the thing to require, so require it of every member a String can reach at that index: it must be eligible, must have it typed String, and must not have blocked it by rebinding. One such member that cannot take it keeps the value ABI for all. A member with no parameter at that index, or one typed as something no String is (an Integer, an Array, a class's object) or boxed, binds no lent slot there and has no say: a `def m = 0` or a `def m(n) = n + 1` in another class took the ABI away from every `m(s)` appending to its String, and the caller's buffer came back as it was (#6179); so did a top-level `def m(p) = yield(p)`. A dispatch that reaches both binds each arm on its own (#6135, #6183). A member whose parameter a proc captures takes it as the shared handle instead, and so does the rest of the group (`handle`, below). */ /* Will yielding method mi be lowered to call its named block as a proc (the post-fixpoint lowering's test: a value use of the block, a call of itself, a yield inside a lifted body)? One that only names its block (`def m(p, &b) = yield(p)`) is spliced like any yielder. */ static int scope_calls_itself(Compiler *c, int mi); static int scope_yields_inside_lifted_body(Compiler *c, int mi); static int yield_lowers_block(Compiler *c, int mi) { Scope *m = &c->scopes[mi]; if (!m->blk_param || !m->blk_param[0]) return 0; return m->blk_param_value_use || scope_calls_itself(c, mi) || scope_yields_inside_lifted_body(c, mi); } static int an_byref_group_member_string(Compiler *c, Scope *m, int pi) { if (pi >= m->nparams || !m->pnames[pi]) return 0; /* a yielding method spliced into each call that gives it a block binds its parameters there, aliasing the caller's String where its block appends (inline_alias_params): no lent slot either. One that names its block can be lowered to keep it, and keeps its say. */ if (m->yields && !m->is_lowered_yield && !yield_lowers_block(c, (int)(m - c->scopes))) return 0; LocalVar *q = scope_local(m, m->pnames[pi]); if (!q || !q->is_param || q->is_block_param) return 1; TyKind t = q->type; /* a boxed one takes the box whatever the group lends: a String it appends to in place reaches it as the handle (convert_byref_handle_params) */ if (t == TY_UNKNOWN || t == TY_NIL || t == TY_INT || t == TY_BIGINT || t == TY_FLOAT || t == TY_BOOL || t == TY_SYMBOL || t == TY_POLY || ty_is_array(t) || ty_is_hash(t) || ty_is_object(t)) return 0; return 1; } /* Can the name group of `nm` take parameter pi by reference: the lent slot, or, where *captured comes back set, the shared handle? 0 when one member keeps the whole group on the value ABI. blocked (the parameters each scope rebinds) may be NULL: none. */ static int an_byref_group_takes_ref(Compiler *c, const char *nm, int pi, const char *elig, const unsigned *blocked, int n, int *captured) { if (!nm || pi < 0 || pi >= 32) return 0; for (int k = 1; k < n; k++) { Scope *m = &c->scopes[k]; if (!m->name || !sp_streq(m->name, nm)) continue; if (m->class_id >= 0 && !m->is_cmethod && !an_class_can_be_reached(c, m->class_id)) continue; if (!an_byref_group_member_string(c, m, pi)) continue; /* A module's method is called through the copies its includers get (their own scopes, judged on their own), so the source is not refused for being one: it takes the group's ABI and keeps the group's answer the same whichever member an_any_scope_by_name finds first. Refused, `def g(a) = super` into an included module's `g` kept the value ABI and the module's appends stopped at its copy. */ /* A yielder the lowering turns into a proc call is a real function, but one the slot ABI is not built for (elig): the group takes the shared handle instead, as for a captured parameter. Refused, every other member of the name kept the value ABI and appended to a copy. */ if (!elig[k] && !m->is_transplanted_source) { if (!yield_lowers_block(c, k)) return 0; *captured = 1; } if (pi >= m->nparams || !m->pnames[pi]) return 0; /* a parameter rebound after its append (`io << x; io = String.new`) cannot be the caller's slot, whose String the rebind would replace: the group takes the shared handle, which the rebind leaves to the caller. Refused, the append before the rebind went to a copy. */ if (blocked && (blocked[k] & (1u << pi))) *captured = 1; LocalVar *q = scope_local(m, m->pnames[pi]); if (!q || !q->is_param || q->is_block_param || q->type != TY_STRING) return 0; /* celled for a proc that can outlive the call (a stored proc, a Thread body) is not a slot the caller can lend: the proc would keep the caller's frame. It is one String the proc, the method and the caller all hold, which is what the shared handle is, so the group takes the handle instead. Refused, the group kept the value ABI: a `super` or a call handing the captured parameter on appended to a copy, and so did the parent that shares the name (#6179). Celled because it is already byref is this group, mid-fixpoint. A cell only lifted iteration blocks made is consumed while the call runs, and the block reads the caller's slot through it exactly as when byref promoted first -- refusing it made the ABI depend on which of the two passes ran first, and the caller's buffer came back empty (#4568). */ if (q->is_cell && !q->byref_out && q->cell_outlives) *captured = 1; } return 1; } static int an_byref_promote_group(Compiler *c, const char *nm, int pi, const char *elig, const unsigned *blocked, int n, unsigned *handle) { int captured = 0; if (!an_byref_group_takes_ref(c, nm, pi, elig, blocked, n, &captured)) return 0; int did = 0; for (int k = 1; k < n; k++) { Scope *m = &c->scopes[k]; if (!m->name || !sp_streq(m->name, nm)) continue; if (m->class_id >= 0 && !m->is_cmethod && !an_class_can_be_reached(c, m->class_id)) continue; if (!an_byref_group_member_string(c, m, pi)) continue; if (captured) handle[k] |= 1u << pi; LocalVar *q = scope_local(m, m->pnames[pi]); if (q && !q->byref_out) { { q->byref_out = 1; comp_touch_scope(q->owner); } { q->is_cell = 1; comp_touch_scope(q->owner); } did = 1; } } return did; } /* The method a plain call `id` names, by its receiver: self or none (the enclosing class's, else a top-level method's), a constant's `new` (its initialize), an object's class. */ static int an_call_target_mi(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); if (!nm) return -1; int recv = nt_ref(nt, id, "receiver"); NodeKind rk = recv >= 0 ? nt_kind(nt, recv) : NK_SelfNode; if (sp_streq(nm, "new") && (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode)) { TyKind ct = comp_ntype(c, id); return ty_is_object(ct) ? comp_method_in_chain(c, ty_object_class(ct), "initialize", NULL) : -1; } if (rk == NK_SelfNode) { /* by name alone, `run(u) { ... }` at the top level found K#run, a different method that keeps its block, and its block parameter was taken for that method's handle */ int mi = comp_self_call_mi(c, id, nm); return mi >= 0 ? mi : an_any_scope_by_name(c, nm); } TyKind rt = comp_ntype(c, recv); return ty_is_object(rt) ? call_target(c, id, ty_object_class(rt), nm, 0, NULL) : -1; } /* Does the code under `node` append to local `vn` in place, or hand it to a parameter that is lent? A spliced yielding method's parameter is lent where the splice binds it as an alias (an_inline_param_lent): `def run2(x) = run(x) { |u| yield u }` lends x to run, whose block yields it on to run2's. */ static int an_lend_nest; /* the splice questions a walk asks, nested */ /* What a walk of an_subtree_lends_local_x is asked, and what it reports back: plan: the call plan's targets are asked too, which are the methods the emitter passes the local's slot to (a class method on a constant, on `self.class`, on a module); capped: the walk stopped at its depth limit. */ typedef struct { int plan, capped; } AnLend; /* Is the local vn one of call's arguments, as written? */ static int an_call_has_local_arg(Compiler *c, int call, const char *vn) { const NodeTable *nt = c->nt; int args = nt_ref(nt, call, "arguments"), n = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &n) : NULL; for (int i = 0; i < n; i++) if (nt_kind(nt, av[i]) == NK_LocalVariableReadNode && nt_str(nt, av[i], "name") && sp_streq(nt_str(nt, av[i], "name"), vn)) return 1; return 0; } static int an_subtree_lends_local_x(Compiler *c, int node, const char *vn, int depth, AnLend *x) { const NodeTable *nt = c->nt; if (node < 0) return 0; /* the walk stops at depth 64 and answers no there; x->capped says it did, for a caller that must not take that no for an answer */ if (depth > 64) { if (x) x->capped = 1; return 0; } if (nt_kind(nt, node) == NK_CallNode) { int r = nt_ref(nt, node, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode && an_str_mutator_name(nt_str(nt, node, "name")) && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), vn)) return 1; int cand[CPT_MAX + 1], nc = 0; cand[nc++] = an_call_target_mi(c, node); if (x && x->plan && an_call_has_local_arg(c, node, vn)) { int pt[CPT_MAX], np = cplan_targets(c, node, pt, CPT_MAX); /* targets the plan cannot list: the call may take the slot */ if (np < 0) return 1; for (int i = 0; i < np; i++) cand[nc++] = pt[i]; } int blk = nt_ref(nt, node, "block"); if (blk >= 0 && nt_kind(nt, blk) != NK_BlockNode) blk = -1; for (int ci = 0; ci < nc; ci++) { int mi = cand[ci]; Scope *m = mi >= 0 ? &c->scopes[mi] : NULL; for (int j = 0; m && j < m->nparams; j++) { int spliced = blk >= 0 && m->yields && !m->is_lowered_yield && j < 32 && an_lend_nest < 4; if (!comp_byref_param(c, m, j) && !spliced) continue; int an = arg_layout_param_node(c, m, node, j, NULL); if (an < 0 || nt_kind(nt, an) != NK_LocalVariableReadNode || !nt_str(nt, an, "name") || !sp_streq(nt_str(nt, an, "name"), vn)) continue; if (comp_byref_param(c, m, j)) return 1; an_lend_nest++; int lent = an_inline_param_lent(c, mi, j, blk); an_lend_nest--; if (lent) return 1; } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (an_subtree_lends_local_x(c, nt_ref_at(nt, node, i), vn, depth + 1, x)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, node, i, &n); for (int k = 0; k < n; k++) if (an_subtree_lends_local_x(c, a[k], vn, depth + 1, x)) return 1; } return 0; } static int an_subtree_lends_local(Compiler *c, int node, const char *vn, int depth) { return an_subtree_lends_local_x(c, node, vn, depth, NULL); } /* The same for code being emitted, where the call plans are settled: also the methods the plan names, and 1 for a subtree the walk cannot see to the bottom of or a call whose targets it cannot list. A caller that needs a no it can rely on (the loop-bound hoist) asks this. */ int an_subtree_may_lend_local(Compiler *c, int node, const char *vn) { AnLend x = { 1, 0 }; return an_subtree_lends_local_x(c, node, vn, 0, &x) || x.capped; } /* Is `node` a yield of spliced method `m`'s block: a `yield`, or a `blk.call(...)` on its own &block, which the splice expands as one? */ static int an_yield_like(Compiler *c, int node, Scope *m) { const NodeTable *nt = c->nt; if (nt_kind(nt, node) == NK_YieldNode) return 1; if (nt_kind(nt, node) != NK_CallNode || !m->blk_param || !m->blk_param[0]) return 0; const char *cn = nt_str(nt, node, "name"); int r = nt_ref(nt, node, "receiver"); return cn && sp_streq(cn, "call") && r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), m->blk_param); } /* Does a spliced block `blk` lend its parameter k: append to it, or hand it to a lent parameter? The splice then binds it as an alias of the variable yielded to it (block_param_wants_alias), and a caller's parameter behind that variable has to be lent too. */ static int an_block_param_lent(Compiler *c, int blk, int k, int n); static int an_block_kw_lent(Compiler *c, int blk, const char *key); /* Does the code under `node` yield a plain read of `vn`, in spliced method `mi`, at a position (or to a keyword) whose parameter some block a call site of `mi` passes lends? */ static int an_subtree_yields_lent(Compiler *c, int node, const char *vn, int mi) { const NodeTable *nt = c->nt; if (node < 0) return 0; NodeKind nk = nt_kind(nt, node); if (nk == NK_DefNode || nk == NK_ClassNode || nk == NK_ModuleNode) return 0; if (an_yield_like(c, node, &c->scopes[mi])) { int aa = nt_ref(nt, node, "arguments"), an = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &an) : NULL; for (int j = 0; j < an; j++) { if (nt_kind(nt, av[j]) == NK_SplatNode) break; /* by keyword, to the blocks' keyword of that name */ const char *key = NULL; int v = av[j]; if (nt_kind(nt, av[j]) == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, av[j], "elements", &en); for (int e = 0; e < en && !key; e++) { int kv; const char *k2 = dyn_kw_elem_key(c, el[e], &kv); if (k2 && kv >= 0 && nt_kind(nt, kv) == NK_LocalVariableReadNode && nt_str(nt, kv, "name") && sp_streq(nt_str(nt, kv, "name"), vn)) { key = k2; v = kv; } } if (!key) continue; } if (nt_kind(nt, v) != NK_LocalVariableReadNode || !nt_str(nt, v, "name") || !sp_streq(nt_str(nt, v, "name"), vn)) continue; for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { int ub = nt_kind(nt, u) == NK_CallNode ? nt_ref(nt, u, "block") : -1; if (ub < 0 || nt_kind(nt, ub) != NK_BlockNode || an_call_target_mi(c, u) != mi) continue; if (key ? an_block_kw_lent(c, ub, key) : an_block_param_lent(c, ub, j, call_plain_argc(c, node))) return 1; } } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (an_subtree_yields_lent(c, nt_ref_at(nt, node, i), vn, mi)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, node, i, &n); for (int q = 0; q < n; q++) if (an_subtree_yields_lent(c, a[q], vn, mi)) return 1; } return 0; } static int an_block_name_lent(Compiler *c, int blk, const char *bp); static int an_block_param_lent(Compiler *c, int blk, int k, int n) { /* a post the position binds is aliased too (emit_block_binds); n is the yield's count of plain arguments */ const char *bp = blk >= 0 ? block_param_at(c, blk, k, n) : NULL; return bp && an_block_name_lent(c, blk, bp); } /* Does literal block `blk` lend its String parameter `bp`: append to it, or yield it on to blocks that do? */ static int an_block_name_lent(Compiler *c, int blk, const char *bp) { Scope *bs = comp_scope_of(c, blk); LocalVar *lv = bs ? scope_local(bs, bp) : NULL; if (!lv || lv->type != TY_STRING || lv->is_cell) return 0; if (an_subtree_lends_local(c, nt_ref(c->nt, blk, "body"), bp, 0)) return 1; /* yielded on (`run(x) { |u| yield u }`) to the blocks the spliced method it is written in is called with, which lend it */ if (!bs->name || !bs->yields || bs->is_lowered_yield || an_lend_nest >= 4) return 0; an_lend_nest++; int lent = an_subtree_yields_lent(c, nt_ref(c->nt, blk, "body"), bp, (int)(bs - c->scopes)); an_lend_nest--; return lent; } /* The same for the keyword parameter of `blk` that keyword `key` binds. */ static int dyn_kw_name_is(const Compiler *c, const char *pn, const char *key); static const char *an_block_kw_param(Compiler *c, int blk, const char *key) { for (int i = 0; blk >= 0 && key; i++) { const char *kp = block_keyword_name(c, blk, i); if (!kp) break; if (dyn_kw_name_is(c, kp, key)) return kp; } return NULL; } static int an_block_kw_lent(Compiler *c, int blk, const char *key) { const char *kp = an_block_kw_param(c, blk, key); if (!kp) return 0; Scope *bs = comp_scope_of(c, blk); LocalVar *lv = bs ? scope_local(bs, kp) : NULL; if (!lv || lv->type != TY_STRING || lv->is_cell) return 0; if (an_subtree_lends_local(c, nt_ref(c->nt, blk, "body"), kp, 0)) return 1; if (!bs->name || !bs->yields || bs->is_lowered_yield || an_lend_nest >= 4) return 0; an_lend_nest++; int lent = an_subtree_yields_lent(c, nt_ref(c->nt, blk, "body"), kp, (int)(bs - c->scopes)); an_lend_nest--; return lent; } /* Does yield (or `blk.call`) `y` hand `pn` to a parameter block `blk` lends, by position or by keyword? */ static int an_yield_lends(Compiler *c, int y, const char *pn, int blk) { const NodeTable *nt = c->nt; int aa = nt_ref(nt, y, "arguments"), an = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &an) : NULL; for (int k = 0; k < an; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) break; if (nt_kind(nt, av[k]) == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, av[k], "elements", &en); for (int e = 0; e < en; e++) { int v; const char *key = dyn_kw_elem_key(c, el[e], &v); if (key && v >= 0 && nt_kind(nt, v) == NK_LocalVariableReadNode && nt_str(nt, v, "name") && sp_streq(nt_str(nt, v, "name"), pn) && an_block_kw_lent(c, blk, key)) return 1; } continue; } if (nt_kind(nt, av[k]) == NK_LocalVariableReadNode && nt_str(nt, av[k], "name") && sp_streq(nt_str(nt, av[k], "name"), pn) && an_block_param_lent(c, blk, k, call_plain_argc(c, y))) return 1; } return 0; } /* Does spliced yielding method `mi` yield its boxed parameter pj to a block parameter a literal block at one of its call sites appends to, in place (a boxed one through the box) or by handing it to a lent parameter? The box then has to hold the caller's handle, as for a boxed parameter the method appends to itself (convert_byref_handle_params). */ /* Does literal block `blk` append to the elements of its rest, and does a yield of the n arguments av[] gather position k into it (the positions past the leading and the optional parameters the count fills, up to the posts)? The rest is then an Array of the arguments' boxes, and a String variable there has to be the shared handle (#6179). */ static int rest_elems_mutated_walk(Compiler *c, int node, const char *rn, int depth); static int block_rest_position(Compiler *c, int blk, const int *av, int n, int k) { const NodeTable *nt = c->nt; if (nt_argv_has(nt, av, n, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK)) return 0; const char *rn = block_rest_name(c, blk); int bp = nt_ref(nt, blk, "parameters"); int pn = bp >= 0 && nt_kind(nt, bp) == NK_BlockParametersNode ? nt_ref(nt, bp, "parameters") : -1; if (!rn || !rn[0] || pn < 0) return 0; int P = 0, O = 0, Q = 0, ot, ps; nt_arr(nt, pn, "requireds", &P); nt_arr(nt, pn, "optionals", &O); nt_arr(nt, pn, "posts", &Q); block_fill(P, O, Q, 1, n, &ot, &ps); if (k < P + ot || k >= ps) return 0; return rest_elems_mutated_walk(c, nt_ref(nt, blk, "body"), rn, 0); } static int an_poly_param_yielded_lent(Compiler *c, int mi, int pj) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (!m->yields || pj < 0 || pj >= m->nparams || !m->pnames[pj]) return 0; for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode || comp_scope_of(c, y) != m) continue; int a = nt_ref(nt, y, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) break; if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode || !nt_str(nt, av[k], "name") || !sp_streq(nt_str(nt, av[k], "name"), m->pnames[pj])) continue; for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { int blk = nt_kind(nt, u) == NK_CallNode ? nt_ref(nt, u, "block") : -1; if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || an_call_target_mi(c, u) != mi) continue; const char *bp = block_param_name(c, blk, k); Scope *bs = bp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, bp) : NULL; if (t && !t->is_cell && (t->type == TY_POLY || t->type == TY_STRING) && an_subtree_lends_local(c, nt_ref(nt, blk, "body"), bp, 0)) return 1; /* or gathers it into a rest it appends to the elements of */ if (block_rest_position(c, blk, av, ac, k)) return 1; } } } return 0; } /* Does inlined yielding method `mi` (an initialize spliced by `new`) lend its parameter j at a call whose literal block is `blk`? The inliner then binds it as an alias of the caller's variable (emit_inline_call_x): the body appends to it or hands it to a lent parameter, or yields it to a block parameter the block lends. */ int an_inline_param_lent(Compiler *c, int mi, int j, int blk) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (j >= m->nparams || j >= 32 || !m->pnames[j] || (m->rest_idx >= 0 && j >= m->rest_idx)) return 0; const char *pn = m->pnames[j]; LocalVar *q = scope_local(m, pn); if (!q || !q->is_param || q->is_block_param || q->type != TY_STRING || q->is_cell) return 0; /* one rebinding the alias cannot follow (an op-assign, a multiple assignment) keeps a copy */ for (int w = comp_lvw_first_sc(c, mi, pn); w >= 0; w = comp_lvw_next_sc(c, w)) if (comp_scope_of(c, w) == m && nt_kind(nt, w) != NK_LocalVariableWriteNode && nt_str(nt, w, "name") && sp_streq(nt_str(nt, w, "name"), pn)) return 0; if (an_subtree_lends_local(c, m->body, pn, 0)) return 1; /* a `yield`, or a `blk.call` the splice expands as one (the scope's own call chain); by position, or by keyword to the block's keyword */ for (int y = comp_kind_first(c, NK_YieldNode); blk >= 0 && y >= 0; y = comp_kind_next(c, y)) if (nt_kind(nt, y) == NK_YieldNode && comp_scope_of(c, y) == m && an_yield_lends(c, y, pn, blk)) return 1; for (int y = m->blk_param && blk >= 0 ? comp_scall_first(c, mi) : -1; y >= 0; y = comp_scall_next(c, y)) if (comp_scope_of(c, y) == m && an_yield_like(c, y, m) && an_yield_lends(c, y, pn, blk)) return 1; return 0; } int a_super_target(Compiler *c, Scope *m); /* The scope indices of the named methods, ordered by name, for the name lookups an_byref_eligible_scopes makes per alias and per literal. */ typedef struct { const char *name; int si; } ByrefName; static int byref_name_cmp(const void *a, const void *b) { const ByrefName *x = a, *y = b; int d = strcmp(x->name, y->name); return d ? d : x->si - y->si; } /* every method named nm keeps the plain ABI */ static void byref_name_ineligible(const ByrefName *idx, int ni, const char *nm, char *elig) { if (!nm) return; int lo = 0, hi = ni; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (strcmp(idx[mid].name, nm) < 0) lo = mid + 1; else hi = mid; } for (int i = lo; i < ni && sp_streq(idx[i].name, nm); i++) elig[idx[i].si] = 0; } /* Which methods the default build may lend a String parameter's slot by address (elig[si], c->nscopes entries): a method with a body and parameters, not yielding, not a constructor, not in a Struct or a native class, and no alias, Symbol or String reaching its name. A name group takes the slot only when every member may (an_byref_group_takes_ref). compute_byref_out_params promotes from it, and --share-strings asks it which parameters it can lend (#6765). */ void an_byref_eligible_scopes(Compiler *c, char *elig) { const NodeTable *nt = c->nt; int n = c->nscopes; memset(elig, 0, (size_t)(n > 0 ? n : 1)); for (int si = 1; si < n; si++) { Scope *s = &c->scopes[si]; if (!s->name || s->def_node < 0 || s->body < 0) continue; if (s->yields || s->is_lowered_yield || s->dm_subst_name || s->cs_synth) continue; if (s->is_transplanted_source) continue; /* An instance method qualifies too: `def add(buf, i); buf << ...; end` is ordinary Ruby, and without the out-param the append landed in a copy and the caller's string stayed empty (#3781). The unique-name rule above still holds, so no override or dispatch sees a different ABI. A class whose instances reach the method through a poly dispatch is fine: those arms fill their arguments through the same emitter. */ if (s->class_id >= 0 && !s->is_cmethod && c->toplevel_class != s->class_id && (c->classes[s->class_id].is_struct || c->classes[s->class_id].is_native_class || c->classes[s->class_id].is_value_type)) continue; /* An OPTIONAL parameter beside the lent one is not an ABI difference, and treating it as one dropped the callee's appends without a word (#4390). The caller materialises every default, so the callee is emitted fixed-arity either way: the two emits of `def fill(io, prefix = nil)` and of the same method with `prefix` required differ in one token, the parameter's own spelling. The default that is a lent parameter's own (`def fill(io = String.new)`) passes a fresh caller-side temp, which is unaliased, so the appends go nowhere -- which is what CRuby answers for that shape too. */ /* A keyword, a rest, a **kwrest or a post beside the lent parameter is no ABI difference either: the keywords are positional C parameters, and every binder that fills one -- a literal keyword, a `**` or merged hash, a splat's element, a gather, a default -- lends what it binds, a temp where no caller variable stands behind the value (emit_lent_temp). So `def m(k:) = k << "x"` called `m(k: s)` lends s like a positional, where refusing the method made the appends stay in a copy. */ if (s->nparams <= 0 || s->nparams > 32) continue; /* A constructor is reached through sp_X_new, which about two dozen emitters (class values, `raise`, `allocate`, the poly `new` dispatch) call with values: lent a slot at the `.new` site alone, the C build stopped on the wrapper's value parameter. */ if (s->name && sp_streq(s->name, "initialize")) continue; elig[si] = 1; } ByrefName *idx = malloc(sizeof *idx * (size_t)(n > 0 ? n : 1)); if (!idx) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int ni = 0; for (int si = 1; si < n; si++) if (elig[si]) idx[ni++] = (ByrefName){ c->scopes[si].name, si }; qsort(idx, (size_t)ni, sizeof *idx, byref_name_cmp); /* an aliased name reaches the method under another spelling; the alias call sites keep the plain ABI, so the method must too */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cls = &c->classes[ci]; for (int a = 0; a < cls->naliases; a++) { byref_name_ineligible(idx, ni, cls->alias_old[a], elig); byref_name_ineligible(idx, ni, cls->alias_new[a], elig); } } /* A name reached by a Symbol or String -- send / method(:x) / define_method / respond_to? / `&:x` / inject(:x) -- takes the plain ABI. When every such reach names its method with a literal, only the names those literals spell are refused; a literal anywhere else (`p :edit`, a Hash key) calls nothing, and refusing its method for it lost the appends of every caller silently (#6040). One reach whose name is only the run time's (`send(m)`, `&blk_sym`) may call any method, and every name any literal spells keeps the plain ABI, as before. */ static const char *const DYN[] = { "send", "__send__", "public_send", "method", "public_method", "instance_method", "public_instance_method", "singleton_method", "define_method", "define_singleton_method", "respond_to?", "alias_method", "method_defined?", "public_method_defined?", "private_method_defined?", "protected_method_defined?", "inject", "reduce", "to_proc", NULL }; int dyn_nonlit = 0; for (int id = 0; id < nt->count && !dyn_nonlit; id++) { NodeKind k = nt_kind(nt, id); int lit = -1; if (k == NK_BlockArgumentNode) lit = nt_ref(nt, id, "expression"); else if (k == NK_CallNode) { const char *cn = nt_str(nt, id, "name"); int hit = 0; for (int d = 0; cn && DYN[d] && !hit; d++) hit = sp_streq(cn, DYN[d]); if (!hit) continue; if (sp_streq(cn, "to_proc")) lit = nt_ref(nt, id, "receiver"); else { int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; lit = ac > 0 ? av[is_reduce_alias(cn) ? ac - 1 : 0] : -2; } } else continue; if (lit == -2) continue; /* no argument names a method */ if (lit < 0) continue; /* `&` forwarding the enclosing block */ NodeKind lk = nt_kind(nt, lit); if (lk == NK_SymbolNode || lk == NK_StringNode) continue; /* inject(0) / reduce(init) { }: an operand, not a method name */ if ((k == NK_CallNode) && comp_ntype(c, lit) != TY_SYMBOL && comp_ntype(c, lit) != TY_STRING && comp_ntype(c, lit) != TY_POLY && comp_ntype(c, lit) != TY_UNKNOWN) continue; if (k == NK_BlockArgumentNode && comp_ntype(c, lit) != TY_SYMBOL && comp_ntype(c, lit) != TY_POLY) continue; /* the `&blk` of a forward that became a literal block (desugar_value_callable_forwards) is no call's any more: its read carries the sentinel name, and it reaches no method */ if (k == NK_BlockArgumentNode && lk == NK_LocalVariableReadNode && sp_streq(nt_str(nt, lit, "name"), "__orphaned__")) continue; dyn_nonlit = 1; } for (int id = 0; id < nt->count; id++) { if (!nt_type(nt, id)) continue; /* a symbol/string literal spelling the name (see DYN above) */ const char *v = NULL; int lit = -1; if (dyn_nonlit) lit = id; else if (nt_kind(nt, id) == NK_BlockArgumentNode) lit = nt_ref(nt, id, "expression"); else if (nt_kind(nt, id) == NK_CallNode) { const char *cn = nt_str(nt, id, "name"); int hit = 0; for (int d = 0; cn && DYN[d] && !hit; d++) hit = sp_streq(cn, DYN[d]); if (hit) { if (sp_streq(cn, "to_proc")) lit = nt_ref(nt, id, "receiver"); else { int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int j = 0; j < ac && lit < 0; j++) { NodeKind jk = nt_kind(nt, av[j]); /* each literal argument: alias_method and define_method name two */ if (jk == NK_SymbolNode || jk == NK_StringNode) { const char *jv = jk == NK_SymbolNode ? nt_str(nt, av[j], "value") : nt_str(nt, av[j], "content"); if (!jv) jv = nt_str(nt, av[j], "unescaped"); byref_name_ineligible(idx, ni, jv, elig); } } } } } if (lit >= 0) { NodeKind lty_k = nt_kind(nt, lit); if (lty_k == NK_SymbolNode) { v = nt_str(nt, lit, "value"); if (!v) v = nt_str(nt, lit, "unescaped"); } else if (lty_k == NK_StringNode) { v = nt_str(nt, lit, "content"); if (!v) v = nt_str(nt, lit, "unescaped"); } } byref_name_ineligible(idx, ni, v, elig); } free(idx); } /* Is `v` a String nobody else holds: a literal, or a builtin String method that answers a new one (`text.gsub(...)`, `text.strip`, `text.dup`)? On a receiver that may also be another kind of object, only a substitution no method of the program's own is named after: no other builtin has one. */ static int an_fresh_string_value(Compiler *c, int v) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0) return 0; NodeKind k = nt_kind(nt, v); if (k == NK_StringNode || k == NK_InterpolatedStringNode) return 1; if (k != NK_CallNode) return 0; int r = nt_ref(nt, v, "receiver"); const char *nm = nt_str(nt, v, "name"); TyKind rt = r >= 0 ? comp_ntype(c, r) : TY_UNKNOWN; if (!nm || (rt != TY_STRING && rt != TY_STRBUF && rt != TY_POLY)) return 0; int sh = bop_share_named(TY_STRING, nm); int fresh = rt == TY_POLY ? sh == BSH_SUBST && an_any_scope_by_name(c, nm) < 0 : sh == BSH_SUBST || sh == BSH_PURE || sp_streq(nm, "dup"); return fresh && cplan_user_fresh(c, v)->dispatch == CP_NONE; } /* Does a write of local `vn` under `node` bind anything but a fresh String? */ static int an_writes_unfresh(Compiler *c, int node, const char *vn, int depth) { const NodeTable *nt = c->nt; if (node < 0 || node >= nt->count) return 0; if (depth > 300) return 1; NodeKind k = nt_kind(nt, node); const char *wn = comp_is_local_write(k) ? nt_str(nt, node, "name") : NULL; if (wn && sp_streq(wn, vn) && (k != NK_LocalVariableWriteNode || !an_fresh_string_value(c, nt_ref(nt, node, "value")))) return 1; const SpNode *nd = &nt->nodes[node]; for (int i = 0; i < nd->nr; i++) if (an_writes_unfresh(c, nd->r[i].ref, vn, depth + 1)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (an_writes_unfresh(c, nd->a[i].ids[j], vn, depth + 1)) return 1; return 0; } /* Can parameter `vn` of scope s only hold a String the method made itself at node `use`? It does when the body rebinds it to a fresh String in a statement ahead of the one holding `use`, and every write of it binds one. Mutated or handed on there, it is not the caller's String: def strip(text) text = text.gsub(/#/, '') text.sub!(/\A /, '') # changes the method's own String text end Lent anyway, the rebind put the group on the shared handle, and a caller passing its own parameter on took the handle too, so its later `text = other(text)` was refused as a mutated parameter. */ static int an_param_fresh_at(Compiler *c, Scope *s, const char *vn, int use) { const NodeTable *nt = c->nt; if (!vn || s->body < 0 || nt_kind(nt, s->body) != NK_StatementsNode) return 0; int bn = 0, ahead = 0; const int *bs = nt_arr(nt, s->body, "body", &bn); for (int j = 0; j < bn && !a_subtree_contains(nt, bs[j], use, 0); j++) { const char *wn = nt_kind(nt, bs[j]) == NK_LocalVariableWriteNode ? nt_str(nt, bs[j], "name") : NULL; if (wn && sp_streq(wn, vn)) ahead = 1; } return ahead && !an_writes_unfresh(c, s->body, vn, 0); } static void compute_byref_out_params(Compiler *c) { const NodeTable *nt = c->nt; int n = c->nscopes; char *elig = calloc((size_t)n, 1); unsigned *blocked = calloc((size_t)n, sizeof(unsigned)); /* per-scope param bitmask */ char *polyr = calloc((size_t)n, 1); /* a POLY receiver reaches the name */ unsigned *cellh = calloc((size_t)n, sizeof(unsigned)); /* a proc captures the param */ /* the calls of a scope that lends: an eligible one, or a yielder the lowering turns into a proc call, whose group takes the shared handle instead (an_byref_group_takes_ref). Its own appends were followed, the parameter it hands on to a byref slot was not, and that append landed in its copy. Asked once per scope: the lowering's test walks the program. */ char *lends = calloc((size_t)n, 1); if (!elig || !blocked || !polyr || !cellh || !lends) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } an_byref_eligible_scopes(c, elig); for (int si = 1; si < n; si++) lends[si] = elig[si] || ((c->scopes[si].yields || c->scopes[si].is_lowered_yield) && yield_lowers_block(c, si)); /* The eligible scopes by name, each name's chained from efirst through enext: a call on a POLY receiver below marks those of its name, found by hash rather than by a walk of every scope at every such call. */ ANameHash enames; memset(&enames, 0, sizeof enames); int *efirst = NULL, ecap = 0; int *enext = malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); if (!enext) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int si = 1; si < n; si++) { enext[si] = -1; const char *sn = c->scopes[si].name; if (!elig[si] || !sn) continue; int e = anh_find(&enames, sn); if (e < 0) { anh_add(&enames, sn); e = anh_find(&enames, sn); if (e >= ecap) { ecap = e + 64; efirst = realloc(efirst, sizeof(int) * (size_t)ecap); if (!efirst) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } efirst[e] = -1; } enext[si] = efirst[e]; efirst[e] = si; } for (int id = 0; id < nt->count; id++) { const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty) continue; /* A POLY receiver reaches the method through the cls_id switch, and that switch hoists its arguments once, by the argument's own type, for every arm to share -- it has no callee to ask, and the arms are what it is choosing between. So it passes the value where a byref arm wants the slot, and the C build stops: expected 'const char **' but argument is of type 'const char *' The uniqueness rule used to hide this: two classes defining one name was exactly the case it refused, and a poly dispatch needs two. Making the group agree (df30ed28) removed that accident and left the dispatch unable to call what it now agrees about. Kept on the value ABI, though, every arm appended to a copy and the caller's String came back empty. So such a name is promoted as any other, and takes the shared handle instead of the slot once the group has settled (below): the dispatch hands a handle arm the caller's handle (emit_poly_shared_arg), and a handle, unlike a slot, is one thing whichever arm the receiver picks. */ if (k == NK_CallNode) { int rcv = nt_ref(nt, id, "receiver"); const char *cn = nt_str(nt, id, "name"); if (rcv >= 0 && cn && comp_ntype(c, rcv) == TY_POLY) { int e = anh_find(&enames, cn); for (int si = e >= 0 ? efirst[e] : -1; si >= 0; si = enext[si]) polyr[si] = 1; } } /* a plain rebind of a param blocks it (see the header comment) */ if (k == NK_LocalVariableWriteNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode || k == NK_LocalVariableTargetNode) { Scope *s = comp_scope_of(c, id); int si = s ? (int)(s - c->scopes) : -1; if (si > 0 && si < n && elig[si]) { int pi = an_param_idx(s, nt_str(nt, id, "name")); if (pi >= 0 && pi < 32) blocked[si] |= 1u << pi; } } } int changed = 1; while (changed) { changed = 0; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { /* transitive through `super`: a parameter a super hands on into the parent's byref slot. A same-named parent shares the name group; one an included module brings is copied under a name of its own, and the child kept the value ABI the lent slot came back through. */ Scope *s = comp_scope_of(c, id); int si = s ? (int)(s - c->scopes) : -1; if (si <= 0 || si >= n || !lends[si] || s->class_id < 0) continue; int mi = a_super_target(c, s); if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams; j++) { if (!comp_byref_param(c, m, j)) continue; int pi = -1; if (k == NK_ForwardingSuperNode) pi = zsuper_param_source(c, s, m, j); else { int an = arg_layout_param_node(c, m, id, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode) pi = an_param_idx(s, nt_str(nt, an, "name")); } if (pi < 0 || pi >= 32) continue; LocalVar *p = scope_local(s, s->pnames[pi]); if (p && p->is_param && p->type == TY_STRING && !p->byref_out && !an_param_fresh_at(c, s, s->pnames[pi], id) && an_byref_promote_group(c, s->name, pi, elig, blocked, n, cellh)) changed = 1; } continue; } if (k != NK_CallNode) continue; Scope *s = comp_scope_of(c, id); int si = s ? (int)(s - c->scopes) : -1; if (si <= 0 || si >= n || !lends[si]) continue; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); NodeKind rk = nt_kind(nt, recv); /* direct mutator: param << ... / param.gsub!(...) */ if (rk == NK_LocalVariableReadNode && an_str_mutator_name(nm)) { const char *vn = nt_str(nt, recv, "name"); int pi = an_param_idx(s, vn); if (pi >= 0 && pi < 32) { LocalVar *p = scope_local(s, vn); /* the whole name group takes it or none of it does; the cell deref forms the body already emits are what the ABI rides on */ if (p && p->is_param && p->type == TY_STRING && !p->byref_out && !an_param_fresh_at(c, s, vn, id) && an_byref_promote_group(c, s->name, pi, elig, blocked, n, cellh)) changed = 1; } } /* transitive through a splice: the param passed to an inlined yielding method (or a yielding initialize) that lends it, or handed by instance_exec to a block that does, is aliased there, so the append reaches this method's own variable and stops at a copy unless the param is lent too */ { int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) != NK_BlockNode) blk = -1; int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int ymi = -1; if (nm && sp_streq(nm, "instance_exec")) ymi = -2; else if ((ymi = an_call_target_mi(c, id)) >= 0 && !c->scopes[ymi].yields) ymi = -1; for (int j = 0; ymi != -1 && j < (ymi >= 0 ? c->scopes[ymi].nparams : ac) && j < 32; j++) { int an = -1; if (ymi == -2) { if (blk < 0 || nt_kind(nt, av[j]) == NK_SplatNode) break; if (an_block_param_lent(c, blk, j, call_plain_argc(c, id))) an = av[j]; } else if (an_inline_param_lent(c, ymi, j, blk)) an = arg_layout_param_node(c, &c->scopes[ymi], id, j, NULL); if (an < 0 || nt_kind(nt, an) != NK_LocalVariableReadNode) continue; const char *vn = nt_str(nt, an, "name"); int pi = an_param_idx(s, vn); if (pi < 0 || pi >= 32) continue; LocalVar *p = scope_local(s, vn); if (p && p->is_param && p->type == TY_STRING && !p->byref_out && !an_param_fresh_at(c, s, vn, id) && an_byref_promote_group(c, s->name, pi, elig, blocked, n, cellh)) changed = 1; } } /* transitive: the param passed on into another method's byref slot */ if (recv < 0 || ((rk == NK_ConstantReadNode || rk == NK_ConstantPathNode || rk == NK_SelfNode))) { int mi = an_any_scope_by_name(c, nm); if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams; j++) { if (!comp_byref_param(c, m, j)) continue; /* the argument the binders give parameter j, a keyword's by name */ int an = arg_layout_param_node(c, m, id, j, NULL); if (an < 0 || nt_kind(nt, an) != NK_LocalVariableReadNode) continue; const char *vn = nt_str(nt, an, "name"); int pi = an_param_idx(s, vn); if (pi < 0 || pi >= 32) continue; LocalVar *p = scope_local(s, vn); if (p && p->is_param && p->type == TY_STRING && !p->byref_out && !an_param_fresh_at(c, s, vn, id) && an_byref_promote_group(c, s->name, pi, elig, blocked, n, cellh)) changed = 1; } } } } /* A name a POLY receiver reaches takes the shared handle where the group settled on the slot (see the POLY receiver note above), and the conversion pass pulls each caller's local into it: `[Loud.new, Quiet.new].each { |v| v.append_into(io) }` left every append in a copy. The whole name group is marked, so every member it promoted converts, and the group still agrees. */ /* A group a captured parameter promoted takes the handle at that index (see an_byref_promote_group). The captured member keeps its cell: the proc reads the handle through it, and the handle, unlike a lent slot, outlives the call. */ for (int si = 1; si < n; si++) { if (!polyr[si] && !cellh[si]) continue; Scope *m = &c->scopes[si]; for (int j = 0; j < m->nparams && j < 32; j++) { if (!polyr[si] && !(cellh[si] & (1u << j))) continue; LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!q || !q->byref_out) continue; { q->byref_out = 0; comp_touch_scope(q->owner); } { q->is_cell = q->cell_outlives; comp_touch_scope(q->owner); } lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } } } /* the share facts built from here on ask the same answer */ free(c->byref_elig); c->byref_elig = elig; c->nbyref_elig = n; free(blocked); free(polyr); free(cellh); free(lends); anh_free(&enames); free(efirst); free(enext); } /* Does scope mi's body contain a call to its own method name on implicit or explicit self? The self-recursion test shared by the yield-lowering decisions: such a method must not take the inline path (expansion cannot terminate) -- it stays a real function with a proc-materialized block. */ static int scope_calls_itself(Compiler *c, int mi) { Scope *m = &c->scopes[mi]; if (!m->name || m->body < 0) return 0; for (int id = 0; id < c->nt->count; id++) { if (c->nscope[id] != mi) continue; NodeKind k = nt_kind(c->nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, m->name)) continue; /* a `super` into Array, spelled as self's call to Array's method (rewrite_array_subclass_super), is no call of the method itself */ if (nt_int(c->nt, id, "builtin_only", 0)) continue; int recv = nt_ref(c->nt, id, "receiver"); NodeKind rk = nt_kind(c->nt, recv); if (recv < 0 || (rk == NK_SelfNode)) return 1; } return 0; } /* The ivars each scope writes with a value that seeds the slot (any write, but a plain `nil` assigned by `@x = nil`), sorted per scope: one walk of the ivar write kinds. ctor_writes_ivar scanned every node of the program for each class and ivar, classes * ivars * nodes (#10325). Built again when the node table changes. */ static const NodeTable *cw_nt; static unsigned cw_ver; static int cw_count, cw_ns; static int *cw_off; /* per scope: start in cw_names, cw_off[ns] the end */ static const char **cw_names; static int cw_cmp(const void *a, const void *b) { return strcmp(*(const char *const *)a, *(const char *const *)b); } static void cw_build(Compiler *c) { const NodeTable *nt = c->nt; static const NodeKind K[] = { NK_InstanceVariableWriteNode, NK_InstanceVariableOperatorWriteNode, NK_InstanceVariableOrWriteNode, NK_InstanceVariableAndWriteNode, NK_InstanceVariableTargetNode }; int ns = c->nscopes > 0 ? c->nscopes : 1; free(cw_off); free(cw_names); cw_off = calloc((size_t)ns + 1, sizeof(int)); int total = 0; for (int pass = 0; pass < 2; pass++) { if (pass == 1) { for (int s = 0, acc = 0; s <= ns; s++) { int k = cw_off[s]; cw_off[s] = acc; acc += k; } cw_names = malloc(sizeof(char *) * (size_t)(total + 1)); } int *fill = pass == 1 ? calloc((size_t)ns + 1, sizeof(int)) : NULL; for (int q = 0; q < 5; q++) NT_FOREACH_KIND(nt, K[q], id) { int s = id < c->node_cap ? c->nscope[id] : -1; const char *wn = nt_str(nt, id, "name"); if (s < 0 || s >= ns || !wn) continue; /* A ctor write of a plain `nil` literal does not SEED the slot with a concrete value -- it explicitly leaves it nil. If every other write (elsewhere) is a scalar, the slot is nullable and must widen to poly, so a nil-only ctor write must not block the poly backstop (#3143). A non-nil write, or any operator/or/and/target write, does seed. */ if (K[q] == NK_InstanceVariableWriteNode && nt_kind(nt, nt_ref(nt, id, "value")) == NK_NilNode) continue; if (pass == 0) { cw_off[s]++; total++; } else cw_names[cw_off[s] + fill[s]++] = wn; } free(fill); } for (int s = 0; s < ns; s++) qsort(cw_names + cw_off[s], (size_t)(cw_off[s + 1] - cw_off[s]), sizeof(char *), cw_cmp); cw_nt = nt; cw_ver = nt->version; cw_count = nt->count; cw_ns = ns; } /* Does scope mi write @ with a value that seeds it? */ static int scope_seeds_ivar(Compiler *c, int mi, const char *ivname) { const NodeTable *nt = c->nt; if (cw_nt != nt || cw_ver != nt->version || cw_count != nt->count || cw_ns < c->nscopes) cw_build(c); if (mi < 0 || mi >= cw_ns) return 0; const char *key = ivname; return bsearch(&key, cw_names + cw_off[mi], (size_t)(cw_off[mi + 1] - cw_off[mi]), sizeof(char *), cw_cmp) != NULL; } /* Does any `initialize` in ci's chain write @? A ctor that exists but never assigns a given ivar leaves it nil on a fresh instance, same as no ctor at all -- so the nil-backstop below must key on the actual write, not on the mere presence of an initialize (#3136). */ static int ctor_writes_ivar(Compiler *c, int ci, const char *ivname) { for (int k = ci; k >= 0; k = c->classes[k].parent) { int mi = comp_method_in_class(c, k, "initialize"); if (mi < 0 || c->scopes[mi].def_node < 0) continue; if (scope_seeds_ivar(c, mi, ivname)) return 1; } return 0; } /* Shared-mutable strings, phase 3 (#3227): a container STORE (array/hash literal element, `arr << s`, `arr[i] = s`) or an `equal?` argument of an in-place-mutated string local promotes it to a shared TY_STRBUF handle DURING the fixpoint, and marks that read in c->strbuf_box so infer types it TY_STRBUF (the container then unifies to a poly variant and emit_boxed wraps the sp_String* as SP_BUILTIN_STRBUF). Unmarked reads of the same local keep demoting to TY_STRING, so ordinary consumers are unchanged. */ /* Both mutation probes below ask "over EVERY call whose receiver names this local (or this ivar), was it mutated in place?" -- and the promotion asks them per candidate, from inside whole-table walks, so a probe that rescans the node table makes the pass O(candidates x nodes). On roundhouse's lobsters emit (41k lines of generated Ruby, the #3115 tree one scale up) the two together are ~35% of compile time in a `sample` profile. The answer is a pure function of call-site syntax and scope homing, so one sweep over the CallNode kind list computes it for EVERY key at once into a (name, key) table the probes then read in O(1). Cached while the scope index is frozen and stamped with comp_scope_index_gen() -- the same invalidation the LWIndex behind local_all_writes_empty_hash rides; while unfrozen it rebuilds per query, which costs one kind-list walk, strictly less than the whole-table scan it replaces. */ typedef struct { const char **name; /* receiver name, borrowed from the node table */ int *key; /* scope index (locals) / class id (ivars) */ signed char *val; /* 1 = mutated in place, -1 = disqualified */ int *next; /* next record in the same bucket, or -1 */ int *head; /* hash buckets: head record index, or -1 */ int cap; /* bucket count (power of two) */ int n, ncap; /* records used / allocated */ } SbMutTab; static unsigned sb_mut_hash(const char *name, int key) { unsigned h = 2166136261u ^ (unsigned)key; for (const char *p = name; p && *p; p++) { h ^= (unsigned char)*p; h *= 16777619u; } return h; } /* `n` bounds the record count: the sweep adds at most one record per call node, so a table sized to the CallNode population never fills. */ static void sb_mut_tab_init(SbMutTab *t, int n) { int cap = 16; while (cap < n * 2) cap <<= 1; t->cap = cap; t->n = 0; t->ncap = n > 0 ? n : 1; t->name = (const char **)malloc(sizeof(const char *) * t->ncap); t->key = (int *)malloc(sizeof(int) * t->ncap); t->val = (signed char *)malloc(sizeof(signed char) * t->ncap); t->next = (int *)malloc(sizeof(int) * t->ncap); t->head = (int *)malloc(sizeof(int) * cap); for (int i = 0; i < cap; i++) t->head[i] = -1; } static void sb_mut_tab_free(SbMutTab *t) { free((void *)t->name); free(t->key); free(t->val); free(t->next); free(t->head); } /* Status cell for (name, key); appends a fresh 0 cell when `add` and absent. */ static signed char *sb_mut_tab_slot(SbMutTab *t, const char *name, int key, int add) { unsigned h = sb_mut_hash(name, key) & (unsigned)(t->cap - 1); for (int r = t->head[h]; r >= 0; r = t->next[r]) if (t->key[r] == key && sp_streq(t->name[r], name)) return &t->val[r]; if (!add || t->n >= t->ncap) return NULL; int r = t->n++; t->name[r] = name; t->key[r] = key; t->val[r] = 0; t->next[r] = t->head[h]; t->head[h] = r; return &t->val[r]; } /* -1 wins over 1 whichever order the calls appear, matching the scans' early `return -1`; 1 only lifts a cell still at 0. */ static void sb_mut_tab_note(SbMutTab *t, const char *name, int key, signed char v) { signed char *slot = sb_mut_tab_slot(t, name, key, 1); if (slot && (v < 0 || *slot == 0)) *slot = v; } static void sb_mut_tabs_build(Compiler *c, SbMutTab *lt, SbMutTab *it, SbMutTab *at, SbMutTab *iat, int toplevel) { const NodeTable *nt = c->nt; int ncalls = 0; nt_nodes_of_kind(nt, NK_CallNode, &ncalls); sb_mut_tab_init(lt, ncalls); sb_mut_tab_init(it, ncalls); sb_mut_tab_init(at, ncalls); sb_mut_tab_init(iat, ncalls); NT_FOREACH_KIND(nt, NK_CallNode, u) { int ur = nt_ref(nt, u, "receiver"); if (ur < 0) continue; NodeKind rk = nt_kind(nt, ur); if (rk != NK_LocalVariableReadNode && rk != NK_InstanceVariableReadNode) continue; const char *urn = nt_str(nt, ur, "name"); const char *un = nt_str(nt, u, "name"); if (!urn || !un) continue; size_t ul = strlen(un); Scope *us = comp_scope_of(c, ur); if (rk == NK_LocalVariableReadNode) { if (sp_str_mutator(un, SP_MUT_LOCAL)) { sb_mut_tab_note(lt, urn, us ? (int)(us - c->scopes) : -1, 1); sb_mut_tab_note(at, urn, us ? (int)(us - c->scopes) : -1, 1); } else if (ul > 0 && un[ul - 1] == '!') sb_mut_tab_note(lt, urn, us ? (int)(us - c->scopes) : -1, -1); continue; } signed char v = 0; /* slice! and setbyte used to count as disqualifying, which kept the ivar a plain string for good, and a mutation through its reader elsewhere (`obj.name << x`) then landed in a copy. */ if (sp_str_mutator(un, SP_MUT_IVAR)) v = 1; else if (ul > 0 && un[ul - 1] == '!') v = -1; int ucid = -1; if (us) ucid = us->class_id >= 0 ? us->class_id : toplevel; if (sp_str_mutator(un, SP_MUT_LOCAL)) sb_mut_tab_note(iat, urn, ucid, 1); if (v) sb_mut_tab_note(it, urn, ucid, v); } } static SbMutTab sb_local_mut_tab, sb_ivar_mut_tab, sb_local_anymut_tab, sb_ivar_anymut_tab; static const NodeTable *sb_mut_nt = NULL; static int sb_mut_ntc = -1; static int sb_mut_toplevel = -1; static unsigned sb_mut_ntver = 0; static unsigned sb_mut_gen = 0; static void sb_mut_tabs_sync(Compiler *c) { unsigned gen = comp_scope_index_gen(); /* A top-level ivar keys on the Toplevel class id, which comp_class_index only answers once that class has been seeded -- a fact that arrives independently of the scope-index epoch, so it joins the stamp rather than being resolved once and cached wrong (the #3227 P4 top-level case). */ int toplevel = c->toplevel_class; /* nt->version as well as count: a rename pass rewrites a call's `name` in place (nt_set_str) without growing the table, and this index is keyed by name. Worst case the version churns and we rebuild per query -- one kind-list walk, still less than the whole-table scan this replaces. */ if (comp_scope_index_is_frozen() && sb_mut_nt == c->nt && sb_mut_ntc == c->nt->count && sb_mut_ntver == c->nt->version && sb_mut_gen == gen && sb_mut_toplevel == toplevel) return; if (sb_mut_nt) { sb_mut_tab_free(&sb_local_mut_tab); sb_mut_tab_free(&sb_ivar_mut_tab); sb_mut_tab_free(&sb_local_anymut_tab); sb_mut_tab_free(&sb_ivar_anymut_tab); } sb_mut_tabs_build(c, &sb_local_mut_tab, &sb_ivar_mut_tab, &sb_local_anymut_tab, &sb_ivar_anymut_tab, toplevel); sb_mut_nt = c->nt; sb_mut_ntc = c->nt->count; sb_mut_ntver = c->nt->version; sb_mut_gen = gen; sb_mut_toplevel = toplevel; } /* In-place mutation status of string local `vn` in scope `vs`: 1 = mutated via a handle-capable mutator, -1 = disqualified (a mutator codegen cannot yet run in place on the handle), 0 = not mutated. */ static int strbuf_mut_kind(Compiler *c, const char *vn, Scope *vs) { if (!vn) return 0; sb_mut_tabs_sync(c); signed char *v = sb_mut_tab_slot(&sb_local_mut_tab, vn, vs ? (int)(vs - c->scopes) : -1, 0); return v ? *v : 0; } /* Is local `vn` in scope `vs` the receiver of any String in-place mutator, whatever other bang methods it also receives? A poly local answers an Array's map! as readily as a String's upcase!, so the -1 that keeps a typed string slot out of the handle set says nothing about it. */ static int strbuf_any_str_mut(Compiler *c, const char *vn, Scope *vs) { if (!vn) return 0; sb_mut_tabs_sync(c); signed char *v = sb_mut_tab_slot(&sb_local_anymut_tab, vn, vs ? (int)(vs - c->scopes) : -1, 0); return v && *v == 1; } /* In-place mutation status of ivar `nm` of class `cid` (same contract as strbuf_mut_kind). The supported set is narrower: the shadow-copy shim cannot rename an ivar, so []=/insert/slice!/setbyte disqualify. */ /* True if ivar `nm` of class `cid` is handed to a method as an argument anywhere in that class's own scopes. Such a call may take the slot by reference, which only works when the receiver itself is a heap object. A keyword hands over its value just as a positional argument does. */ static int an_ivar_passed_as_arg(Compiler *c, int cid, const char *nm) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int args = nt_ref(nt, id, "arguments"); if (args < 0) continue; int an = 0; const int *av = nt_arr(nt, args, "arguments", &an); for (int k = 0; k < an; k++) { int en = 1; const int *el = &av[k]; int kw = nt_kind(nt, av[k]) == NK_KeywordHashNode; if (kw) el = nt_arr(nt, av[k], "elements", &en); for (int e = 0; e < en; e++) { int v = kw ? nt_ref(nt, el[e], "value") : el[e]; if (nt_kind(nt, v) != NK_InstanceVariableReadNode) continue; const char *ivn = nt_str(nt, v, "name"); if (!ivn || !nm || !sp_streq(ivn, nm)) continue; Scope *sc = comp_scope_of(c, v); if (sc && sc->class_id == cid) return 1; } } } return 0; } static int strbuf_ivar_mut_kind(Compiler *c, int cid, const char *nm) { if (!nm) return 0; sb_mut_tabs_sync(c); signed char *v = sb_mut_tab_slot(&sb_ivar_mut_tab, nm, cid, 0); return v ? *v : 0; } /* The ivar node a local write's value hands over as the slot's own object: a read, or a plain / or / and write of it (the value of the write is the slot), through single-statement parentheses. A read is also reached through the calls whose value is their receiver -- `+@s` (the slot's own String unless it is frozen), an append chain `@s << x`, and the String methods str_self_call names (`@s.to_s`, `@s.insert(i, x)`) -- since the local then names the slot's object as `l = @s` does. -1 for anything else. */ int strbuf_ivar_alias_value(const NodeTable *nt, int v) { int stepped = 0; for (;;) { while (v >= 0 && nt_kind(nt, v) == NK_ParenthesesNode) { int body = nt_ref(nt, v, "body"); int n = 0; const int *st = body >= 0 && nt_kind(nt, body) == NK_StatementsNode ? nt_arr(nt, body, "body", &n) : NULL; if (!st || n != 1) return -1; v = st[0]; } if (v < 0 || nt_kind(nt, v) != NK_CallNode) break; const char *cn = nt_str(nt, v, "name"); int cr = nt_ref(nt, v, "receiver"); int ca = nt_ref(nt, v, "arguments"); int cac = 0; if (ca >= 0) nt_arr(nt, ca, "arguments", &cac); if (!cn || cr < 0 || nt_ref(nt, v, "block") >= 0) return -1; if (!((cac == 1 && (is_append_concat(cn))) || (cac == 0 && sp_streq(cn, "+@")) || str_self_call(nt, v))) return -1; v = cr; stepped = 1; } if (v < 0) return -1; NodeKind k = nt_kind(nt, v); /* a write under such a call runs inside the call's own value, which the write-handle emitter would skip */ if (stepped) return k == NK_InstanceVariableReadNode ? v : -1; return k == NK_InstanceVariableReadNode || k == NK_InstanceVariableWriteNode || k == NK_InstanceVariableOrWriteNode || k == NK_InstanceVariableAndWriteNode ? v : -1; } /* Promote ivar slot (cid, iv) to the shared handle if eligible; returns 1 on a state change. */ static int strbuf_promote_ivar(Compiler *c, int cid, const char *nm) { if (cid < 0) return 0; ClassInfo *ci = &c->classes[cid]; int iv = comp_ivar_index(ci, nm); if (iv < 0) return 0; if (ci->ivar_types[iv] != TY_STRING && ci->ivar_types[iv] != TY_STRBUF) return 0; if (ci->ivar_types[iv] == TY_STRBUF && ci->ivar_str_shared[iv]) return 0; /* An inherited slot has one representation in the whole family: an ancestor's methods reach it through their own struct. So the promotion starts at the topmost ancestor holding the String slot, and a class pinned below one that is not (an --rbs seed on a subclass's writer) no longer leaves the two apart (#8390). */ int top = cid; for (int p = c->classes[cid].parent; p >= 0; p = c->classes[p].parent) { int pi = comp_ivar_index(&c->classes[p], nm); if (pi < 0) break; TyKind pt = c->classes[p].ivar_types[pi]; if (pt != TY_STRING && pt != TY_STRBUF) break; top = p; } ClassInfo *tc = &c->classes[top]; int ti = comp_ivar_index(tc, nm); ivar_set_type(tc, ti, TY_STRBUF); { tc->ivar_str_shared[ti] = 1; comp_touch_class(tc->idx); } ivar_set_type(ci, iv, TY_STRBUF); { ci->ivar_str_shared[iv] = 1; comp_touch_class(ci->idx); } /* The inherited field has this representation in every descendant, including one with no ivar node of its own (and so no share holder): the ancestor's methods reach it through their own struct, in either build. sh_ivar already joins a child's holder to its ancestor's; carry the promotion to the passive copies of that slot. A subclass constructed alone (`B.new(buf)` with A's methods appending to the slot) was left a value slot and its layout refused. */ { int nk = 0; const int *ks = comp_descendants(c, top, &nk); for (int k = 0; k < nk; k++) { ClassInfo *sc = &c->classes[ks[k]]; int si = comp_ivar_index(sc, nm); if (si < 0 || (sc->ivar_types[si] != TY_STRING && sc->ivar_types[si] != TY_STRBUF)) continue; ivar_set_type(sc, si, TY_STRBUF); { sc->ivar_str_shared[si] = 1; comp_touch_class(sc->idx); } } } return 1; } /* Every String instance variable, class variable and global holds the handle, as every String local does (one-string) */ static int all_handle_static_slots(Compiler *c) { int changed = 0; for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; for (int i = 0; i < ci->nivars; i++) if (ci->ivar_types[i] == TY_STRING || (ci->ivar_types[i] == TY_STRBUF && !ci->ivar_str_shared[i])) changed |= strbuf_promote_ivar(c, k, ci->ivars[i]); for (int i = 0; i < ci->ncvars; i++) if (ci->cvar_types[i] == TY_STRING) { cvar_set_type(ci, i, TY_STRBUF); ci->cvar_str_shared[i] = 1; changed = 1; } } for (int g = 0; g < c->ngvars; g++) { LocalVar *gv = &c->gvars[g]; if (gv->type != TY_STRING || gv->rbs_seeded) continue; lv_set_type(gv, TY_STRBUF); { gv->str_shared = 1; comp_touch_scope(gv->owner); } changed = 1; } return changed; } /* The class whose C-global ivar slots `self` reads at `node`: Toplevel at top level or in a top-level method, the class in one of its class methods; -1 elsewhere. */ static int an_static_self(Compiler *c, int node) { Scope *s = comp_scope_of(c, node); if (!s) return -1; if (s->is_cmethod) return s->class_id; if (s->class_id >= 0) return -1; return c->toplevel_class; } /* The ivar a method of class `sc` (a class method when `cm`, else a top-level method) reads or memoizes (`def buf = (@buf ||= +"")`), owned by the class: the slot a call of it hands out. */ static const char *an_static_reader_ivar(Compiler *c, int call, int sc, int cm, const char *mn, int *defc, char *buf, size_t cap) { if (sc < 0 || !mn) return NULL; int owner = sc; int rmi = cm ? call_target(c, call, sc, mn, CT_CMETHOD, &owner) : call_target(c, call, -1, mn, CT_TOP, NULL); if (rmi < 0) return NULL; if (!c->scopes[rmi].is_cmethod && c->scopes[rmi].class_id >= 0) return NULL; const char *ivn = an_memo_reader_ivar(c, rmi); int last = scope_body_last(c, rmi); if (!ivn && last >= 0 && nt_kind(c->nt, last) == NK_InstanceVariableReadNode) ivn = nt_str(c->nt, last, "name"); if (!ivn) return NULL; snprintf(buf, cap, "%s", ivn); *defc = owner; return buf; } /* A reflective read, `obj.instance_variable_get(:@iv)` with a literal name, of an ivar in the layout of a statically typed object's class: the slot it reads (emit_object_ivar_call), as a reader of that ivar hands it out. The ivar's name into buf and *defc the class, else NULL. */ static const char *an_ivar_get_ivar_of(Compiler *c, int node, int *defc, char *buf, size_t cap) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, node, "name"); int recv = nt_ref(nt, node, "receiver"), a = nt_ref(nt, node, "arguments"), argc = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &argc) : NULL; if (!nm || recv < 0 || argc != 1 || bop_share_named(BOP_ANY_RECV, nm) != BSH_IVAR_GET) return NULL; NodeKind ak = nt_kind(nt, av[0]); const char *sym = ak == NK_SymbolNode ? nt_str(nt, av[0], "value") : ak == NK_StringNode ? nt_str(nt, av[0], "content") : NULL; TyKind rt = infer_type(c, recv); if (!sym || sym[0] != '@' || !ty_is_object(rt)) return NULL; int cid = ty_object_class(rt); if (cid < 0 || cid >= c->nclasses || c->classes[cid].is_native_class || call_target(c, node, cid, nm, 0, NULL) >= 0) return NULL; /* (a Struct's members are no ivars of CRuby's: a read answers nil) */ int iv = comp_ivar_index(&c->classes[cid], sym); if (iv < 0 || (c->classes[cid].is_struct && iv < c->classes[cid].nmembers)) return NULL; snprintf(buf, cap, "%s", sym); *defc = cid; return buf; } /* When `node` is an argument-less reader call (attr or a simple `def m = @iv` method) over an object receiver or implicit self, resolve the backing ivar; so is a reflective read of a literal ivar name (an_ivar_get_ivar_of): returns the ivar name (into buf) and sets *defc, else NULL. */ static const char *an_reader_ivar_of(Compiler *c, int node, int *defc, char *buf, size_t cap) { const NodeTable *nt = c->nt; if (node < 0 || nt_kind(nt, node) != NK_CallNode) return NULL; if (nt_ref(nt, node, "block") >= 0) return NULL; { const char *ig = an_ivar_get_ivar_of(c, node, defc, buf, cap); if (ig) return ig; } { int a2 = nt_ref(nt, node, "arguments"); int an2 = 0; if (a2 >= 0) nt_arr(nt, a2, "arguments", &an2); if (an2 != 0) return NULL; } int rrecv = nt_ref(nt, node, "receiver"); int rcid; if (rrecv < 0) { rcid = ie_receiverless_self_class(c, node); if (rcid < 0) { Scope *ns = comp_scope_of(c, node); return an_static_reader_ivar(c, node, an_static_self(c, node), ns && ns->is_cmethod, nt_str(nt, node, "name"), defc, buf, cap); } } else { TyKind rrt = infer_type(c, rrecv); if (rrt == TY_CLASS) return an_static_reader_ivar(c, node, class_recv_static_ci(c, rrecv), 1, nt_str(nt, node, "name"), defc, buf, cap); if (!ty_is_object(rrt)) return NULL; rcid = ty_object_class(rrt); } const char *mn = nt_str(nt, node, "name"); if (!mn) return NULL; *defc = rcid; if (comp_reader_in_chain(c, rcid, mn, defc)) { snprintf(buf, cap, "@%s", comp_resolve_alias(c, rcid, mn)); return buf; } int rmi = call_target(c, node, rcid, mn, 0, defc); if (rmi < 0) return NULL; int last2 = scope_body_last(c, rmi); /* a memoizing `def m = (@iv ||= v)` hands out the slot too */ const char *ivn = an_memo_reader_ivar(c, rmi); if (!ivn && last2 >= 0 && nt_kind(nt, last2) == NK_InstanceVariableReadNode) ivn = nt_str(nt, last2, "name"); if (!ivn) return NULL; snprintf(buf, cap, "%s", ivn); if (*defc < 0) *defc = c->scopes[rmi].class_id; return buf; } /* Does local `vn` participate in a pure alias (`x = vn` or `vn = x`)? */ /* The LocalVariableReadNode an aliasing string write bottoms out at: a bare local read, or a value-position `<<`/`concat` chain over one (the chain's value IS its base object). Parens unwrap. -1 when the shape is neither. */ static int an_strbuf_alias_source(Compiler *c, int v) { const NodeTable *nt = c->nt; int via_self = 0; for (int depth = 0; v >= 0 && depth < 64; depth++) { const char *vt = nt_type(nt, v); NodeKind vk = nt_kind(nt, v); if (!vt) return -1; if (vk == NK_ParenthesesNode) { int pb = nt_ref(nt, v, "body"); int bn = 0; const int *bb = pb >= 0 ? nt_arr(nt, pb, "body", &bn) : NULL; if (bn != 1) return -1; v = bb[0]; continue; } /* `to_s` and the rest are any class's names: only a String's answer is the String itself */ if (vk == NK_LocalVariableReadNode) { if (via_self) { Scope *vs = comp_scope_of(c, v); LocalVar *vl = vs ? scope_local(vs, nt_str(nt, v, "name")) : NULL; if (!vl || (vl->type != TY_STRING && vl->type != TY_STRBUF)) return -1; } return v; } /* a chained assignment (`u = t = s`): its value is t's object, and the write node names t as a read does */ if (vk == NK_LocalVariableWriteNode) return v; if (vk == NK_CallNode) { const char *cn = nt_str(nt, v, "name"); int cr = nt_ref(nt, v, "receiver"); int ca = nt_ref(nt, v, "arguments"); int cac = 0; if (ca >= 0) nt_arr(nt, ca, "arguments", &cac); if (cn && (is_append_concat(cn)) && cr >= 0 && cac == 1) { v = cr; continue; } /* +s is s itself unless s is frozen (sp_String_uplus decides) */ if (cn && sp_streq(cn, "+@") && cr >= 0 && cac == 0) { v = cr; continue; } /* the String methods whose value is their receiver (str_self_call) */ if (str_self_call(nt, v)) { v = cr; via_self = 1; continue; } return -1; } return -1; } return -1; } /* The local reads a write's value may hand over as the written object: the one an_strbuf_alias_source finds, or each arm's of a conditional (`h = c ? x : g`, `if`/`unless`, `g || d`, the right of `a && g`, each `when` and `else` of a `case`), an arm that is another value being a fresh String the write wraps. Each such read is a second name for the target on the path that takes it, so it joins the target's alias set (promote_local_alias_pairs), and the write emits the handle per arm (emit_strbuf_cond_value). Answers the count written to `out` (at most `cap`). */ typedef struct { Compiler *c; int *out; int cap, n; } AliasLeaves; static int alias_leaf(const VpLeaf *lf, void *ud) { AliasLeaves *a = ud; if (lf->kind == VPK_NIL || lf->kind == VPK_GAVE_UP) return 0; int one = an_strbuf_alias_source(a->c, lf->node); if (one >= 0) a->out[a->n++] = one; return a->n >= a->cap; } /* (the arms are read through the iterator: if, unless, case, `||` both operands and `&&` its right; a missing arm is nil, and the walk has no depth) */ static int an_strbuf_alias_leaves(Compiler *c, int v, int *out, int cap) { if (v < 0 || cap <= 0) return 0; AliasLeaves a = { c, out, cap, 0 }; for_each_value_position(c->nt, v, VPS_IF | VPS_UNLESS | VPS_CASE | VPS_LOGIC, alias_leaf, &a); return a.n; } /* The local read a multiple assignment's value hands target `t` (a LocalVariableTargetNode among its `lefts`): `t, u = s, 1` makes t another name for s, element for target, when the value is an Array literal with no splat. -1 for anything else. */ static int an_masgn_alias_in(Compiler *c, int lhs, int value, int t, int depth) { const NodeTable *nt = c->nt; if (value < 0 || nt_kind(nt, value) != NK_ArrayNode || depth > 8) return -1; int ln = 0; const int *lefts = nt_arr(nt, lhs, "lefts", &ln); int en = 0; const int *els = nt_arr(nt, value, "elements", &en); for (int i = 0; i < en; i++) if (nt_kind(nt, els[i]) == NK_SplatNode) return -1; for (int i = 0; i < ln && i < en; i++) { if (lefts[i] == t) return an_strbuf_alias_source(c, els[i]); /* `(t, u), v = [s, 1], 2`: a nested target list takes an element that is an Array literal the same way */ if (nt_kind(nt, lefts[i]) == NK_MultiTargetNode) { int r = an_masgn_alias_in(c, lefts[i], els[i], t, depth + 1); if (r >= 0) return r; } } return -1; } static int an_masgn_alias_source(Compiler *c, int mw, int t) { const NodeTable *nt = c->nt; int ln = 0; const int *lefts = nt_arr(nt, mw, "lefts", &ln); int value = nt_ref(nt, mw, "value"); if (value < 0 || nt_kind(nt, value) != NK_ArrayNode) return -1; int en = 0; const int *els = nt_arr(nt, value, "elements", &en); for (int i = 0; i < en; i++) if (nt_kind(nt, els[i]) == NK_SplatNode) return -1; for (int i = 0; i < ln && i < en; i++) if (lefts[i] == t) return an_strbuf_alias_source(c, els[i]); return -1; } /* The aliasing writes (`b = a`, `b = a << x`) of every scope, collected in one walk of the table. Asked once per call site of a lent parameter, a walk each made the pass (sites x nodes): a method defined in a thousand classes, each calling its own with a local, is a thousand sites. The writes are syntax, so one collection serves a whole pass. */ typedef struct { int *head, *next; const char **wn, **rn; int n; } ALocalAliases; /* A cache cut short would read a missing alias as none, and promotion would keep a slot or a copy where the handle is needed: a failed allocation stops the compile instead, as argov_reserve does. */ static void an_local_aliases_add(ALocalAliases *t, int *cap, int si, const char *wn, const char *rn) { if (!wn || !rn || sp_streq(wn, rn)) return; if (t->n == *cap) { *cap = *cap ? *cap * 2 : 64; int *nn = (int *)realloc(t->next, sizeof(int) * (size_t)*cap); const char **nw = nn ? (const char **)realloc(t->wn, sizeof(char *) * (size_t)*cap) : NULL; const char **nr = nw ? (const char **)realloc(t->rn, sizeof(char *) * (size_t)*cap) : NULL; if (!nn || !nw || !nr) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } t->next = nn; t->wn = nw; t->rn = nr; } t->wn[t->n] = wn; t->rn[t->n] = rn; t->next[t->n] = t->head[si]; t->head[si] = t->n; t->n++; } static void an_local_aliases_build(Compiler *c, ALocalAliases *t) { const NodeTable *nt = c->nt; memset(t, 0, sizeof *t); t->head = (int *)malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); if (!t->head) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < c->nscopes; i++) t->head[i] = -1; int cap = 0; for (int w = comp_kind_first(c, NK_LocalVariableWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; Scope *ws = comp_scope_of(c, w); int si = ws ? (int)(ws - c->scopes) : -1; if (si < 0 || si >= c->nscopes) continue; const char *wn = nt_str(nt, w, "name"); int lv[16]; int nl = an_strbuf_alias_leaves(c, nt_ref(nt, w, "value"), lv, 16); for (int l = 0; l < nl; l++) an_local_aliases_add(t, &cap, si, wn, nt_str(nt, lv[l], "name")); } /* `h ||= g`, `h &&= g` */ for (int k = 0; k < 2; k++) for (int w = comp_kind_first(c, k ? NK_LocalVariableAndWriteNode : NK_LocalVariableOrWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != (k ? NK_LocalVariableAndWriteNode : NK_LocalVariableOrWriteNode)) continue; Scope *ws = comp_scope_of(c, w); int si = ws ? (int)(ws - c->scopes) : -1; if (si < 0 || si >= c->nscopes) continue; int lv[16]; int nl = an_strbuf_alias_leaves(c, nt_ref(nt, w, "value"), lv, 16); for (int l = 0; l < nl; l++) an_local_aliases_add(t, &cap, si, nt_str(nt, w, "name"), nt_str(nt, lv[l], "name")); } /* `t, u = s, 1`: each target an element names */ for (int mw = comp_kind_first(c, NK_MultiWriteNode); mw >= 0; mw = comp_kind_next(c, mw)) { if (nt_kind(nt, mw) != NK_MultiWriteNode) continue; Scope *ws = comp_scope_of(c, mw); int si = ws ? (int)(ws - c->scopes) : -1; if (si < 0 || si >= c->nscopes) continue; int ln = 0; const int *lefts = nt_arr(nt, mw, "lefts", &ln); for (int i = 0; i < ln; i++) { if (nt_kind(nt, lefts[i]) != NK_LocalVariableTargetNode) continue; int sv = an_masgn_alias_source(c, mw, lefts[i]); if (sv >= 0) an_local_aliases_add(t, &cap, si, nt_str(nt, lefts[i], "name"), nt_str(nt, sv, "name")); } } } static void an_local_aliases_free(ALocalAliases *t) { free(t->head); free(t->next); free(t->wn); free(t->rn); memset(t, 0, sizeof *t); } static int an_local_has_alias(Compiler *c, const ALocalAliases *t, const char *vn, Scope *vs) { int si = vs ? (int)(vs - c->scopes) : -1; if (!t->head || si < 0 || si >= c->nscopes) return 0; for (int r = t->head[si]; r >= 0; r = t->next[r]) if (sp_streq(t->wn[r], vn) || sp_streq(t->rn[r], vn)) return 1; return 0; } static int an_param_mutated_in_place(Compiler *c, int mi, int pi); static int an_local_pure_alias_of(Compiler *c, int mi, const char *ln, const char *pn, int depth); /* Does scope `bs` hand local `vn` to a user method's parameter that the method appends to in place (`grow(t)`)? Asked inside the fixpoint, so the method's body answers, not the byref slots computed after it. */ static int an_local_handed_to_appender(Compiler *c, Scope *bs, const char *vn) { const NodeTable *nt = c->nt; int si = (int)(bs - c->scopes); for (int u = comp_scall_first(c, si); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || comp_scope_of(c, u) != bs) continue; int mi = an_call_target_mi(c, u); if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams; j++) { int an = arg_layout_param_node(c, m, u, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode && nt_str(nt, an, "name") && sp_streq(nt_str(nt, an, "name"), vn) && an_param_mutated_in_place(c, mi, j)) return 1; } } return 0; } /* Is block parameter `bp` (of block scope `bs`) appended to through another name: a local only ever written from it (`t = x`, or a chain `u = t`) that is appended to in place, handed to a method that appends to it, or is the shared handle? The element the iterator bound is then mutated as surely as by `x << y`, so the parameter has to be the handle the alias shares (promote_local_alias_pairs). */ static int an_block_param_alias_mutated(Compiler *c, const ALocalAliases *t, Scope *bs, const char *bp) { int si = bs ? (int)(bs - c->scopes) : -1; if (!t->head || si < 0 || si >= c->nscopes || !bp) return 0; const char *names[8]; int nn = 0; names[nn++] = bp; for (int i = 0; i < nn; i++) for (int r = t->head[si]; r >= 0; r = t->next[r]) { if (!sp_streq(t->rn[r], names[i])) continue; const char *wn = t->wn[r]; int seen = 0; for (int j = 0; j < nn && !seen; j++) seen = sp_streq(names[j], wn); if (seen) continue; LocalVar *wl = scope_local(bs, wn); if (!wl || wl->is_param || wl->is_cell) continue; /* only a local that is never another String: one also rebound (`line = line.strip`, the rebinding #5669 desugars a reassigned parameter to) appends to what it was rebound to */ if (!an_local_pure_alias_of(c, si, wn, bp, 0)) continue; if (strbuf_mut_kind(c, wn, bs) == 1 || (wl->type == TY_STRBUF && wl->str_shared) || an_local_handed_to_appender(c, bs, wn)) return 1; if (nn < 8) names[nn++] = wn; } return 0; } /* Is local `to` written, in scope si, from local `from` (`to = from`), or through a chain of such writes? */ static int an_local_aliases_reach(const ALocalAliases *t, int si, const char *from, const char *to) { if (!t->head || si < 0 || !from || !to) return 0; const char *names[8]; int nn = 0; names[nn++] = from; for (int i = 0; i < nn; i++) for (int r = t->head[si]; r >= 0; r = t->next[r]) { if (!sp_streq(t->rn[r], names[i])) continue; if (sp_streq(t->wn[r], to)) return 1; int seen = 0; for (int j = 0; j < nn && !seen; j++) seen = sp_streq(names[j], t->wn[r]); if (!seen && nn < 8) names[nn++] = t->wn[r]; } return 0; } /* Is this argument node a shared-handle slot read (a str_shared local or a shared ivar)? */ static int strbuf_container_stores_string(Compiler *c, const char *contn, Scope *conts); static int strbuf_container_stores_nonstring(Compiler *c, const char *contn, Scope *conts); /* A receiverless builtin raise leaves no value for a tail to share. An override in the enclosing class's chain can return a value. */ static int an_call_raises(Compiler *c, int node) { const NodeTable *nt = c->nt; return node >= 0 && nt_kind(nt, node) == NK_CallNode && nt_ref(nt, node, "receiver") < 0 && nt_str(nt, node, "name") && is_raise_alias(nt_str(nt, node, "name")) && !an_bare_call_class_owned(c, node) && call_target(c, node, -1, nt_str(nt, node, "name"), CT_TOP, NULL) < 0; } /* Is the last statement of statement list `st` a shared handle's slot (an_arg_is_shared_handle)? */ static int an_stmts_last_shared(Compiler *c, int st) { int n = 0; const int *b = st >= 0 && nt_kind(c->nt, st) == NK_StatementsNode ? nt_arr(c->nt, st, "body", &n) : NULL; if (n == 0) return 0; /* an arm that raises leaves no value (a method's body ahead of its rescue, `def r(x); raise "e"; rescue; x; end`) */ int l = b[n - 1]; if (an_call_raises(c, l)) return 1; return an_arg_is_shared_handle(c, l); } int an_arg_is_shared_handle(Compiler *c, int node) { const NodeTable *nt = c->nt; if (node < 0) return 0; if (repr_self_shared(c, node)) return 1; int ops[3]; if (strbuf_route_clamp(c, node, ops)) return 1; if (nt_kind(nt, node) == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, node, "name"); Scope *vs = vn ? comp_scope_of(c, node) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; /* str_shared is the durable flag; the slot type may transiently read TY_STRING until the post-fixpoint re-assert runs */ return lv && lv->str_shared && (lv->type == TY_STRBUF || lv->type == TY_STRING); } if (nt_kind(nt, node) == NK_InstanceVariableReadNode) { const char *vn = nt_str(nt, node, "name"); int cid = vn ? comp_ivar_owner(c, node) : -1; if (cid < 0) return 0; int iv = comp_ivar_index(&c->classes[cid], vn); return iv >= 0 && c->classes[cid].ivar_types[iv] == TY_STRBUF && c->classes[cid].ivar_str_shared[iv]; } /* a global holding the handle (--share-strings): its read, or its write (`($g = s)`), whose value is the slot */ { int g = node; while (g >= 0 && nt_kind(nt, g) == NK_ParenthesesNode) { int body = nt_ref(nt, g, "body"); int k = 0; const int *st = body >= 0 && nt_kind(nt, body) == NK_StatementsNode ? nt_arr(nt, body, "body", &k) : NULL; g = k > 0 ? st[k - 1] : -1; } if (g >= 0 && repr_static_share(c, g)) return 1; /* any write whose slot holds the rule's handle (an ivar's `=`, `||=`, `&&=`): its value is that slot, and the write publishes it */ if (g >= 0 && repr_write_share(c, g)) return 1; /* a begin's value is its body's last value, or a rescue's or its else's: each must be one (an ensure's is dropped, and so is the body's when an else follows it) */ if (g >= 0 && nt_kind(nt, g) == NK_BeginNode) { int el = nt_ref(nt, g, "else_clause"); int ok = el >= 0 || an_stmts_last_shared(c, nt_ref(nt, g, "statements")); for (int rc = nt_ref(nt, g, "rescue_clause"); ok && rc >= 0; rc = nt_ref(nt, rc, "subsequent")) ok = an_stmts_last_shared(c, nt_ref(nt, rc, "statements")); if (ok && el >= 0) ok = an_stmts_last_shared(c, nt_ref(nt, el, "statements")); return ok; } /* `x.then { |v| v }`: a block that is one read of a handle answers it, and that read publishes it */ if (g >= 0 && nt_kind(nt, g) == NK_CallNode && nt_str(nt, g, "name") && is_then_alias(nt_str(nt, g, "name")) && nt_ref(nt, g, "receiver") >= 0 && nt_ref(nt, g, "arguments") < 0) { int blk = nt_ref(nt, g, "block"); int body = blk >= 0 && nt_kind(nt, blk) == NK_BlockNode ? nt_ref(nt, blk, "body") : -1; int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; return bn == 1 && nt_kind(nt, bb[0]) == NK_LocalVariableReadNode && an_arg_is_shared_handle(c, bb[0]); } } /* `h[:k]` / `a[0]` -- an element of a container that holds strings. The container-store rules make those elements shared handles as soon as one is mutated through, so the element read hands a handle over the same way a promoted local does; counting it as a plain value left the OTHER call sites' plain locals unpromoted, and their appends died in a copy. */ if (container_elem_read_p(nt, node)) { int cr = nt_ref(nt, node, "receiver"); if (cr < 0 || nt_kind(nt, cr) != NK_LocalVariableReadNode) return 0; const char *cn = nt_str(nt, cr, "name"); Scope *cs = comp_scope_of(c, cr); return cn && cs && strbuf_container_stores_string(c, cn, cs) && !strbuf_container_stores_nonstring(c, cn, cs); } return 0; } static int strbuf_slot_eligible(Compiler *c, const char *vn, Scope *vs, LocalVar *lv); static int strbuf_mut_kind(Compiler *c, const char *vn, Scope *vs); /* Demand every string stored into container local (contn, conts) to be a shared handle: stored eligible locals promote, everything else marks for a fresh-handle wrap at the store site. Returns changed. */ /* True when some value STORED into container local `contn` is a string: the evidence that makes a mutator on an element read out of it a string mutation. Without it a name-keyed mutator table cannot tell `out << "x"` (a string append) from `b0 << 4` (an integer shift), and an Integer element bound to a local was promoted to a string handle (#3971). */ /* The element a COLLECTING iterator stores into the array it builds: the tail of its block. `["a"].map { |s| s.dup }` fills the array with fresh mutable strings exactly as `["a".dup]` does, and the two shapes have to be read the same way -- only the literal was, so mutating an element of a mapped array was silently dropped (#4037). Answers -1 when the value is not one. */ static int strbuf_map_block_tail(Compiler *c, int val) { const NodeTable *nt = c->nt; if (val < 0 || nt_kind(nt, val) != NK_CallNode) return -1; const char *mn = nt_str(nt, val, "name"); /* map / collect only: flat_map's block tail is an ARRAY of elements, not one element, so its stores are a level deeper than this reads. That shape is still dropped. */ if (!mn || !(is_map_alias(mn))) return -1; int blk = nt_ref(nt, val, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return -1; /* A user method of the name that answers every receiver the call can have (CP_DIRECT) returns what it returns, not its block's values. */ if (cplan_user_fresh(c, val)->dispatch == CP_DIRECT) return -1; int body = nt_ref(nt, blk, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) return -1; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); return bn > 0 ? bb[bn - 1] : -1; } /* The values a store walk collects, grown to the call's or literal's whole length: a fixed buffer left the values past it out of the walk (a `push` or an Array literal of 70 Strings). */ typedef struct { int *v; int n, cap; } StoreVals; static void store_vals_add(StoreVals *sv, int node) { if (sv->n == sv->cap) { sv->cap = sv->cap ? sv->cap * 2 : 16; sv->v = realloc(sv->v, sizeof(int) * (size_t)sv->cap); if (!sv->v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } sv->v[sv->n++] = node; } /* The values Array call w (`<<`, push, unshift, `[]=`, prepend and insert, a store with a block, fill) stores into its receiver, into sv; answers how many. insert's first argument is the index. */ static int array_call_store_values(Compiler *c, int w, StoreVals *sv) { const NodeTable *nt = c->nt; const char *wcn = nt_str(nt, w, "name"); int a = nt_ref(nt, w, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; sv->n = 0; if (!wcn) return 0; int add = array_unseen_add_kind(wcn); /* prepend and insert under --share-strings, whose rule demands what they store (the default build refuses a named String they add: sa_refuse) */ if (is_push_unshift(wcn) || ((add == ARRAY_ADD_PREPEND || (add == ARRAY_ADD_INSERT && an >= 2)))) for (int e = add == ARRAY_ADD_INSERT ? 1 : 0; e < an; e++) store_vals_add(sv, av[e]); else if (is_index_assign(wcn) && an >= 2) store_vals_add(sv, av[an - 1]); /* under --share-strings a store with a block (not rewritten to []=) stores its value as well (#6765) */ else if (is_store_alias(wcn) && an == 2 && nt_ref(nt, w, "block") >= 0) store_vals_add(sv, av[1]); else if (is_fill_name(wcn) && an >= 1 && an <= 3 && nt_ref(nt, w, "block") < 0) store_vals_add(sv, av[0]); else if (is_fill_name(wcn) && an <= 2) { int blk = nt_ref(nt, w, "block"); int body = blk >= 0 && nt_kind(nt, blk) == NK_BlockNode ? nt_ref(nt, blk, "body") : -1; int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; if (bn > 0) store_vals_add(sv, bb[bn - 1]); } /* under --share-strings the pushes chained on a push answer its container too (`o << a << b`): their values are stored as a separate `o << b` stores b, which the store walks otherwise never see for a call whose receiver is not the container itself */ if (is_push_unshift(wcn)) for (int p = comp_recv_parent(c, w); p >= 0 && nt_kind(nt, p) == NK_CallNode; p = comp_recv_parent(c, p)) { const char *pn = nt_str(nt, p, "name"); if (!pn || !is_push_unshift(pn)) break; int pa = nt_ref(nt, p, "arguments"), pan = 0; const int *pav = pa >= 0 ? nt_arr(nt, pa, "arguments", &pan) : NULL; for (int e = 0; e < pan; e++) store_vals_add(sv, pav[e]); } return sv->n; } /* The values node `w` stores into container local (contn, conts): the elements of an array/hash literal written to it (with `map_tail`, also a collecting iterator's block tail), or the arguments of a push/<nt; sv->n = 0; NodeKind wk = nt_kind(nt, w); if (wk == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, contn) || comp_scope_of(c, w) != conts) return 0; int val = nt_ref(nt, w, "value"); if (val < 0) return 0; NodeKind vk = nt_kind(nt, val); if (vk == NK_ArrayNode) { int en = 0; const int *el = nt_arr(nt, val, "elements", &en); for (int e = 0; e < en; e++) store_vals_add(sv, el[e]); } else if (map_tail && strbuf_map_block_tail(c, val) >= 0) store_vals_add(sv, strbuf_map_block_tail(c, val)); else if (vk == NK_HashNode || vk == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, val, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_AssocNode) store_vals_add(sv, nt_ref(nt, el[e], "value")); } } else if (wk == NK_CallNode) { int wr = nt_ref(nt, w, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode) return 0; const char *wrn = nt_str(nt, wr, "name"); if (!wrn || !sp_streq(wrn, contn) || comp_scope_of(c, wr) != conts) return 0; array_call_store_values(c, w, sv); } return sv->n; } /* What a walk over the values stored into a container does with each one: demand it into a shared handle, or ask whether it is (provably not) a string. The bits above SB_KIND_MASK count how many container levels lie between the walked expression and the strings: `r[0][0] << "!"` walks `r` one level out, so the values stored into r are themselves containers whose stores are walked in turn. */ enum { SB_DEMAND, SB_HAS_STRING, SB_HAS_NONSTRING, SB_DEMAND_NAMED, SB_KIND_MASK = 3, SB_NEST1 = 4 }; #define SB_KIND(m) ((m) & SB_KIND_MASK) /* What one outermost store walk has already walked: an expression, a method's return, an ivar's or a local container's stores, each by mode, with the shallowest depth it was walked at and its answer. The walks reach the same method or container along many paths -- a method's return through every call naming it, a local through each read of it -- and each reached it again in full, so a large program's walk grew with the number of paths, not of the things walked. A thing walked again within the same walk, no shallower than before, answers what it answered (0 while its walk is still running: a cycle adds nothing); its demands are already made. The memo is dropped when the outermost walk returns. Inside the share pass (sb_share_begin / sb_share_end) it is kept across the outermost walks instead: the pass starts one from every holder slot and every store it visits, and the walks fan out over the same methods, containers and parameters, so each slot walked the web the one before it had walked. A thing an earlier walk found changing nothing, in the same state, changes nothing now, and its demands are made: a generation ends wherever the pass reports a change (sb_share_step, as a walk begins: the pass's `changed` is folded into `any` and cleared), so no walk reads a record left before a write it has not seen, and the records that survive to a later walk all answer 0 -- a walk that changed something ended the generation it wrote in. */ enum { SBM_EXPR, SBM_RET, SBM_IVAR, SBM_LOCAL }; typedef struct { unsigned long long key; int depth, res, gen, walk; } SbMemo; static SbMemo *sb_memo; static int sb_memo_cap, sb_memo_n, sb_memo_gen = 1, sb_walk_nest, sb_walk_id; static int sb_shared, *sb_share_changed, *sb_share_any, *sb_share_prev_changed, *sb_share_prev_any; /* the pass's `changed` and `any`; the generation ends here, and again at the end */ static void sb_share_begin(int *changed, int *any) { sb_share_prev_changed = sb_share_changed; sb_share_prev_any = sb_share_any; /* (the pass around the rule: one level) */ sb_shared++; sb_memo_gen++; sb_memo_n = 0; sb_share_changed = changed; sb_share_any = any; } /* a change the pass reported since the last step ends the generation */ static void sb_share_step(void) { if (sb_share_changed && *sb_share_changed) { sb_memo_gen++; sb_memo_n = 0; *sb_share_any = 1; *sb_share_changed = 0; } } /* ... and what was folded into `any` goes back into `changed` */ static void sb_share_end(void) { if (sb_share_changed) *sb_share_changed |= *sb_share_any; sb_shared--; sb_memo_gen++; sb_memo_n = 0; sb_share_changed = sb_share_prev_changed; sb_share_any = sb_share_prev_any; sb_share_prev_changed = sb_share_prev_any = NULL; } static unsigned long long sb_memo_key(int kind, int a, int b, int mode) { return ((unsigned long long)(unsigned)kind << 60) ^ ((unsigned long long)(unsigned)mode << 52) ^ ((unsigned long long)(unsigned)a << 24) ^ (unsigned long long)(unsigned)(b + 1); } static SbMemo *sb_memo_slot(unsigned long long key) { if (sb_memo_n * 2 >= sb_memo_cap) { int oc = sb_memo_cap; SbMemo *old = sb_memo; sb_memo_cap = oc ? oc * 2 : 1024; sb_memo = calloc((size_t)sb_memo_cap, sizeof *sb_memo); if (!sb_memo) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } sb_memo_n = 0; for (int i = 0; i < oc; i++) if (old[i].gen == sb_memo_gen) { *sb_memo_slot(old[i].key) = old[i]; sb_memo_n++; } free(old); } unsigned long long h = key * 0x9E3779B97F4A7C15ull; for (int i = (int)(h >> 40) & (sb_memo_cap - 1);; i = (i + 1) & (sb_memo_cap - 1)) if (sb_memo[i].gen != sb_memo_gen || sb_memo[i].key == key) return &sb_memo[i]; } /* Enter a walk of (kind, a, b, mode) at depth: 1 with *res set when it has been walked already, else 0 (the caller walks it and sb_memo_leave's). */ static int sb_memo_enter(int kind, int a, int b, int mode, int depth, int *res) { if (sb_walk_nest++ == 0) { sb_walk_id++; if (!sb_shared) { sb_memo_gen++; sb_memo_n = 0; } else sb_share_step(); } unsigned long long key = sb_memo_key(kind, a, b, mode); SbMemo *m = sb_memo_slot(key); if (m->gen == sb_memo_gen && m->depth <= depth) { *res = m->res; sb_walk_nest--; return 1; } if (m->gen != sb_memo_gen) sb_memo_n++; m->key = key; m->gen = sb_memo_gen; m->depth = depth; m->res = 0; m->walk = sb_walk_id; return 0; } static int sb_memo_leave(int kind, int a, int b, int mode, int res) { SbMemo *m = sb_memo_slot(sb_memo_key(kind, a, b, mode)); if (m->gen == sb_memo_gen && m->key == sb_memo_key(kind, a, b, mode)) m->res |= res; sb_walk_nest--; return res; } static int strbuf_container_source_walk(Compiler *c, int node, int depth, int mode); static int strbuf_store_leaf(Compiler *c, int sn, int depth, int mode); /* Some value stored into this container is a string (mode SB_HAS_STRING), or provably NOT one (SB_HAS_NONSTRING): a local bound from a method result or another variable answers for what that expression built. */ /* The nodes that can store into a local named `name`, ascending: its writes, and the calls on a read of it (strbuf_container_store_values answers 0 for every other node). Rebuilt when the node table changes; each query walked every node of the program, once per container element a call argument reads. */ static ANameHash g_cst_names; static int *g_cst_first, *g_cst_next, g_cst_cap = 0, g_cst_cnt = -1; static const NodeTable *g_cst_nt; static unsigned g_cst_ver; /* The same nodes chained by (scope, name) as well, where the scope is the one strbuf_container_store_values tests: the write's own, or the receiver read's. The by-name chain carries every store of a local of that name in the program -- thousands for a common name on a Rails application -- and the walks below asked about one scope's local, so each ask read and rejected every other scope's stores: a fifth of the storage phase. A bucket's chain is ascending too, and may mix other (scope, name) pairs, which the walks' own tests reject as before. */ static int *g_cst_sc_head, *g_cst_sc_next, g_cst_sc_nb; static unsigned cst_sc_bucket(int si, const char *name) { return (sp_strhash(name) ^ ((unsigned)si * 2654435761u)) & (unsigned)(g_cst_sc_nb - 1); } static void cst_ready(Compiler *c) { const NodeTable *nt = c->nt; if (g_cst_nt != nt || g_cst_ver != nt->version || g_cst_cnt != nt->count) { anh_free(&g_cst_names); memset(&g_cst_names, 0, sizeof g_cst_names); free(g_cst_first); free(g_cst_next); free(g_cst_sc_head); free(g_cst_sc_next); g_cst_cap = 64; g_cst_first = malloc(sizeof(int) * (size_t)g_cst_cap); int *last = malloc(sizeof(int) * (size_t)g_cst_cap); g_cst_next = malloc(sizeof(int) * (size_t)(nt->count > 0 ? nt->count : 1)); g_cst_sc_nb = 16; while (g_cst_sc_nb < nt->count && g_cst_sc_nb < (1 << 22)) g_cst_sc_nb <<= 1; g_cst_sc_head = malloc(sizeof(int) * (size_t)g_cst_sc_nb); int *sc_last = malloc(sizeof(int) * (size_t)g_cst_sc_nb); g_cst_sc_next = malloc(sizeof(int) * (size_t)(nt->count > 0 ? nt->count : 1)); if (!g_cst_first || !last || !g_cst_next || !g_cst_sc_head || !sc_last || !g_cst_sc_next) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int b = 0; b < g_cst_sc_nb; b++) g_cst_sc_head[b] = -1; for (int w = 0; w < nt->count; w++) { NodeKind wk = nt_kind(nt, w); const char *nm = NULL; int sn = w; /* the node whose scope the walks test */ if (wk == NK_LocalVariableWriteNode) nm = nt_str(nt, w, "name"); else if (wk == NK_CallNode) { int r = nt_ref(nt, w, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode) { nm = nt_str(nt, r, "name"); sn = r; } } if (!nm) continue; g_cst_next[w] = -1; g_cst_sc_next[w] = -1; unsigned b = cst_sc_bucket((int)(comp_scope_of(c, sn) - c->scopes), nm); if (g_cst_sc_head[b] < 0) g_cst_sc_head[b] = w; else g_cst_sc_next[sc_last[b]] = w; sc_last[b] = w; int k = anh_find(&g_cst_names, nm); if (k < 0) { anh_add(&g_cst_names, nm); k = g_cst_names.n - 1; if (k >= g_cst_cap) { g_cst_cap *= 2; g_cst_first = realloc(g_cst_first, sizeof(int) * (size_t)g_cst_cap); last = realloc(last, sizeof(int) * (size_t)g_cst_cap); if (!g_cst_first || !last) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } g_cst_first[k] = w; } else g_cst_next[last[k]] = w; last[k] = w; } free(last); free(sc_last); g_cst_nt = nt; g_cst_ver = nt->version; g_cst_cnt = nt->count; } } static int cst_first(Compiler *c, const char *name) { cst_ready(c); int k = name ? anh_find(&g_cst_names, name) : -1; return k >= 0 ? g_cst_first[k] : -1; } /* the stores that can be local `name` of `scope`'s, ascending, among others of the bucket: cst_first's list for the scope, when the walk has one */ static int cst_first_sc(Compiler *c, Scope *scope, const char *name) { cst_ready(c); if (!name || !scope) return cst_first(c, name); return g_cst_sc_head[cst_sc_bucket((int)(scope - c->scopes), name)]; } static int cst_next_sc(Scope *scope, int w) { return scope ? g_cst_sc_next[w] : g_cst_next[w]; } /* The same indexed stores used by String sharing, for containers that cross * the C extension boundary. The caller owns the returned producer list. */ int *cext_container_stores(Compiler *c, const char *name, Scope *scope, int *count) { StoreVals all = {0}, values = {0}; for (int w = cst_first_sc(c, scope, name); w >= 0; w = cst_next_sc(scope, w)) { strbuf_container_store_values(c, w, name, scope, 1, &values); for (int j = 0; j < values.n; j++) store_vals_add(&all, values.v[j]); } free(values.v); *count = all.n; return all.v; } /* The values element stores (`@q.push(x)`, `@q << x`, `@q[i] = x`) put into * the container an instance variable of class `cls` holds. */ int *cext_ivar_container_stores(Compiler *c, const char *name, int cls, int *count) { const NodeTable *nt = c->nt; StoreVals all = {0}, values = {0}; NT_FOREACH_KIND(nt, NK_CallNode, call) { int r = nt_ref(nt, call, "receiver"); if (r < 0 || nt_kind(nt, r) != NK_InstanceVariableReadNode) continue; const char *rn = nt_str(nt, r, "name"); if (!rn || !sp_streq(rn, name) || comp_ivar_owner(c, r) != cls) continue; values.n = 0; array_call_store_values(c, call, &values); for (int j = 0; j < values.n; j++) store_vals_add(&all, values.v[j]); } free(values.v); *count = all.n; return all.v; } static int strbuf_container_stores_kind(Compiler *c, const char *contn, Scope *conts, int depth, int mode) { const NodeTable *nt = c->nt; if (depth > 8) return 0; StoreVals stores = {0}; int found = 0; /* the indexed nodes in ascending order, then any the walk itself added */ int n0 = nt->count; int *cand = NULL, ncand = 0; for (int w = cst_first_sc(c, conts, contn); w >= 0; w = cst_next_sc(conts, w)) { if (!(ncand & (ncand - 1))) cand = realloc(cand, sizeof(int) * (size_t)(ncand ? ncand * 2 : 1)); cand[ncand++] = w; } for (int i = 0; !found && i < ncand + (nt->count - n0); i++) { int w = i < ncand ? cand[i] : n0 + (i - ncand); int nst = strbuf_container_store_values(c, w, contn, conts, SB_KIND(mode) == SB_HAS_STRING, &stores); for (int e = 0; e < nst && !found; e++) found = strbuf_store_leaf(c, stores.v[e], depth, mode); if (!found && nst == 0 && nt_kind(nt, w) == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, w, "name"); if (wn && sp_streq(wn, contn) && comp_scope_of(c, w) == conts && strbuf_container_source_walk(c, nt_ref(nt, w, "value"), depth + 1, mode)) found = 1; } } free(cand); free(stores.v); return found; } static int strbuf_container_stores_string(Compiler *c, const char *contn, Scope *conts) { return strbuf_container_stores_kind(c, contn, conts, 0, SB_HAS_STRING); } /* The complement of the test above: some value stored into this container is provably NOT a string. Such a container is heterogeneous, so a local bound from one of its elements is not an alias for a string, whatever the mutator-name table says about the calls made on it (#4240). Only a store whose type the analysis actually knows counts -- an UNKNOWN one says nothing, and answering yes on it would give up the promotion everywhere. */ static int strbuf_container_stores_nonstring(Compiler *c, const char *contn, Scope *conts) { return strbuf_container_stores_kind(c, contn, conts, 0, SB_HAS_NONSTRING); } /* Is every write of local `rn` (read at `recv`) an array literal? Then the local BUILT its array and a widened slot keeps naming it; bound from an element or an ivar read it is another name for storage something else holds, and the widening's converted copy would cut the alias (#4412). */ static int pw_local_owns_array(Compiler *c, int recv, const char *rn) { const NodeTable *nt = c->nt; Scope *ls = comp_scope_of(c, recv); int owns = 1, saw = 0; for (int w = comp_kind_first(c, NK_LocalVariableWriteNode); w >= 0 && owns; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, rn) || comp_scope_of(c, w) != ls) continue; saw = 1; int wv = nt_ref(nt, w, "value"); if (wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) owns = 0; } return saw && owns; } /* Does method scope `mi` mutate its parameter `name` in place (a push, a store, a bang), directly or by handing it to a receiverless user method that does? The promote reconciliation keeps such a parameter at its kind: the argument is passed by reference only while the two agree (#4480). */ static int pw_scope_mutates_param(Compiler *c, int mi, const char *name) { static const char *const muts[] = { "<<", "push", "append", "unshift", "prepend", "insert", "[]=", "concat", "pop", "shift", "delete", "delete_at", "delete_if", "clear", "replace", "fill", "map!", "collect!", "select!", "filter!", "reject!", "sort!", "sort_by!", "uniq!", "compact!", "reverse!", "shuffle!", "rotate!", "slice!", "keep_if", "flatten!", NULL }; const NodeTable *nt = c->nt; for (int q = 0; q < nt->count; q++) { if (c->nscope[q] != mi || nt_kind(nt, q) != NK_CallNode) continue; const char *cnm = nt_str(nt, q, "name"); if (!cnm) continue; int r = nt_ref(nt, q, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode) { const char *rn = nt_str(nt, r, "name"); if (rn && sp_streq(rn, name)) for (int k = 0; muts[k]; k++) if (sp_streq(cnm, muts[k])) return 1; } if (r >= 0 && nt_kind(nt, r) != NK_SelfNode) continue; int aa = nt_ref(nt, q, "arguments"); int an = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &an) : NULL; for (int k = 0; k < an; k++) { if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode) continue; const char *vn = nt_str(nt, av[k], "name"); if (!vn || !sp_streq(vn, name)) continue; /* passed on: conservatively, a callee that takes it may mutate it */ return 1; } } return 0; } /* Iterators whose block receives the ELEMENT as its first parameter. */ static int strbuf_elem_first_iterator(const char *n) { static const char *const names[] = { "each", "each_with_index", "each_with_object", "each_entry", "reverse_each", "cycle", "map", "collect", "map!", "collect!", "flat_map", "collect_concat", "select", "filter", "select!", "filter!", "find_all", "reject", "reject!", "delete_if", "keep_if", "filter_map", "find", "detect", "find_index", "index", "rindex", "count", "any?", "all?", "none?", "one?", "sum", "min_by", "max_by", "minmax_by", "sort_by", "sort_by!", "group_by", "partition", "take_while", "drop_while", "uniq", "tally_by", "to_h", NULL }; return str_in(n, names); } /* The nodes that can store into a local, keyed by (name, scope): a write of it, and a call on it as the receiver. The question below was answered by a walk of the whole node table per container, and it is asked per container per round -- the largest single term of lobsters' analysis. Built once per promote_shared_stored_strings run (a round's desugars rename and re-point nodes in between) and again if the table grows; each list is ascending, so the stores are met in the order the walk met them. */ typedef struct SbStoreEnt { const char *name; Scope *sc; int *ids; int n, cap; struct SbStoreEnt *next; } SbStoreEnt; /* (the buckets grow with the program: a fixed 4096 left a large one's chains hundreds long, walked by every add and every lookup) */ static SbStoreEnt **sb_store_tab; static unsigned sb_store_nb; static const NodeTable *sb_store_nt; static int sb_store_count = -1; static int sb_store_valid; static unsigned sb_store_hash(const char *nm, Scope *sc) { unsigned h = 2166136261u ^ (unsigned)(uintptr_t)sc; for (; *nm; nm++) { h ^= (unsigned char)*nm; h *= 16777619u; } return h; } static void svc_clear(void); static void sb_store_clear(void) { svc_clear(); for (unsigned b = 0; b < sb_store_nb; b++) { for (SbStoreEnt *e = sb_store_tab[b]; e; ) { SbStoreEnt *nx = e->next; free(e->ids); free(e); e = nx; } sb_store_tab[b] = NULL; } sb_store_valid = 0; } static void sb_store_add(const char *nm, Scope *sc, int id) { unsigned b = sb_store_hash(nm, sc) & (sb_store_nb - 1); SbStoreEnt *e = sb_store_tab[b]; while (e && !(e->sc == sc && sp_streq(e->name, nm))) e = e->next; if (!e) { e = calloc(1, sizeof *e); e->name = nm; e->sc = sc; e->next = sb_store_tab[b]; sb_store_tab[b] = e; } if (e->n == e->cap) { e->cap = e->cap ? e->cap * 2 : 4; e->ids = realloc(e->ids, sizeof(int) * (size_t)e->cap); } e->ids[e->n++] = id; } static const int *sb_store_nodes(Compiler *c, const char *nm, Scope *sc, int *n) { const NodeTable *nt = c->nt; if (!sb_store_valid || sb_store_nt != nt || sb_store_count != nt->count) { sb_store_clear(); unsigned nb = 4096; while (nb < (unsigned)nt->count / 2 && nb < (1u << 24)) nb <<= 1; if (nb != sb_store_nb) { free(sb_store_tab); sb_store_tab = calloc(nb, sizeof *sb_store_tab); if (!sb_store_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } sb_store_nb = nb; } for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, id, "name"); if (wn) sb_store_add(wn, comp_scope_of(c, id), id); } /* `(h[k] ||= []) << s` hands out the element it reads or stores */ else if (k == NK_CallNode || k == NK_IndexOrWriteNode) { int r = nt_ref(nt, id, "receiver"); if (r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode) continue; const char *rn = nt_str(nt, r, "name"); if (rn) sb_store_add(rn, comp_scope_of(c, r), id); } } sb_store_nt = nt; sb_store_count = nt->count; sb_store_valid = 1; } for (SbStoreEnt *e = sb_store_tab[sb_store_hash(nm, sc) & (sb_store_nb - 1)]; e; e = e->next) if (e->sc == sc && sp_streq(e->name, nm)) { *n = e->n; return e->ids; } *n = 0; return NULL; } /* The values each indexed store stores, kept beside the index (and dropped with it): a container's stores are walked once per outermost walk that reaches the container, the walk memo being dropped between, and every walk read each store's name, receiver and arguments over again -- on a Rails program 9% of a sampled window of the share pass. A write of a call's value is not kept: the collecting iterator's tail it may store goes with the call's plan, which moves as the pass promotes. The values go with the table's version and edits as well as with the index: an edit that replaces a store's argument array moves neither the node count nor the version, only edits. */ static int *svc_off, *svc_n, *svc_pool, svc_cap, svc_pool_n, svc_pool_cap; static const NodeTable *svc_nt; static unsigned svc_ver; static unsigned long svc_edits; static void svc_clear(void) { free(svc_off); free(svc_n); svc_off = svc_n = NULL; svc_cap = 0; svc_pool_n = 0; } static int sb_store_values(Compiler *c, int w, const char *contn, Scope *conts, StoreVals *sv) { const NodeTable *nt = c->nt; if (svc_nt != nt || svc_ver != nt->version || svc_edits != nt->edits) { svc_clear(); svc_nt = nt; svc_ver = nt->version; svc_edits = nt->edits; } if (w >= svc_cap) { int cap = nt->count > w + 1 ? nt->count : w + 1; svc_off = realloc(svc_off, sizeof(int) * (size_t)cap); svc_n = realloc(svc_n, sizeof(int) * (size_t)cap); if (!svc_off || !svc_n) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = svc_cap; i < cap; i++) svc_off[i] = -2; svc_cap = cap; } if (svc_off[w] >= 0) { sv->n = 0; for (int i = 0; i < svc_n[w]; i++) store_vals_add(sv, svc_pool[svc_off[w] + i]); return sv->n; } int n = strbuf_container_store_values(c, w, contn, conts, 1, sv); if (svc_off[w] == -1) return n; if (nt_kind(nt, w) == NK_LocalVariableWriteNode) { int val = nt_ref(nt, w, "value"); if (val >= 0 && nt_kind(nt, val) == NK_CallNode) { svc_off[w] = -1; return n; } } if (svc_pool_n + n > svc_pool_cap) { svc_pool_cap = (svc_pool_n + n) * 2 + 256; svc_pool = realloc(svc_pool, sizeof(int) * (size_t)svc_pool_cap); if (!svc_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } svc_off[w] = svc_pool_n; svc_n[w] = n; for (int i = 0; i < n; i++) svc_pool[svc_pool_n++] = sv->v[i]; return n; } static int strbuf_demand_value_leaves(Compiler *c, int node, int depth); /* One value stored into a container whose elements are mutated through it: an eligible string local promotes, a string expression marks for a fresh-handle wrap at the store site. */ static int strbuf_block_param_source_walk(Compiler *c, const char *vn, Scope *vs, int depth, int mode, int leaf, int *bound); /* Does value `v` show it can be a String: a String-typed expression, a branch of a conditional that is one, or an element read of an Array or Hash literal holding one? Unlike poly_var_may_hold_string, a value it cannot see answers no: what asks is a refusal. */ static int poly_value_shows_string(Compiler *c, int v); static int poly_leaf_shows_string(const VpLeaf *lf, void *ud) { Compiler *c = ud; const NodeTable *nt = c->nt; /* (a walk cut short saw nothing: a value it cannot see answers no) */ if (lf->kind != VPK_EXPR && lf->kind != VPK_JUMP) return 0; int v = lf->node; TyKind vt = infer_type(c, v); if (vt == TY_STRING || vt == TY_STRBUF) return 1; if (nt_kind(nt, v) != NK_CallNode) return 0; int r = nt_paren_single(nt, nt_ref(nt, v, "receiver")); if (r < 0 || !container_elem_read_p(nt, v)) return 0; int en = 0; const int *el = NULL; if (nt_kind(nt, r) == NK_ArrayNode) el = nt_arr(nt, r, "elements", &en); for (int e = 0; e < en; e++) if (poly_value_shows_string(c, el[e])) return 1; if (nt_kind(nt, r) == NK_HashNode) { el = nt_arr(nt, r, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_AssocNode && poly_value_shows_string(c, nt_ref(nt, el[e], "value"))) return 1; } return 0; } static int poly_value_shows_string(Compiler *c, int v) { return for_each_value_position(c->nt, v, VP_AND_LEFT | VPS_IF | VPS_UNLESS | VPS_LOGIC, poly_leaf_shows_string, c); } /* Can boxed local (vn, vs) hold a String, by what one of its writes shows? */ static int poly_local_shows_string(Compiler *c, const char *vn, Scope *vs) { const NodeTable *nt = c->nt; for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) if (nt_kind(nt, w) == NK_LocalVariableWriteNode && comp_scope_of(c, w) == vs && sp_streq(nt_str(nt, w, "name"), vn) && poly_value_shows_string(c, nt_ref(nt, w, "value"))) return 1; return 0; } /* Is block parameter (vn, vs) bound only by the yields of user methods (`each { |e| acc << e }` over a yielding each, inlined or taking its block as a proc), which hand over what they yield? A Struct's each is the compiler's own, over members the share facts do not follow. */ static int block_param_user_yield_only(Compiler *c, const char *vn, Scope *vs) { const NodeTable *nt = c->nt; int any = 0; /* the calls whose literal block's parameters live in vs (comp_bcall_first) */ for (int w = comp_bcall_first(c, (int)(vs - c->scopes)); w >= 0; w = comp_bcall_next(c, w)) { int blk = nt_ref(nt, w, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || comp_scope_of(c, blk) != vs) continue; int named = 0; for (int k = 0; k < 4 && !named; k++) { const char *bp = block_param_name(c, blk, k); if (!bp) break; named = sp_streq(bp, vn); } if (!named) continue; int mi = an_call_target_mi(c, w); Scope *m = mi >= 0 ? &c->scopes[mi] : NULL; if (!m || !(m->yields || m->is_lowered_yield) || (m->class_id >= 0 && c->classes[m->class_id].is_struct)) return 0; any = 1; } return any; } /* Is n a call on a boxed receiver whose answer is a new String whatever class the receiver is (`v.upcase`): a builtin of a name no method of the program defines, which String's own share row and every other builtin family's that has it answer with a value they were not handed (BSH_PURE)? One that can answer its receiver, an element or an argument (`itself`, `first`, `max`), or a name String has no row for (`value`, `resume`, `deq`, which answer what another object holds), is no new String. */ static int strbuf_poly_call_fresh(Compiler *c, int n) { const NodeTable *nt = c->nt; if (n < 0 || nt_kind(nt, n) != NK_CallNode || nt_ref(nt, n, "receiver") < 0 || nt_ref(nt, n, "block") >= 0 || container_elem_read_p(nt, n) || infer_type(c, n) != TY_POLY || infer_type(c, nt_ref(nt, n, "receiver")) != TY_POLY) return 0; const char *nm = nt_str(nt, n, "name"); if (!nm || an_any_scope_by_name(c, nm) >= 0 || bop_share_named(TY_STRING, nm) != BSH_PURE) return 0; static const TyKind fams[] = { BOP_ANY_ARRAY, BOP_ANY_HASH, BOP_ANY_RECV }; for (size_t i = 0; i < sizeof fams / sizeof fams[0]; i++) { int sh = bop_share_named(fams[i], nm); if (sh && sh != BSH_PURE) return 0; } return 1; } static int lift_poly_read_store(Compiler *c, int a); /* The demanded stores of a String's own conversion strbuf_demand_store_leaf took, checked once the types settle (sa_check_conversion_stores) */ static int *sa_conv, sa_nconv, sa_cconv; static const NodeTable *sa_conv_nt; /* The local a String's own conversion reads (`s.to_s`, `s.to_str`, `s.itself`, no program method of the name reaching it), whose value is that local's String; -1 for any other node. */ int an_str_conversion_local(Compiler *c, int v) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0 || nt_kind(nt, v) != NK_CallNode || nt_ref(nt, v, "block") >= 0 || call_plain_argc(c, v) != 0) return -1; const char *nm = nt_str(nt, v, "name"); int r = nt_paren_single(nt, nt_ref(nt, v, "receiver")); if (!nm || r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode || !(sp_streq(nm, "to_s") || sp_streq(nm, "to_str") || sp_streq(nm, "itself"))) return -1; TyKind rt = comp_ntype(c, r); int ms[CPT_MAX]; return (rt == TY_STRING || rt == TY_STRBUF) && cplan_targets(c, v, ms, CPT_MAX) == 0 ? r : -1; } static int strbuf_demand_store_leaf(Compiler *c, int sn, int depth); /* --share-strings: the Strings of nested literal lit (an Array's elements, a Hash's values) are demanded as the outer literal's are, but for a String literal: it is frozen, so no mutation needs its handle (its plain box raises the FrozenError). */ static int strbuf_demand_nested_literal(Compiler *c, int lit, int depth) { const NodeTable *nt = c->nt; if (depth > 8) return 0; int en = 0, changed = 0, hash = nt_kind(nt, lit) == NK_HashNode; const int *el = nt_arr(nt, lit, "elements", &en); for (int e = 0; e < en; e++) { int v = hash ? (nt_kind(nt, el[e]) == NK_AssocNode ? nt_ref(nt, el[e], "value") : -1) : el[e]; if (v < 0 || nt_kind(nt, nt_paren_single(nt, v)) == NK_StringNode) continue; changed |= strbuf_demand_store_leaf(c, v, depth); } return changed; } static int strbuf_demand_store_leaf(Compiler *c, int sn, int depth) { const NodeTable *nt = c->nt; if (sn < 0 || c->strbuf_box[sn]) return 0; /* Parentheses around one statement are transparent: the demand is the statement's, as if they were not there, and the parenthesis carries the mark only once the statement does. emit_boxed_impl boxes the statement itself, so a mark on the parenthesis alone stored a plain box over a copy, and `r << (+"a")` or `r << (c ? s : t)` lost a later `r[0] << x`; the parenthesis keeps its mark for a call argument that ran first into a temp, which is boxed by the parenthesis's own type. */ int snp = nt_paren_single(nt, sn); if (snp >= 0 && snp != sn) { int ch = strbuf_demand_store_leaf(c, snp, depth); if (c->strbuf_box[snp]) { comp_set_strbuf_box(c, sn, 1); ch = 1; } return ch; } /* --share-strings: a nested Array or Hash literal whose own elements the rule shares (`[[+"ab", 1], 2]` with `id(x[0][0]).upcase!`) */ NodeKind snk = nt_kind(nt, nt_paren_single(nt, sn)); if ((snk == NK_ArrayNode || snk == NK_HashNode)) return strbuf_demand_nested_literal(c, nt_paren_single(nt, sn), depth + 1); /* `a << (s << "y")`, `a << +s`, `a << sb` with `def sb = @s`: the element would be a fresh handle over a copy of s's or @s's String (sa_refuse); `+s` is s itself unless s is frozen. So would `a << id(s)` with `def id(x) = x` (sa_refuse_stored_call; not under --share-strings, where the form with a local between compiles) */ int snu = nt_paren_single(nt, sn); TyKind snt = snu >= 0 && nt_kind(nt, snu) == NK_CallNode ? infer_type(c, snu) : TY_UNKNOWN; if (snt == TY_STRING || snt == TY_STRBUF) { /* `h[k] = s.to_s`: a String's own conversion answers the String, so the element is the local's handle once the local holds one (emit_boxed_strbuf boxes the receiver's handle for the marked call). Refusing the store here, as the copying routes below are, refused one whose local widened past a String only in a later round */ char rb[256]; int rdefc = -1; int as = an_strbuf_alias_source(c, snu); int reader = an_reader_ivar_of(c, snu, &rdefc, rb, sizeof rb) != NULL; (void)as; (void)reader; } if (nt_kind(nt, sn) == NK_LocalVariableReadNode) { const char *snm = nt_str(nt, sn, "name"); Scope *sns = comp_scope_of(c, sn); LocalVar *snv = (snm && sns) ? scope_local(sns, snm) : NULL; if (!snv) return 0; /* a block parameter is what the iterator hands it: those strings are the ones stored */ /* a parameter no element iterator binds holds a String the walk cannot reach (refuse_stored_block_param); a boxed one is asked about once the mutation is known to be a String's (refuse_string_read_copies) */ if (snv->is_block_param) { /* The share rule's slot already carries the handle. A literal stores that read without demanding its iterator's sources again. */ if (an_arg_is_shared_handle(c, sn)) { comp_set_strbuf_box(c, sn, 1); return 1; } int bound = 0; int ch = strbuf_block_param_source_walk(c, snm, sns, depth, SB_DEMAND, 1, &bound); if (!bound && (snv->type == TY_STRING || snv->type == TY_STRBUF)) { /* --share-strings: the route is the rule's (share_route_defer, in the refusal). A parameter the rule shares holds the handle what binds it hands over (share_default_apply), or the seal refuses it by name, and the element is that handle (`each { |e| acc << e }` over a yielding each) */ int yo = block_param_user_yield_only(c, snm, sns); if (yo && c->share && repr_local_shares(c, sns, snv)) { if (snv->type != TY_STRBUF || !snv->str_shared) { lv_set_type(snv, TY_STRBUF); { snv->str_shared = 1; comp_touch_scope(snv->owner); } ch = 1; } if (!c->strbuf_box[sn]) { comp_set_strbuf_box(c, sn, 1); ch = 1; } } } return ch; } /* a method parameter stores the caller's string: the parameter becomes a handle, and the byref/handle pass pulls each caller's argument in */ if (snv->is_param && !snv->is_cell && an_param_idx(sns, snm) >= 0 && (snv->type == TY_STRING || snv->type == TY_STRBUF)) { lv_set_type(snv, TY_STRBUF); { snv->str_shared = 1; comp_touch_scope(snv->owner); } { snv->byref_out = 0; comp_touch_scope(snv->owner); } comp_set_strbuf_box(c, sn, 1); return 1; } /* a poly parameter stored into the container (`def <<(v) = @values << v` reached through a poly receiver; its type may not be settled yet, and a read that ends up typed is not lifted): the read lifts a String it holds into the handle, stored back, and the callers' Strings are pulled in as for any parameter appended to */ if (snv->is_param && !snv->is_cell && !snv->is_block_param && an_param_idx(sns, snm) >= 0 && (snv->type == TY_POLY || snv->type == TY_UNKNOWN)) { if (c->poly_strbuf_lift[sn]) return 0; c->poly_strbuf_lift[sn] = 1; snv->poly_lift |= POLY_LIFT_APPENDED; return 1; } /* A boxed local (`s = [+"xy", 1][k]`) has no handle route here: the element the container keeps is a copy of the String it holds, and a mutation through the element (`[s][0].prepend(x)`, `[s].each { |e| e << x }`) is lost or finds no method. Refused (#6765) rather than compiled with the change lost; a container flowing in is left out, its own mutations reach it. */ if (snv->type == TY_POLY && !snv->is_param && !snv->poly_ctr && poly_local_shows_string(c, snm, sns)) { /* Under --share-strings the stored read is lifted into the handle (lift_poly_read: it boxes the local's String as the handle and writes that back into the local, and lift_poly_alias_reads carries it to the names the local was copied from or to): the element is the local's own String, which the route then carries where the rule shares it (share_node_carries_handle), and a copy no other name sees where it does not */ int ch = 0; { ch = lift_poly_read_store(c, sn); return ch; } } if (!strbuf_slot_eligible(c, snm, sns, snv)) return 0; if (strbuf_mut_kind(c, snm, sns) < 0) return 0; lv_set_type(snv, TY_STRBUF); { snv->str_shared = 1; comp_touch_scope(snv->owner); } comp_set_strbuf_box(c, sn, 1); return 1; } /* an ivar stores its own String the way a local does: the slot takes the handle and the element is that handle. Wrapped fresh at the store site, the element was a copy, and an append through it never reached the ivar (#6179). */ if (nt_kind(nt, sn) == NK_InstanceVariableReadNode) { const char *ivn = nt_str(nt, sn, "name"); int icid = comp_ivar_owner(c, sn); if (ivn && icid >= 0 && strbuf_ivar_mut_kind(c, icid, ivn) >= 0) { int ch = strbuf_promote_ivar(c, icid, ivn); int iv = comp_ivar_index(&c->classes[icid], ivn); if (iv >= 0 && c->classes[icid].ivar_str_shared[iv]) { comp_set_strbuf_box(c, sn, 1); return 1; } if (ch) return 1; } } TyKind st2 = infer_type(c, sn); if (st2 == TY_POLY) { int ch = strbuf_demand_value_leaves(c, sn, 0); /* --share-strings: a boxed call's new String (`k << v.upcase` over a boxed v, strbuf_poly_call_fresh) is a plain box, which a mutation through the element cannot change: `k[0] << "!"` appended to a copy and the element kept its old text. The store lifts it into a handle (sp_poly_strbuf_lift), so the element is the String the mutation reaches; no other name holds that String. */ if (strbuf_poly_call_fresh(c, snu) && !c->poly_strbuf_lift[snu]) { c->poly_strbuf_lift[snu] = STRBUF_LIFT_FRESH_CALL; ch = 1; } return ch; } if (st2 != TY_STRING && st2 != TY_STRBUF) return 0; comp_set_strbuf_box(c, sn, 1); return 1; } static int strbuf_store_leaf(Compiler *c, int sn, int depth, int mode) { if (mode >= SB_NEST1) return strbuf_container_source_walk(c, sn, depth + 1, mode - SB_NEST1); if (mode == SB_DEMAND_NAMED || mode == SB_DEMAND) return strbuf_demand_store_leaf(c, sn, depth); TyKind st = sn >= 0 ? infer_type(c, sn) : TY_UNKNOWN; if (mode == SB_HAS_STRING) return st == TY_STRING || st == TY_STRBUF; return st != TY_UNKNOWN && st != TY_POLY && st != TY_STRING && st != TY_STRBUF; } static int strbuf_demand_container_stores_here(Compiler *c, const char *contn, Scope *conts, int depth, int mode) { const NodeTable *nt = c->nt; int changed = 0; if (depth > 8) return 0; /* every store into this container local (same scope): array/hash literal writes, push/<<, []= */ int nsn = 0; const int *sns0 = sb_store_nodes(c, contn, conts, &nsn); StoreVals stores = {0}; for (int si = 0; si < nsn; si++) { int nst = sb_store_values(c, sns0[si], contn, conts, &stores); for (int e3 = 0; e3 < nst; e3++) changed |= strbuf_store_leaf(c, stores.v[e3], depth, mode); /* the strings sit a level in (`h.each { |k, vs| vs.each(&:strip!) }`) and a push lands in an element the container hands out (`h[k] << s`): the push stores into that inner container */ int orw = nt_kind(nt, sns0[si]) == NK_IndexOrWriteNode; if (orw) changed |= strbuf_store_leaf(c, nt_ref(nt, sns0[si], "value"), depth, mode); if (mode >= SB_NEST1 && nst == 0 && (orw || container_elem_read_p(nt, sns0[si]))) { int p = comp_recv_parent(c, sns0[si]); StoreVals inner = {0}; int ni = p >= 0 && nt_kind(nt, p) == NK_CallNode ? array_call_store_values(c, p, &inner) : 0; for (int e3 = 0; e3 < ni; e3++) changed |= strbuf_store_leaf(c, inner.v[e3], depth, mode - SB_NEST1); free(inner.v); } /* bound from a method result, an ivar or another local: the container is whatever that expression built (`x = mk; x[0] << "q"`) */ if (nst == 0 && nt_kind(nt, sns0[si]) == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, sns0[si], "name"); if (wn && sp_streq(wn, contn) && comp_scope_of(c, sns0[si]) == conts) changed |= strbuf_container_source_walk(c, nt_ref(nt, sns0[si], "value"), depth + 1, mode); } } free(stores.v); return changed; } /* A container that is a PARAMETER has no stores of its own: they live at the call sites, in whatever the callers pass. `def bang(a) = a.each { |w| w << "#" }` mutates the caller's elements through the parameter, so the demand has to reach the caller's container -- and a caller that passes its own parameter on carries it one level further. Without this the callee's block param became a handle while the caller's array stayed typed, and the two did not even agree on the element's C type. A literal argument is its own store site: its string elements mark for the handle wrap directly. */ 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 sb_call_reaches(Compiler *c, int u, int mi, Scope *m); static int strbuf_demand_local_container(Compiler *c, const char *vn, Scope *vs, int depth, int mode); /* After inference, the argument bindings no longer move. Keep their reverse edges on the compiler: a parameter query visits its sources, not every call with the same name. A bare super carries a source parameter instead of an argument node. Before storage the existing per-pass walk is used. */ typedef struct { int next, node, scope, param, loose; } SbArgEdge; /* Plain argument lists of up to SB_PLAIN_MAX - 1 arguments, bound to a method by its layout, give the same parameter -> argument-index map whatever the call: `plain` holds it per (method, count), built when first asked, only while the index is being built (strbuf_arg_index_build). */ enum { SB_PLAIN_MAX = 8 }; typedef struct SbArgIndex { int *off, *head; SbArgEdge *edges; int n, cap, changed; int **plain; } SbArgIndex; void strbuf_arg_index_free(Compiler *c) { SbArgIndex *x = c->sb_args; if (!x) return; free(x->off); free(x->head); free(x->edges); free(x); c->sb_args = NULL; } /* 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); /* The calls that can reach method scope m, ascending, as an_call_targets_scope decides it: those named as m is, those under a name some class aliases a method as, and `new` for an initialize. Every other call answers no there, so the walk below asks only these -- it asked every call of the program for each parameter it followed, and a large program's walk spent its time in that scan. Collected up front: the walk can rebuild the by-name lists. */ static int an_alias_name(Compiler *c, const char *nm); static int an_alias_by_old_first(Compiler *c, const char *nm); static int *ao_next; static const char **ao_new; static int sb_int_cmp(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; return (x > y) - (x < y); } /* v[0..n), made of nr ascending runs starting at run[0..nr) (run[nr] == n), in ascending order: adjacent runs merged pairwise until one is left. v is freed; the merged copy is returned. */ static int *sb_merge_runs(int *v, int n, int *run, int nr) { int *w = malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); if (!w) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } while (nr > 1) { int k = 0; for (int r = 0; r < nr; r += 2) { int lo = run[r], mid = run[r + 1 < nr ? r + 1 : nr], hi = run[r + 2 < nr ? r + 2 : nr]; int i = lo, j = mid, o = lo; while (i < mid && j < hi) w[o++] = v[i] <= v[j] ? v[i++] : v[j++]; while (i < mid) w[o++] = v[i++]; while (j < hi) w[o++] = v[j++]; run[k++] = lo; } run[k] = n; nr = k; int *t = v; v = w; w = t; } free(w); return v; } int *an_scope_call_candidates(Compiler *c, Scope *m, int *n) { int cap = 16, cnt = 0; int *v = malloc(sizeof(int) * (size_t)cap); if (!v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } const char *names[2] = { m->name, sp_streq(m->name ? m->name : "", "initialize") ? "new" : NULL }; ANameHash seen; memset(&seen, 0, sizeof seen); int nnames = 0; const char **all = NULL; for (int i = 0; i < 2; i++) if (names[i] && !anh_has(&seen, names[i])) { anh_add(&seen, names[i]); all = realloc(all, sizeof *all * (size_t)(nnames + 1)); all[nnames++] = names[i]; } /* ... and the alias names that lead to one of them: an alias resolves by name, from its new name to its old (comp_resolve_alias_ex), so only a name whose chain of old names reaches the method's can call it. Every alias name of the program was a candidate before, and each method asked every call on all of them through its receiver's classes. The names found are walked in turn (`all` grows under the walk), each through the aliases of that old name: reading every class's aliases per step, for every method the pass walked back from, was 4% of a Rails program's share pass. */ for (int i = 0; i < nnames; i++) for (int e = an_alias_by_old_first(c, all[i]); e >= 0; e = ao_next[e]) { const char *an = ao_new[e]; if (anh_has(&seen, an)) continue; anh_add(&seen, an); all = realloc(all, sizeof *all * (size_t)(nnames + 1)); all[nnames++] = an; } /* each name's chain is ascending (an_calls_named_first), so v is one ascending run per name: the runs are merged, not sorted. qsort of a common name's calls, asked again for each method of the name, was most of this function on a Rails program. A run out of order is sorted as before. */ int *run = malloc(sizeof(int) * (size_t)(nnames + 1)), runs_sorted = 1; if (!run) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < nnames; i++) { run[i] = cnt; for (int u = an_calls_named_first(c, all[i]); u >= 0; u = an_calls_named_next(u)) { if (cnt == cap) { cap *= 2; v = realloc(v, sizeof(int) * (size_t)cap); if (!v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } if (cnt > run[i] && v[cnt - 1] > u) runs_sorted = 0; v[cnt++] = u; } } run[nnames] = cnt; anh_free(&seen); free(all); if (!runs_sorted) qsort(v, (size_t)cnt, sizeof(int), sb_int_cmp); else if (nnames > 1) v = sb_merge_runs(v, cnt, run, nnames); free(run); *n = cnt; return v; } /* The calls that reach method scope mi (an_call_targets_scope), ascending. During one promote_shared_stored_strings pass the answer for a method is kept: the pass walks back to the same method's callers from many containers, and asking every candidate call again -- a common name's calls, each resolved through its receiver's possible classes -- was what a large program's pass spent its time on. The pass marks Strings shared; it does not change what a call resolves to. Outside the pass nothing is kept. The caller frees the list. */ static int sb_targets_gen, sb_targets_on; typedef struct { int gen, n; int *ids; } SbTargets; static SbTargets *sb_targets; static int sb_targets_cap; static int *strbuf_scope_callers(Compiler *c, int mi, Scope *m, int *n) { const NodeTable *nt = c->nt; if (sb_targets_on && mi < sb_targets_cap && sb_targets[mi].gen == sb_targets_gen) { *n = sb_targets[mi].n; int *cp = malloc(sizeof(int) * (size_t)(*n > 0 ? *n : 1)); if (!cp) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(cp, sb_targets[mi].ids, sizeof(int) * (size_t)*n); return cp; } int ncand = 0, k = 0; int *cand = an_scope_call_candidates(c, m, &ncand); for (int i = 0; i < ncand; i++) { int u = cand[i]; if (nt_kind(nt, u) != NK_CallNode || !sb_call_reaches(c, u, mi, m)) continue; cand[k++] = u; } *n = k; if (sb_targets_on) { if (mi >= sb_targets_cap) { int oc = sb_targets_cap; sb_targets_cap = c->nscopes > mi ? c->nscopes + 64 : mi + 64; sb_targets = realloc(sb_targets, sizeof *sb_targets * (size_t)sb_targets_cap); if (!sb_targets) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memset(sb_targets + oc, 0, sizeof *sb_targets * (size_t)(sb_targets_cap - oc)); } free(sb_targets[mi].ids); sb_targets[mi].ids = malloc(sizeof(int) * (size_t)(k > 0 ? k : 1)); if (!sb_targets[mi].ids) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(sb_targets[mi].ids, cand, sizeof(int) * (size_t)k); sb_targets[mi].n = k; sb_targets[mi].gen = sb_targets_gen; } return cand; } 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; LocalVar *plv = scope_local(ps, pn); if (!plv || !plv->is_param || plv->is_block_param) return 0; 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; int ncand = 0; SbArgIndex *x = c->sb_args; int e = x ? x->head[x->off[mi] + pj] : -1; int *cand = x ? NULL : strbuf_scope_callers(c, mi, ps, &ncand); for (int ci = 0; x ? e >= 0 : ci < ncand; ci++) { int an; if (x) { SbArgEdge *a = &x->edges[e]; e = a->next; if (a->loose) continue; if (a->scope >= 0) { Scope *s = &c->scopes[a->scope]; changed |= strbuf_demand_local_container(c, s->pnames[a->param], s, depth + 1, mode); continue; } an = a->node; } else { int u = cand[ci]; if (nt_kind(nt, u) != NK_CallNode) continue; an = arg_layout_param_node(c, ps, u, pj, NULL); } if (an < 0) continue; NodeKind ak = nt_kind(nt, an); if (ak == NK_LocalVariableReadNode) { const char *avn = nt_str(nt, an, "name"); Scope *avs = avn ? comp_scope_of(c, an) : NULL; LocalVar *alv = avs ? scope_local(avs, avn) : NULL; if (!alv) continue; if (alv->is_param && !alv->is_block_param) changed |= strbuf_demand_param_container_stores(c, avn, avs, depth + 1, mode); else if (ty_is_array(alv->type) || ty_is_hash(alv->type) || alv->type == TY_UNKNOWN) changed |= strbuf_demand_local_container(c, avn, avs, depth + 1, mode); } /* a literal argument is a store site of its own: an element that is a String local promotes to the handle like any stored one (marked alone, the local's plain `const char *` went into the handle's box and the C did not build) */ else if (ak == NK_ArrayNode && mode == SB_DEMAND) { int en = 0; const int *el = nt_arr(nt, an, "elements", &en); for (int e = 0; e < en; e++) changed |= strbuf_store_leaf(c, el[e], depth, mode); } else changed |= strbuf_container_source_walk(c, an, depth + 1, mode); } free(cand); if (x && changed && (SB_KIND(mode) == SB_DEMAND || SB_KIND(mode) == SB_DEMAND_NAMED)) x->changed = 1; return changed; } /* A block parameter names what the iterator hands the block: the receiver itself for `tap`/`then`, an element of it for `each`/`map`/..., and the memo for each_with_object's second parameter. With `leaf` the parameter is itself the value stored, not a container of them. *bound (when given) is set when one of those iterators binds the parameter. */ static int strbuf_block_param_source_walk(Compiler *c, const char *vn, Scope *vs, int depth, int mode, int leaf, int *bound) { const NodeTable *nt = c->nt; int changed = 0; if (depth > 8) return 0; /* the calls whose literal block's parameters live in vs (comp_bcall_first) */ for (int w = comp_bcall_first(c, (int)(vs - c->scopes)); w >= 0; w = comp_bcall_next(c, w)) { int blk = nt_ref(nt, w, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || comp_scope_of(c, blk) != vs) continue; int k = 0; const char *bp = NULL; for (; k < 4; k++) { bp = block_param_name(c, blk, k); if (!bp || sp_streq(bp, vn)) break; } if (k >= 4 || !bp) continue; const char *itn = nt_str(nt, w, "name"); int recv = nt_ref(nt, w, "receiver"); int a = nt_ref(nt, w, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!itn) continue; /* the builtin's own copy carries the receiver as its first argument (desugar_builtin_enum_calls) */ char nb[128]; if (recv < 0 && strncmp(itn, "__enum_", 7) == 0 && an >= 1) { const char *bn = itn + 7; const char *sep = strstr(bn, "__"); if (!sep || (size_t)(sep - bn) >= sizeof nb) continue; memcpy(nb, bn, (size_t)(sep - bn)); nb[sep - bn] = 0; itn = nb; recv = av[0]; av++; an--; } if (recv < 0) continue; int self = -1; if (k == 0 && is_tap_alias(itn)) self = recv; else if (k == 1 && sp_streq(itn, "each_with_object") && an >= 1) self = av[0]; int elem = ty_is_hash(infer_type(c, recv)) ? (k == 1 && (is_each_or_pair(itn))) || (k == 0 && sp_streq(itn, "each_value")) : k == 0 && strbuf_elem_first_iterator(itn); /* an iterator binding the receiver's elements binds the parameter: the same test as the element walk below, `each_value` included */ if (bound && (self >= 0 || elem)) *bound = 1; if (self >= 0) { changed |= leaf ? strbuf_store_leaf(c, self, depth + 1, mode) : strbuf_container_source_walk(c, self, depth + 1, mode); continue; } if (elem) changed |= strbuf_container_source_walk(c, recv, depth + 1, leaf ? mode : mode + SB_NEST1); } return changed; } /* A local container handed to a method that stores into the parameter binding it (`def keep(a) = (a << n)`, `keep(a)`): the walk follows a container's stores in its own scope and back to the callers of a parameter, not on into a callee, so a String stored there stays a copy the caller's element mutation never reaches. Refused (#6765), unless the callee's own demand already made the stored value the handle. */ /* Does `n` hold node `x`, short of nothing (a block or a def inside runs no earlier than n does)? */ static int an_subtree_has(const NodeTable *nt, int n, int x) { if (n < 0) return 0; if (n == x) return 1; const SpNode *nd = &nt->nodes[n]; for (int i = 0; i < nd->nr; i++) if (an_subtree_has(nt, nd->r[i].ref, x)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (an_subtree_has(nt, nd->a[i].ids[j], x)) return 1; return 0; } /* Does store `st` of method scope m run after a statement of m's body that binds parameter pn to a new Array or Hash (`pn = []`)? The store then fills that container, not the caller's: the body's statements run in order, once per call, so whatever a later statement holds runs after the rebinding. A store before it, or with no such statement, still reaches the caller's container. */ static int store_after_param_rebind(Compiler *c, Scope *m, const char *pn, int st) { const NodeTable *nt = c->nt; int bn = 0; const int *bb = m->body >= 0 && nt_kind(nt, m->body) == NK_StatementsNode ? nt_arr(nt, m->body, "body", &bn) : NULL; int i = 0; for (; i < bn; i++) { int w = nt_paren_single(nt, bb[i]), v = w >= 0 && nt_kind(nt, w) == NK_LocalVariableWriteNode ? nt_paren_single(nt, nt_ref(nt, w, "value")) : -1; if (v >= 0 && sp_streq(nt_str(nt, w, "name"), pn) && comp_scope_of(c, w) == m && (nt_kind(nt, v) == NK_ArrayNode || nt_kind(nt, v) == NK_HashNode)) break; } for (int j = i + 1; j < bn; j++) if (an_subtree_has(nt, bb[j], st)) return 1; return 0; } /* Does call u hand a read of local `vn` as an argument, where arg_layout_param_node could answer it: a positional, or a keyword's value? The walk below resolved every call's target before asking, and a scope's calls are asked per container local, every round. */ static int call_passes_local(const NodeTable *nt, int u, const char *vn) { int a = nt_ref(nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int i = 0; i < ac; i++) { NodeKind k = nt_kind(nt, av[i]); if (k == NK_LocalVariableReadNode) { const char *an = nt_str(nt, av[i], "name"); if (an && sp_streq(an, vn)) return 1; } else if (k == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, av[i], "elements", &en); for (int e = 0; e < en; e++) { if (nt_kind(nt, el[e]) != NK_AssocNode) continue; int v = nt_ref(nt, el[e], "value"); if (v >= 0 && nt_kind(nt, v) == NK_LocalVariableReadNode && nt_str(nt, v, "name") && sp_streq(nt_str(nt, v, "name"), vn)) return 1; } } } return 0; } /* The calls of a scope that hand local (vn, vs) as an argument, by (scope, name), kept while the node table and the scopes hold still, as an_local_array_changed's answers are: the test reads only the calls' arguments. The refusal walk is asked per container local, every round, and each ask read every call of the scope: on a Rails program late in its fixpoint that reading was 6% of a sampled window. The ids live in one pool the entries index by offset. */ typedef struct { int sidx; const char *name; int off, n; } RcmEntry; static RcmEntry *rcm_tab; static int rcm_cap, rcm_n; static int *rcm_ids; static int rcm_ids_n, rcm_ids_cap; static const NodeTable *rcm_nt; static unsigned rcm_ver, rcm_gen; static int rcm_count, rcm_nscopes = -1; static RcmEntry *rcm_slot(int sidx, const char *name) { unsigned h = (sp_strhash(name) ^ ((unsigned)sidx * 2654435761u)) & (unsigned)(rcm_cap - 1); while (rcm_tab[h].name && !(rcm_tab[h].sidx == sidx && sp_streq(rcm_tab[h].name, name))) h = (h + 1) & (unsigned)(rcm_cap - 1); return &rcm_tab[h]; } static RcmEntry *rcm_find(Compiler *c, int sidx, const char *name) { const NodeTable *nt = c->nt; if (rcm_nt != nt || rcm_ver != nt->version || rcm_count != nt->count || rcm_gen != comp_scope_index_gen() || rcm_nscopes != c->nscopes) { free(rcm_tab); rcm_cap = 1024; rcm_n = 0; rcm_ids_n = 0; rcm_tab = calloc((size_t)rcm_cap, sizeof *rcm_tab); if (!rcm_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } rcm_nt = nt; rcm_ver = nt->version; rcm_count = nt->count; rcm_gen = comp_scope_index_gen(); rcm_nscopes = c->nscopes; } if (rcm_n * 2 >= rcm_cap) { RcmEntry *old = rcm_tab; int ocap = rcm_cap; rcm_cap *= 2; rcm_tab = calloc((size_t)rcm_cap, sizeof *rcm_tab); if (!rcm_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < ocap; i++) if (old[i].name) *rcm_slot(old[i].sidx, old[i].name) = old[i]; free(old); } return rcm_slot(sidx, name); } /* Those calls, copied out for the caller: the walk under each one may ask for another local's, which can move the pool. */ static int *rcm_calls(Compiler *c, const char *vn, Scope *vs, int *n) { const NodeTable *nt = c->nt; int sidx = (int)(vs - c->scopes); RcmEntry *e = rcm_find(c, sidx, vn); if (!e->name) { int off = rcm_ids_n; for (int u = comp_scall_first(c, sidx); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || !call_passes_local(nt, u, vn)) continue; if (rcm_ids_n == rcm_ids_cap) { rcm_ids_cap = rcm_ids_cap ? rcm_ids_cap * 2 : 1024; rcm_ids = realloc(rcm_ids, sizeof(int) * (size_t)rcm_ids_cap); if (!rcm_ids) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } rcm_ids[rcm_ids_n++] = u; } e->sidx = sidx; e->name = vn; e->off = off; e->n = rcm_ids_n - off; rcm_n++; } *n = e->n; if (e->n == 0) return NULL; int *out = malloc(sizeof(int) * (size_t)e->n); if (!out) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(out, rcm_ids + e->off, sizeof(int) * (size_t)e->n); return out; } static void refuse_callee_container_stores(Compiler *c, const char *vn, Scope *vs) { const NodeTable *nt = c->nt; StoreVals st = {0}; int nu = 0; int *us = rcm_calls(c, vn, vs, &nu); for (int k = 0; k < nu; k++) { int u = us[k]; int mi = comp_scope_of(c, u) == vs ? an_call_target_mi(c, u) : -1; Scope *m = mi > 0 ? &c->scopes[mi] : NULL; for (int j = 0; m && j < m->nparams; j++) { int a = arg_layout_param_node(c, m, u, j, NULL); const char *pn = m->pnames[j]; if (!pn || a < 0 || nt_kind(nt, a) != NK_LocalVariableReadNode || !sp_streq(nt_str(nt, a, "name"), vn)) continue; int ns = 0; const int *sn = sb_store_nodes(c, pn, m, &ns); for (int i = 0; i < ns; i++) { /* `def keep(a) = (a = []; a << s)`: a store into the method's own Array */ if (store_after_param_rebind(c, m, pn, sn[i])) continue; int n = strbuf_container_store_values(c, sn[i], pn, m, 0, &st); for (int e = 0; e < n; e++) { int l = nt_paren_single(nt, st.v[e]); TyKind lt = infer_type(c, l); LocalVar *ll = nt_kind(nt, l) == NK_LocalVariableReadNode ? scope_local(comp_scope_of(c, l), nt_str(nt, l, "name")) : NULL; (void)lt; (void)ll; } } } } free(st.v); free(us); } /* The stores into container local (vn, vs): its own, and for a parameter or a block parameter, those of what the caller or the iterator binds it to. */ static int strbuf_demand_local_container(Compiler *c, const char *vn, Scope *vs, int depth, int mode) { LocalVar *lv = scope_local(vs, vn); if (!lv || depth > 8) return 0; int si = (int)(vs - c->scopes), li = (int)(lv - vs->locals), res; if (sb_memo_enter(SBM_LOCAL, si, li, mode, depth, &res)) return res; int changed = strbuf_demand_container_stores_here(c, vn, vs, depth, mode); if (lv->is_block_param) changed |= strbuf_block_param_source_walk(c, vn, vs, depth, mode, 0, NULL); else if (lv->is_param) changed |= strbuf_demand_param_container_stores(c, vn, vs, depth, mode); else if (mode == SB_DEMAND) refuse_callee_container_stores(c, vn, vs); return sb_memo_leave(SBM_LOCAL, si, li, mode, changed); } static int strbuf_demand_container_stores(Compiler *c, const char *contn, Scope *conts) { return strbuf_demand_local_container(c, contn, conts, 0, SB_DEMAND); } /* The literal Symbol key a Hash element read or store names (`h[:k]`, `h.fetch(:k)`, `h[:k] = v`, `h.store(:k, v)`, `h[:k] ||= v`), or NULL. */ static const char *hash_literal_sym_key(const NodeTable *nt, int id) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode && k != NK_IndexOrWriteNode) return NULL; int a = nt_ref(nt, id, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (k == NK_CallNode) { const char *nm = nt_str(nt, id, "name"); int want = !nm ? 0 : sp_streq(nm, "[]") || sp_streq(nm, "fetch") ? 1 : is_store_alias(nm) ? 2 : 0; if (!want || an != want || nt_ref(nt, id, "block") >= 0) return NULL; } else if (an != 1) return NULL; return nt_kind(nt, av[0]) == NK_SymbolNode ? nt_str(nt, av[0], "value") : NULL; } /* Is v never a String: a literal of another class, or of a type that is none? */ static int hash_value_no_string(Compiler *c, int v) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0) return 0; switch (nt_kind(nt, v)) { case NK_ArrayNode: case NK_HashNode: case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_TrueNode: case NK_FalseNode: case NK_NilNode: return 1; default: break; } TyKind t = infer_type(c, v); return t != TY_UNKNOWN && t != TY_POLY && t != TY_STRING && t != TY_STRBUF; } static int hash_local_key_no_string(Compiler *c, const char *vn, Scope *vs, const char *key, int depth); /* Is v a new Hash no other name holds, with no String under `key` or under a key that is no literal Symbol: a literal, a bare `Hash.new`, or what a call to one user method answers when each of its values is one, or a local of it that holds none there? */ static int hash_new_no_string_at(Compiler *c, int v, const char *key, int depth) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); NodeKind k = v >= 0 ? nt_kind(nt, v) : NK_NONE; if (k == NK_HashNode) { int en = 0; const int *el = nt_arr(nt, v, "elements", &en); for (int e = 0; e < en; e++) { if (nt_kind(nt, el[e]) != NK_AssocNode) return 0; /* `**opts` */ int ek = nt_ref(nt, el[e], "key"); const char *sk = nt_kind(nt, ek) == NK_SymbolNode ? nt_str(nt, ek, "value") : NULL; if (sk && !sp_streq(sk, key)) continue; if (!hash_value_no_string(c, nt_ref(nt, el[e], "value"))) return 0; } return 1; } if (k != NK_CallNode || depth > 4) return 0; int r = nt_ref(nt, v, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_ConstantReadNode && sp_streq(nt_str(nt, r, "name"), "Hash")) return sp_streq(nt_str(nt, v, "name"), "new") && nt_ref(nt, v, "arguments") < 0 && nt_ref(nt, v, "block") < 0; const CallPlan *p = cplan_user_fresh(c, v); int mi = p->dispatch == CP_DIRECT ? p->mi : -1; if (mi < 0 || mi >= c->nscopes) return 0; Scope *m = &c->scopes[mi]; int out[32]; int n = method_value_leaves_or_nil(c, mi, out, 32); if (n <= 0) return 0; for (int i = 0; i < n; i++) { int l = nt_paren_single(nt, out[i]); int ok = nt_kind(nt, l) == NK_LocalVariableReadNode && comp_scope_of(c, l) == m ? hash_local_key_no_string(c, nt_str(nt, l, "name"), m, key, depth + 1) : hash_new_no_string_at(c, l, key, depth + 1); if (!ok) return 0; } return 1; } /* Is read r of a local a value method scope si answers: its body's tail (through a begin/rescue body), or one of its values? */ static int scope_answers_leaf(const VpLeaf *leaf, void *ud) { return leaf->kind == VPK_EXPR && leaf->node == *(int *)ud; } static int scope_answers_read(Compiler *c, int si, int r) { const NodeTable *nt = c->nt; int b = c->scopes[si].body; if (b >= 0 && for_each_value_position(nt, b, VPS_BEGIN | VP_BEGIN_BOTH, scope_answers_leaf, &r)) return 1; int out[32]; int n = method_value_leaves_or_nil(c, si, out, 32); for (int i = 0; i < n; i++) if (nt_paren_single(nt, out[i]) == r) return 1; return 0; } /* Is read r of a local an argument of `p r` (`pp`, `puts`, `print`) whose value is dropped or its method's, which hands the value on no further than answering it does? */ static int scope_prints_read(Compiler *c, int si, int r) { const NodeTable *nt = c->nt; for (int u = comp_scall_first(c, si); u >= 0; u = comp_scall_next(c, u)) { const char *un = nt_ref(nt, u, "receiver") < 0 ? nt_str(nt, u, "name") : NULL; if (!un || !(sp_streq(un, "p") || sp_streq(un, "pp") || sp_streq(un, "puts") || sp_streq(un, "print")) || an_user_defines_method(c, un) || !(comp_value_dropped(c, u) || scope_answers_read(c, si, u))) continue; int ua = nt_ref(nt, u, "arguments"), uc = 0; const int *uv = ua >= 0 ? nt_arr(nt, ua, "arguments", &uc) : NULL; for (int k = 0; k < uc; k++) if (nt_paren_single(nt, uv[k]) == r) return 1; } return 0; } /* Does Hash local (vn, vs) never hold a String under literal Symbol key `key`? Each value it stores there, or under a key that is no literal Symbol, is none (hash_value_no_string), and no String moves there from another key out of sight: no other name holds the Hash. Each write is a statement storing a new Hash (hash_new_no_string_at); each read is a store's receiver, the receiver of a call that hands out no Hash and moves no value, an argument `p` prints, or a value its method answers. A value another key hands out (`h[:k] = h[:j]`) is a stored value like any other, and a possible String. */ static int hash_local_key_no_string(Compiler *c, const char *vn, Scope *vs, const char *key, int depth) { static const char *const keep[] = { "[]", "fetch", "dig", "key?", "has_key?", "include?", "member?", "delete", "size", "length", "empty?", "any?", "keys", "values", "values_at", "fetch_values", "inspect", "to_s", "==", "frozen?", NULL }; const NodeTable *nt = c->nt; LocalVar *lv = vn ? scope_local(vs, vn) : NULL; if (!lv || lv->is_param || lv->is_block_param || depth > 4) return 0; int si = (int)(vs - c->scopes), saw = 0; for (int w = comp_lvw_first_sc(c, si, vn); w >= 0; w = comp_lvw_next_sc(c, w)) { const char *wn = nt_str(nt, w, "name"); if (w >= c->node_cap || c->nscope[w] != si || !wn || !sp_streq(wn, vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode || !comp_value_dropped(c, w) || !hash_new_no_string_at(c, nt_ref(nt, w, "value"), key, depth)) return 0; saw = 1; } if (!saw) return 0; for (int e = comp_vsite_first(c, VS_READ, NK_LocalVariableReadNode, vn, si); e >= 0; e = comp_vsite_next(c, e)) { int r = comp_vsite_var(c, e); if (nt_kind(nt, r) != NK_LocalVariableReadNode || comp_scope_of(c, r) != vs || !sp_streq(nt_str(nt, r, "name"), vn)) continue; int p = comp_recv_parent(c, r); /* comp_recv_parent indexes calls: `h[:k] ||= v`'s receiver is looked up */ for (int u = p < 0 ? comp_kind_first(c, NK_IndexOrWriteNode) : -1; u >= 0 && p < 0; u = comp_kind_next(c, u)) if (nt_paren_single(nt, nt_ref(nt, u, "receiver")) == r) p = u; NodeKind pk = p >= 0 ? nt_kind(nt, p) : NK_NONE; const char *pn = pk == NK_CallNode ? nt_str(nt, p, "name") : NULL; if (pk == NK_IndexOrWriteNode || (pn && is_store_alias(pn))) { const char *sk = hash_literal_sym_key(nt, p); if (sk && !sp_streq(sk, key)) continue; int a = nt_ref(nt, p, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; int v = pk == NK_IndexOrWriteNode ? nt_ref(nt, p, "value") : an == 2 ? av[1] : -1; if (!hash_value_no_string(c, v)) return 0; continue; } if (pn && str_in(pn, keep)) continue; if (p < 0 && (scope_prints_read(c, si, r) || scope_answers_read(c, si, r))) continue; return 0; } return 1; } /* The values stored into container ivar (cid, ivn): what is written to it, and what is pushed or []='d into it. */ static int strbuf_ivar_source_walk_body(Compiler *c, int cid, const char *ivn, int depth, int mode); static int strbuf_ivar_source_walk(Compiler *c, int cid, const char *ivn, int depth, int mode) { int iv = comp_ivar_index(&c->classes[cid], ivn), res; if (iv < 0) return strbuf_ivar_source_walk_body(c, cid, ivn, depth, mode); if (sb_memo_enter(SBM_IVAR, cid, iv, mode, depth, &res)) return res; return sb_memo_leave(SBM_IVAR, cid, iv, mode, strbuf_ivar_source_walk_body(c, cid, ivn, depth, mode)); } static int strbuf_ivar_source_walk_body(Compiler *c, int cid, const char *ivn, int depth, int mode) { const NodeTable *nt = c->nt; int changed = 0; /* An ivar an --rbs seed pins to a typed String container (`@out: Hash[String, String]`) keeps plain Strings in its slots: a demand that its stores become handles cannot be met there, and only left a store's value a handle its slot has no conversion for (#8302). Under --share-strings a String Array or String Hash the seed pins is boxed (share_default_apply), so its stores are demanded; Hash[Integer, String] has no boxed form. */ if (SB_KIND(mode) == SB_DEMAND && class_ivar_pinned(&c->classes[cid], ivn)) { int iv = comp_ivar_index(&c->classes[cid], ivn); TyKind it = iv >= 0 ? c->classes[cid].ivar_types[iv] : TY_UNKNOWN; if (repr_typed_str_container(it) && it == TY_INT_STR_HASH) return 0; } /* its `=`, `||=` and `&&=` (VS_STORE) */ for (int e = comp_vsite_first(c, VS_STORE, NK_InstanceVariableReadNode, ivn, cid); e >= 0; e = comp_vsite_next(c, e)) { int w = comp_vsite_node(c, e); const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ivn) || comp_ivar_owner(c, w) != cid) continue; changed |= strbuf_container_source_walk(c, nt_ref(nt, w, "value"), depth + 1, mode); } /* the calls on it (VS_RECV): asking every call of the program, for each ivar the walk reaches, was a large program's costliest line */ StoreVals stores = {0}; for (int e = comp_vsite_first(c, VS_RECV, NK_InstanceVariableReadNode, ivn, cid); e >= 0; e = comp_vsite_next(c, e)) { int w = comp_vsite_node(c, e); if (nt_kind(nt, w) != NK_CallNode) continue; int r = nt_ref(nt, w, "receiver"); if (r < 0 || nt_kind(nt, r) != NK_InstanceVariableReadNode) continue; const char *rn = nt_str(nt, r, "name"); if (!rn || !sp_streq(rn, ivn) || comp_ivar_owner(c, r) != cid) continue; int nst = array_call_store_values(c, w, &stores); for (int e = 0; e < nst; e++) changed |= strbuf_store_leaf(c, stores.v[e], depth, mode); } free(stores.v); /* `@h[k] ||= v` and `@h[k] &&= v` store v, as a local container's do (strbuf_demand_container_stores_here): the write is no call */ for (int q = 0; q < 2; q++) NT_FOREACH_KIND(nt, q ? NK_IndexAndWriteNode : NK_IndexOrWriteNode, w) { int r = nt_paren_single(nt, nt_ref(nt, w, "receiver")); const char *rn = r >= 0 && nt_kind(nt, r) == NK_InstanceVariableReadNode ? nt_str(nt, r, "name") : NULL; if (!rn || !sp_streq(rn, ivn) || comp_ivar_owner(c, r) != cid) continue; int v = nt_ref(nt, w, "value"); if (v >= 0) changed |= strbuf_store_leaf(c, v, depth, mode); } return changed; } /* A builtin call whose result holds the receiver's own element objects, as CRuby's shallow copies and element reads do: `a + b`, `a.dup`, `a[0, 2]`, `a.select { }` are new arrays over the same strings, and `a[0]`, `a.first`, `h.fetch(k)` are one of them -- one container level further in. Answers 1 (with *changed filled) when `node` is such a call. */ static int strbuf_elem_sharing_call(Compiler *c, int node, int depth, int mode, int *changed) { static const char *const same[] = { "dup", "clone", "to_a", "to_ary", "entries", "values", "itself", "freeze", "take", "drop", "select", "filter", "reject", "find_all", "sort", "sort_by", "reverse", "rotate", "uniq", "compact", "shuffle", "take_while", "drop_while", "values_at", "+", "-", "&", "|", "*", "concat", "merge", "union", "difference", "intersection", NULL }; static const char *const elem[] = { "fetch", "find", "detect", "min", "max", "min_by", "max_by", "pop", "shift", "at", NULL }; const NodeTable *nt = c->nt; const char *mn = nt_str(nt, node, "name"); int recv = nt_ref(nt, node, "receiver"); if (!mn || recv < 0) return 0; TyKind rt = infer_type(c, recv); int a = nt_ref(nt, node, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if ((rt == TY_STRING || rt == TY_POLY) && an == 1 && is_partition_family(mn) && infer_type(c, av[0]) == TY_STRING && an_call_target_mi(c, node) < 0) { *changed |= strbuf_store_leaf(c, av[0], depth, mode); return 1; } if (!ty_is_array(rt) && !ty_is_hash(rt)) return 0; int nest = -1; if (is_slice_alias(mn)) nest = (an == 1 && nt_kind(nt, av[0]) != NK_RangeNode) || ty_is_hash(rt) ? 1 : 0; else if (is_element_pick(mn)) nest = an == 0 ? 1 : 0; else if (sp_streq(mn, "dig")) nest = an; else if (sp_streq(mn, "flatten")) { /* one level flattened: the result holds elements of both depths */ *changed |= strbuf_container_source_walk(c, recv, depth + 1, mode + SB_NEST1); nest = 0; } else if (is_push_unshift(mn)) { for (int e = 0; e < an; e++) *changed |= strbuf_store_leaf(c, av[e], depth, mode); nest = 0; } else { for (int k = 0; same[k] && nest < 0; k++) if (sp_streq(mn, same[k])) nest = 0; for (int k = 0; elem[k] && nest < 0; k++) if (sp_streq(mn, elem[k])) nest = 1; } if (nest < 0) return 0; *changed |= strbuf_container_source_walk(c, recv, depth + 1, mode + nest * SB_NEST1); /* the operands of a combining call contribute their elements as well */ if (nest == 0 && (sp_streq(mn, "+") || sp_streq(mn, "|") || sp_streq(mn, "concat") || sp_streq(mn, "merge") || sp_streq(mn, "union"))) for (int e = 0; e < an; e++) *changed |= strbuf_container_source_walk(c, av[e], depth + 1, mode); return 1; } /* The values stored into the container that expression `node` evaluates to -- a literal's elements, a collecting block's tail, the stores into a local, ivar or global, the return tails of the user methods a call can reach -- each demanded into a shared handle or tested, per `mode`. Without following those, a mutation through an element of a method's result (`x = mk; x[1] << "q"`) landed in a copy. */ /* A String global Array `grn` holds that is no handle, or -1. A global's Array is a String Array, or a boxed one its pushes fill with plain Strings: the walk below demands only what its writes store, and a String Array cannot hold a handle at all. A frozen literal raises FrozenError on mutation either way. */ /* Is variable-site entry e (comp_vsite_first) one of global grn's? The chains carry hash collisions. */ static int gvar_site_is(Compiler *c, int e, const char *grn) { int v = comp_vsite_var(c, e); const char *vn = v >= 0 ? nt_str(c->nt, v, "name") : NULL; NodeKind vk = v >= 0 ? nt_kind(c->nt, v) : NK_NONE; const char *vrn = vn && (vk == NK_GlobalVariableReadNode || vk == NK_GlobalVariableWriteNode) ? comp_resolve_gvar(c, vn + 1) : NULL; return vrn && sp_streq(vrn, grn); } /* --share-strings: the values stored into global grn's container where the rule shares its elements: its pushes' and `<<`'s, and the last argument of its `[]=` and `store`, each demanded into a shared handle (a fresh String wrapped as a new one), as a local's or an ivar's stores are. Without them a global's Array or Hash kept the plain String, and a change through an element was lost. */ /* Is variable-site entry e of a global (kind NK_GlobalVariableReadNode, grn its resolved name), or of a constant or a class variable (NK_ConstantReadNode, NK_ClassVariableReadNode) named name? (A chain carries hash collisions.) */ static int static_site_is(Compiler *c, NodeKind kind, int e, const char *name) { if (kind == NK_GlobalVariableReadNode) return gvar_site_is(c, e, name); int v = comp_vsite_var(c, e); const char *vn = v >= 0 ? nt_str(c->nt, v, "name") : NULL; return vn && sp_streq(vn, name); } static int strbuf_static_store_walk(Compiler *c, NodeKind kind, const char *grn, int depth) { const NodeTable *nt = c->nt; int changed = 0; StoreVals stores = {0}; for (int e = comp_vsite_first(c, VS_RECV, kind, grn, -1); e >= 0; e = comp_vsite_next(c, e)) { int u = comp_vsite_node(c, e); if (!static_site_is(c, kind, e, grn)) continue; int nst = array_call_store_values(c, u, &stores); for (int k = 0; k < nst; k++) { /* a block parameter whose String the rule shares holds the handle what binds it hands over (share_default_apply, or the seal refuses it by name): it is stored as itself, the handle the container (a poly Array of handles) then holds */ int sv = nt_paren_single(nt, stores.v[k]); Scope *svs = sv >= 0 && nt_kind(nt, sv) == NK_LocalVariableReadNode ? comp_scope_of(c, sv) : NULL; LocalVar *slv = svs ? scope_local(svs, nt_str(nt, sv, "name")) : NULL; if (slv && slv->is_block_param && repr_local_shares(c, svs, slv)) continue; changed |= strbuf_store_leaf(c, stores.v[k], depth, SB_DEMAND); } } free(stores.v); return changed; } static int strbuf_gvar_store_walk(Compiler *c, const char *grn, int depth) { return strbuf_static_store_walk(c, NK_GlobalVariableReadNode, grn, depth); } /* May a method defined in class k answer a call on a receiver of class rcid: k is rcid or a subclass of it, a module, or no class? */ static int strbuf_class_may_answer(Compiler *c, int k, int rcid) { if (k < 0 || k >= c->nclasses || comp_class_is_module(c, &c->classes[k])) return 1; for (int d = 0; k >= 0 && k < c->nclasses && d < 64; k = c->classes[k].parent, d++) if (k == rcid) return 1; return 0; } /* The containers method mi answers: its body's last value and each `return v`; for `def mk = yield`, the block blk of the call. */ static int strbuf_method_ret_source_walk(Compiler *c, int mi, int blk, int depth, int mode); /* --share-strings: a value of method mi that is a `super` (`def parts = super`): the containers the method it calls answers, handed the super's own block or, with none, mi's (blk) */ static int strbuf_super_ret_walk(Compiler *c, int mi, int v, int blk, int depth, int mode) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0 || (nt_kind(nt, v) != NK_SuperNode && nt_kind(nt, v) != NK_ForwardingSuperNode)) return -1; Scope *m = &c->scopes[mi]; int t = m->class_id >= 0 ? a_super_target(c, m) : -1; if (t < 0 || t == mi) return 0; int sb = nt_ref(nt, v, "block"); return strbuf_method_ret_source_walk(c, t, sb >= 0 && nt_kind(nt, sb) == NK_BlockNode ? sb : blk, depth + 1, mode); } static int strbuf_method_ret_source_walk_body(Compiler *c, int mi, int blk, int depth, int mode); static int strbuf_method_ret_source_walk(Compiler *c, int mi, int blk, int depth, int mode) { int res; if (depth > 8) return 0; if (sb_memo_enter(SBM_RET, mi, blk, mode, depth, &res)) return res; return sb_memo_leave(SBM_RET, mi, blk, mode, strbuf_method_ret_source_walk_body(c, mi, blk, depth, mode)); } static int strbuf_method_ret_source_walk_body(Compiler *c, int mi, int blk, int depth, int mode) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; int changed = 0, sw; if (depth > 8) return 0; int last = scope_body_last(c, mi); if (last >= 0 && nt_kind(nt, last) == NK_YieldNode) { if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode) changed |= strbuf_container_source_walk(c, nt_ref(nt, blk, "body"), depth + 1, mode); } else if ((sw = strbuf_super_ret_walk(c, mi, last, blk, depth, mode)) >= 0) changed |= sw; else changed |= strbuf_container_source_walk(c, last, depth + 1, mode); for (int u = comp_ret_first(c, mi); u >= 0; u = comp_ret_next(c, u)) { if (nt_kind(nt, u) != NK_ReturnNode || comp_scope_of(c, u) != m) continue; int ra = nt_ref(nt, u, "arguments"); int rn2 = 0; const int *rv2 = ra >= 0 ? nt_arr(nt, ra, "arguments", &rn2) : NULL; if (rn2 != 1) continue; if ((sw = strbuf_super_ret_walk(c, mi, rv2[0], blk, depth, mode)) >= 0) changed |= sw; else changed |= strbuf_container_source_walk(c, rv2[0], depth + 1, mode); } return changed; } static int strbuf_container_source_walk_body(Compiler *c, int node, int depth, int mode); static int strbuf_container_source_walk(Compiler *c, int node, int depth, int mode) { int res; if (node < 0 || depth > 8) return 0; if (sb_memo_enter(SBM_EXPR, node, 0, mode, depth, &res)) return res; return sb_memo_leave(SBM_EXPR, node, 0, mode, strbuf_container_source_walk_body(c, node, depth, mode)); } static int strbuf_container_source_walk_body(Compiler *c, int node, int depth, int mode) { const NodeTable *nt = c->nt; if (c->n_cext_extensions && node >= 0) { int producer = nt_int(nt, node, "cext_producer", -1); if (producer >= 0) return strbuf_container_source_walk(c, producer, depth, mode); } if (node < 0 || depth > 8) return 0; int changed = 0; switch (nt_kind(nt, node)) { case NK_ArrayNode: { int en = 0; const int *el = nt_arr(nt, node, "elements", &en); for (int e = 0; e < en; e++) changed |= strbuf_store_leaf(c, el[e], depth, mode); return changed; } case NK_HashNode: case NK_KeywordHashNode: { int en = 0; const int *el = nt_arr(nt, node, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_AssocNode) changed |= strbuf_store_leaf(c, nt_ref(nt, el[e], "value"), depth, mode); return changed; } case NK_ParenthesesNode: return strbuf_container_source_walk(c, nt_ref(nt, node, "body"), depth + 1, mode); case NK_StatementsNode: { int n = 0; const int *b = nt_arr(nt, node, "body", &n); return n > 0 ? strbuf_container_source_walk(c, b[n - 1], depth + 1, mode) : 0; } case NK_ElseNode: return strbuf_container_source_walk(c, nt_ref(nt, node, "statements"), depth + 1, mode); case NK_IfNode: return strbuf_container_source_walk(c, nt_ref(nt, node, "statements"), depth + 1, mode) | strbuf_container_source_walk(c, nt_ref(nt, node, "subsequent"), depth + 1, mode); case NK_UnlessNode: return strbuf_container_source_walk(c, nt_ref(nt, node, "statements"), depth + 1, mode) | strbuf_container_source_walk(c, nt_ref(nt, node, "else_clause"), depth + 1, mode); case NK_OrNode: case NK_AndNode: return strbuf_container_source_walk(c, nt_ref(nt, node, "left"), depth + 1, mode) | strbuf_container_source_walk(c, nt_ref(nt, node, "right"), depth + 1, mode); case NK_LocalVariableReadNode: { const char *vn = nt_str(nt, node, "name"); Scope *vs = vn ? comp_scope_of(c, node) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv) return 0; if (SB_KIND(mode) != SB_DEMAND) return lv->is_param ? 0 : strbuf_container_stores_kind(c, vn, vs, depth + 1, mode); return strbuf_demand_local_container(c, vn, vs, depth + 1, mode); } case NK_InstanceVariableReadNode: { const char *ivn = nt_str(nt, node, "name"); int cid = ivn ? comp_ivar_owner(c, node) : -1; return cid < 0 ? 0 : strbuf_ivar_source_walk(c, cid, ivn, depth + 1, mode); } case NK_GlobalVariableReadNode: { const char *gn = nt_str(nt, node, "name"); const char *grn = gn ? comp_resolve_gvar(c, gn + 1) : NULL; if (!grn) return 0; for (int w = comp_kind_first(c, NK_GlobalVariableWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_GlobalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); const char *wrn = wn ? comp_resolve_gvar(c, wn + 1) : NULL; if (!wrn || !sp_streq(wrn, grn)) continue; changed |= strbuf_container_source_walk(c, nt_ref(nt, w, "value"), depth + 1, mode); } if (mode == SB_DEMAND) changed |= strbuf_gvar_store_walk(c, grn, depth + 1); return changed; } case NK_CallNode: { int tail = strbuf_map_block_tail(c, node); if (tail >= 0) return SB_KIND(mode) == SB_HAS_NONSTRING ? 0 : strbuf_store_leaf(c, tail, depth, mode); const char *mn = nt_str(nt, node, "name"); if (!mn) return 0; int recv = nt_ref(nt, node, "receiver"); int blk = nt_ref(nt, node, "block"); int nsh = strbuf_elem_sharing_call(c, node, depth, mode, &changed); if (nsh) return changed; /* an attr_reader has no body to walk: its ivar is the container */ { char ivb[300]; int defc = -1; const char *riv = an_reader_ivar_of(c, node, &defc, ivb, sizeof ivb); if (riv && defc >= 0) return strbuf_ivar_source_walk(c, defc, riv, depth + 1, mode); } /* `Hash.new { |hh, k| hh[k] = v }`: the default block's first parameter is the Hash itself, and what it stores there is the Hash's */ int hnew = sp_streq(mn, "new") && recv >= 0 && nt_kind(nt, recv) == NK_ConstantReadNode && nt_str(nt, recv, "name") && sp_streq(nt_str(nt, recv, "name"), "Hash"); /* `Hash.new(v)`: a read of a missing key answers v itself (not counted as a value the Hash holds when asking for a non-String) */ if (hnew && blk < 0 && SB_KIND(mode) != SB_HAS_NONSTRING) { int ha = nt_ref(nt, node, "arguments"), hn = 0; const int *hv = ha >= 0 ? nt_arr(nt, ha, "arguments", &hn) : NULL; if (hn >= 1 && nt_kind(nt, hv[0]) != NK_KeywordHashNode) return strbuf_store_leaf(c, hv[0], depth, mode); } if (hnew && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode) { const char *hp = block_param_name(c, blk, 0); Scope *hs = hp ? comp_scope_of(c, blk) : NULL; if (hs && SB_KIND(mode) != SB_DEMAND) return strbuf_container_stores_kind(c, hp, hs, depth + 1, mode); return hs ? strbuf_demand_container_stores_here(c, hp, hs, depth + 1, mode) : 0; } /* `Array.new(n) { ... }` fills the array with its block's tail */ if (sp_streq(mn, "new") && recv >= 0 && nt_kind(nt, recv) == NK_ConstantReadNode && nt_str(nt, recv, "name") && sp_streq(nt_str(nt, recv, "name"), "Array")) { /* --share-strings: `Array.new(n, s)` fills every slot with s itself, so s is what it stores (and a poly form holds its handle, infer_call's Array.new arm) */ int fa = nt_ref(nt, node, "arguments"), fn = 0; const int *fv = fa >= 0 ? nt_arr(nt, fa, "arguments", &fn) : NULL; /* A shallow copy stores the source's elements, including fresh Strings that have no holder of their own. */ if (fn == 1 && blk < 0 && ty_is_array(infer_type(c, fv[0]))) return strbuf_container_source_walk(c, fv[0], depth + 1, mode); if (fn == 2 && blk < 0) return strbuf_store_leaf(c, fv[1], depth, mode); if (SB_KIND(mode) == SB_HAS_NONSTRING || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; int body = nt_ref(nt, blk, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) return 0; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); return bn > 0 ? strbuf_store_leaf(c, bb[bn - 1], depth, mode) : 0; } if (recv >= 0 && nt_kind(nt, recv) != NK_SelfNode && !ty_is_object(infer_type(c, recv))) return 0; /* every user method of the name a call resolves to: by name, but on a receiver of a known class only that class's own (resolved through its chain), its subclasses' and a module's. Walking every class's method of a common name (each class's to_a) from each call was quadratic in the classes. */ int rcid = recv >= 0 && nt_kind(nt, recv) != NK_SelfNode ? ty_object_class(infer_type(c, recv)) : -1; int rdef = rcid >= 0 ? comp_method_in_chain(c, rcid, mn, NULL) : -1; for (int mi = an_scopes_named_first(c, mn); mi >= 1; mi = an_scopes_named_next(c, mn, mi)) { Scope *m = &c->scopes[mi]; if (rcid >= 0 && mi != rdef && !strbuf_class_may_answer(c, m->class_id, rcid)) continue; changed |= strbuf_method_ret_source_walk(c, mi, blk, depth, mode); } return changed; } default: return 0; } } /* An opaque extension may change any String it can obtain from a container. * Demand those producer values explicitly; no Ruby []/append body exists from * which the ordinary mutation summaries could discover the inner effect. */ int cext_demand_reader(Compiler *c, int call) { const char *name = nt_str(c->nt, call, "name"); int recv = nt_ref(c->nt, call, "receiver"); if (!name || recv < 0) return 0; int changed = 0; size_t size = strlen(name) + 2; char *ivar = malloc(size); if (!ivar) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } snprintf(ivar, size, "@%s", name); for (int k = 0; k < c->nclasses; k++) { int owner = -1; if (cls_fact_excludes(c, recv, k) || comp_resolve_member(c, k, name, 0, NULL, NULL) != SP_MEMBER_ATTR || !comp_reader_in_chain(c, k, name, &owner)) continue; changed |= strbuf_promote_ivar(c, owner, ivar); } free(ivar); return changed; } int cext_demand_string(Compiler *c, int value) { if (share_frozen_literal(c, value)) return 0; if(nt_kind(c->nt,value)==NK_InstanceVariableReadNode) { Scope *scope=comp_scope_of(c,value); const char *name=nt_str(c->nt,value,"name"); if(scope && scope->class_id>=0 && name) { int changed = strbuf_promote_ivar(c,scope->class_id,name); if (!c->strbuf_box[value]) { comp_set_strbuf_box(c, value, 1); changed = 1; } return changed; } } if (nt_kind(c->nt, value) == NK_ConstantReadNode) { const char *name = nt_str(c->nt, value, "name"); LocalVar *slot = name ? comp_const(c, name) : NULL; if (slot && (slot->type == TY_STRING || slot->type == TY_STRBUF)) { int changed = slot->type != TY_STRBUF || !slot->str_shared || !c->strbuf_box[value]; lv_set_type(slot, TY_STRBUF); { slot->str_shared = 1; comp_touch_scope(slot->owner); } comp_set_strbuf_box(c, value, 1); /* All reads of this holder must retain its identity across a C mutation. */ for (int read = 0; read < c->nt->count; read++) { NodeKind kind = nt_kind(c->nt, read); if (kind != NK_ConstantReadNode && kind != NK_ConstantPathNode) continue; const char *key = nt_str(c->nt, read, "name"); if (!key || !sp_streq(key, name) || c->strbuf_box[read]) continue; comp_set_strbuf_box(c, read, 1); changed = 1; } return changed; } } if (nt_kind(c->nt, value) == NK_CallNode) { int targets[CPT_MAX]; /* Walk user method return producers in cext_widen_containers. Demanding * a copy of the call here would lose the producer's alias identity. */ if (cplan_targets(c, value, targets, CPT_MAX)>0) return 0; int owner = -1; char ivar[256]; if (an_reader_ivar_of(c, value, &owner, ivar, sizeof ivar)) return strbuf_promote_ivar(c,owner,ivar); } return strbuf_demand_store_leaf(c, value, 0); } int cext_demand_container_strings(Compiler *c, int value) { return strbuf_container_source_walk(c, value, 0, SB_DEMAND); } /* Slot-shape eligibility for the shared handle: parameters, cells, byref out-params and rbs-pinned slots wait for the cross-method phase. */ static int strbuf_slot_eligible_shape(Compiler *c, const char *vn, Scope *vs, LocalVar *lv); static int strbuf_slot_eligible(Compiler *c, const char *vn, Scope *vs, LocalVar *lv) { if (!lv) return 0; if (lv->type != TY_STRING && lv->type != TY_STRBUF) return 0; return strbuf_slot_eligible_shape(c, vn, vs, lv); } /* shape-only eligibility (no type gate): the ALIAS arm may see the local while its type is still a transient UNKNOWN/POLY mid-fixpoint */ static int strbuf_slot_eligible_shape(Compiler *c, const char *vn, Scope *vs, LocalVar *lv) { const NodeTable *nt = c->nt; /* an rbs-seeded String slot may still take the handle REPRESENTATION: the pin constrains the Ruby-level type, not the C storage (#3227) */ if (!lv || lv->is_param) return 0; /* A local written from a HASH or ARRAY literal is not a string, whatever a mutator name suggests. The evidence that promotes a slot is a name-keyed mutator table, and `[]=`, `insert`, `slice!` and `setbyte` are Hash's and Array's as much as String's -- so `data = parse_object(s); data[k] = v` demanded the callee's returned `out = {}` into the shared set, and the Hash came back through a `const char *` return (#4177). The write is what the local IS; a shared name is only what was done to it. Checked syntactically because it has to hold in the fixpoint's first round, before any type has settled -- the promotion is sticky once made. */ /* both walks below ran over the WHOLE node table once per candidate local (and this shape check runs per fixpoint round): the (scope, name) write chain and the per-scope call chain make each walk the handful of nodes the scope actually holds. The chains carry hash collisions and every filter stays. */ int vsi = (int)(vs - c->scopes); for (int w = comp_lvw_first_sc(c, vsi, vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; if (comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; int wv = nt_ref(nt, w, "value"); if (wv < 0) continue; NodeKind vk = nt_kind(nt, wv); if (vk == NK_HashNode || vk == NK_KeywordHashNode || vk == NK_ArrayNode) return 0; } for (int u = comp_scall_first(c, vsi); u >= 0; u = comp_scall_next(c, u)) { NodeKind uk = nt_kind(nt, u); if (uk != NK_CallNode) continue; if (comp_scope_of(c, u) != vs) continue; /* the group's answer, not the unique one: see an_any_scope_by_name -- but not a builtin value's call, which none of them takes */ if (an_call_recv_builtin(c, u)) continue; int mi = an_any_scope_by_name(c, nt_str(nt, u, "name")); if (mi < 0) continue; for (int j = 0; j < c->scopes[mi].nparams; j++) { if (!comp_byref_param(c, &c->scopes[mi], j)) continue; int a2 = arg_layout_param_node(c, &c->scopes[mi], u, j, NULL); const char *an2 = a2 >= 0 && nt_kind(nt, a2) == NK_LocalVariableReadNode ? nt_str(nt, a2, "name") : NULL; if (an2 && sp_streq(an2, vn)) return 0; } } return 1; } /* Is local `ln` of scope `mi` only ever another name for local `pn` (every write `ln = pn`, `ln = t` with t such an alias, or `ln = (pn << x)`)? Then an append through it is an append to pn's object (promote_local_alias_pairs shares the two), and a parameter appended to that way is appended to in place: a nil argument must not widen it to a boxed copy. */ static int an_local_pure_alias_of(Compiler *c, int mi, const char *ln, const char *pn, int depth) { const NodeTable *nt = c->nt; if (depth > 3 || sp_streq(ln, pn)) return 0; int any = 0; for (int w = comp_lvw_first_sc(c, mi, ln); w >= 0; w = comp_lvw_next_sc(c, w)) { /* the chain is a hash bucket: another local's write can share it, and taken for one of ln's it made a pure alias look rebound */ if (c->nscope[w] != mi || !nt_str(nt, w, "name") || !sp_streq(nt_str(nt, w, "name"), ln)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) return 0; int v = an_strbuf_alias_source(c, nt_ref(nt, w, "value")); const char *vn = v >= 0 ? nt_str(nt, v, "name") : NULL; if (!vn) return 0; if (!sp_streq(vn, pn) && !an_local_pure_alias_of(c, mi, vn, pn, depth + 1)) return 0; any = 1; } return any; } /* The calls of each scope whose name is a String mutator's (or a bang name), chained in node order: the comp_scall_first chain with the name test already applied. an_param_mutated_in_place is asked for every argument of every resolvable call site, every fixpoint round, and testing each of the callee's calls against the mutator table was most of its cost on a large program. Rebuilt with the node table (a rename moves its version) or when scopes are added, as that chain is. */ static int *smc_head, *smc_next, smc_nscopes, smc_count, smc_built; static unsigned smc_version; static const NodeTable *smc_nt; static int smc_first(Compiler *c, int si) { const NodeTable *nt = c->nt; if (!smc_built || smc_nt != nt || smc_version != nt->version || smc_count != nt->count || smc_nscopes < c->nscopes) { free(smc_head); free(smc_next); int n = nt->count, ns = c->nscopes > 0 ? c->nscopes : 1; smc_head = malloc((size_t)ns * sizeof(int)); smc_next = malloc((size_t)(n > 0 ? n : 1) * sizeof(int)); /* no chain would answer "mutates nothing" for every method, which drops the handle a callee's append needs */ if (!smc_head || !smc_next) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } smc_built = 1; for (int s = 0; s < ns; s++) smc_head[s] = -1; for (int u = n - 1; u >= 0; u--) { /* reverse: chains run in node order */ smc_next[u] = -1; if (nt_kind(nt, u) != NK_CallNode) continue; const char *un = nt_str(nt, u, "name"); if (!un || (!an_str_mutator_name(un) && !(bop_name_mutates(un, 0)))) continue; int s = c->nscope ? c->nscope[u] : 0; if (s < 0 || s >= ns) s = 0; smc_next[u] = smc_head[s]; smc_head[s] = u; } smc_nt = nt; smc_version = nt->version; smc_count = n; smc_nscopes = ns; } return si >= 0 && si < smc_nscopes ? smc_head[si] : -1; } static int smc_next_of(int u) { return u >= 0 && u < smc_count ? smc_next[u] : -1; } /* Does scope `mi` mutate its parameter `pi` in place (`p << x`, `p.gsub!`)? The byref machinery answers the same question, but it is computed after the fixpoint (compute_byref_out_params), so this pass -- which runs inside it -- asks the body directly. Only a plain local read of the parameter counts, and not one after the body rebinds it to a fresh String, as the byref pass asks (an_param_fresh_at). Encoding and frozen-state changes under sharing need the same parameter handle as byte mutations. */ static int an_param_mutated_in_place(Compiler *c, int mi, int pi) { if (mi < 0 || mi >= c->nscopes) return 0; Scope *m = &c->scopes[mi]; if (cext_scope(c, mi)) return 1; if (pi < 0 || pi >= m->nparams || !m->pnames[pi]) return 0; const char *pn = m->pnames[pi]; const NodeTable *nt = c->nt; /* the callee's OWN calls, not the program's: the sweep below asks this for every argument of every resolvable call site, every fixpoint round, and the whole-table walk made that quadratic on a large program (the same chain strbuf_slot_eligible_shape moved to), and of those only the ones a mutator's name calls (smc_first) */ for (int u = smc_first(c, mi); u >= 0; u = smc_next_of(u)) { int ur = nt_ref(nt, u, "receiver"); if (ur < 0 || nt_kind(nt, ur) != NK_LocalVariableReadNode) continue; const char *urn = nt_str(nt, ur, "name"); if (urn && sp_streq(urn, pn) && !an_param_fresh_at(c, m, pn, u)) return 1; if (urn && an_local_pure_alias_of(c, mi, urn, pn, 0)) return 1; } return 0; } /* The demand below is a depth-bounded walk with no visited set: a poly variable's read goes to every write of it, and those values read other variables in turn, so a variable with W writes was walked W^hops times (a machine-generated method appends to every boxed local it has). The memo remembers a variable whose writes were walked from some depth and changed nothing -- marked no node, promoted no local -- and skips its walk from that depth or deeper. Nothing having changed since, the skipped walk would see what the remembered one saw with less budget left, and so also change nothing. Every change moves the generation on, which forgets every entry; so does each run of the demand block, as other passes run between them. Off (sbd_memo_on) outside that block. */ typedef struct { int kind, owner; const char *name; unsigned gen; int depth; } SbdMemo; static SbdMemo *sbd_memo; static int sbd_memo_cap, sbd_memo_n, sbd_memo_on; static unsigned sbd_gen = 1; static SbdMemo *sbd_memo_slot(int kind, int owner, const char *name, int add) { if (add && (sbd_memo_n + 1) * 2 > sbd_memo_cap) { int ncap = sbd_memo_cap ? sbd_memo_cap * 2 : 256; SbdMemo *nm = calloc((size_t)ncap, sizeof(SbdMemo)); if (!nm) return NULL; for (int i = 0; i < sbd_memo_cap; i++) { if (!sbd_memo[i].name) continue; unsigned h = (sp_strhash(sbd_memo[i].name) ^ ((unsigned)sbd_memo[i].owner * 2654435761u) ^ (unsigned)sbd_memo[i].kind) & (unsigned)(ncap - 1); while (nm[h].name) h = (h + 1) & (unsigned)(ncap - 1); nm[h] = sbd_memo[i]; } free(sbd_memo); sbd_memo = nm; sbd_memo_cap = ncap; } if (!sbd_memo_cap) return NULL; unsigned h = (sp_strhash(name) ^ ((unsigned)owner * 2654435761u) ^ (unsigned)kind) & (unsigned)(sbd_memo_cap - 1); for (; sbd_memo[h].name; h = (h + 1) & (unsigned)(sbd_memo_cap - 1)) if (sbd_memo[h].kind == kind && sbd_memo[h].owner == owner && sp_streq(sbd_memo[h].name, name)) return &sbd_memo[h]; if (!add) return NULL; sbd_memo[h].kind = kind; sbd_memo[h].owner = owner; sbd_memo[h].name = name; sbd_memo[h].gen = 0; sbd_memo_n++; return &sbd_memo[h]; } /* Is this variable's walk from `depth` known to change nothing? */ static int sbd_memo_covers(int kind, int owner, const char *name, int depth) { if (!sbd_memo_on || !name) return 0; SbdMemo *e = sbd_memo_slot(kind, owner, name, 0); return e && e->gen == sbd_gen && e->depth <= depth; } /* Its walk from `depth`, begun at generation `gen0`, is done: remembered when nothing changed on the way. */ static void sbd_memo_done(int kind, int owner, const char *name, int depth, unsigned gen0) { if (!sbd_memo_on || !name || sbd_gen != gen0) return; SbdMemo *e = sbd_memo_slot(kind, owner, name, 1); if (e) { e->gen = gen0; e->depth = depth; } } /* The writes of a poly local, ivar or global, each demanded in turn: the variable is another name for whatever was written to it. */ static int strbuf_demand_local_writes(Compiler *c, const char *vn, Scope *vs, int depth) { const NodeTable *nt = c->nt; int changed = 0; if (!vn || !vs) return 0; int vsi = (int)(vs - c->scopes); if (sbd_memo_covers(0, vsi, vn, depth)) return 0; unsigned gen0 = sbd_gen; for (int w = comp_lvw_first_sc(c, vsi, vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode || comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; changed |= strbuf_demand_value_leaves(c, nt_ref(nt, w, "value"), depth); } sbd_memo_done(0, vsi, vn, depth, gen0); return changed; } static int strbuf_demand_ivar_writes(Compiler *c, int cid, const char *ivn, int depth) { const NodeTable *nt = c->nt; int changed = 0; if (sbd_memo_covers(1, cid, ivn, depth)) return 0; unsigned gen0 = sbd_gen; for (int w = comp_kind_first(c, NK_InstanceVariableWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_InstanceVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ivn) || comp_ivar_owner(c, w) != cid) continue; changed |= strbuf_demand_value_leaves(c, nt_ref(nt, w, "value"), depth); } sbd_memo_done(1, cid, ivn, depth, gen0); return changed; } static int strbuf_demand_gvar_writes(Compiler *c, const char *grn, int depth) { const NodeTable *nt = c->nt; int changed = 0; if (sbd_memo_covers(2, 0, grn, depth)) return 0; unsigned gen0 = sbd_gen; for (int w = comp_kind_first(c, NK_GlobalVariableWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_GlobalVariableWriteNode) continue; const char *wn = nt_str(nt, w, "name"); const char *wrn = wn ? comp_resolve_gvar(c, wn + 1) : NULL; if (!wrn || !sp_streq(wrn, grn)) continue; changed |= strbuf_demand_value_leaves(c, nt_ref(nt, w, "value"), depth); } sbd_memo_done(2, 0, grn, depth, gen0); return changed; } static int strbuf_poly_ivar_read(Compiler *c, int node, int *cid) { const char *ivn = nt_str(c->nt, node, "name"); *cid = ivn ? comp_ivar_owner(c, node) : -1; if (*cid < 0) return 0; int iv = comp_ivar_index(&c->classes[*cid], ivn); return iv >= 0 && c->classes[*cid].ivar_types[iv] == TY_POLY; } static LocalVar *strbuf_poly_gvar(Compiler *c, int node, const char **grn) { const char *gn = nt_str(c->nt, node, "name"); *grn = gn ? comp_resolve_gvar(c, gn + 1) : NULL; LocalVar *glv = *grn ? comp_gvar(c, *grn) : NULL; return glv && glv->type == TY_POLY ? glv : NULL; } /* Demand the String values `node` can evaluate to into shared handles, through the branches of a conditional and the writes of a poly variable it reads: a string-valued expression marks for a fresh-handle wrap, an eligible string local promotes. */ static int strbuf_demand_elem_arg(Compiler *c, int an); typedef struct { Compiler *c; int depth; int changed; } StrbufDemand; static int strbuf_demand_leaf(const VpLeaf *lf, void *ud) { StrbufDemand *w = ud; Compiler *c = w->c; const NodeTable *nt = c->nt; int node = lf->node, depth = w->depth; if (lf->kind != VPK_EXPR && lf->kind != VPK_JUMP) return 0; switch (nt_kind(nt, node)) { case NK_InstanceVariableReadNode: { int cid; if (!strbuf_poly_ivar_read(c, node, &cid)) return 0; w->changed |= strbuf_demand_ivar_writes(c, cid, nt_str(nt, node, "name"), depth + 4); return 0; } case NK_GlobalVariableReadNode: { const char *grn; if (!strbuf_poly_gvar(c, node, &grn)) return 0; w->changed |= strbuf_demand_gvar_writes(c, grn, depth + 4); return 0; } case NK_LocalVariableReadNode: { if (c->strbuf_box[node]) return 0; const char *vn = nt_str(nt, node, "name"); Scope *vs = vn ? comp_scope_of(c, node) : NULL; LocalVar *vlv = vs ? scope_local(vs, vn) : NULL; if (vlv && vlv->type == TY_POLY && !vlv->is_param) { w->changed |= strbuf_demand_local_writes(c, vn, vs, depth + 4); return 0; } if (!vlv || !strbuf_slot_eligible(c, vn, vs, vlv)) return 0; if (strbuf_mut_kind(c, vn, vs) < 0) return 0; lv_set_type(vlv, TY_STRBUF); { vlv->str_shared = 1; comp_touch_scope(vlv->owner); } comp_set_strbuf_box(c, node, 1); sbd_gen++; w->changed = 1; return 0; } default: { if (c->strbuf_box[node]) return 0; /* an element read (`v = h[:a]`): the variable names the String the container holds, so the container's Strings become handles */ if (infer_type(c, node) == TY_POLY) { int d = strbuf_demand_elem_arg(c, node); if (d >= 0) sbd_gen++; /* it may have marked stores, changed or not */ w->changed |= d > 0 ? d : 0; return 0; } if (infer_type(c, node) != TY_STRING) return 0; comp_set_strbuf_box(c, node, 1); sbd_gen++; w->changed = 1; return 0; } } } /* (the walk is the arms of a conditional, each leaf demanded in turn; `depth` counts the variables hopped through, not the shapes) */ static int strbuf_demand_value_leaves(Compiler *c, int node, int depth) { if (node < 0 || depth > 16) return 0; StrbufDemand w = { c, depth, 0 }; for_each_value_position(c->nt, node, VP_AND_LEFT | VPS_IF | VPS_UNLESS | VPS_CASE | VPS_CASE_IN | VPS_LOGIC, strbuf_demand_leaf, &w); return w.changed; } /* String methods that only READ the receiver's bytes and keep no pointer past the call: the live handle can serve them through sp_String_cstr, with no copy. Deliberately narrow -- nothing here allocates from the pointer or retains it, so no collection can run while an interior pointer is held. */ static int an_str_read_only_accessor(const char *n) { return sp_streq(n, "getbyte") || sp_streq(n, "bytesize") || sp_streq(n, "length") || sp_streq(n, "size") || sp_streq(n, "empty?"); } const char *proc_param_name(Compiler *c, int create, int idx); /* A block whose body a proc inside captures its parameter from is wrapped in a lambda called at once (desugar_block_capture_wrap): `{ |a| ->(__cap_a) { body }.call(a) }`. The body's mutation of `a` is then one of the wrapper's parameter, and it is the block's element all the same: CRuby's `arr.each { |a| -> { a.upcase! }.call }` changes the Array. Answers 1 when the block `blk`'s body is such a wrapper call handing it `bp` and the wrapper's parameter there is mutated in place. */ int cap_wrap_mutates_param(Compiler *c, int blk, const char *bp) { const NodeTable *nt = c->nt; if (!nt_int(nt, blk, "cap_wrapped", 0)) return 0; int body = nt_ref(nt, blk, "body"), bn = 0; const int *bv = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; if (bn != 1 || nt_kind(nt, bv[0]) != NK_CallNode) return 0; int lam = nt_ref(nt, bv[0], "receiver"); if (lam < 0 || nt_kind(nt, lam) != NK_LambdaNode || !nt_int(nt, lam, "cap_iife", 0)) return 0; int a = nt_ref(nt, bv[0], "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode || !nt_str(nt, av[k], "name") || !sp_streq(nt_str(nt, av[k], "name"), bp)) continue; const char *wn = proc_param_name(c, lam, k); if (wn && strbuf_mut_kind(c, wn, comp_scope_of(c, lam)) == 1) return 1; } return 0; } /* The blocks that calls pass, listed by the method each call names, built once a pass on first need: asked of every call for each block that yields its parameter on, the question grew with the square of the program (scale-test). */ typedef struct { int *head, *next, *blk, n, ns, built; } ACallerBlocks; static void an_caller_blocks_build(Compiler *c, ACallerBlocks *t) { const NodeTable *nt = c->nt; t->ns = c->nscopes > 0 ? c->nscopes : 1; int nc = nt->count > 0 ? nt->count : 1; t->head = malloc(sizeof(int) * (size_t)t->ns); t->next = malloc(sizeof(int) * (size_t)nc); t->blk = malloc(sizeof(int) * (size_t)nc); for (int i = 0; i < t->ns; i++) t->head[i] = -1; t->n = 0; t->built = 1; 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; int ub = nt_ref(nt, u, "block"); if (ub < 0 || nt_kind(nt, ub) != NK_BlockNode) continue; int mi = an_call_target_mi(c, u); if (mi < 0 || mi >= t->ns) continue; t->blk[t->n] = ub; t->next[t->n] = t->head[mi]; t->head[mi] = t->n++; } } static void an_caller_blocks_free(ACallerBlocks *t) { if (!t->built) return; free(t->head); free(t->next); free(t->blk); t->built = 0; } /* A digest of every type the share classes read: inside the fixpoint the rule is applied only once a whole round has left the types as they were, so a guess made before the types settle (an untyped receiver read as a call the walk cannot follow) does not leave a handle behind, which the flags would keep. */ /* (the slots' types, not the nodes': a node's type is a cache that the passes of one round type and retype, so the same settled program reads differently at each place in the round, and the rule's wait for the types to hold still saw a swing that never ends, #10325) */ static unsigned share_types_digest(Compiler *c) { unsigned h = 2166136261u; for (int s = 0; s < c->nscopes; s++) { for (int l = 0; l < c->scopes[s].nlocals; l++) { h ^= (unsigned)c->scopes[s].locals[l].type; h *= 16777619u; } } for (int k = 0; k < c->nclasses; k++) { for (int i = 0; i < c->classes[k].nivars; i++) { h ^= (unsigned)c->classes[k].ivar_types[i]; h *= 16777619u; } for (int i = 0; i < c->classes[k].ncvars; i++) { h ^= (unsigned)c->classes[k].cvar_types[i]; h *= 16777619u; } } return h; } /* --share-strings (#6765): apply the one rule. Every slot that can hold a String is the shared handle (slot_holders); this sets the flags repr_of reads for them, through each holder kind's own representation: a local or a parameter takes STRBUF + str_shared (a lent parameter gives up its slot, and convert_byref_handle_params then pulls its callers), an ivar or a global the shared slot, and a container whose elements share demands the handle at each of its stores. A holder whose kind has no handle yet is left as it is; repr_seal refuses it. The route rules after this only carry the handles it made, and add none it did not (#6765's stats count them). */ /* --share-strings: ivar `name` of class cid is a box (it also holds nil, or another kind) whose class the rule shares: each String written into it (an ivar write, an attribute writer, instance_variable_set) is boxed as its handle (emit_boxed's lift), so a `<<` through the box appends to the String every name holds rather than replacing the slot's own copy. */ /* (a read of a holder is not lifted: the rule makes that holder the handle, whose box is the handle already) */ static int share_lift_value(Compiler *c, int v); static int share_lift_jumps(Compiler *c, int n, NodeKind k); /* An arm of a conditional value: its last statement, through parentheses and an `else`, which the iterator peels. No shape is looked through, so the arm is the leaf, whatever it is: share_lift_value takes a conditional in an arm apart, or marks it whole where it is String-typed (it needs the conditional itself, which a flat walk of its arms does not give). */ typedef struct { Compiler *c; int changed; } ShareLiftArm; static int share_lift_arm(const VpLeaf *lf, void *ud) { ShareLiftArm *a = ud; if (lf->kind == VPK_EXPR || lf->kind == VPK_JUMP) a->changed = share_lift_value(a->c, lf->node); return 0; } static int share_lift_arms(Compiler *c, int n) { ShareLiftArm a = { c, 0 }; for_each_value_position(c->nt, n, VPS_NONE, share_lift_arm, &a); return a.changed; } static int share_lift_value(Compiler *c, int v) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, v); if (k == NK_LocalVariableReadNode || k == NK_InstanceVariableReadNode || k == NK_GlobalVariableReadNode || k == NK_ClassVariableReadNode || k == NK_ConstantReadNode || k == NK_ConstantPathNode) return 0; /* (nor is a frozen literal: nothing changes it in place, and its box is the one object each evaluation answers, which a new handle would not be) */ if (k == NK_StringNode) return 0; /* a String-typed value, a conditional's included, is boxed whole */ if (k != NK_BeginNode && infer_type(c, v) == TY_STRING) { int changed = 0; if (!c->poly_strbuf_lift[v]) { c->poly_strbuf_lift[v] = 1; changed = 1; } /* a `||` or `&&` marked whole marks its operands too, as one of mixed types does below: the box of each is what an enclosing logic node or the return unboxes (an_boxed_logic_ok accepts them as it does). A conditional's arms are lifted by its own box (emit_boxed_cond_arms) */ if (k == NK_OrNode || k == NK_AndNode) changed |= share_lift_arms(c, nt_ref(nt, v, "left")) | share_lift_arms(c, nt_ref(nt, v, "right")); return changed; } /* a conditional value of mixed types: each arm that can be a String (the boxed slot's value boxes each arm on its own). A `case`, `case`/`in` and `begin` value is its arms' and handlers' values, as the typing unifies them: an `else` replaces a `begin`'s body, `ensure` adds none. */ switch (k) { case NK_IfNode: case NK_UnlessNode: return share_lift_arms(c, nt_ref(nt, v, "statements")) | share_lift_arms(c, nt_ref(nt, v, k == NK_IfNode ? "subsequent" : "else_clause")); case NK_OrNode: case NK_AndNode: return share_lift_arms(c, nt_ref(nt, v, "left")) | share_lift_arms(c, nt_ref(nt, v, "right")); case NK_CaseNode: case NK_CaseMatchNode: { int nw = 0, changed = 0; const int *arms = nt_arr(nt, v, "conditions", &nw); for (int i = 0; i < nw; i++) changed |= share_lift_arms(c, nt_ref(nt, arms[i], "statements")); return changed | share_lift_arms(c, nt_ref(nt, v, "else_clause")); } case NK_BeginNode: { int els = nt_ref(nt, v, "else_clause"); int changed = share_lift_arms(c, els >= 0 ? els : nt_ref(nt, v, "statements")); /* Expression-position begins still box their String result whole; boxed tails use the arm flags instead. Keep both facts. */ if (infer_type(c, v) == TY_STRING && !c->poly_strbuf_lift[v]) { c->poly_strbuf_lift[v] = 1; changed = 1; } for (int r = nt_ref(nt, v, "rescue_clause"); r >= 0; r = nt_ref(nt, r, "subsequent")) changed |= share_lift_arms(c, nt_ref(nt, r, "statements")); return changed; } case NK_RescueModifierNode: return share_lift_arms(c, nt_ref(nt, v, "expression")) | share_lift_arms(c, nt_ref(nt, v, "rescue_expression")); /* a loop's value is what a `break` gives it, as is the value of a call whose literal block breaks */ case NK_WhileNode: case NK_UntilNode: return share_lift_jumps(c, nt_ref(nt, v, "statements"), NK_BreakNode); case NK_CallNode: { int blk = nt_ref(nt, v, "block"); return blk >= 0 && nt_kind(nt, blk) == NK_BlockNode ? share_lift_jumps(c, nt_ref(nt, blk, "body"), NK_BreakNode) : 0; } default: return 0; } } /* Each `next` (k NK_NextNode) or `break` (NK_BreakNode) under n that leaves what n is the body of, not one in a nested block, lambda, method or loop: its value is boxed as share_lift_value boxes. */ static int share_lift_jumps(Compiler *c, int n, NodeKind k) { const NodeTable *nt = c->nt; if (n < 0) return 0; NodeKind nk = nt_kind(nt, n); if (nk == NK_BlockNode || nk == NK_LambdaNode || nk == NK_DefNode || nk == NK_WhileNode || nk == NK_UntilNode || nk == NK_ForNode) return 0; int changed = 0; if (nk == k) { int args = nt_ref(nt, n, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (argc == 1 && nt_paren_single(nt, argv[0]) >= 0) changed |= share_lift_value(c, nt_paren_single(nt, argv[0])); } for (int i = 0; i < nt_num_refs(nt, n); i++) changed |= share_lift_jumps(c, nt_ref_at(nt, n, i), k); for (int i = 0; i < nt_num_arrs(nt, n); i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) changed |= share_lift_jumps(c, ids[j], k); } return changed; } /* A parameter is stored to by its call sites: each argument bound to it is boxed as the handle too (the settled argument index), and its default. An argument that is a parameter handed on is a read, which its own holder covers. The index is built at storage, so the rule's later applications lift them; resolving the callers of every sharing parameter in each fixpoint round would scan every `new` for every class. The lift marks no type, so storage is soon enough. */ /* A literal String is frozen and static, so no name can change it; boxed as it is, every site of the literal stays the one object (equal?). */ static int share_lift_arg(Compiler *c, int v) { return v >= 0 && nt_kind(c->nt, v) != NK_StringNode ? share_lift_value(c, v) : 0; } static int share_lift_param_args(Compiler *c, int mi, int pj) { const NodeTable *nt = c->nt; Scope *ps = &c->scopes[mi]; int changed = 0; SbArgIndex *x = c->sb_args; if (!x) return 0; for (int e = x->head[x->off[mi] + pj]; e >= 0; e = x->edges[e].next) if (x->edges[e].scope < 0) changed |= share_lift_arg(c, nt_paren_single(nt, x->edges[e].node)); /* and the default a call that omits the argument takes */ if (ps->pdefault && ps->pdefault[pj] >= 0) changed |= share_lift_arg(c, nt_paren_single(nt, ps->pdefault[pj])); return changed; } /* The same for local `name` of scope `scope`: its `=`, `||=` and `&&=` (comp_lvw_first_sc, the local write index), and for a parameter the arguments of its calls. */ static int share_lift_poly_local_stores(Compiler *c, int scope, const char *name) { const NodeTable *nt = c->nt; int changed = 0; LocalVar *plv = scope_local(&c->scopes[scope], name); int pj = plv && plv->is_param && !plv->is_block_param ? an_param_idx(&c->scopes[scope], name) : -1; if (pj >= 0) changed |= share_lift_param_args(c, scope, pj); for (int w = comp_lvw_first_sc(c, scope, name); w >= 0; w = comp_lvw_next_sc(c, w)) { NodeKind k = nt_kind(nt, w); if ((k != NK_LocalVariableWriteNode && k != NK_LocalVariableOrWriteNode && k != NK_LocalVariableAndWriteNode) || !sp_streq(nt_str(nt, w, "name"), name) || comp_scope_of(c, w) != &c->scopes[scope]) continue; int v = nt_paren_single(nt, nt_ref(nt, w, "value")); if (v >= 0) changed |= share_lift_value(c, v); } return changed; } /* The same for a global's, a class variable's or a constant's writes (holder kind `kind`, the share facts' name): `=`, `||=` and `&&=`. A class variable is every class's of the name, as the facts key it. */ static int share_lift_poly_static_stores(Compiler *c, int kind, const char *name) { const NodeTable *nt = c->nt; int changed = 0; NodeKind rk = kind == SHK_GVAR ? NK_GlobalVariableReadNode : kind == SHK_CVAR ? NK_ClassVariableReadNode : NK_ConstantReadNode; const char *want = kind == SHK_GVAR && name && name[0] == '$' ? comp_resolve_gvar(c, name + 1) : name; if (!want) return 0; for (int e = comp_vsite_first(c, VS_STORE, rk, want, -1); e >= 0; e = comp_vsite_next(c, e)) { int w = comp_vsite_node(c, e); const char *wn = nt_str(nt, w, "name"); const char *rn = kind == SHK_GVAR && wn && wn[0] == '$' ? comp_resolve_gvar(c, wn + 1) : wn; NodeKind wk = nt_kind(nt, w); int wkind = wk == NK_GlobalVariableWriteNode || wk == NK_GlobalVariableOrWriteNode || wk == NK_GlobalVariableAndWriteNode ? SHK_GVAR : wk == NK_ClassVariableWriteNode || wk == NK_ClassVariableOrWriteNode || wk == NK_ClassVariableAndWriteNode ? SHK_CVAR : wk == NK_ConstantWriteNode || wk == NK_ConstantOrWriteNode || wk == NK_ConstantAndWriteNode ? SHK_CONST : -1; if (!rn || !sp_streq(rn, want) || wkind != kind) continue; /* a chain's collisions */ int v = nt_paren_single(nt, nt_ref(nt, w, "value")); if (v >= 0) changed |= share_lift_value(c, v); } return changed; } /* Has share_lift_poly_ivar_stores lifted the writer calls of ivar `name` (`o.name = v`, `o.instance_variable_set(:@name, v)`) since the types and the node table last moved? Those calls are the same for every class's ivar of the name, and share_lift_value reads only the types (each change of which comp_ty_edits counts), the nodes and the marks it sets, so a second walk at the same counts marks nothing. A program of many classes with one attribute name walked all the writer calls once per class (#10546). Answers 0, and records the counts, when the walk is to run. */ static int sl_writers_lifted(Compiler *c, const char *name) { static const Compiler *mc; static const NodeTable *mnt; static char **key; static unsigned long *at; static unsigned *atv; static int cap, n; if (mc != c || mnt != c->nt) { for (int i = 0; i < cap; i++) free(key[i]); free(key); free(at); free(atv); key = NULL; at = NULL; atv = NULL; cap = n = 0; mc = c; mnt = c->nt; } if (n * 2 >= cap) { int ocap = cap; char **ok = key; unsigned long *oa = at; unsigned *ov = atv; cap = ocap ? ocap * 2 : 64; key = calloc((size_t)cap, sizeof *key); at = calloc((size_t)cap, sizeof *at); atv = calloc((size_t)cap, sizeof *atv); if (!key || !at || !atv) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < ocap; i++) { if (!ok[i]) continue; unsigned j = sp_strhash(ok[i]) & (unsigned)(cap - 1); while (key[j]) j = (j + 1) & (unsigned)(cap - 1); key[j] = ok[i]; at[j] = oa[i]; atv[j] = ov[i]; } free(ok); free(oa); free(ov); } unsigned j = sp_strhash(name) & (unsigned)(cap - 1); while (key[j] && !sp_streq(key[j], name)) j = (j + 1) & (unsigned)(cap - 1); if (key[j] && at[j] == comp_ty_edits && atv[j] == c->nt->version) return 1; if (!key[j]) { key[j] = strdup(name); if (!key[j]) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } n++; } at[j] = comp_ty_edits; atv[j] = c->nt->version; return 0; } static int share_lift_poly_ivar_stores(Compiler *c, int cid, const char *name) { const NodeTable *nt = c->nt; int changed = 0; /* its `=`, `||=` and `&&=` (the variable-site chains) */ for (int e = comp_vsite_first(c, VS_STORE, NK_InstanceVariableReadNode, name, cid); e >= 0; e = comp_vsite_next(c, e)) { int w = comp_vsite_node(c, e); NodeKind wk = nt_kind(nt, w); const char *wn = nt_str(nt, w, "name"); int v = nt_paren_single(nt, nt_ref(nt, w, "value")); if ((wk != NK_InstanceVariableWriteNode && wk != NK_InstanceVariableOrWriteNode && wk != NK_InstanceVariableAndWriteNode) || !wn || !sp_streq(wn, name) || v < 0 || comp_ivar_owner(c, w) != cid) continue; changed |= share_lift_value(c, v); } /* the calls of the two names that can write it, not every call: this ran once per boxed ivar holder each round */ size_t ln = strlen(name); char wname[256]; snprintf(wname, sizeof wname, "%s=", name + 1); const char *writers[2] = { wname, "instance_variable_set" }; for (int q = sl_writers_lifted(c, name) ? 2 : 0; q < 2; q++) for (int w = an_calls_named_first(c, writers[q]); w >= 0; w = an_calls_named_next(w)) { const char *cn = nt_str(nt, w, "name"); int a = nt_ref(nt, w, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!cn || nt_ref(nt, w, "receiver") < 0) continue; int v = -1; /* `o.name = v` (the attribute writer of the ivar) */ if (an == 1 && strlen(cn) == ln && cn[ln - 1] == '=' && strncmp(cn, name + 1, ln - 1) == 0) v = av[0]; /* `o.instance_variable_set(:@name, v)` */ else if (an == 2 && is_ivar_set(cn) && nt_kind(nt, av[0]) == NK_SymbolNode && nt_str(nt, av[0], "value") && sp_streq(nt_str(nt, av[0], "value"), name)) v = av[1]; if (v >= 0) changed |= share_lift_value(c, v); } /* a Struct member's: its constructor's argument, and a `[]=` that can reach it (struct_aset_receiver), as its writer's above */ ClassInfo *ci = &c->classes[cid]; int m = ci->is_struct && !ci->is_data ? comp_member_index(ci, name) : -1; if (m < 0) return changed; /* the calls of the three names that can write a member -- `new`, `[]` (is_struct_constructor) and `[]=` -- off the calls-by-name index, merged into node order; every call of the program was read for each member of each boxed Struct holder, every round. The rest of the body acts on no other name. */ static const char *const member_writers[3] = { "new", "[]", "[]=" }; int heads[3]; for (int q = 0; q < 3; q++) heads[q] = an_calls_named_first(c, member_writers[q]); for (;;) { int q = -1; for (int k = 0; k < 3; k++) if (heads[k] >= 0 && (q < 0 || heads[k] < heads[q])) q = k; if (q < 0) break; int w = heads[q]; heads[q] = an_calls_named_next(w); if (nt_kind(nt, w) != NK_CallNode) continue; const char *cn = nt_str(nt, w, "name"); int r = nt_ref(nt, w, "receiver"), a = nt_ref(nt, w, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!cn || r < 0) continue; if (is_struct_constructor(cn) && (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode) && const_node_class_index(c, r) == cid) { if (an == 1 && nt_kind(nt, av[0]) == NK_KeywordHashNode) { int kn = 0; const int *ke = nt_arr(nt, av[0], "elements", &kn); for (int e = 0; e < kn; e++) { int key = nt_kind(nt, ke[e]) == NK_AssocNode ? nt_ref(nt, ke[e], "key") : -1; if (key >= 0 && nt_kind(nt, key) == NK_SymbolNode && sp_streq(nt_str(nt, key, "value"), name + 1)) changed |= share_lift_value(c, nt_paren_single(nt, nt_ref(nt, ke[e], "value"))); } } else if (m < an && nt_kind(nt, av[m]) != NK_SplatNode) changed |= share_lift_value(c, nt_paren_single(nt, av[m])); continue; } int one, how = struct_aset_receiver(c, w, &one), lo, hi, reach = how == 1 && one == cid; if (how == 2) { int nk = 0; const int *ks = poly_recv_classes(c, w, &nk); reach = !ks; for (int i = 0; ks && i < nk && !reach; i++) reach = ks[i] == cid; } if (reach && struct_aset_members(c, w, cid, &lo, &hi) && m >= lo && m < hi) changed |= share_lift_value(c, nt_paren_single(nt, av[1])); } return changed; } /* share_default_apply's global, constant or class variable container (holder kind `kind`, SHK_GVAR, SHK_CONST or SHK_CVAR, and name `name`): answers whether anything changed. */ static int share_static_container(Compiler *c, int kind, const char *name) { const NodeTable *nt = c->nt; NodeKind rk = kind == SHK_GVAR ? NK_GlobalVariableReadNode : kind == SHK_CVAR ? NK_ClassVariableReadNode : NK_ConstantReadNode; const char *grn = kind != SHK_GVAR ? name : comp_resolve_gvar(c, name[0] == '$' ? name + 1 : name); if (!grn) return 0; int changed = 0; if (kind == SHK_CVAR) { /* a class variable is every class's of the name, as the facts key it; a top-level one (CRuby raises RuntimeError on its access) is left to the seal's refusal */ int any = 0, tl = c->toplevel_class; if (tl >= 0 && comp_cvar_index(&c->classes[tl], grn) >= 0) return 0; for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; int i = comp_cvar_index(ci, grn); if (i < 0 || (!ty_is_array(ci->cvar_types[i]) && !ty_is_hash(ci->cvar_types[i]))) continue; any = 1; if (!g_one_string && /* (the one-String mode: the typed container holds the handle) */ (ci->cvar_types[i] == TY_STR_ARRAY || ci->cvar_types[i] == TY_POLY_ARRAY) && !(ci->cvar_elems_shared[i] & ELEMS_POLY)) { cvar_set_type(ci, i, TY_POLY_ARRAY); { ci->cvar_elems_shared[i] |= ELEMS_POLY; comp_touch_class(ci->idx); } changed = 1; } } if (!any) return 0; } else { LocalVar *gv = kind == SHK_GVAR ? comp_gvar(c, grn) : comp_const(c, grn); if (!gv || (!ty_is_array(gv->type) && !ty_is_hash(gv->type))) return 0; if (!g_one_string && (gv->type == TY_STR_ARRAY || gv->type == TY_POLY_ARRAY) && !gv->rbs_seeded && (gv->type != TY_POLY_ARRAY || !gv->elems_shared)) { lv_set_type(gv, TY_POLY_ARRAY); gv->elems_shared = 1; changed = 1; } } /* a global's `=` (VS_WRITE), a constant's and a class variable's `=`, `||=` and `&&=` (VS_STORE: the index keeps no VS_WRITE for them) */ for (int e = comp_vsite_first(c, kind == SHK_GVAR ? VS_WRITE : VS_STORE, rk, grn, -1); e >= 0; e = comp_vsite_next(c, e)) if (static_site_is(c, rk, e, grn)) changed |= strbuf_container_source_walk(c, nt_ref(nt, comp_vsite_node(c, e), "value"), 1, SB_DEMAND); return changed | strbuf_static_store_walk(c, rk, grn, 1); } /* Collects into out[] the Array and Hash literals method value v can be: v itself, a `return`'s argument, the last statement of a list, the arms of an `if`, `unless` or `case`, and the body, rescues and else of a `begin`. Answers the new count, which counts what out[] (cap long) had no room for. */ typedef struct { const NodeTable *nt; int *out, n, cap; } RetLits; static int share_ret_literals(const NodeTable *nt, int v, int *out, int n, int cap); static int share_ret_lit_leaf(const VpLeaf *lf, void *ud) { RetLits *r = ud; if (lf->kind == VPK_NIL || lf->kind == VPK_GAVE_UP) return 0; const NodeTable *nt = r->nt; switch (nt_kind(nt, lf->node)) { case NK_ArrayNode: case NK_HashNode: if (r->n < r->cap) r->out[r->n] = lf->node; r->n++; return 0; /* a begin is a leaf of the walk: its body, its else and its rescues, in that order */ case NK_BeginNode: { int v = lf->node; r->n = share_ret_literals(nt, nt_ref(nt, v, "statements"), r->out, r->n, r->cap); r->n = share_ret_literals(nt, nt_ref(nt, v, "else_clause"), r->out, r->n, r->cap); for (int rc = nt_ref(nt, v, "rescue_clause"); rc >= 0; rc = nt_ref(nt, rc, "subsequent")) r->n = share_ret_literals(nt, nt_ref(nt, rc, "statements"), r->out, r->n, r->cap); return 0; } default: return 0; } } static int share_ret_literals(const NodeTable *nt, int v, int *out, int n, int cap) { RetLits r = { nt, out, n, cap }; for_each_value_position(nt, v, VP_FOLLOW_RETURN | VPS_IF | VPS_UNLESS | VPS_CASE | VPS_CASE_IN, share_ret_lit_leaf, &r); return r.n; } /* The literals of typed Strings (`const char *` elements, which cannot hold the shared handle) method mi answers, as its last value or through a `return` (`return [@s] if ...`); none unless its value is a typed String Array or Hash. */ static int share_ret_typed_str_literals(Compiler *c, int mi, int *out, int cap) { TyKind t = c->scopes[mi].ret; if (!(ty_is_array(t) && ty_array_elem(t) == TY_STRING) && !(ty_is_hash(t) && ty_hash_val(t) == TY_STRING)) return 0; int n = share_ret_literals(c->nt, scope_body_value(c, mi), out, 0, cap); for (int u = comp_ret_first(c, mi); u >= 0; u = comp_ret_next(c, u)) if (nt_kind(c->nt, u) == NK_ReturnNode && comp_scope_of(c, u) == &c->scopes[mi]) n = share_ret_literals(c->nt, u, out, n, cap); return n; } static int share_ret_typed_str_literal(Compiler *c, int mi) { return share_ret_typed_str_literals(c, mi, NULL, 0) > 0; } /* The Strings of each such literal are demanded as handles: the walk of a method's value follows an `if`'s arms, and this reaches the literals under a `case` and a `begin` as well. Answers whether anything changed. */ static int share_ret_demand_literals(Compiler *c, int mi) { int changed = 0, n = share_ret_typed_str_literals(c, mi, NULL, 0); if (n <= 0) return 0; int *lits = malloc(sizeof(int) * (size_t)n); if (!lits) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } share_ret_typed_str_literals(c, mi, lits, n); for (int k = 0; k < n; k++) changed |= strbuf_container_source_walk(c, lits[k], 1, SB_DEMAND); free(lits); return changed; } /* --share-strings: method mi answers a box whose class the rule shares: each String it answers (its last value, a `begin` body's arms, each `return`'s) is boxed as its handle, as a boxed local's stores are (share_lift_value), so a `<<` through the box its caller keeps appends to the String every name holds. */ static int share_lift_poly_returns(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; int last = scope_body_last(c, mi), changed = 0; /* a body with its own rescue is a begin, whose arms answer */ if (last < 0 && m->body >= 0 && nt_kind(nt, m->body) == NK_BeginNode) last = m->body; if (last >= 0) changed |= share_lift_value(c, nt_paren_single(nt, last)); for (int u = comp_ret_first(c, mi); u >= 0; u = comp_ret_next(c, u)) { if (nt_kind(nt, u) != NK_ReturnNode || comp_scope_of(c, u) != m) continue; int ra = nt_ref(nt, u, "arguments"), rn = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rn) : NULL; if (rn == 1 && nt_paren_single(nt, rv[0]) >= 0) changed |= share_lift_value(c, nt_paren_single(nt, rv[0])); } return changed; } /* --share-strings: method mi answers Arrays whose elements the rule shares: each String a `return a, b` gathers into its new Array is boxed as its handle, as an element a literal stores is. */ static int share_lift_multi_returns(Compiler *c, int mi) { const NodeTable *nt = c->nt; int changed = 0; for (int u = comp_ret_first(c, mi); u >= 0; u = comp_ret_next(c, u)) { if (nt_kind(nt, u) != NK_ReturnNode || comp_scope_of(c, u) != &c->scopes[mi]) continue; int ra = nt_ref(nt, u, "arguments"), rn = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rn) : NULL; /* a value in parentheses is their last statement's, which the Array holds; `()` and a jump hold nothing to lift */ for (int i = 0, v; rn > 1 && i < rn; i++) if (nt_kind(nt, rv[i]) != NK_SplatNode && vp_one(nt, rv[i], VPS_NONE | VP_PARENS_ONLY, &v) == VP1_NODE) changed |= share_lift_value(c, v); } return changed; } /* an instance method a pattern calls to read an object's parts */ static int share_pattern_method(const Scope *m) { return m->class_id >= 0 && m->name && (sp_streq(m->name, "deconstruct") || sp_streq(m->name, "deconstruct_keys")); } /* A literal argument a user method's parameters name the elements of: the argument itself, the operand of `*`, or that of a `**` among keyword arguments. Its stores carry the handles where the rule shares them. */ static int share_demand_literal_arg(Compiler *c, int a) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, a); if (k == NK_KeywordHashNode) { int changed = 0, en = 0; const int *el = nt_arr(nt, a, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_AssocSplatNode) changed |= share_demand_literal_arg(c, nt_ref(nt, el[e], "value")); return changed; } if (k == NK_SplatNode) a = nt_ref(nt, a, "expression"); a = nt_paren_single(c->nt, a); if (a < 0 || (nt_kind(nt, a) != NK_ArrayNode && nt_kind(nt, a) != NK_HashNode)) return 0; return strbuf_container_source_walk(c, a, 0, SB_DEMAND); } /* The methods that read their rest parameter: a read of it, or a zsuper, which hands it on (per scope, 1 or 0). */ static unsigned char *rest_param_reads(Compiler *c) { const NodeTable *nt = c->nt; unsigned char *r = calloc((size_t)(c->nscopes > 0 ? c->nscopes : 1), 1); if (!r) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, n) { Scope *s = comp_scope_of(c, n); const char *ln = nt_str(nt, n, "name"); if (s && ln && s->rest_idx >= 0 && s->pnames[s->rest_idx] && sp_streq(s->pnames[s->rest_idx], ln)) r[s - c->scopes] = 1; } NT_FOREACH_KIND(nt, NK_ForwardingSuperNode, n) { Scope *s = comp_scope_of(c, n); if (s) r[s - c->scopes] = 1; } return r; } /* A user method's rest parameter holds its callers' arguments as its elements: where the method reads them, each argument the call's layout leaves to the rest (`def first(*items)`, `first(self.text)`) is stored as its handle, as a literal's element is. A splat's elements are the literal's (share_demand_literal_arg) or the run time's. */ static int share_demand_rest_args(Compiler *c, int mi, const int *argv, int argc, const unsigned char *rest_read) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (m->rest_idx < 0 || !m->pnames[m->rest_idx] || !rest_read[mi]) return 0; LocalVar *rv = scope_local(m, m->pnames[m->rest_idx]); if (!rv) return 0; int kwh = nt_argv_kwh(nt, argv, argc); int pos_argc = kwh >= 0 ? argc - 1 : argc; int plain_argc = pos_argc; for (int k = 0; k < pos_argc; k++) { if (nt_kind(nt, argv[k]) == NK_ForwardingArgumentsNode) return 0; if (nt_kind(nt, argv[k]) == NK_SplatNode && k < plain_argc) plain_argc = k; } if (plain_argc == 0) return 0; ArgLayout L; arg_layout(c, m, argv, pos_argc, kwh, 0, &L); int changed = 0; /* Demand the plain prefix; the splat and its tail keep their fallback. A gather binds its fixed leading parameters from that prefix, even though their layout entries are ARG_GATHERED rather than ARG_NODE. Its posts can take prefix sources too: ask which can reach the rest. */ for (int k = 0; k < plain_argc; k++) { int bound = 0; if (L.gather) bound = !gather_reaches(c, m, argv, L.pos_argc, L.gather_kwh, k, m->rest_idx); else for (int j = 0; j < L.n && !bound; j++) bound = L.from[j] == ARG_NODE && L.arg[j] == k; if (!bound) changed |= strbuf_store_leaf(c, argv[k], 0, SB_DEMAND); } arg_layout_free(&L); return changed; } /* An element read can itself be the container whose String elements are shared: [[s]][0][0] must demand s at the inner store. The existing container-source walk carries the extra element depth back to it. A temporary map's elements likewise come from its block's tail. */ static int share_demand_nested_read(Compiler *c, int call, int recv) { if (nt_ref(c->nt, call, "block") >= 0 || (!container_elem_read_p(c->nt, recv) && strbuf_map_block_tail(c, recv) < 0)) return 0; TyKind rt = comp_ntype(c, recv); if (rt == TY_POLY && container_elem_read_p(c->nt, call)) return strbuf_container_source_walk(c, recv, 0, SB_DEMAND_NAMED); if (!ty_is_array(rt) && !ty_is_hash(rt)) return 0; int sh = bop_share_named(ty_is_array(rt) ? BOP_ANY_ARRAY : BOP_ANY_HASH, nt_str(c->nt, call, "name")); if (sh != BSH_ELEM && sh != BSH_FETCH && sh != BSH_ELEM_N) return 0; return strbuf_container_source_walk(c, recv, 0, SB_DEMAND_NAMED); } /* A String Array ivar the rule boxes (its elements shared) is boxed in the whole family, as strbuf_promote_ivar promotes a String slot: an ancestor's methods reach the slot through their own struct, so it is boxed from the topmost ancestor holding it as a String Array through every descendant's copy, a passive one with no holder of its own included (an abstract class between them). A copy an --rbs seed pins to Array[String] is boxed too: the seed gives the Ruby type, not the C form. So is a Hash an --rbs seed pins to Hash[String, String] (`hash`: the family's slots are that Hash, boxed as the poly Hash). */ /* Set once the rule has widened a holder's type to the poly container (here and in one_reflect_widen): the stores written from such a holder join their slots again (an_phase_storage), as the locals do. */ static int g_share_widened_holder; static int share_box_ivar_family(Compiler *c, int cid, const char *nm, int hash) { TyKind typed = hash ? TY_STR_STR_HASH : TY_STR_ARRAY, boxed = hash ? TY_STR_POLY_HASH : TY_POLY_ARRAY; int top = cid; for (int p = c->classes[cid].parent; p >= 0; p = c->classes[p].parent) { int pi = comp_ivar_index(&c->classes[p], nm); if (pi < 0 || (c->classes[p].ivar_types[pi] != typed && c->classes[p].ivar_types[pi] != boxed)) break; top = p; } int nk = 0, changed = 0; const int *ks = comp_descendants(c, top, &nk); for (int k = -1; k < nk; k++) { ClassInfo *kc = &c->classes[k < 0 ? top : ks[k]]; int ki = comp_ivar_index(kc, nm); if (ki < 0 || kc->ivar_types[ki] != typed) continue; ivar_set_type(kc, ki, boxed); if (!hash) { kc->ivar_elems_shared[ki] |= ELEMS_POLY; comp_touch_class(kc->idx); } changed = g_share_widened_holder = 1; } return changed; } /* The names a program reaches by a name it computes or a block that rebinds self: an instance variable, a class variable or a constant named to instance_variable_get and its kin, or used inside an instance_eval, instance_exec, class_eval or module_eval block. A holder so reached has uses no read node stands for, so its class keeps the poly form. `all`: some name is not a literal, so every holder is reached. */ typedef struct { const char **n; int cnt, cap, all, locals; } StrhReflect; static void strh_reflect_add(StrhReflect *r, const char *nm) { if (!nm) { r->all = 1; return; } if (r->cnt == r->cap) { r->cap = r->cap ? r->cap * 2 : 16; r->n = realloc(r->n, sizeof(char *) * (size_t)r->cap); if (!r->n) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } r->n[r->cnt++] = nm; } /* the name a literal Symbol or String argument spells, or NULL */ static const char *strh_lit_name(const NodeTable *nt, int a) { if (a < 0) return NULL; NodeKind k = nt_kind(nt, a); if (k == NK_SymbolNode) return nt_str(nt, a, "value") ? nt_str(nt, a, "value") : nt_str(nt, a, "unescaped"); if (k == NK_StringNode) return nt_str(nt, a, "content"); return NULL; } /* every variable read, written or targeted in the subtree of node n */ static void strh_reflect_vars(const NodeTable *nt, int n, StrhReflect *r, int depth) { if (n < 0 || depth > 200) return; const char *ty = nt_type(nt, n); if (ty && (!strncmp(ty, "InstanceVariable", 16) || !strncmp(ty, "ClassVariable", 13))) { const char *nm = nt_str(nt, n, "name"); if (nm) strh_reflect_add(r, nm); } for (int i = 0; i < nt_num_refs(nt, n); i++) strh_reflect_vars(nt, nt_ref_at(nt, n, i), r, depth + 1); for (int i = 0; i < nt_num_arrs(nt, n); i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) strh_reflect_vars(nt, ids[j], r, depth + 1); } } static void strh_reflect_scan(Compiler *c, StrhReflect *r) { const NodeTable *nt = c->nt; for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { const char *nm = nt_str(nt, n, "name"); if (!nm) continue; int ca = nt_ref(nt, n, "arguments"), argc = 0, blk = nt_ref(nt, n, "block"); const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &argc) : NULL; int first = 0; if (is_send_family(nm)) { /* `send(:instance_variable_get, :@l)`: the name asked is the second */ const char *m = argc >= 1 ? strh_lit_name(nt, av[0]) : NULL; if (!m) { strh_reflect_add(r, NULL); continue; } nm = m; first = 1; } if (is_reflective_access(nm)) { /* (a constant named by a path, `H::L` or `::L`, is no name this reads: every constant is reached) */ const char *rn = argc > first ? strh_lit_name(nt, av[first]) : NULL; strh_reflect_add(r, rn && strstr(rn, "::") ? NULL : rn); } else if (is_reflective_listing(nm)) strh_reflect_add(r, NULL); else if (is_local_reflection(nm)) { strh_reflect_add(r, NULL); r->locals = 1; } else if (is_eval_exec_family(nm)) { if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && !first) strh_reflect_vars(nt, blk, r, 0); else strh_reflect_add(r, NULL); } } } enum { STRH_CAND = 1, STRH_BAD = 2 }; /* One-String mode: a container a reflective call reaches by its name (const_get, instance_variable_get, a send of either, a listing, eval) takes values no inference sees stored, so a String Array held there keeps the general form, as it did under the two representations (`Object.const_get("H::L") << 64`). Only a name the program spells is taken: a name it computes reaches every holder, and widening them all left each String Array of a program with a dynamic send boxed, where the bytes build kept them typed. Answers whether a type changed. */ static int one_reflect_named(const StrhReflect *r, const char *nm) { if (nm && nm[0] == ':' && nm[1] == ':') nm += 2; for (int i = 0; i < r->cnt; i++) if (sp_streq(r->n[i], nm)) return 1; return 0; } static int one_reflect_widen(Compiler *c) { StrhReflect rf; memset(&rf, 0, sizeof rf); strh_reflect_scan(c, &rf); int changed = 0; if (rf.cnt > 0) { for (int k = 0; k < c->nclasses; k++) { ClassInfo *kc = &c->classes[k]; for (int i = 0; i < kc->nivars; i++) if (kc->ivar_types[i] == TY_STR_ARRAY && one_reflect_named(&rf, kc->ivars[i])) { ivar_set_type(kc, i, TY_POLY_ARRAY); changed = 1; } for (int i = 0; i < kc->ncvars; i++) if (kc->cvar_types[i] == TY_STR_ARRAY && one_reflect_named(&rf, kc->cvars[i])) { cvar_set_type(kc, i, TY_POLY_ARRAY); changed = 1; } } for (int i = 0; i < c->nconsts; i++) if (c->consts[i].type == TY_STR_ARRAY && !c->consts[i].rbs_seeded && one_reflect_named(&rf, c->consts[i].name)) { lv_set_type(&c->consts[i], TY_POLY_ARRAY); changed = 1; } } free(rf.n); if (changed) g_share_widened_holder = 1; return changed; } /* The places a String can be held, as the rule below asks them: every local and parameter, every instance variable of every class, each name of a class variable, every global and constant, and every method's value, whose type can hold a String (sh_may_hold). Under the one representation every String is shared, so each of them is one the rule shares. */ static ShareHolder *slot_holders(Compiler *c, int *out_n) { int cap = 64, n = 0; ShareHolder *hs = malloc(sizeof *hs * (size_t)cap); if (!hs) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } #define SLOT_PUSH(k, s_, l_, cid_, nm_) do { \ if (n == cap) { cap *= 2; hs = realloc(hs, sizeof *hs * (size_t)cap); \ if (!hs) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } \ hs[n].kind = (unsigned char)(k); hs[n].scope = (s_); hs[n].local = (l_); \ hs[n].cid = (cid_); hs[n].name = (nm_); hs[n].node = -1; n++; } while (0) for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nlocals; i++) { /* (the loop variable of a define_method an `each` over literals unrolls is no slot: collect_dm_each_unroll) */ if (sc->dm_subst_name && sc->locals[i].name && sp_streq(sc->locals[i].name, sc->dm_subst_name)) continue; if (sh_may_hold(c, sc->locals[i].type)) SLOT_PUSH(SHK_LOCAL, s, i, -1, NULL); } } for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; for (int i = 0; i < ci->nivars; i++) if (sh_may_hold(c, ci->ivar_types[i])) SLOT_PUSH(SHK_IVAR, -1, -1, k, ci->ivars[i]); } for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; for (int i = 0; i < ci->ncvars; i++) { int seen = 0; for (int q = 0; q < n && !seen; q++) seen = hs[q].kind == SHK_CVAR && sp_streq(hs[q].name, ci->cvars[i]); if (!seen && sh_may_hold(c, ci->cvar_types[i])) SLOT_PUSH(SHK_CVAR, -1, -1, -1, ci->cvars[i]); } } for (int i = 0; i < c->ngvars; i++) if (sh_may_hold(c, c->gvars[i].type)) SLOT_PUSH(SHK_GVAR, -1, -1, -1, c->gvars[i].name); for (int i = 0; i < c->nconsts; i++) if (sh_may_hold(c, c->consts[i].type)) SLOT_PUSH(SHK_CONST, -1, -1, -1, c->consts[i].name); for (int s = 0; s < c->nscopes; s++) if (c->scopes[s].def_node >= 0 && sh_may_hold(c, c->scopes[s].ret)) SLOT_PUSH(SHK_RET, s, -1, -1, NULL); #undef SLOT_PUSH *out_n = n; return hs; } /* share_default_apply's last answer was the wait below: the types moved since the round before, and the rule waits a round for them to hold still */ static int g_share_waited; static int share_default_apply(Compiler *c, int in_fixpoint) { unsigned d_in = 0; g_share_waited = 0; if (in_fixpoint) { /* types that moved since the last round: ask for one more round, which applies the rule if they then hold still. The re-narrow loop clears the slots the rule promoted at the top of each iteration, and its re-derive gives them back as the rule found them: the types it was last applied over, which it applies to again. Waiting there instead skipped every other iteration, so the promoted slots alternated and the loop ran to its round cap. */ d_in = share_types_digest(c); if (d_in != c->share_sig && d_in != c->share_in_sig) { c->share_sig = d_in; g_share_waited = 1; return 1; } } share_facts_build(c); int changed = one_reflect_widen(c), any = 0; sb_share_begin(&changed, &any); /* (the walks below share their records; sb_share_end folds `any` back) */ int nh = 0; ShareHolder *slots = slot_holders(c, &nh); for (int h = 0; h < nh; h++) { const ShareHolder *sh = &slots[h]; if (sh->kind == SHK_LOCAL) { Scope *s = &c->scopes[sh->scope]; LocalVar *lv = &s->locals[sh->local]; if (ty_is_array(lv->type) || ty_is_hash(lv->type)) { changed |= strbuf_demand_container_stores(c, lv->name, s); continue; } /* a box holds the handle: each String written into it is boxed as its handle, as an ivar's are (share_lift_poly_ivar_stores) */ if (lv->type == TY_POLY) { changed |= share_lift_poly_local_stores(c, sh->scope, lv->name); continue; } if (lv->type != TY_STRING && lv->type != TY_STRBUF) continue; if (repr_of_slot(c, lv).share && !lv->byref_out) continue; if (lv->is_param && !lv->is_block_param) { /* an --rbs seed declares the Ruby type, String, and not its C form: a seeded parameter takes the handle as an inferred one does (reassert_rbs_param_seeds keeps it) */ if (lv->rbs_seeded && lv->rbs_type != TY_STRING) continue; if (lv->byref_out) { { lv->byref_out = 0; comp_touch_scope(lv->owner); } { lv->is_cell = 0; comp_touch_scope(lv->owner); } } lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } changed = 1; } /* a block's parameter holds the handle as a local does: what binds it (a yield, an iterator) hands over the handle */ else if (lv->is_block_param) { lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } changed = 1; } else if (strbuf_slot_eligible(c, lv->name, s, lv)) { lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } changed = 1; } } else if (sh->kind == SHK_IVAR) { int iv = comp_ivar_index(&c->classes[sh->cid], sh->name); if (iv < 0) continue; TyKind it = c->classes[sh->cid].ivar_types[iv]; if (ty_is_array(it) || ty_is_hash(it)) { changed |= strbuf_ivar_source_walk(c, sh->cid, sh->name, 0, SB_DEMAND); /* a poly Array holds its Strings as handles, as a local's does: a String Array written into it is wrapped element by element */ ClassInfo *ci = &c->classes[sh->cid]; if (it == TY_POLY_ARRAY && !(ci->ivar_elems_shared[iv] & ELEMS_POLY)) { { ci->ivar_elems_shared[iv] |= ELEMS_POLY; comp_touch_class(ci->idx); } changed = 1; changed |= share_box_ivar_family(c, sh->cid, sh->name, 0); } continue; } changed |= strbuf_promote_ivar(c, sh->cid, sh->name); if (it == TY_POLY) changed |= share_lift_poly_ivar_stores(c, sh->cid, sh->name); } /* a class variable, in each class with one of the name, as an ivar does */ else if (sh->kind == SHK_CVAR) { for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; int i = sh->name ? comp_cvar_index(ci, sh->name) : -1; /* a box holds the handle: its Strings are stored as the handle */ if (i >= 0 && ci->cvar_types[i] == TY_POLY) { changed |= share_lift_poly_static_stores(c, SHK_CVAR, sh->name); continue; } if (i < 0 || (ci->cvar_types[i] != TY_STRING && ci->cvar_types[i] != TY_STRBUF)) continue; if (repr_of_cvar(c, k, i).share) continue; cvar_set_type(ci, i, TY_STRBUF); ci->cvar_str_shared[i] = 1; changed = 1; } /* the facts key a class variable by name: a String one class holds and a container another holds of the same name are one holder, so the container's elements are converted here too */ changed |= share_static_container(c, SHK_CVAR, sh->name); } /* a global's or a constant's Array or Hash whose elements the rule shares holds them as handles, as a local's does: a String Array settles in its poly form, and what its writes and stores give it is demanded into the handle */ else if ((sh->kind == SHK_GVAR || sh->kind == SHK_CONST || sh->kind == SHK_CVAR) && share_static_container(c, sh->kind, sh->name)) { changed = 1; } /* a method's value whose elements the rule shares, when they would be copies otherwise: a typed String Array or Hash literal it answers (`def pair = [@a, @b]`, `t = o.pair[0]`), which the seal refuses (repr_share_seal), and a deconstruct's, whose parts a pattern binds (its boxes hold what each element boxes as). The containers it answers hold the handles, as a local's do. */ /* An empty Hash factory return can follow its construction context into the boxed-value variant. Other return producer shapes keep their existing inferred layout and downstream sharing behavior. */ else if (sh->kind == SHK_RET && (share_ret_typed_str_literal(c, sh->scope) || share_pattern_method(&c->scopes[sh->scope]))) changed |= strbuf_method_ret_source_walk(c, sh->scope, -1, 0, SB_DEMAND) | share_ret_demand_literals(c, sh->scope); /* a method answering a box the rule shares: the Strings it answers are boxed as handles (share_lift_poly_returns) */ else if (sh->kind == SHK_RET && c->scopes[sh->scope].ret == TY_POLY) changed |= share_lift_poly_returns(c, sh->scope); /* and one whose Arrays' elements it shares: what a multiple return gathers (share_lift_multi_returns) */ else if (sh->kind == SHK_RET) changed |= share_lift_multi_returns(c, sh->scope); /* a global or a constant holds the handle the way a top-level ivar's C global does */ else if ((sh->kind == SHK_GVAR || sh->kind == SHK_CONST)) { LocalVar *gv = sh->kind == SHK_CONST ? comp_const(c, sh->name) : comp_gvar(c, sh->name[0] == '$' ? sh->name + 1 : sh->name); /* a box holds the handle: its Strings are stored as the handle */ if (gv && gv->type == TY_POLY) { changed |= share_lift_poly_static_stores(c, sh->kind, sh->name); continue; } if (!gv || (gv->type != TY_STRING && gv->type != TY_STRBUF)) continue; if (repr_of_slot(c, gv).share) continue; lv_set_type(gv, TY_STRBUF); { gv->str_shared = 1; comp_touch_scope(gv->owner); } changed = 1; } } free(slots); /* A retained literal can be nested in a box instead of named by a holder. Its recorded stores demand the same handles as a holder's stores; inference then selects the existing boxed-element form. Apply the holder decisions first, so their handles are visible to the store walk even while a block parameter still infers as String. */ /* The block values a call stores (map's: its block's last value and each `next`'s) into a container: a String one is boxed as its handle, as a boxed local's stores are (share_lift_value). */ NT_FOREACH_KIND(c->nt, NK_CallNode, n) { if (!share_call_stores_block(c, n) || !comp_scope_of(c, n)->reachable) continue; int body = nt_ref(c->nt, nt_ref(c->nt, n, "block"), "body"), bn = 0; const int *bb = body >= 0 && nt_kind(c->nt, body) == NK_StatementsNode ? nt_arr(c->nt, body, "body", &bn) : NULL; if (bn == 0) continue; changed |= share_lift_value(c, nt_paren_single(c->nt, bb[bn - 1])); changed |= share_lift_jumps(c, body, NK_NextNode); } /* A typed String container literal's elements, and the argument an Array.new fill stores, are stored as their handles: what each element is built of is demanded into the handle. */ static const NodeKind lit_kinds[] = { NK_ArrayNode, NK_HashNode }; for (size_t k = 0; k < sizeof lit_kinds / sizeof *lit_kinds; k++) NT_FOREACH_KIND(c->nt, lit_kinds[k], n) { if (!repr_str_literal_shares(c, n) || share_literal_unkept(c, n)) continue; changed |= strbuf_container_source_walk(c, n, 0, SB_DEMAND); /* A known attribute's conditional write need not infer its RHS. */ infer_type(c, n); } NT_FOREACH_KIND(c->nt, NK_CallNode, n) { int recv = nt_ref(c->nt, n, "receiver"), args = nt_ref(c->nt, n, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(c->nt, args, "arguments", &argc) : NULL; if (recv < 0 || argc < 2 || nt_kind(c->nt, argv[1]) == NK_SplatNode || (nt_kind(c->nt, recv) != NK_ConstantReadNode && nt_kind(c->nt, recv) != NK_ConstantPathNode) || !is_array_new(nt_str(c->nt, recv, "name"), nt_str(c->nt, n, "name")) || !comp_scope_of(c, n)->reachable) continue; changed |= strbuf_container_source_walk(c, n, 0, SB_DEMAND); infer_type(c, n); } /* a container literal no holder names, iterated in place by a block (`[+"a"].each { |x| x << y }`), whose elements the rule shares: its stores are the handles the block's parameters bind. An indexed literal hands on those same elements without a block: only the share row's element-reading answers extend this to calls without blocks, the iterators that answer one of the elements (`[a, b].max`) among them. That arm follows named elements of an unanchored literal: a fresh element has no earlier owner, and a retained container has its own store demand above. A pattern's subject carries the same handles into its bound locals. */ static const NodeKind kinds[] = { NK_CallNode, NK_CaseMatchNode, NK_MatchRequiredNode, NK_MatchPredicateNode }; for (size_t k = 0; k < sizeof kinds / sizeof *kinds; k++) NT_FOREACH_KIND(c->nt, kinds[k], n) { int r, blk = -1; if (kinds[k] == NK_CallNode) { blk = nt_ref(c->nt, n, "block"); r = nt_ref(c->nt, n, "receiver"); } else r = nt_ref(c->nt, n, kinds[k] == NK_CaseMatchNode ? "predicate" : "value"); if (r < 0) continue; r = nt_paren_single(c->nt, r); if (kinds[k] == NK_CallNode) changed |= share_demand_nested_read(c, n, r); if (r < 0 || (nt_kind(c->nt, r) != NK_ArrayNode && nt_kind(c->nt, r) != NK_HashNode)) continue; /* (a blockless builtin that reads an element of the literal: the literal can be reached again, and its element is no store) */ if (kinds[k] == NK_CallNode && (blk < 0 || nt_kind(c->nt, blk) != NK_BlockNode)) continue; changed |= strbuf_container_source_walk(c, r, 0, SB_DEMAND); } /* The same holds when a literal is passed to a user method, whole or spread with `*` or `**`, and for each argument a rest parameter gathers: the callee's parameters name those elements again. A literal handed to a builtin (`p [a, b]`), a proc or a block's value is left as before. The call's targets are the plan's, as the facts read them. */ unsigned char *rest_read = rest_param_reads(c); NT_FOREACH_KIND(c->nt, NK_CallNode, n) { int args = nt_ref(c->nt, n, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(c->nt, args, "arguments", &an) : NULL; if (an == 0) continue; int mis[CPT_MAX], cnt = cplan_targets(c, n, mis, CPT_MAX); if (cnt <= 0) continue; for (int i = 0; i < an; i++) changed |= share_demand_literal_arg(c, av[i]); for (int t = 0; t < cnt; t++) changed |= share_demand_rest_args(c, mis[t], av, an, rest_read); } free(rest_read); sb_share_end(); /* The next round must settle on the types AFTER these promotions. Comparing with the pre-promotion types asks for another inference round even when no dependent type moved. */ if (in_fixpoint && changed) { unsigned d_out = share_types_digest(c); /* the same promotions over the same types as last time only re-fill what the re-narrow loop cleared: no change for its exits to wait on */ if (d_in == c->share_in_sig && d_out == c->share_sig) changed = 0; c->share_in_sig = d_in; c->share_sig = d_out; } return changed; } /* Pure-alias pairs, as a pass of its own: promote_shared_stored_strings runs it in the fixpoint, and the post-fixpoint handle loop again, since a local convert_byref_handle_params pulls into the handle there (`t = s; grow(t)`) takes its alias source along only through this rule. */ static int promote_local_alias_pair(Compiler *c, Scope *ws, const char *srcn, const char *tgtn); static int promote_local_alias_pairs(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; /* Pure-alias pairs (`s2 = s1`): when either endpoint of the alias is in-place mutated, both share the one handle -- CRuby's mutable String objects -- regardless of which mutator (a bang-only alias set shares too). Non-literal writes are fine: the write emitter wraps them in sp_String_new, which inherits the source's frozen state. */ for (int w = comp_kind_first(c, NK_LocalVariableWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; int value = nt_ref(nt, w, "value"); /* the aliasing shapes: `s2 = s1`, the value-position append chain `s2 = (s1 << x)`, whose value IS the base object, and each arm of a conditional (an_strbuf_alias_leaves) */ int lv[16]; int nl = an_strbuf_alias_leaves(c, nt_ref(nt, w, "value"), lv, 16); for (int l = 0; l < nl; l++) changed |= promote_local_alias_pair(c, comp_scope_of(c, w), nt_str(nt, lv[l], "name"), nt_str(nt, w, "name")); } /* `h ||= g` and `h &&= g` are `h = g` on the path that writes */ for (int k = 0; k < 2; k++) for (int w = comp_kind_first(c, k ? NK_LocalVariableAndWriteNode : NK_LocalVariableOrWriteNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != (k ? NK_LocalVariableAndWriteNode : NK_LocalVariableOrWriteNode)) continue; int lv[16]; int nl = an_strbuf_alias_leaves(c, nt_ref(nt, w, "value"), lv, 16); for (int l = 0; l < nl; l++) changed |= promote_local_alias_pair(c, comp_scope_of(c, w), nt_str(nt, lv[l], "name"), nt_str(nt, w, "name")); } /* A nested target binds out of a boxed Array and would append to a copy. */ for (int t = comp_kind_first(c, NK_LocalVariableTargetNode); t >= 0; t = comp_kind_next(c, t)) { const char *tn = nt_str(nt, t, "name"); Scope *ts = comp_scope_of(c, t); if (!tn || !ts || strbuf_mut_kind(c, tn, ts) != 1) continue; for (int mw = comp_kind_first(c, NK_MultiWriteNode); mw >= 0; mw = comp_kind_next(c, mw)) { if (comp_scope_of(c, mw) != ts || an_masgn_alias_source(c, mw, t) >= 0) continue; int source = an_masgn_alias_in(c, mw, nt_ref(nt, mw, "value"), t, 0); if (source < 0 || (comp_ntype(c, source) != TY_STRING && comp_ntype(c, source) != TY_STRBUF)) continue; } } /* `t, u = s, 1` names s as t, as `t = s` does (an_masgn_alias_source) */ for (int mw = comp_kind_first(c, NK_MultiWriteNode); mw >= 0; mw = comp_kind_next(c, mw)) { if (nt_kind(nt, mw) != NK_MultiWriteNode) continue; int ln = 0; const int *lefts = nt_arr(nt, mw, "lefts", &ln); for (int i = 0; i < ln; i++) { if (nt_kind(nt, lefts[i]) != NK_LocalVariableTargetNode) continue; int sv = an_masgn_alias_source(c, mw, lefts[i]); if (sv >= 0) changed |= promote_local_alias_pair(c, comp_scope_of(c, mw), nt_str(nt, sv, "name"), nt_str(nt, lefts[i], "name")); } } return changed; } static int promote_local_alias_pair(Compiler *c, Scope *ws, const char *srcn, const char *tgtn) { int changed = 0; if (!srcn || !tgtn || !ws || sp_streq(srcn, tgtn)) return 0; LocalVar *srcv = scope_local(ws, srcn); LocalVar *tgtv = scope_local(ws, tgtn); if (!srcv || !tgtv) return 0; if (srcv->str_shared && tgtv->str_shared) return 0; /* settled */ /* A block parameter is bound by its iterator, which hands over a shared handle only when the iteration rule made the container's elements handles (the parameter is then already a shared strbuf). Otherwise the element is a plain string, and promoting the parameter through an alias -- `|s| s = s__bpin` once #5669 rebinds a reassigned parameter -- declared an sp_String * the iterator assigns a const char * to. */ if ((srcv->is_block_param && !(srcv->type == TY_STRBUF && srcv->str_shared)) || (tgtv->is_block_param && !(tgtv->type == TY_STRBUF && tgtv->str_shared))) return 0; /* The source may be the method's own parameter (`def m(y) = (t = y; t << x)`). strbuf_slot_eligible turns parameters away, since a local slot's representation is the scope's own choice and a parameter's is its callers' too, so the pair was skipped and the append landed in t's copy, lost even to the method's own later reads of y. The parameter takes the handle instead, as one handed on to a handle parameter does (convert_byref_handle_params), and that pass then pulls its callers. */ int src_param = srcv->is_param && !srcv->is_block_param && !srcv->rbs_seeded && an_param_idx(ws, srcn) >= 0 && (srcv->type == TY_STRING || srcv->type == TY_STRBUF); if ((!src_param && !strbuf_slot_eligible(c, srcn, ws, srcv)) || !strbuf_slot_eligible(c, tgtn, ws, tgtv)) return 0; int ms = strbuf_mut_kind(c, srcn, ws); int mt = strbuf_mut_kind(c, tgtn, ws); if (ms < 0 || mt < 0) return 0; /* a disqualifying mutator */ /* Lent to a parameter that appends (a byref slot) is mutated in place too: the callee's append writes the new String back through the slot, and the other name kept the old one (an_local_lent). */ if (ms == 0 && an_local_lent(c, srcn, ws)) ms = 1; if (mt == 0 && an_local_lent(c, tgtn, ws)) mt = 1; /* An endpoint another rule made the handle counts as mutated: being the handle is the conclusion that evidence was for. The alias set is then mutated through it somewhere, by a later alias (`t = s; u = t; u << x`) or by a callee it was handed to (`t = s; grow(t)`), and the source still holding its own bytes kept the old String. */ int hs = srcv->type == TY_STRBUF && srcv->str_shared; int ht = tgtv->type == TY_STRBUF && tgtv->str_shared; if (ms != 1 && mt != 1 && !hs && !ht) return 0; /* alias set never mutated */ if (src_param && (srcv->type != TY_STRBUF || !srcv->str_shared || srcv->byref_out)) { if (srcv->byref_out) { { srcv->byref_out = 0; comp_touch_scope(srcv->owner); } { srcv->is_cell = 0; comp_touch_scope(srcv->owner); } } lv_set_type(srcv, TY_STRBUF); { srcv->str_shared = 1; comp_touch_scope(srcv->owner); } changed = 1; } /* A slot that has widened past String cannot carry the shared-mutable REPRESENTATION: strbuf is an sp_String handle, and a poly slot may hold anything. Boxing keeps the handle, so identity and in-place mutation still travel; forcing strbuf back on it only fought whatever widened it, every round, to the fixpoint's cap (#4116). */ if (srcv->type != TY_POLY && (srcv->type != TY_STRBUF || !srcv->str_shared)) { lv_set_type(srcv, TY_STRBUF); { srcv->str_shared = 1; comp_touch_scope(srcv->owner); } changed = 1; } if (tgtv->type != TY_POLY && (tgtv->type != TY_STRBUF || !tgtv->str_shared)) { lv_set_type(tgtv, TY_STRBUF); { tgtv->str_shared = 1; comp_touch_scope(tgtv->owner); } changed = 1; } return changed; } /* Does the block of `itn` over `recv` bind the VALUE of a Hash local or a Hash literal? It does for `h.each_value { |v| }`, `h.each { |k, v| }` / `each_pair`, and an element iterator over `h.values`. Answers the Hash's read in *hrecv and the value's parameter position in *vi. */ static int dyn_param_appended(Compiler *c, int mi, int j); /* An element read handed to a parameter that is appended to (`m(h[:a])`, `m(a[0][1])`, `m(@a[0])`): the container's Strings have to become handles together, or the read stays a copy and the callee appends to it instead of the stored String. The same demand the container-store rules make when a shared String is put IN; here the evidence is what is done to what comes OUT. Answers -1 when `an` is no element read of a container, else whether it changed anything. The walk can come back to the same read: a container that stores an element of itself (`@m[to] = @m[from]`) has that read among its stores, and demanding a poly store starts a fresh demand of it, depth and all. That recursed until the stack ran out. A read whose walk is already under way answers 0 -- the walk on the stack covers it. */ static int *sb_elem_active, sb_elem_nactive, sb_elem_cap; static int strbuf_demand_elem_arg(Compiler *c, int an) { const NodeTable *nt = c->nt; if (an < 0 || nt_kind(nt, an) != NK_CallNode || !container_elem_read_p(nt, an)) return -1; int rr = nt_ref(nt, an, "receiver"); if (rr < 0) return -1; /* a container a variable holds, at the bottom of any element reads: an Array a call answers (`a.map { .. }[0]`) is a fresh one nothing else names, and demanding its sources only widened what they flow into */ { int base = rr; for (int k = 0; k < 16 && nt_kind(nt, base) == NK_CallNode && container_elem_read_p(nt, base); k++) base = nt_ref(nt, base, "receiver"); if (base < 0 || (nt_kind(nt, base) != NK_LocalVariableReadNode && nt_kind(nt, base) != NK_InstanceVariableReadNode)) return -1; } TyKind rt = infer_type(c, rr); const char *cn = NULL; Scope *cs = NULL; if (ty_is_array(rt) || ty_is_hash(rt)) { if (nt_kind(nt, rr) == NK_LocalVariableReadNode) { cn = nt_str(nt, rr, "name"); cs = cn ? comp_scope_of(c, rr) : NULL; if (!cn || !cs) return 0; } } else if (!container_elem_read_p(nt, rr)) return -1; for (int k = 0; k < sb_elem_nactive; k++) if (sb_elem_active[k] == an) return 0; if (sb_elem_nactive == sb_elem_cap) { int ncap = sb_elem_cap ? sb_elem_cap * 2 : 16; int *na = realloc(sb_elem_active, (size_t)ncap * sizeof(int)); if (!na) return 0; sb_elem_active = na; sb_elem_cap = ncap; } sb_elem_active[sb_elem_nactive++] = an; int r; if (cn) r = strbuf_demand_container_stores(c, cn, cs); /* an element of an ivar's container, of another element, of a method's result: the stores that reach it, as for a mutator through one */ else r = strbuf_container_source_walk(c, rr, 0, SB_DEMAND); sb_elem_nactive--; return r; } /* Every ReturnNode grouped by the scope comp_scope_of gives it, in node order: scope i's are (*list)[(*start)[i] .. (*start)[i + 1]). The caller frees both. Out of memory stops the compile: without the grouping the deep-return loop would skip its remaining call sites. */ static void an_returns_by_scope(Compiler *c, int **start, int **list) { const NodeTable *nt = c->nt; int ns = c->nscopes > 0 ? c->nscopes : 1, nr = 0; int *st = calloc((size_t)ns + 1, sizeof(int)); if (!st) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int u = 0; u < nt->count; u++) { if (nt_kind(nt, u) != NK_ReturnNode) continue; st[(int)(comp_scope_of(c, u) - c->scopes) + 1]++; nr++; } for (int i = 0; i < ns; i++) st[i + 1] += st[i]; int *ls = malloc((size_t)(nr > 0 ? nr : 1) * sizeof(int)); int *fill = malloc((size_t)ns * sizeof(int)); if (!ls || !fill) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(fill, st, (size_t)ns * sizeof(int)); for (int u = 0; u < nt->count; u++) if (nt_kind(nt, u) == NK_ReturnNode) ls[fill[comp_scope_of(c, u) - c->scopes]++] = u; free(fill); *start = st; *list = ls; } /* --share-strings: does statement list `st` end in nil, or in a tail an_tail_is_shared_handle accepts? */ /* What a return tail answers, counted by an_tail_is_shared_handle: shared slot reads and nils. */ /* fresh_ok (--share-strings, the pickup's own ask): a tail answering a fresh String (share_node_fresh) is admitted too, counted in fresh; bangs counts the reads that are a bang on a handle (ret_pub_bang) */ typedef struct { int reads, nils, fresh_ok, fresh, mi, param, arg_depth, bangs, in_begin; } TailCount; /* Is node a String `||` or `&&` whose left operand is a String, so that it answers one of its operands or nil (never false): the tail walk takes its operands as arms, and emit_tail_value emits them so. */ int an_tail_logic_op(Compiler *c, int node) { NodeKind k = node >= 0 ? nt_kind(c->nt, node) : NK_NONE; if (k != NK_OrNode && k != NK_AndNode) return 0; TyKind t = comp_ntype(c, node), lt = comp_ntype(c, nt_ref(c->nt, node, "left")); return (t == TY_STRING || t == TY_STRBUF) && (lt == TY_STRING || lt == TY_STRBUF); } /* The return walk's state: per method, how far it is decided (st), and how many decisions where an answer was asked (an_ret_settle) are open. prev (the fresh walk only): per method, what the previous round found for it -- 0 no, 1 only new Strings, 2 + p its parameter p -- which a method still being walked (a cycle) answers this round; with meet set, each method's answer this round is also held to no more than its prev (an_fresh_meet) */ typedef struct RetHandles { unsigned char *st; int *prev; int *ret_start, *ret_list; int fresh, settling, meet; int *par; } RetHandles; static int an_tail_handle(Compiler *c, RetHandles *R, int n, TailCount *tc); static int an_tail_is_shared_handle(Compiler *c, int node, int nil_ok, TailCount *n, RetHandles *R); /* A box a seeded String method answers: its unboxing at the return publishes the handle the box holds (sp_poly_unbox_s_pub). */ /* A boxed `x || y` / `x && y` builds its box from its operands. A String call's operand is boxed from the handle the call publishes (its pickup), as an unseeded method's is (share_lift_seeded_tail marks it), where it holds one, or lifted into a new handle where it answers a new String; any other call's answer is boxed from its bytes, which holds no handle, so that operand keeps the tail refused. A box, a variable or a literal operand is boxed whole. */ static int an_boxed_logic_ok(Compiler *c, int node) { const NodeTable *nt = c->nt; node = nt_paren_single(nt, node); if (node < 0) return 0; NodeKind k = nt_kind(nt, node); if (k == NK_OrNode || k == NK_AndNode) return an_boxed_logic_ok(c, nt_ref(nt, node, "left")) && an_boxed_logic_ok(c, nt_ref(nt, node, "right")); return k != NK_CallNode || c->ntype[node] == TY_POLY || repr_call_returns_handle(c, node) || share_value_fresh(c, node, 0) || share_plan_box_handle(c, node); } static int an_tail_boxed_string(Compiler *c, int node, TailCount *tc) { int mi = tc->mi; if (tc->in_begin || mi <= 0 || !c->scopes[mi].ret_rbs_seeded || c->scopes[mi].ret != TY_STRING || node < 0 || c->ntype[node] != TY_POLY) return 0; /* the kinds whose unboxing at the return is the one that publishes */ NodeKind k = nt_kind(c->nt, node); if (k == NK_OrNode || k == NK_AndNode) { if (!an_boxed_logic_ok(c, node)) return 0; /* its call operands are boxed as an unseeded method's are (the lift emit_boxed reads), a call by the handle it publishes */ share_lift_value(c, node); return 1; } return k == NK_LocalVariableReadNode || k == NK_InstanceVariableReadNode || k == NK_CallNode; } static int an_stmts_tail_shared(Compiler *c, int st, int nil_ok, TailCount *tc, RetHandles *R) { int n = 0; const int *b = st >= 0 && nt_kind(c->nt, st) == NK_StatementsNode ? nt_arr(c->nt, st, "body", &n) : NULL; if (st < 0) { tc->nils += nil_ok; return nil_ok; } return n > 0 && (R ? an_tail_handle(c, R, b[n - 1], tc) : an_tail_is_shared_handle(c, b[n - 1], nil_ok, tc, NULL)); } /* A method tail that publishes the shared handle it answers, for the deep-return pickup (an_returns_shared_handles): a shared slot's read (an_arg_is_shared_handle, counted in tc->reads), and under --share-strings a conditional each of whose arms ends in one (`f ? x : y`, an elsif chain) and a begin each of whose arms does (its body's, each rescue's, its else's; an ensure's value is dropped). With nil_ok, nil too, and an arm that answers it, written or missing (`x if f`, `if f; else; x; end`, `begin; x; rescue; nil; end`), counted in tc->nils: nil publishes nothing, and the pickup reads the call's nil as nil whatever an earlier read published (an_tail_answers_nil). With the settled return analysis, each arm asks an_tail_handle too: a call can publish the handle, and its method's active-visit guard bounds recursive arms. Case/when and case/in share the same arm list for the pickup and settled return walk. A builtin raise leaves no value, so contributes neither a read nor nil; nor does a loop that never completes (`while true` with no break of its own, loop_never_completes), whose `return`s the walk takes apart. */ static int an_tail_is_shared_handle(Compiler *c, int node, int nil_ok, TailCount *tc, RetHandles *R) { const NodeTable *nt = c->nt; NodeKind k = node >= 0 ? nt_kind(nt, node) : NK_NONE; if ((an_call_raises(c, node) || loop_never_completes(c, node))) return 1; if (k == NK_NilNode) { if (g_repr_check && tc->mi > 0 && (!R || !R->fresh)) repr_channel_predict(c, node, tc->mi, RCH_NIL); tc->nils += nil_ok; return nil_ok; } if (k == NK_ParenthesesNode) return an_stmts_tail_shared(c, nt_ref(nt, node, "body"), nil_ok, tc, R); if (k == NK_BeginNode) { /* the body's value is the begin's only when no else follows it (a box its arms answer is unboxed in the begin's result slot, which does not publish: in_begin) */ int be = nt_ref(nt, node, "else_clause"), ok = 1; tc->in_begin++; if (be < 0 && !an_stmts_tail_shared(c, nt_ref(nt, node, "statements"), nil_ok, tc, R)) ok = 0; for (int rc = nt_ref(nt, node, "rescue_clause"); ok && rc >= 0; rc = nt_ref(nt, rc, "subsequent")) if (!an_stmts_tail_shared(c, nt_ref(nt, rc, "statements"), nil_ok, tc, R)) ok = 0; ok = ok && (be < 0 || an_stmts_tail_shared(c, nt_ref(nt, be, "statements"), nil_ok, tc, R)); tc->in_begin--; return ok; } if ((k == NK_CaseNode || k == NK_CaseMatchNode)) { int n = 0; const int *arms = nt_arr(nt, node, "conditions", &n); for (int i = 0; i < n; i++) if (!an_stmts_tail_shared(c, nt_ref(nt, arms[i], "statements"), nil_ok, tc, R)) return 0; int el = nt_ref(nt, node, "else_clause"); /* Without an else, a failed pattern raises NoMatchingPatternError. */ if (el < 0 && k == NK_CaseMatchNode) return 1; return an_stmts_tail_shared(c, el >= 0 ? nt_ref(nt, el, "statements") : -1, nil_ok, tc, R); } if (R && k == NK_ReturnNode) { int args = nt_ref(nt, node, "arguments"), n = 0; const int *v = args >= 0 ? nt_arr(nt, args, "arguments", &n) : NULL; if (n == 0) { tc->nils += nil_ok; return nil_ok; } return n == 1 && an_tail_handle(c, R, v[0], tc); } if (k == NK_RescueModifierNode) { int e = nt_ref(nt, node, "expression"), r = nt_ref(nt, node, "rescue_expression"); tc->in_begin++; int ok = R ? an_tail_handle(c, R, e, tc) && an_tail_handle(c, R, r, tc) : an_tail_is_shared_handle(c, e, nil_ok, tc, NULL) && an_tail_is_shared_handle(c, r, nil_ok, tc, NULL); tc->in_begin--; return ok; } /* A String `x || y` answers x, or y where x answers nil: both are arms (`a.squeeze!("b") || a` answers a's String either way). A String `x && y` answers y, or the nil x answers: y is an arm beside a nil. */ if (an_tail_logic_op(c, node)) { int l = nt_ref(nt, node, "left"), r = nt_ref(nt, node, "right"); if (k == NK_AndNode) { if (!nil_ok) return 0; tc->nils++; } else if (!(R ? an_tail_handle(c, R, l, tc) : an_tail_is_shared_handle(c, l, nil_ok, tc, NULL))) return 0; return R ? an_tail_handle(c, R, r, tc) : an_tail_is_shared_handle(c, r, nil_ok, tc, NULL); } if (k != NK_IfNode && k != NK_UnlessNode) { if (R && R->fresh) return share_value_fresh(c, node, 0); int ok = an_arg_is_shared_handle(c, node) || strbuf_builtin_tail(c, node) || an_tail_boxed_string(c, node, tc); tc->reads += ok; /* a bang on a handle (strbuf_builtin_tail) answers it, or nil */ if (ok && strbuf_bang_tail(c, node)) { tc->nils += nil_ok; tc->bangs++; } if (g_repr_check && tc->mi > 0 && ok) repr_channel_predict(c, node, tc->mi, RCH_PUBLISH); return ok; } int el = nt_ref(nt, node, k == NK_IfNode ? "subsequent" : "else_clause"); if (!an_stmts_tail_shared(c, nt_ref(nt, node, "statements"), nil_ok, tc, R)) return 0; if (el < 0) { tc->nils += nil_ok; return nil_ok; } return nt_kind(nt, el) == NK_ElseNode ? an_stmts_tail_shared(c, nt_ref(nt, el, "statements"), nil_ok, tc, R) : R ? an_tail_handle(c, R, el, tc) : an_tail_is_shared_handle(c, el, nil_ok, tc, NULL); } /* Can method mi answer nil, through its last statement or a `return`? The same walk that admitted the tail for the pickup counts its nils, so the two cannot disagree (asking a conditional's arms apart missed an empty one); a last statement it does not admit answers yes, and the returns' nils are the ones an_returns_shared_handles counted. */ int an_tail_answers_nil(Compiler *c, int mi) { TailCount tc = { 0, 0 }; return !an_tail_is_shared_handle(c, scope_body_last(c, mi), 1, &tc, NULL) || tc.nils > 0 || c->scopes[mi].ret_nil_pickup; } /* --share-strings, once the analysis settles: a method whose every return path reads a String the rule shares (an_returns_shared_handles), or calls (or `super`s into) methods that each answer one, answers that String, and the read that answers it publishes the handle last (sp_strbuf_read_pub): any call of the method can take the handle as the deep-return pickup does (Scope.ret_handle). A method spliced at its calls (one that yields, a proc form) has no call to pick up from; one typed other than String answers no handle to pick up. Fresh String and nil arms use the pickup's existing fresh-tail clearing and nil check. Each method is decided once, its tail calls' targets first (a cycle answers no in the handle walk, and in the fresh walk what an_fresh_returns' previous round found), and so is a method whose answer a tail's own test asks (a builtin tail over a call, strbuf_builtin_tail, asks whether that call publishes): the walk is Compiler.ret_walk while it runs, and scope_ret_handle, scope_ret_fresh and scope_ret_param decide such a method where they are asked, not in the order the methods are defined. Each such decision runs on the stack of the one that asked it (about a dozen frames a link), so a chain of them is bounded (RH_SETTLE_MAX, well inside the 8 MB main stack the compiler runs on): past the bound an answer is read as the walk has it so far, undecided (no), and the method is decided where the walk reaches it. */ enum { RH_UNSEEN, RH_BUSY, RH_YES, RH_NO }; enum { RH_SETTLE_MAX = 256 }; static int an_ret_handle(Compiler *c, RetHandles *R, int mi); /* A super in an instance method `inspect` or `to_s` (no arguments of its own) of a plain class -- no ancestor of the program defines the name, none embeds a builtin or is a Struct, a native class or an Exception, whose to_s is its message -- reaches Kernel#inspect or Kernel#to_s, which build a new String each call. */ int an_super_object_text(Compiler *c, int n, int mi) { if (mi <= 0 || mi >= c->nscopes) return 0; const Scope *s = &c->scopes[mi]; if (!s->name || s->is_cmethod || s->class_id < 0 || s->nparams != 0 || (!sp_streq(s->name, "inspect") && !sp_streq(s->name, "to_s"))) return 0; NodeKind k = nt_kind(c->nt, n); if (k != NK_SuperNode && k != NK_ForwardingSuperNode) return 0; if (k == NK_SuperNode && nt_ref(c->nt, n, "arguments") >= 0) return 0; int cid = s->class_id; ClassInfo *ci = &c->classes[cid]; /* (a reopened builtin -- Exception, String, Object -- has its own inspect and to_s behind the program's) */ if (comp_class_is_module(c, ci) || is_builtin_reopen(ci->name) || ci->is_struct || ci->is_data || ci->is_native_class || ci->nsub_root > 0 || comp_ary_root(c, cid) >= 0 || class_inherits_builtin_exception(c, cid) || class_is_exc_subclass(c, cid)) return 0; return ci->parent < 0 || comp_method_in_chain(c, ci->parent, s->name, NULL) < 0; } /* Method mi is decided: yes or no, and for the handle walk its field. */ static int an_ret_decided(Compiler *c, RetHandles *R, int mi, int yes) { R->st[mi] = yes ? RH_YES : RH_NO; if (!R->fresh) c->scopes[mi].ret_handle = (unsigned char)yes; return yes; } /* A parameter's identity survives mutation, but not rebinding. The local write index is a bucket: check both its scope and name. All borrowed tails must name the same parameter of the method being decided. */ static int an_tail_param(Compiler *c, int n, TailCount *tc) { const NodeTable *nt = c->nt; if (n < 0 || nt_kind(nt, n) != NK_LocalVariableReadNode) return 0; Scope *m = &c->scopes[tc->mi]; const char *pn = nt_str(nt, n, "name"); if (!pn || comp_scope_of(c, n) != m) return 0; int pi = an_param_idx(m, pn); if (pi < 0 || (tc->param >= 0 && tc->param != pi)) return 0; for (int w = comp_lvw_first_sc(c, tc->mi, pn); w >= 0; w = comp_lvw_next_sc(c, w)) if (comp_scope_of(c, w) == m && nt_str(nt, w, "name") && sp_streq(nt_str(nt, w, "name"), pn)) return 0; tc->param = pi; return 1; } /* A conditional's arm a in the fresh walk: what its statements end in, a nested conditional's arms, or nil when it is empty or missing. */ static int an_tail_arm(Compiler *c, RetHandles *R, int a, TailCount *tc) { const NodeTable *nt = c->nt; a = nt_paren_single(nt, a); if (a >= 0 && nt_kind(nt, a) == NK_ElseNode) a = nt_ref(nt, a, "statements"); if (a >= 0 && nt_kind(nt, a) == NK_StatementsNode) { int n = 0; const int *bb = nt_arr(nt, a, "body", &n); a = n > 0 ? bb[n - 1] : -1; } if (a < 0 || nt_kind(nt, a) == NK_NilNode) { tc->nils++; return 1; } return an_tail_handle(c, R, a, tc); } /* Does tail node n answer a shared handle it publishes last: a handle's read, or a call or `super` whose every target method does? A nullable tail can also answer nil, which the existing pickup tests before using the return channel. Count it with the pickup's own tail predicate. */ /* Is tail n a String method answering its receiver (`<<`, concat, force_encoding: the String share row's BSH_RECV), called on a typed String no reopening gives a method of the name? It answers what its receiver answers (`fast_ymd << 'T00:00:00+0000'`, `internal_strftime(fmt).force_encoding(enc)`). */ static int an_tail_recv_answer(Compiler *c, int n) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, n, "receiver"); const char *nm = nt_str(nt, n, "name"); if (recv < 0 || !nm || nt_ref(nt, n, "block") >= 0) return 0; TyKind rt = comp_ntype(c, recv); if (rt != TY_STRING && rt != TY_STRBUF) return 0; if (bop_share_named(TY_STRING, nm) != BSH_RECV) return 0; int si = comp_core_class(c, CORE_STRING); return si < 0 || call_target(c, n, si, nm, 0, NULL) < 0; } /* Is tail n `x.tap { ... }` on a String no reopening gives a tap, Kernel's tap, whose literal block takes no parameter and leaves by no `break`? It answers x, and the block has no name for it (`take(n) = @b.byteslice(@pos, n).tap { @pos += n }`). */ static int an_tail_tap_blind(Compiler *c, int n) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, n, "receiver"), blk = nt_ref(nt, n, "block"); if (recv < 0 || !is_tap_name(nt_str(nt, n, "name")) || nt_ref(nt, n, "arguments") >= 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode || nt_ref(nt, blk, "parameters") >= 0 || nt_safe_nav(nt, n) || call_breaks(c, n) || comp_method_index(c, "tap") >= 0) return 0; TyKind rt = comp_ntype(c, recv); if (rt != TY_STRING && rt != TY_STRBUF) return 0; int si = comp_core_class(c, CORE_STRING); return si < 0 || comp_method_in_chain(c, si, "tap", NULL) < 0; } /* ---- a local a method builds its answer up in (#9258) ---- `out = +""; ...; out << x; ...; out`: every write of the local is a new String and every read the receiver of an append or the method's answer, so no other name ever holds the String it answers, whatever share class the walk gave it. How a value is used, for the walk below: */ enum { BL_OTHER, BL_DROP, BL_RET, BL_RECV }; /* Is v a new String each time it runs: an interpolation, `+"lit"` or `+` of one, `"lit" * n`, `"lit".dup`, String.new? */ static int bl_new_string(const NodeTable *nt, int v) { v = nt_paren_single(nt, v); NodeKind k = nt_kind(nt, v); if (k == NK_InterpolatedStringNode) return 1; if (k != NK_CallNode) return 0; const char *nm = nt_str(nt, v, "name"); int r = nt_ref(nt, v, "receiver"); NodeKind rk = r >= 0 ? nt_kind(nt, r) : NK_NONE; if (sp_streq(nm, "+@")) return rk == NK_StringNode || bl_new_string(nt, r); if (sp_streq(nm, "*") || sp_streq(nm, "dup")) return rk == NK_StringNode || rk == NK_InterpolatedStringNode; if (sp_streq(nm, "new")) return rk == NK_ConstantReadNode && sp_streq(nt_str(nt, r, "name"), "String"); return 0; } static int bl_is_append(const NodeTable *nt, int n) { if (n < 0 || nt_kind(nt, n) != NK_CallNode || nt_ref(nt, n, "block") >= 0) return 0; const char *nm = nt_str(nt, n, "name"); return nm && (sp_streq(nm, "<<") || sp_streq(nm, "concat")); } /* Is n the local (name, at block depth depth) or an append chain on it? */ static int bl_alias(const NodeTable *nt, int n, const char *name, int depth) { n = nt_paren_single(nt, n); if (nt_kind(nt, n) == NK_LocalVariableReadNode) return sp_streq(nt_str(nt, n, "name"), name) && nt_int(nt, n, "depth", 0) == depth; return bl_is_append(nt, n) && bl_alias(nt, nt_ref(nt, n, "receiver"), name, depth); } static int bl_walk(Compiler *c, int n, const char *name, int depth, int use, int ret); /* Does the subtree at n read or write a local of the name, at any block depth? (a block's or lambda's parameter list: a default such as `|k = out|` hands the String to another name) */ static int bl_names_local(const NodeTable *nt, int n, const char *name) { if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if ((k == NK_LocalVariableReadNode || k == NK_LocalVariableWriteNode || k == NK_LocalVariableTargetNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode) && sp_streq(nt_str(nt, n, "name"), name)) return 1; for (int i = 0; i < nt_num_refs(nt, n); i++) if (bl_names_local(nt, nt_ref_at(nt, n, i), name)) return 1; for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (bl_names_local(nt, av[j], name)) return 1; } return 0; } /* each child of n but skip, used as BL_OTHER; ret is the use a `return`'s argument gets: BL_RET where it leaves the method, BL_OTHER inside a lambda or a block that may run as one */ static int bl_children(Compiler *c, int n, int skip, const char *name, int depth, int ret) { const NodeTable *nt = c->nt; for (int i = 0; i < nt_num_refs(nt, n); i++) { int r = nt_ref_at(nt, n, i); if (r != skip && !bl_walk(c, r, name, depth, BL_OTHER, ret)) return 0; } for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (!bl_walk(c, av[j], name, depth, BL_OTHER, ret)) return 0; } return 1; } /* statements: all but the last dropped, the last used as use */ static int bl_stmts(Compiler *c, int st, const char *name, int depth, int use, int ret) { const NodeTable *nt = c->nt; if (st < 0) return 1; if (nt_kind(nt, st) != NK_StatementsNode) return bl_walk(c, st, name, depth, use, ret); int bn = 0; const int *bb = nt_arr(nt, st, "body", &bn); for (int i = 0; i < bn; i++) if (!bl_walk(c, bb[i], name, depth, i + 1 < bn ? BL_DROP : use, ret)) return 0; return 1; } /* A call: an append to the local (its value, the same String, only dropped, appended to again or answered), or a call whose literal block's value its iterator drops (an_iter_discards_block). A `return` in that block leaves the method; in another call's block, which may run as a lambda (`lambda`, `define_method`, a block kept and called later), it may answer the block's caller instead. */ static int bl_call(Compiler *c, int n, const char *name, int depth, int use, int ret) { const NodeTable *nt = c->nt; if (bl_is_append(nt, n) && bl_alias(nt, nt_ref(nt, n, "receiver"), name, depth)) { if (use == BL_OTHER) return 0; return bl_walk(c, nt_ref(nt, n, "receiver"), name, depth, BL_RECV, ret) && bl_children(c, n, nt_ref(nt, n, "receiver"), name, depth, ret); } int bk = nt_ref(nt, n, "block"); if (bk < 0 || nt_kind(nt, bk) != NK_BlockNode) return bl_children(c, n, -1, name, depth, ret); int disc = an_iter_discards_block(c, n); if (bl_names_local(nt, nt_ref(nt, bk, "parameters"), name)) return 0; return bl_children(c, n, bk, name, depth, ret) && bl_stmts(c, nt_ref(nt, bk, "body"), name, depth + 1, disc ? BL_DROP : BL_OTHER, disc ? ret : BL_OTHER); } static int bl_walk(Compiler *c, int n, const char *name, int depth, int use, int ret) { const NodeTable *nt = c->nt; if (n < 0) return 1; NodeKind k = nt_kind(nt, n); int mine = (k == NK_LocalVariableReadNode || k == NK_LocalVariableWriteNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode || k == NK_LocalVariableTargetNode) && sp_streq(nt_str(nt, n, "name"), name) && nt_int(nt, n, "depth", 0) == depth; switch (k) { case NK_DefNode: return 1; case NK_LocalVariableReadNode: return !mine || use == BL_RECV || use == BL_RET; case NK_LocalVariableWriteNode: if (mine && (use != BL_DROP || !bl_new_string(nt, nt_ref(nt, n, "value")))) return 0; return bl_walk(c, nt_ref(nt, n, "value"), name, depth, BL_OTHER, ret); case NK_LocalVariableOperatorWriteNode: case NK_LocalVariableOrWriteNode: case NK_LocalVariableAndWriteNode: case NK_LocalVariableTargetNode: if (mine) return 0; break; case NK_StatementsNode: return bl_stmts(c, n, name, depth, use, ret); case NK_ParenthesesNode: return bl_stmts(c, nt_ref(nt, n, "body"), name, depth, use, ret); case NK_IfNode: case NK_UnlessNode: return bl_walk(c, nt_ref(nt, n, "predicate"), name, depth, BL_OTHER, ret) && bl_stmts(c, nt_ref(nt, n, "statements"), name, depth, use, ret) && bl_walk(c, nt_ref(nt, n, k == NK_IfNode ? "subsequent" : "else_clause"), name, depth, use, ret); case NK_ElseNode: return bl_stmts(c, nt_ref(nt, n, "statements"), name, depth, use, ret); case NK_ReturnNode: { int a = nt_ref(nt, n, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; for (int i = 0; i < an; i++) if (!bl_walk(c, av[i], name, depth, an == 1 ? ret : BL_OTHER, ret)) return 0; return 1; } case NK_BlockNode: case NK_LambdaNode: /* (a lambda, or a block reached other than as its call's literal block: a `return` in it may answer its caller) */ if (bl_names_local(nt, nt_ref(nt, n, "parameters"), name)) return 0; return bl_stmts(c, nt_ref(nt, n, "body"), name, depth + 1, BL_OTHER, BL_OTHER); case NK_CallNode: return bl_call(c, n, name, depth, use, ret); default: break; } return bl_children(c, n, -1, name, depth, ret); } /* Is tail n of method mi a read of a local the method builds its answer up in (above)? */ static int an_builder_local_fresh(Compiler *c, int mi, int n) { const NodeTable *nt = c->nt; if (mi <= 0 || n < 0 || nt_kind(nt, n) != NK_LocalVariableReadNode || nt_int(nt, n, "depth", 0) != 0) return 0; Scope *m = &c->scopes[mi]; if (m->def_node < 0 || nt_kind(nt, m->def_node) != NK_DefNode || comp_scope_of(c, n) != m) return 0; const char *name = nt_str(nt, n, "name"); for (int j = 0; j < m->nparams; j++) if (m->pnames[j] && sp_streq(m->pnames[j], name)) return 0; return bl_stmts(c, nt_ref(nt, m->def_node, "body"), name, 0, BL_RET, BL_RET); } /* Does the subtree at n hold a return of its method (not one in a nested method or lambda)? */ static int an_has_method_return(const NodeTable *nt, int n) { if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode || k == NK_LambdaNode) return 0; if (k == NK_ReturnNode) return 1; for (int i = 0; i < nt_num_refs(nt, n); i++) if (an_has_method_return(nt, nt_ref_at(nt, n, i))) return 1; for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (an_has_method_return(nt, av[j])) return 1; } return 0; } /* Does instance method nm of class or module k come from the program? */ static int an_defines_instance_method(Compiler *c, int k, const char *nm) { for (int mi = 0; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; if (m->class_id == k && !m->is_cmethod && m->def_node >= 0 && m->name && sp_streq(m->name, nm)) return 1; } return 0; } /* Does a bare super in instance method nm of class cid reach Object's own nm? Every class of the chain is a plain class under Object (no Struct, no Array or Hash subclass, no superclass the program does not define but Object), none of them nor a module any includes defines nm above cid, no builtin module but Comparable and Enumerable is included, and nothing reopens Object or Kernel, or includes a module at the top level, with its own nm (a method or an attr_reader). */ static int an_super_reaches_object(Compiler *c, int cid, const char *nm) { const NodeTable *nt = c->nt; for (int k = cid; k >= 0; k = c->classes[k].parent) { ClassInfo *ci = &c->classes[k]; if (ci->is_struct || ci->ary_root > 0 || ci->def_node < 0 || nt_kind(nt, ci->def_node) != NK_ClassNode) return 0; /* (an attr_reader of the name answers its ivar's String) */ if (k != cid && (an_defines_instance_method(c, k, nm) || comp_is_reader(ci, nm))) return 0; for (int i = 0; i < ci->nincluded_mods; i++) { int mk = ci->included_mods[i]; if (an_defines_instance_method(c, mk, nm) || (mk >= 0 && comp_is_reader(&c->classes[mk], nm))) return 0; } for (int i = 0; i < ci->nincluded_mod_names; i++) if (!sp_streq(ci->included_mod_names[i], "Comparable") && !sp_streq(ci->included_mod_names[i], "Enumerable")) return 0; if (ci->parent < 0) { int sup = nt_ref(nt, ci->def_node, "superclass"); if (sup >= 0 && !(nt_kind(nt, sup) == NK_ConstantReadNode && sp_streq(nt_str(nt, sup, "name"), "Object"))) return 0; } } int ob = comp_core_class(c, CORE_OBJECT), ke = comp_core_class(c, CORE_KERNEL); if ((ob >= 0 && (an_defines_instance_method(c, ob, nm) || comp_is_reader(&c->classes[ob], nm))) || (ke >= 0 && (an_defines_instance_method(c, ke, nm) || comp_is_reader(&c->classes[ke], nm)))) return 0; for (int i = 0; i < c->ntoplevel_includes; i++) { int mk = c->toplevel_includes[i]; if (an_defines_instance_method(c, mk, nm) || (mk >= 0 && comp_is_reader(&c->classes[mk], nm))) return 0; } return 1; } /* See share.h. A method no way out of which answers (its last statement raises, and no return leaves it earlier) answers no String; Object's inspect and to_s answer a new one each call. */ int an_target_answers_new(Compiler *c, int mi) { if (mi <= 0 || mi >= c->nscopes) return 0; const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (m->def_node < 0 || nt_kind(nt, m->def_node) != NK_DefNode) return 0; int body = nt_ref(nt, m->def_node, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode || an_has_method_return(nt, body)) return 0; int last = scope_body_last(c, mi); if (last < 0) return 0; if (isa_node_diverges(c, last)) return 1; NodeKind lk = nt_kind(nt, last); if (lk != NK_ForwardingSuperNode && lk != NK_SuperNode) return 0; if (!m->name || m->is_cmethod || m->class_id < 0) return 0; if (!sp_streq(m->name, "inspect") && !sp_streq(m->name, "to_s")) return 0; return an_super_reaches_object(c, m->class_id, m->name); } /* The fresh walk's tail call n past the plan's list (#9258): a new String where the builtin arms answer one and every method of the name does, or answers none or Object's own (an_target_answers_new). */ static int an_tail_name_fresh(Compiler *c, RetHandles *R, int n, TailCount *tc) { if (!share_builtin_fresh(c, n)) return 0; int *mis = malloc(sizeof(int) * (size_t)(c->nscopes + 1)); if (!mis) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int cnt = share_name_targets(c, n, mis, c->nscopes + 1), ok = cnt > 0; for (int i = 0; ok && i < cnt; i++) { if (an_target_answers_new(c, mis[i])) continue; Scope *m = &c->scopes[mis[i]]; if (!an_ret_handle(c, R, mis[i]) || !m->ret_fresh) ok = 0; } free(mis); if (ok) tc->reads++; return ok; } /* The handle walk's tail call n past the plan's list (#9258): every method it can run (share_name_targets) publishes the handle, answers a new String (which then clears the side channel at its tails, ret_pub_fresh), or answers a String literal where nothing between the pickup's reset and its return can publish another (share_plan_literal_pickup_clean), and one publishes -- as for a call whose targets the plan lists. */ static int an_tail_name_handle(Compiler *c, RetHandles *R, int n, TailCount *tc) { int *mis = malloc(sizeof(int) * (size_t)(c->nscopes + 1) * 2); if (!mis) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int *fresh_t = mis + c->nscopes + 1; int cnt = share_name_targets(c, n, mis, c->nscopes + 1), ok = cnt > 0, npub = 0, nfresh = 0; for (int i = 0; ok && i < cnt; i++) { Scope *m = &c->scopes[mis[i]]; if (an_ret_handle(c, R, mis[i])) { npub++; continue; } if (scope_always_raises(c, mis[i]) || share_plan_literal_pickup_clean(c, n, mis[i])) continue; if (m->ret_fresh && m->ret == TY_STRING) fresh_t[nfresh++] = mis[i]; else ok = 0; } if (ok && npub == 0) ok = 0; if (ok && nfresh > 0) { for (int i = 0; i < nfresh; i++) c->scopes[fresh_t[i]].ret_pub_fresh = 1; tc->fresh++; } free(mis); if (!ok) return 0; tc->reads++; if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); return 1; } static int an_tail_handle(Compiler *c, RetHandles *R, int n, TailCount *tc) { const NodeTable *nt = c->nt; /* the fresh walk: a receiver-answering String method's tail is its receiver's (a new String appended to is one still), and so is a tap's whose block cannot name it */ if (R->fresh && n >= 0 && nt_kind(nt, n) == NK_CallNode && (an_tail_recv_answer(c, n) || an_tail_tap_blind(c, n))) return an_tail_handle(c, R, nt_ref(nt, n, "receiver"), tc); if (!R->fresh && c->n_cext_extensions && n >= 0 && nt_kind(nt, n) == NK_InstanceVariableReadNode && comp_ntype(c, n) == TY_STRBUF) { if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); tc->reads++; return 1; } if(!R->fresh && c->n_cext_extensions && n>=0 && nt_kind(nt,n)==NK_LocalVariableReadNode) { Scope *scope=comp_scope_of(c,n); const char *name=nt_str(nt,n,"name"); LocalVar *local=scope && name ? scope_local(scope,name) : NULL; if(local && local->type==TY_STRBUF) { if(g_repr_check)repr_channel_predict(c,n,tc->mi,RCH_PUBLISH); tc->reads++;return 1; } } /* the fresh walk: a local the method builds its answer up in */ if (R->fresh && an_builder_local_fresh(c, tc->mi, n)) { tc->fresh++; return 1; } if (an_tail_is_shared_handle(c, n, 1, tc, R)) return 1; if (R->fresh && an_tail_param(c, n, tc)) return 1; NodeKind k = n >= 0 ? nt_kind(nt, n) : NK_NONE; /* the fresh walk: a conditional answers what each arm ends in (a missing arm, nil); a builtin's new String, as the share walk found */ if (R->fresh && (k == NK_IfNode || k == NK_UnlessNode)) return an_tail_arm(c, R, nt_ref(nt, n, "statements"), tc) && an_tail_arm(c, R, nt_ref(nt, n, k == NK_IfNode ? "subsequent" : "else_clause"), tc); if (k == NK_CallNode && c->ntype[n] == TY_NIL) { tc->nils++; return 1; } if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { /* a super into a native class's binding that keeps the String it is handed answers that String's handle (#9258) */ if (!R->fresh && repr_super_native_keeps(c, n)) { if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); tc->reads++; return 1; } /* Exception's text publishes the message's handle (the super emitter's exception arm) */ int tos[16]; if (!R->fresh && super_reaches_exc_text(c, n, 1) && (!is_message_name(c->scopes[tc->mi].name) || exc_message_super_to_s(c, n, tos, 16) >= 0)) { if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); tc->reads++; return 1; } const CallPlan *p = cplan_user_fresh(c, n); int mi = p->mi; /* Kernel#inspect / #to_s through super: a new String (an_super_object_text) */ if (R->fresh && (p->dispatch != CP_DIRECT || mi <= 0) && an_super_object_text(c, n, tc->mi)) { if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_CLEAR); tc->fresh++; return 1; } if (p->dispatch != CP_DIRECT || mi <= 0 || !an_ret_handle(c, R, mi)) return 0; if (g_repr_check && !R->fresh) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); tc->reads++; return !R->fresh || c->scopes[mi].ret_fresh; } if (k != NK_CallNode || (!R->fresh && c->ntype[n] != TY_STRING)) return 0; if (R->fresh && cplan_user_fresh(c, n)->via == UC_POLY && !share_builtin_fresh(c, n)) return 0; int mis[CPT_MAX]; int cnt = cplan_targets(c, n, mis, CPT_MAX); /* past the plan's list: every method of the name (#9258) */ if (R->fresh && cnt == CPT_UNKNOWN) return an_tail_name_fresh(c, R, n, tc); if (!R->fresh && cnt == CPT_UNKNOWN) return an_tail_name_handle(c, R, n, tc); /* A raising arm contributes no String. A user override still returns. */ if (R->fresh && cnt == 0 && isa_node_diverges(c, n) && !bare_call_class_owned(c, n)) return 1; if (cnt <= 0) return 0; /* (the handle walk: a target answering only new Strings, as the fresh walk settled, beside targets that publish a handle, clears the return channel at its tails, ret_pub_fresh, so the pickup wraps its String) */ int fresh_t[CPT_MAX], nfresh = 0, npub = 0; for (int i = 0; i < cnt; i++) { /* (a target that cannot bind the arguments only raises) */ if (R->fresh && !cplan_target_binds(c, n, mis[i])) continue; /* (a class method no box can reach, or a method of a class the receiver's class fact rules out: an inspect of a box that holds its own object or a String reaches no other class's inspect, #9258) */ if (R->fresh && (!share_target_reachable(c, n, mis[i]) || !share_target_armed(c, n, mis[i]))) continue; Scope *m = &c->scopes[mis[i]]; if (!an_ret_handle(c, R, mis[i])) { if (R->fresh || !m->ret_fresh || m->ret != TY_STRING) return 0; fresh_t[nfresh++] = mis[i]; continue; } npub++; if (R->fresh && !m->ret_fresh) return 0; } if (nfresh > 0) { if (npub == 0) return 0; for (int i = 0; i < nfresh; i++) c->scopes[fresh_t[i]].ret_pub_fresh = 1; tc->fresh++; } tc->reads++; if (g_repr_check && !R->fresh) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); return 1; } /* Is body tail n an append chain over a shared handle's read (`buf << a << b`)? The tail appends in place and answers the base's read, which publishes it (emit_stmt_tail_inner's `<<` arm). */ static int an_tail_append_chain(Compiler *c, int n) { int chain[64], base = -1; return n >= 0 && nt_kind(c->nt, n) == NK_CallNode && str_append_chain(c, n, chain, &base) > 0 && an_arg_is_shared_handle(c, base); } /* Is `return` node ret inside a begin with an ensure (a def's or a block's own `ensure` is one)? Its value is held in a result slot until the ensure has run, which does not publish a box's handle. */ static int an_return_in_ensure(Compiler *c, RetHandles *R, int ret) { const NodeTable *nt = c->nt; if (!R->par) R->par = an_parent_map(nt); if (!R->par) return 1; for (int p = R->par[ret]; p >= 0; p = R->par[p]) { NodeKind k = nt_kind(nt, p); if (k == NK_DefNode) return 0; if (k == NK_BeginNode && nt_ref(nt, p, "ensure_clause") >= 0) return 1; } return 0; } static int an_ret_handle_body(Compiler *c, RetHandles *R, int mi) { Scope *m = &c->scopes[mi]; /* A String reopening's proc form reads the same incoming handle. */ if (!R->fresh && m->def_node >= 0 && m->is_proc_form && m->ret == TY_STRING && repr_self_handle(c, mi)) return 1; /* A proc form is emitted as a function and publishes its tails just as a non-yielding method does; only the original inline body has no call. */ return !(m->def_node < 0 || (!R->fresh && !m->is_proc_form && (m->yields || m->is_lowered_yield)) || (m->ret != TY_STRING && (!R->fresh || m->ret != TY_POLY))); } /* Is n, the last statement of a method no signature types (not an arm of it, and no `return`, whose value may wait in an ensure's result slot, which publishes nothing), an iterator over a box answering its receiver (`s.scan(re) { }`, strbuf_iter_box_tail)? It answers the box's String, so it publishes the box's handle, as a seeded method's tail does (an_tail_boxed_string); a box with none (a new String, nil) clears the side channel, and it may answer nil. */ static int an_ret_box_tail(Compiler *c, RetHandles *R, int n, TailCount *tc) { if (R->fresh || c->scopes[tc->mi].ret_rbs_seeded || strbuf_iter_box_tail(c, n) < 0) return 0; if (g_repr_check) repr_channel_predict(c, n, tc->mi, RCH_PUBLISH); tc->reads++; tc->nils++; return 1; } /* The fresh walk's answer for method mi this round, taken as no more than the previous round's: only new Strings ranks above its parameter p, which ranks above no, and two different parameters meet at no. A method's answer can then only fall from round to round, so the rounds settle (a method answering its parameter once a cycle answers no, and no once it answers its parameter, would turn round forever), and where they settle every answer holds of the answers the round read. */ static void an_fresh_meet(Compiler *c, RetHandles *R, int mi) { Scope *m = &c->scopes[mi]; int p = R->prev[mi]; if (p == 1 || R->st[mi] != RH_YES) return; if (p == 0 || (!m->ret_fresh && m->ret_param != p - 2)) { R->st[mi] = RH_NO; m->ret_fresh = 0; m->ret_param = -1; return; } m->ret_fresh = 0; m->ret_param = p - 2; } static int an_ret_handle(Compiler *c, RetHandles *R, int mi) { if (R->st[mi] != RH_UNSEEN) return R->st[mi] == RH_YES || (R->st[mi] == RH_BUSY && R->prev && R->prev[mi]); Scope *m = &c->scopes[mi]; if (!R->fresh && m->yields && repr_self_handle(c, mi)) { int pf = scope_proc_form_of(c, mi); R->st[mi] = RH_BUSY; return an_ret_decided(c, R, mi, pf >= 0 && an_ret_handle(c, R, pf)); } if (!an_ret_handle_body(c, R, mi)) return an_ret_decided(c, R, mi, 0); R->st[mi] = RH_BUSY; const NodeTable *nt = c->nt; int ok = 1, saw = 0; int last = scope_body_last(c, mi); /* a body with its own rescue is a begin, whose arms answer */ if (last < 0 && m->body >= 0 && nt_kind(nt, m->body) == NK_BeginNode) last = m->body; /* The settled walk accepts fresh arms beside handle reads, as the earlier pickup does; their tails clear the return channel. */ TailCount tc = { .fresh_ok = 1, .mi = mi, .param = -1 }; if (last >= 0) { saw = 1; ok = an_ret_box_tail(c, R, last, &tc) || an_tail_handle(c, R, last, &tc); /* The implicit append tail publishes its base's handle read too. */ if (!ok && !R->fresh && an_tail_append_chain(c, last)) { tc.reads++; ok = 1; } } for (int r = R->ret_start[mi]; ok && r < R->ret_start[mi + 1]; r++) { int ra = nt_ref(nt, R->ret_list[r], "arguments"); int rn = 0; const int *rv = ra >= 0 ? nt_arr(nt, ra, "arguments", &rn) : NULL; saw = 1; if (rn == 0) tc.nils++; else { int ens = an_return_in_ensure(c, R, R->ret_list[r]); tc.in_begin += ens; ok = rn == 1 && an_tail_handle(c, R, rv[0], &tc); tc.in_begin -= ens; } } if (tc.nils && !R->fresh) m->ret_nil_pickup = 1; if (ok && tc.reads > 0 && tc.fresh) m->ret_pub_fresh = 1; if (ok && tc.reads > 0 && tc.bangs && !R->fresh) m->ret_pub_bang = 1; /* An all-raising body publishes no handle for a caller to pick up (a fresh String answered beside no handle read is the fresh pickup's own). */ an_ret_decided(c, R, mi, ok && saw && (R->fresh || tc.reads > 0)); if (R->fresh) { m->ret_fresh = R->st[mi] == RH_YES && tc.param < 0; m->ret_param = R->st[mi] == RH_YES ? tc.param : -1; if (R->meet) an_fresh_meet(c, R, mi); } return R->st[mi] == RH_YES; } /* The fresh walk (ret_fresh, ret_param) is a greatest fixpoint: a method reached again while it is being walked -- a cycle, such as an inspect that calls inspect on a box that may hold its own object -- answers what the previous round found for it, every method answering only new Strings at first. Rounds repeat until no method's answer changes; each only takes answers away (an_fresh_meet), so a cycle stays fresh only if every way out of it answers a new String. Each round starts from the marks the rounds do not own. (A walk that does not settle within its rounds falls back to taking a cycle as not fresh, from no method known fresh.) */ static void an_fresh_returns(Compiler *c, RetHandles *R) { int n = c->nscopes; unsigned char *pub = malloc((size_t)n + 1); R->prev = malloc(((size_t)n + 1) * sizeof(int)); if (!pub || !R->prev) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int mi = 0; mi <= n; mi++) R->prev[mi] = 1; R->fresh = 1; R->meet = 1; for (int mi = 1; mi < n; mi++) { pub[mi] = c->scopes[mi].ret_pub_fresh; /* (a method whose body the walk does not take answers no new String) */ c->scopes[mi].ret_fresh = (unsigned char)an_ret_handle_body(c, R, mi); c->scopes[mi].ret_param = -1; } for (int round = 0, changed = 1; changed; round++) { /* (the fallback round: a cycle is not fresh, as nothing it answers is known yet) */ if (round == 16) { R->meet = 0; for (int mi = 0; mi <= n; mi++) R->prev[mi] = 0; for (int mi = 1; mi < n; mi++) { c->scopes[mi].ret_fresh = 0; c->scopes[mi].ret_param = -1; } } memset(R->st, 0, (size_t)n + 1); for (int mi = 1; mi < n; mi++) c->scopes[mi].ret_pub_fresh = pub[mi]; for (int mi = 1; mi < n; mi++) an_ret_handle(c, R, mi); changed = 0; for (int mi = 1; mi < n; mi++) { /* (the whole answer: a method answering its parameter is not one answering a new String, to a cycle reading it) */ int y = R->st[mi] != RH_YES ? 0 : c->scopes[mi].ret_fresh ? 1 : 2 + c->scopes[mi].ret_param; if (round < 16 && y != R->prev[mi]) { changed = 1; R->prev[mi] = y; } } } free(pub); free(R->prev); R->prev = NULL; R->meet = 0; } static void an_mark_handle_returns(Compiler *c) { RetHandles R = { 0 }; R.st = calloc((size_t)c->nscopes + 1, 1); if (!R.st) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } an_returns_by_scope(c, &R.ret_start, &R.ret_list); c->ret_walk = &R; /* The sibling fact, a new String or a parameter on every path (ret_fresh, ret_param), uses the same tails, return index and active-visit guard; only its leaf question differs, and it reads no handle fact. It is decided first: the handle walk's own tests read it (a boxed to_s whose every user method answers a handle or a new String, repr_boxed_to_s_operand; a call answering a new String beside handle reads, share_call_fresh), and read before it they saw no method fresh. Each method is asked once per round; the fresh fact takes a cycle as its rounds settle (an_fresh_returns), the handle fact as not publishing. */ an_fresh_returns(c, &R); memset(R.st, 0, (size_t)c->nscopes + 1); R.fresh = 0; for (int mi = 1; mi < c->nscopes; mi++) an_ret_handle(c, &R, mi); c->ret_walk = NULL; free(R.st); free(R.ret_start); free(R.ret_list); free(R.par); /* A deep-return pickup marked while the types settled, on a call whose method answers a box after all (a parameter that settled poly): the box holds the String itself, and the pickup's String read of it would not build. Unmarked, the call hands its box on. */ const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, n) { if (!c->strbuf_box[n] || c->ntype[n] != TY_POLY) continue; int mis[CPT_MAX]; int cnt = cplan_targets(c, n, mis, CPT_MAX), poly = cnt > 0; for (int i = 0; i < cnt && poly; i++) poly = c->scopes[mis[i]].ret == TY_POLY; if (poly) comp_set_strbuf_box(c, n, 0); } } /* The accessors of analyze.h. A fact is decided here only while its own walk runs: the fresh walk runs first and reads no handle fact, and the handle walk reads the fresh facts it settled. */ static void an_ret_settle(Compiler *c, int mi, int fresh) { RetHandles *R = c->ret_walk; if (!R || R->fresh != fresh || mi <= 0 || mi >= c->nscopes || R->settling >= RH_SETTLE_MAX) return; R->settling++; an_ret_handle(c, R, mi); R->settling--; } int scope_ret_handle(Compiler *c, int mi) { an_ret_settle(c, mi, 0); return mi > 0 && mi < c->nscopes && c->scopes[mi].ret_handle; } /* Is every value node v can answer a String literal: v itself, the last statement of a list, the arms of an `if`, `unless` or `case`? A `return` anywhere in the method is not followed, so it answers 0. */ static int an_tail_not_str_literal(const VpLeaf *leaf, void *ud) { return leaf->kind != VPK_EXPR || nt_kind(ud, leaf->node) != NK_StringNode; } static int an_tail_str_literals(const NodeTable *nt, int v) { return v >= 0 && !for_each_value_position(nt, v, VPS_IF | VPS_UNLESS | VPS_CASE, an_tail_not_str_literal, (void *)nt); } /* (a subtree too deep to walk counts as one with a return) */ static int an_subtree_has_return(const NodeTable *nt, int n, int depth) { if (n < 0) return 0; if (depth > 200) return 1; NodeKind k = nt_kind(nt, n); if (k == NK_ReturnNode) return 1; if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return 0; const SpNode *nd = &nt->nodes[n]; for (int i = 0; i < nd->nr; i++) if (an_subtree_has_return(nt, nd->r[i].ref, depth + 1)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (an_subtree_has_return(nt, nd->a[i].ids[j], depth + 1)) return 1; return 0; } /* ---- what a method's value can be, for its String literals (#9258) ---- AL_LIT a String literal, AL_NIL nil (a missing arm, a bare `return`), AL_OTHER anything else; none where the value only raises. A call of the program's methods (its targets listed, on self) is what they answer, a method asked again while it is being asked (a cycle) answering nothing of its own; a `return` is followed to its value. */ enum { AL_LIT = 1, AL_NIL = 2, AL_OTHER = 4 }; static int al_method(Compiler *c, int mi, unsigned char *busy, int depth); /* The parameter method mi hands on as it is: a body that is a read of one (`def zork(src) = src`), never written, with no return; -1 for any other method. */ static int al_param_passthrough(Compiler *c, int mi) { const NodeTable *nt = c->nt; if (mi <= 0 || mi >= c->nscopes) return -1; Scope *m = &c->scopes[mi]; int b = m->body, t = b; if (b < 0 || an_subtree_has_return(nt, b, 0)) return -1; if (nt_kind(nt, b) == NK_StatementsNode) { int bn = 0; const int *bv = nt_arr(nt, b, "body", &bn); if (bn != 1) return -1; t = bv[0]; } if (nt_kind(nt, t) != NK_LocalVariableReadNode || !nt_str(nt, t, "name")) return -1; int p = an_param_idx(m, nt_str(nt, t, "name")); if (p < 0 || p == m->rest_idx || p == m->kwrest_idx) return -1; return p; } static int al_value(Compiler *c, int v, unsigned char *busy, int depth) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0) return AL_NIL; if (depth > 64) return AL_OTHER; switch (nt_kind(nt, v)) { case NK_StringNode: return AL_LIT; case NK_NilNode: return AL_NIL; case NK_StatementsNode: { int bn = 0; const int *b = nt_arr(nt, v, "body", &bn); for (int i = 0; i + 1 < bn; i++) if (isa_node_diverges(c, b[i])) return 0; return bn > 0 ? al_value(c, b[bn - 1], busy, depth + 1) : AL_NIL; } case NK_ElseNode: return al_value(c, nt_ref(nt, v, "statements"), busy, depth + 1); case NK_IfNode: case NK_UnlessNode: { int sub = nt_ref(nt, v, nt_kind(nt, v) == NK_IfNode ? "subsequent" : "else_clause"); return al_value(c, nt_ref(nt, v, "statements"), busy, depth + 1) | (sub >= 0 ? al_value(c, sub, busy, depth + 1) : AL_NIL); } case NK_CaseNode: { int wn = 0, m = 0; const int *w = nt_arr(nt, v, "conditions", &wn); for (int k = 0; k < wn; k++) m |= al_value(c, nt_ref(nt, w[k], "statements"), busy, depth + 1); int e = nt_ref(nt, v, "else_clause"); return m | (e >= 0 ? al_value(c, e, busy, depth + 1) : AL_NIL); } case NK_OrNode: case NK_AndNode: { int l = al_value(c, nt_ref(nt, v, "left"), busy, depth + 1); if (l & AL_OTHER) return AL_OTHER; int r = al_value(c, nt_ref(nt, v, "right"), busy, depth + 1); /* `a || b` is a where a is a literal, b where it is nil; `a && b` the reverse */ return nt_kind(nt, v) == NK_OrNode ? (l & AL_LIT) | ((l & AL_NIL) ? r : 0) : (l & AL_NIL) | ((l & AL_LIT) ? r : 0); } case NK_CallNode: { if (isa_node_diverges(c, v)) return 0; int recv = nt_ref(nt, v, "receiver"); if (nt_ref(nt, v, "block") >= 0) return AL_OTHER; int tg[CPT_MAX], n = cplan_targets(c, v, tg, CPT_MAX), m = 0; if (n <= 0) return AL_OTHER; if (!share_plan_targets_cover(c, v)) return AL_OTHER; /* (a receiver other than self only where every target hands on the argument it binds, whose value is asked: `I.new.zork("lit")` with `def zork(src) = src`) */ int other_recv = recv >= 0 && nt_kind(nt, recv) != NK_SelfNode; for (int i = 0; i < n; i++) { int p = al_param_passthrough(c, tg[i]); if (p >= 0) { int a = arg_layout_param_source(c, &c->scopes[tg[i]], v, p, NULL); m |= a >= 0 ? al_value(c, a, busy, depth + 1) : AL_OTHER; } else if (other_recv) return AL_OTHER; else m |= al_method(c, tg[i], busy, depth + 1); } return m; } default: return AL_OTHER; } } /* the returns of the subtree at n (not a nested method's or lambda's); vdepth is the value walk's depth, which a return's value continues */ static int al_returns(Compiler *c, int n, unsigned char *busy, int depth, int vdepth) { const NodeTable *nt = c->nt; if (n < 0) return 0; if (depth > 200) return AL_OTHER; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode || k == NK_LambdaNode || k == NK_ClassNode || k == NK_ModuleNode) return 0; if (k == NK_ReturnNode) { int a = nt_ref(nt, n, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an == 0) return AL_NIL; return an == 1 ? al_value(c, av[0], busy, vdepth + 1) : AL_OTHER; } int m = 0; const SpNode *nd = &nt->nodes[n]; for (int i = 0; i < nd->nr; i++) m |= al_returns(c, nd->r[i].ref, busy, depth + 1, vdepth); for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) m |= al_returns(c, nd->a[i].ids[j], busy, depth + 1, vdepth); return m; } /* the methods one question may still ask (a call graph that fans out asks the same method again on each path; past the budget it is AL_OTHER) */ static int g_al_budget; static int al_method(Compiler *c, int mi, unsigned char *busy, int depth) { if (mi <= 0 || mi >= c->nscopes || c->scopes[mi].body < 0 || depth > 16 || --g_al_budget < 0) return AL_OTHER; if (busy[mi]) return 0; busy[mi] = 1; int body = c->scopes[mi].body; int m = al_value(c, body, busy, depth) | al_returns(c, body, busy, 0, depth); busy[mi] = 0; return m; } /* See analyze.h. */ int scope_ret_str_literal(Compiler *c, int mi) { if (mi <= 0 || mi >= c->nscopes || c->scopes[mi].body < 0) return 0; const NodeTable *nt = c->nt; int body = c->scopes[mi].body; if (an_tail_str_literals(nt, body) && !an_subtree_has_return(nt, body, 0)) return 1; unsigned char *busy = calloc((size_t)c->nscopes + 1, 1); if (!busy) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_al_budget = 4096; int m = al_method(c, mi, busy, 0); free(busy); return m == AL_LIT; } int scope_ret_fresh(Compiler *c, int mi) { an_ret_settle(c, mi, 1); return mi > 0 && mi < c->nscopes && c->scopes[mi].ret_fresh; } int scope_ret_param(Compiler *c, int mi) { an_ret_settle(c, mi, 1); return mi > 0 && mi < c->nscopes ? c->scopes[mi].ret_param : -1; } /* A String builtin answering a new Array of new Strings nothing else holds (`s.split(",")`, `s.scan(re)`, `s.lines`, `s.chars`), or Dir's listing of a directory (`Dir.entries(d)`, `Dir.children(d)`, `Dir.glob(p)`), or such an Array sorted (`Dir.children(d).sort`): a new Array of the same Strings, whose first Array nothing reads again. */ static int sb_fresh_string_array_call(Compiler *c, int n) { const NodeTable *nt = c->nt; n = nt_paren_single(nt, n); if (n < 0 || nt_kind(nt, n) != NK_CallNode || nt_ref(nt, n, "block") >= 0) return 0; const char *mn = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"); if (!mn || r < 0) return 0; if (sp_streq(mn, "sort") && nt_ref(nt, n, "arguments") < 0 && !an_user_defines_method(c, mn)) return infer_type(c, n) == TY_STR_ARRAY && sb_fresh_string_array_call(c, r); if (nt_kind(nt, r) == NK_ConstantReadNode && sp_streq(nt_str(nt, r, "name"), "Dir") && comp_core_class(c, CORE_DIR) < 0 && (sp_streq(mn, "entries") || sp_streq(mn, "children") || sp_streq(mn, "glob"))) return infer_type(c, n) == TY_STR_ARRAY; if (!sp_streq(mn, "split") && !sp_streq(mn, "scan") && !sp_streq(mn, "lines") && !sp_streq(mn, "chars")) return 0; TyKind rt = infer_type(c, r); return (rt == TY_STRING || rt == TY_STRBUF) && infer_type(c, n) == TY_STR_ARRAY; } /* Does the subtree at n hold a node that leaves a block iteration early (next, redo, retry) or writes local `pn`? */ static int sb_block_body_blocks_rewrite(const NodeTable *nt, int n, const char *pn) { if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_NextNode || k == NK_RedoNode || k == NK_RetryNode) return 1; if ((k == NK_LocalVariableWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableTargetNode) && sp_streq(nt_str(nt, n, "name"), pn)) return 1; const SpNode *nd = &nt->nodes[n]; for (int i = 0; i < nd->nr; i++) if (sb_block_body_blocks_rewrite(nt, nd->r[i].ref, pn)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (sb_block_body_blocks_rewrite(nt, nd->a[i].ids[j], pn)) return 1; return 0; } /* `s.scan(re).each { |x| x.gsub!(...) }.join`: each answers the fresh Array it walked, and its answer is read, so the in-place mutation of each element must be seen there -- but a String Array's element binds the parameter as a copy. Nothing else holds that Array or its Strings, so storing the parameter's final value back over its element is the same program: the block becomes `{ |x| ...; x }` under map!, which answers the Array too. Only a block that runs to its end every time (no next, redo or retry) and never rebinds the parameter; a break answers the break value either way and leaves the Array unread. */ static int desugar_fresh_array_each_writeback(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int w = comp_kind_first(c, NK_CallNode); w >= 0 && w < n0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_CallNode || !sp_streq(nt_str(nt, w, "name"), "each")) continue; int blk = nt_ref(nt, w, "block"), recv = nt_ref(nt, w, "receiver"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || nt_ref(nt, w, "arguments") >= 0) continue; if (comp_value_dropped(c, w) || !sb_fresh_string_array_call(c, recv)) continue; int bp = nt_ref(nt, blk, "parameters"); if (bp < 0 || nt_kind(nt, bp) != NK_BlockParametersNode) continue; int pn = nt_ref(nt, bp, "parameters"); if (pn < 0) continue; int nreq = 0, nopt = 0, npost = 0, nkw = 0; const int *req = nt_arr(nt, pn, "requireds", &nreq); nt_arr(nt, pn, "optionals", &nopt); nt_arr(nt, pn, "posts", &npost); nt_arr(nt, pn, "keywords", &nkw); if (nreq != 1 || nopt || npost || nkw || nt_ref(nt, pn, "rest") >= 0 || nt_ref(nt, pn, "keyword_rest") >= 0 || nt_ref(nt, pn, "block") >= 0 || nt_kind(nt, req[0]) != NK_RequiredParameterNode) continue; const char *pname = nt_str(nt, req[0], "name"); Scope *bs = comp_scope_of(c, blk); int body = nt_ref(nt, blk, "body"); if (!pname || !bs || body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; if (strbuf_mut_kind(c, pname, bs) != 1 || sb_block_body_blocks_rewrite(nt, body, pname)) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); int base = nt->count; int rd = nt_new_node(nt, "LocalVariableReadNode"); if (rd < 0) continue; nt_node_set_str(nt, rd, "name", pname); nt_node_set_int(nt, rd, "depth", 0); int *nb = malloc(sizeof(int) * (size_t)(bn + 1)); if (!nb) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(nb, bb, sizeof(int) * (size_t)bn); nb[bn] = rd; int st = nt_new_node(nt, "StatementsNode"); if (st < 0) { free(nb); continue; } nt_node_set_arr(nt, st, "body", nb, bn + 1); free(nb); nt_node_set_ref(nt, blk, "body", st); nt_set_str(nt, w, "name", "map!"); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = c->nscope[blk]; changed = 1; } return changed; } /* Defined in analyze_pass.c. */ int is_fresh_array(Compiler *c, int v); /* map!/collect! over an Array nothing else holds (a literal, or the fresh result of split, scan, map, ...) whose block answers another kind than the Array's elements: the loop stored each value back into the typed receiver (`sp_StrArray_set(a, i, )`) and the C did not build. No one can see the receiver change, so map, which builds an Array of the block's kind, is the same program. A local receiver widens instead (widen_arrays_from_map_bang). */ static int desugar_fresh_array_map_bang(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; for (int w = comp_kind_first(c, NK_CallNode); w >= 0; w = comp_kind_next(c, w)) { if (nt_kind(nt, w) != NK_CallNode) continue; const char *nm = nt_str(nt, w, "name"); if (!nm || (!sp_streq(nm, "map!") && !sp_streq(nm, "collect!"))) continue; int blk = nt_ref(nt, w, "block"), recv = nt_ref(nt, w, "receiver"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || nt_ref(nt, w, "arguments") >= 0) continue; int body = nt_ref(nt, blk, "body"); int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; if (bn <= 0 || !is_fresh_array(c, recv)) continue; TyKind rt = infer_type(c, recv), tt = infer_type(c, bb[bn - 1]); if (tt == TY_UNKNOWN || !ty_is_array(rt) || rt == TY_POLY_ARRAY || tt == ty_array_elem(rt)) continue; nt_set_str(nt, w, "name", sp_streq(nm, "map!") ? "map" : "collect"); changed = 1; } return changed; } /* A parameter RETAINED in a shared-handle ivar (`def initialize(b) @bt = b end`, then `@bt << "x"` somewhere) has to arrive as the handle. The byref machinery above only covers a callee that mutates the parameter ITSELF; a callee that merely stores it and mutates it later, through the ivar, kept the caller's string as a value and the two holders drifted apart -- which docs/limitations.md promises they do not (#4363). Marking the parameter is all this rule does; convert_byref_handle_params then pulls every call site's argument into the shared set, exactly as it does for a byref parameter that has just become a handle. */ /* Is this argument already a shared handle? Either by name -- an_arg_is_shared_handle reads the slot -- or through a reader over a shared slot (`K.new(obj.reader)`), where the handle reaches the argument by a call rather than a name and the slot-reading form cannot see it. */ static int an_arg_hands_handle(Compiler *c, int node) { if (node < 0) return 0; if (an_arg_is_shared_handle(c, node)) return 1; { char ivb[300]; int defc = -1; const char *ivn = an_reader_ivar_of(c, node, &defc, ivb, sizeof ivb); if (ivn && defc >= 0) { int ivx = comp_ivar_index(&c->classes[defc], ivn); if (ivx >= 0 && c->classes[defc].ivar_str_shared[ivx]) return 1; } } return 0; } /* The method scope after `i` that has the same name, or -1: a name group walked without a scan of every scope per call site. The table is built once per scope-index epoch, as an_any_scope_by_name's is; before the index is frozen, names can still change, so the answer is a scan. */ static int an_same_name_next(Compiler *c, int i) { const char *nm = c->scopes[i].name; if (!nm) return -1; if (comp_scope_index_is_frozen()) { static int *next = NULL, stamp_n = -1; static unsigned stamp_gen; if (stamp_n != c->nscopes || stamp_gen != comp_scope_index_gen()) { free(next); next = (int *)malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); int *last = (int *)malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); ANameHash names; memset(&names, 0, sizeof names); /* -1 is the group's end: a missing table would cut every group short */ if (!next || !last) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int k = c->nscopes - 1; k >= 1; k--) { const char *sn = c->scopes[k].name; next[k] = -1; if (!sn) continue; int h = anh_find(&names, sn); if (h >= 0) { next[k] = last[h]; last[h] = k; } else { anh_add(&names, sn); last[names.n - 1] = k; } } anh_free(&names); free(last); stamp_n = c->nscopes; stamp_gen = comp_scope_index_gen(); } return next[i]; } for (int k = i + 1; k < c->nscopes; k++) if (c->scopes[k].name && sp_streq(c->scopes[k].name, nm)) return k; return -1; } /* The call targets of one CallNode, as a list that grows. A call on a name more than one class defines can reach several scopes, and a receiver the analysis cannot pin can reach every one of them. With `want` set the list is not built: the walk only answers whether that one scope is a target (`hit`), which is what an_call_targets_scope asks for every scope of the name, and listing a wide group's overrides for each was quadratic. */ typedef struct { int *v; int n, cap; int want, hit; } ACallTargets; /* scope -> the list act_add last put it in, so a group of a thousand candidates is deduplicated without comparing each against the rest */ static unsigned *act_mark, act_stamp; static int act_nmark; static void act_reset(Compiler *c, ACallTargets *t) { t->n = 0; t->want = -1; t->hit = 0; if (act_nmark < c->nscopes) { unsigned *nm = (unsigned *)realloc(act_mark, sizeof(unsigned) * (size_t)c->nscopes); if (nm) { memset(nm + act_nmark, 0, sizeof(unsigned) * (size_t)(c->nscopes - act_nmark)); act_mark = nm; act_nmark = c->nscopes; } } if (++act_stamp == 0) { memset(act_mark, 0, sizeof(unsigned) * (size_t)act_nmark); act_stamp = 1; } } static void act_add(ACallTargets *t, int mi) { if (mi < 0) return; if (t->want >= 0) { if (mi == t->want) t->hit = 1; return; } if (mi < act_nmark) { if (act_mark[mi] == act_stamp) return; act_mark[mi] = act_stamp; } else for (int k = 0; k < t->n; k++) if (t->v[k] == mi) return; if (t->n == t->cap) { int ncap = t->cap ? t->cap * 2 : 8; int *nv = (int *)realloc(t->v, sizeof(int) * (size_t)ncap); /* a dropped target is a caller left out of the handle's pull */ if (!nv) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } t->v = nv; t->cap = ncap; } t->v[t->n++] = mi; } /* Every override of `un` below class `cid` (class methods when `cm`): a dispatch on a receiver of that class can land on any of them, the way the binders bind a bare call's arguments to each descendant override. */ static void act_add_overrides(Compiler *c, ACallTargets *t, int cid, const char *un, int cm) { if (cid < 0) return; if (t->want >= 0) { Scope *w = &c->scopes[t->want]; if (w->class_id >= 0 && w->class_id != cid && !w->is_cmethod == !cm && cr_class_is_ancestor(c, cid, w->class_id) && (cm ? comp_cmethod_in_class(c, w->class_id, un) : comp_method_in_class(c, w->class_id, un)) == t->want) t->hit = 1; return; } int nd = 0; const int *ds = comp_descendants(c, cid, &nd); for (int d = 0; d < nd && ds; d++) act_add(t, cm ? comp_cmethod_in_class(c, ds[d], un) : comp_method_in_class(c, ds[d], un)); } /* Does a class below `cid` define `un` (a class method when `cm`)? */ static int act_has_override(Compiler *c, int cid, const char *un, int cm) { int nd = 0; const int *ds = comp_descendants(c, cid, &nd); for (int d = 0; d < nd && ds; d++) if ((cm ? comp_cmethod_in_class(c, ds[d], un) : comp_method_in_class(c, ds[d], un)) >= 0) return 1; return 0; } /* The classes that alias a method under `nm`, as a list in class order: the first entry, or -1, then al_next[e]; al_cls[e] is the class. Hashed once per alias table generation, since the question is asked for every call site, and a scan of every class's aliases per site was most of a large program's late passes. */ static int *al_cls, *al_next; static int an_alias_first(Compiler *c, const char *nm) { static ANameHash names; static int *first, *last, stamp_nc = -1; static unsigned stamp_gen; if (stamp_nc != c->nclasses || stamp_gen != comp_table_gen) { anh_free(&names); memset(&names, 0, sizeof names); free(first); free(last); free(al_cls); free(al_next); first = last = al_cls = al_next = NULL; int ne = 0, cap = 0; for (int k = 0; k < c->nclasses; k++) for (int a = 0; a < c->classes[k].naliases; a++) { const char *an = c->classes[k].alias_new[a]; int h = anh_find(&names, an); if (h >= 0 && al_cls[last[h]] == k) continue; /* this class has it */ if (ne == cap) { cap = cap ? cap * 2 : 64; first = realloc(first, sizeof(int) * (size_t)cap); last = realloc(last, sizeof(int) * (size_t)cap); al_cls = realloc(al_cls, sizeof(int) * (size_t)cap); al_next = realloc(al_next, sizeof(int) * (size_t)cap); } al_cls[ne] = k; al_next[ne] = -1; if (h >= 0) { al_next[last[h]] = ne; last[h] = ne; } else { h = names.n; anh_add(&names, an); first[h] = last[h] = ne; } ne++; } stamp_nc = c->nclasses; stamp_gen = comp_table_gen; } int h = nm && names.n > 0 ? anh_find(&names, nm) : -1; return h >= 0 ? first[h] : -1; } /* Is `nm` a name some class aliases a method under? */ static int an_alias_name(Compiler *c, const char *nm) { return an_alias_first(c, nm) >= 0; } /* The aliases of a method named `nm` (their old name), in class order: the first entry, or -1, then ao_next[e]; ao_new[e] is the entry's new name. The by-old-name twin of the list above, with its stamps, for the walk that follows a method's name to the names that call it. */ static int an_alias_by_old_first(Compiler *c, const char *nm) { static ANameHash names; static int *first, *last, stamp_nc = -1; static unsigned stamp_gen; if (stamp_nc != c->nclasses || stamp_gen != comp_table_gen) { anh_free(&names); memset(&names, 0, sizeof names); free(first); free(last); free(ao_next); free(ao_new); first = last = ao_next = NULL; ao_new = NULL; int ne = 0, cap = 0; for (int k = 0; k < c->nclasses; k++) for (int a = 0; a < c->classes[k].naliases; a++) { const char *an = c->classes[k].alias_new[a], *ao = c->classes[k].alias_old[a]; if (!an || !ao) continue; int h = anh_find(&names, ao); if (ne == cap) { cap = cap ? cap * 2 : 64; first = realloc(first, sizeof(int) * (size_t)cap); last = realloc(last, sizeof(int) * (size_t)cap); ao_next = realloc(ao_next, sizeof(int) * (size_t)cap); ao_new = realloc(ao_new, sizeof *ao_new * (size_t)cap); if (!first || !last || !ao_next || !ao_new) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } ao_next[ne] = -1; ao_new[ne] = an; if (h >= 0) { ao_next[last[h]] = ne; last[h] = ne; } else { h = names.n; anh_add(&names, ao); first[h] = last[h] = ne; } ne++; } stamp_nc = c->nclasses; stamp_gen = comp_table_gen; } int h = nm && names.n > 0 ? anh_find(&names, nm) : -1; return h >= 0 ? first[h] : -1; } /* The scopes a call on name `un`, defined by more than one method or reached under an alias, can reach. Resolved the way the binders resolve it: a class method on a constant receiver, a bare call or a call on `self` against the enclosing definition's chain, an instance call against its receiver's class, with the overrides below that class. A receiver the analysis cannot pin (a poly, a class value) can reach any method of the name, so it answers all of them: the caller then hands every candidate what the one that wants the most wants. */ /* Does call u's receiver hold a builtin value that reaches no user method of the name: a value of a builtin type (an Array constant, a String) whose builtin class, where the program reopens it, and Object do not define it? Such a call is no edge to a same-named user method: `LAST[0] = s` on an Array constant counted as a call of every `[]=`, and a Widget's String ivar was promoted to a shared handle (#8564). */ static int an_recv_builtin_without(Compiler *c, int u, const char *un) { const NodeTable *nt = c->nt; int rc = nt_ref(nt, u, "receiver"); if (rc < 0) return 0; NodeKind rk = nt_kind(nt, rc); if (rk == NK_SelfNode) return 0; if ((rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) && const_node_class_index(c, rc) >= 0) return 0; TyKind rt = infer_type(c, rc); if (rt == TY_POLY || rt == TY_UNKNOWN || rt == TY_CLASS || rt == TY_VOID || rt == TY_NIL || ty_is_object(rt)) return 0; int bc = core_of_type(rt); int bci = bc >= 0 && (bc == CORE_STRING || builtin_class_of_type(rt)) ? comp_core_class(c, bc) : -1; if (bci >= 0 && call_target(c, u, bci, un, 0, NULL) >= 0) return 0; int oci = comp_core_class(c, CORE_OBJECT); if (oci >= 0 && call_target(c, u, oci, un, 0, NULL) >= 0) return 0; return 1; } static void an_call_targets_nonunique(Compiler *c, int u, const char *un, ACallTargets *t, int ae) { const NodeTable *nt = c->nt; int first = an_any_scope_by_name(c, un); /* `ae`: the first class aliasing a method under the name (an_alias_first), resolved by the caller, which has asked it already: a hash of the name each, and a candidate walk asks this for every call of a name */ int aliased = ae >= 0; if (first < 0 && !aliased) return; int rc = nt_ref(nt, u, "receiver"); NodeKind rk = rc >= 0 ? nt_kind(nt, rc) : NK_SelfNode; int n0 = t->n; int cci = -1; if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { const char *cn = nt_str(nt, rc, "name"); cci = cn ? comp_class_index(c, cn) : -1; } if (cci >= 0) { act_add(t, call_target(c, u, cci, un, CT_CMETHOD, NULL)); return; } /* (a constant that is not a class holds a value: typed below like any receiver, so `LAST[0] = s` on an Array constant reaches no user `[]=` and promotes none of its String ivars, #8564) */ if (rk == NK_SelfNode) { int mi = comp_self_call_mi(c, u, un); if (mi < 0 && rc < 0) mi = comp_cbody_call_mi(c, u, un); if (mi < 0) mi = comp_included_method_index(c, un, u); act_add(t, mi); Scope *self = comp_scope_of(c, u); int any = mi >= 0; if (self && self->class_id >= 0) { if (self->is_cmethod) act_add_overrides(c, t, self->class_id, un, 1); act_add_overrides(c, t, self->class_id, un, 0); /* only the overrides define it: resolved all the same */ if (!any && t->want >= 0) any = act_has_override(c, self->class_id, un, self->is_cmethod) || act_has_override(c, self->class_id, un, 0); } if (any || t->n > n0) return; } else { TyKind rt = infer_type(c, rc); if (ty_is_object(rt)) { int cid = ty_object_class(rt); act_add(t, call_target(c, u, cid, un, 0, NULL)); act_add_overrides(c, t, cid, un, 0); return; } if (rt != TY_POLY && rt != TY_UNKNOWN && rt != TY_CLASS) return; } if (t->want >= 0 && c->scopes[t->want].name && sp_streq(c->scopes[t->want].name, un)) t->hit = 1; else for (int k = first; k >= 0 && t->want < 0; k = an_same_name_next(c, k)) act_add(t, k); /* and every method a class aliases under the name */ for (int e = ae; e >= 0; e = al_next[e]) { act_add(t, call_target(c, u, al_cls[e], un, 0, NULL)); act_add(t, call_target(c, u, al_cls[e], un, CT_CMETHOD, NULL)); } } /* Which scopes does CallNode `u` statically call? The forward form of an_call_targets_scope, so one walk can fill a table for every scope at once instead of re-deciding per candidate. The two must agree; the order of the arms is the same (a unique user method named `new` wins over the constructor reading). */ static void an_call_targets_of(Compiler *c, int u, ACallTargets *t) { const NodeTable *nt = c->nt; act_reset(c, t); const char *un = nt_str(nt, u, "name"); if (!un) return; /* BOTH arms, because an_call_targets_scope answers for both and a call can satisfy each against a different scope: with a method named `new` in the program, `K.new(s)` matches that scope by name AND resolves to K#initialize as a constructor. Resolving to a single scope dropped one of them -- whichever arm ran second -- and the call site vanished from that scope's chain, which is how `Holder.new(s)` stopped being evidence and the copy came back. Under --share-strings a unique name still has to be this call's own target: StringIO.open must not pull File.open's path into its shared init. */ int byname = an_unique_scope_by_name(c, un); if (byname >= 0 && c->scopes[byname].name && sp_streq(c->scopes[byname].name, un) && cplan_user_fresh(c, u)->mi == byname && !an_recv_builtin_without(c, u, un)) act_add(t, byname); /* A name more than one method defines resolves by the call itself, and so does an alias, which names no scope of its own. By unique name alone such a call had no target, so its site was missing from every chain: a callee whose parameter became a shared handle -- a POLY parameter's pull (#5957) reaching `Rooms.show_into` beside a `Users.show_into` -- never pulled the caller's local into the handle, and the call handed it a fresh copy that took every append (#6065). */ if (!sp_streq(un, "new")) { int ae = an_alias_first(c, un); if (byname < 0 || ae >= 0) an_call_targets_nonunique(c, u, un, t, ae); return; } int rc = nt_ref(nt, u, "receiver"); if (rc < 0 || nt_kind(nt, rc) != NK_ConstantReadNode) return; const char *cn = nt_str(nt, rc, "name"); int cid = cn ? comp_class_index(c, cn) : -1; if (cid < 0) return; /* asked of THIS class, not the whole program: a dynamic `new` elsewhere only matters where its shape could construct this one */ if (an_class_dynamic_new_risk(c, cid)) return; int mi = comp_method_in_chain(c, cid, "initialize", NULL); if (mi < 0 || mi >= c->nscopes) return; Scope *m2 = &c->scopes[mi]; if (!m2->name || m2->class_id < 0 || m2->is_cmethod) return; act_add(t, mi); } /* The targets of each candidate call, kept for one promote_shared_stored_strings pass as strbuf_scope_callers keeps each method's callers. The callers of every method of a name are asked of the same calls, and each call was resolved once per method of its name, an_call_targets_nonunique typing its receiver and walking the chain every time: on a Rails program 22% of a sampled window of the share pass. A call's targets are resolved once per pass through an_call_targets_of, which answers for every scope at once and agrees with an_call_targets_scope, and each method asks whether it is among them. Outside the pass nothing is kept. */ static int *sbc_off, *sbc_n, *sbc_pool, sbc_cap, sbc_pn, sbc_pcap, sbc_gen = -1; static int sb_call_reaches(Compiler *c, int u, int mi, Scope *m) { if (!sb_targets_on || u < 0) return an_call_targets_scope(c, u, mi, m); if (sbc_gen != sb_targets_gen) { sbc_gen = sb_targets_gen; sbc_pn = 0; for (int i = 0; i < sbc_cap; i++) sbc_off[i] = -1; } if (u >= sbc_cap) { int cap = c->nt->count > u + 1 ? c->nt->count : u + 1; sbc_off = realloc(sbc_off, sizeof(int) * (size_t)cap); sbc_n = realloc(sbc_n, sizeof(int) * (size_t)cap); if (!sbc_off || !sbc_n) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = sbc_cap; i < cap; i++) sbc_off[i] = -1; sbc_cap = cap; } if (sbc_off[u] < 0) { ACallTargets t = { NULL, 0, 0, -1, 0 }; an_call_targets_of(c, u, &t); if (sbc_pn + t.n > sbc_pcap) { sbc_pcap = (sbc_pn + t.n) * 2 + 256; sbc_pool = realloc(sbc_pool, sizeof(int) * (size_t)sbc_pcap); if (!sbc_pool) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } sbc_off[u] = sbc_pn; sbc_n[u] = t.n; for (int k = 0; k < t.n; k++) sbc_pool[sbc_pn++] = t.v[k]; free(t.v); } for (int k = 0; k < sbc_n[u]; k++) if (sbc_pool[sbc_off[u] + k] == mi) return 1; return 0; } static void strbuf_arg_index_edge(SbArgIndex *x, int h, int a, int scope, int p, int loose) { if (x->n == x->cap) { if (x->cap > INT_MAX / 2) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } x->cap = x->cap ? x->cap * 2 : 64; x->edges = realloc(x->edges, sizeof *x->edges * (size_t)x->cap); if (!x->edges) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } x->edges[x->n] = (SbArgEdge){ x->head[h], a, scope, p, loose }; x->head[h] = x->n++; } static void strbuf_arg_index_call(Compiler *c, SbArgIndex *x, int u, int mi) { Scope *m = &c->scopes[mi]; Scope *s = nt_kind(c->nt, u) == NK_ForwardingSuperNode ? comp_scope_of(c, u) : NULL; int *map = NULL; if (!s && m->nparams > 0) { int ac = 0, args = nt_ref(c->nt, u, "arguments"); const int *av = args >= 0 ? nt_arr(c->nt, args, "arguments", &ac) : NULL; map = malloc(sizeof *map * (size_t)m->nparams); if (!map) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } arg_layout_param_map(c, m, av, ac, map); } for (int j = 0; j < m->nparams; j++) { int p = s ? zsuper_param_source(c, s, m, j) : -1; int a = map ? map[j] : -1; if (a < 0 && p < 0) continue; strbuf_arg_index_edge(x, x->off[mi] + j, a, s ? (int)(s - c->scopes) : -1, p, 0); } free(map); } /* The same for the arguments of call u from index `shift` on (`bind_call` takes its receiver first), bound through the method's own layout: an optional before a required one, a post and a keyword take the argument the call gives them, not the one at their position. `loose`: the call may not reach mi at all, so only the lift of the argument reads the edge. */ static void strbuf_arg_index_args(Compiler *c, SbArgIndex *x, int u, int mi, int shift, int loose) { const NodeTable *nt = c->nt; int ac = 0, args = nt_ref(nt, u, "arguments"); const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; int np = c->scopes[mi].nparams, n = ac - shift; if (n < 0 || np <= 0) return; int plain = n < SB_PLAIN_MAX && !nt_argv_has(nt, av + shift, n, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK | NT_ARG_FORWARDING); int *map = NULL, **memo = NULL; if (plain) { if (!x->plain) { x->plain = calloc((size_t)c->nscopes * SB_PLAIN_MAX, sizeof *x->plain); if (!x->plain) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } memo = &x->plain[mi * SB_PLAIN_MAX + n]; map = *memo; } int fresh = !map; if (fresh) { map = malloc(sizeof *map * (size_t)np); if (!map) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } arg_layout_param_map(c, &c->scopes[mi], av + shift, n, map); /* a plain list is the same nodes at the same places for every call */ if (plain) for (int j = 0; j < np; j++) { int at = -1; for (int k = 0; k < n && map[j] >= 0; k++) if (av[shift + k] == map[j]) at = k; map[j] = at; } if (memo) *memo = map; } for (int j = 0; j < np; j++) { int a = plain ? (map[j] >= 0 ? av[shift + map[j]] : -1) : map[j]; if (a >= 0) strbuf_arg_index_edge(x, x->off[mi] + j, a, -1, -1, loose); } if (!memo) free(map); } /* A call through a Method object: `m.call(v)` (`m.(v)`, `m[v]`, `m === v`) on a Method, on the Proc its to_proc made, or on a curried one, and `bind_call(obj, v)` on an UnboundMethod. It is the only site of a method reached that way, so it binds each method the receiver may hold, as dyn_reach_method_node reads them (a bound builtin's wrapper takes its receiver first and is left alone). A typed receiver that names none (one handed in as an argument, kept in an instance variable; not a Proc built only from literal blocks, which holds none) may hold any method the program names with `method` or `instance_method`: they take the arguments too, for the lift alone, which only costs a lift where it was not needed. One kept in an Array or a Hash is a boxed receiver, whose dispatch publishes its arguments as the argument types state, not as lifted. */ static int dyn_is_proc_literal(Compiler *c, int n); /* Is Proc-valued expression v built only from literal blocks and lambdas (through `curry` or `to_proc`, a local's every write)? Then it holds no method. */ static int sb_proc_only_literal(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 4) return 0; NodeKind vk = nt_kind(nt, v); if (dyn_is_proc_literal(c, v) || vk == NK_BlockNode) return 1; if (vk == NK_CallNode) { const char *nm = nt_str(nt, v, "name"); return is_proc_conversion_name(nm) && sb_proc_only_literal(c, nt_ref(nt, v, "receiver"), depth + 1); } if (vk != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; int saw = 0; for (int w = vs ? comp_lvw_first_sc(c, (int)(vs - c->scopes), vn) : -1; w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs || !nt_str(nt, w, "name") || !sp_streq(nt_str(nt, w, "name"), vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode || !sb_proc_only_literal(c, nt_ref(nt, w, "value"), depth + 1)) return 0; saw = 1; } return saw; } static void strbuf_arg_index_method_call(Compiler *c, SbArgIndex *x, int u, int **all, int *nall) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, u, "receiver"); const char *cn = nt_str(nt, u, "name"); if (recv < 0 || !cn) return; TyKind rt = comp_ntype(c, recv); int bind = is_bind_call(cn); if (bind ? rt != TY_METHOD : !(is_method_invoke(cn) && (rt == TY_METHOD || rt == TY_PROC || rt == TY_CURRY))) return; int *mns = NULL, nmn = rt == TY_METHOD ? method_recv_nodes(c, recv, &mns) : rt == TY_PROC ? proc_to_proc_method_nodes(c, recv, &mns) : 0; for (int i = 0; i < nmn; i++) { int tmi = method_obj_target_mi(c, mns[i]); if (tmi < 0 || method_call_param_shift(c, mns[i], tmi)) continue; if (bind) strbuf_arg_index_args(c, x, u, tmi, 1, 0); else strbuf_arg_index_call(c, x, u, tmi); } free(mns); if (nmn > 0 || (rt != TY_METHOD && sb_proc_only_literal(c, recv, 0))) return; if (!*all) { int cap = 0; *all = NULL; NT_FOREACH_KIND(nt, NK_CallNode, n) { int tmi = method_obj_target_mi(c, n), dup = 0; if (tmi < 0 || method_call_param_shift(c, n, tmi)) continue; for (int j = 0; j < *nall; j++) dup |= (*all)[j] == tmi; if (dup) continue; if (*nall == cap) { cap = cap ? cap * 2 : 8; *all = realloc(*all, sizeof **all * (size_t)cap); if (!*all) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } (*all)[(*nall)++] = tmi; } if (!*all) *all = malloc(sizeof **all); } for (int i = 0; i < *nall; i++) strbuf_arg_index_args(c, x, u, (*all)[i], bind ? 1 : 0, 1); } /* Built once at storage, after desugaring and target inference. The later String representation changes leave these argument bindings intact. Ordinary calls use the forward twin of the old caller predicate; new and super add the targets their call plans resolve, without a name scan. */ static void strbuf_arg_index_build(Compiler *c) { if (c->sb_args) return; SbArgIndex *x = calloc(1, sizeof *x); if (!x) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } x->off = malloc(sizeof *x->off * ((size_t)c->nscopes + 1)); if (!x->off) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } x->off[0] = 0; for (int i = 0; i < c->nscopes; i++) { if (c->scopes[i].nparams > INT_MAX - x->off[i]) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } x->off[i + 1] = x->off[i] + c->scopes[i].nparams; } x->head = malloc(sizeof *x->head * ((size_t)x->off[c->nscopes] + 1)); if (!x->head) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < x->off[c->nscopes]; i++) x->head[i] = -1; ACallTargets t = {0}; int *allm = NULL, nallm = 0; const NodeTable *nt = c->nt; for (int u = nt->count - 1; u >= 0; u--) { NodeKind k = nt_kind(nt, u); if (k == NK_CallNode) { if (is_new_name(nt_str(nt, u, "name"))) { int mis[CPT_MAX], nmi = cplan_initializers(c, u, mis, CPT_MAX); for (int i = 0; i < nmi; i++) strbuf_arg_index_call(c, x, u, mis[i]); if (nmi > 0) continue; } strbuf_arg_index_method_call(c, x, u, &allm, &nallm); an_call_targets_of(c, u, &t); for (int i = 0; i < t.n; i++) strbuf_arg_index_call(c, x, u, t.v[i]); } else if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { int mi = cplan_user_fresh(c, u)->mi; if (mi >= 0) strbuf_arg_index_call(c, x, u, mi); } } free(t.v); free(allm); if (x->plain) for (int i = 0; i < c->nscopes * SB_PLAIN_MAX; i++) free(x->plain[i]); free(x->plain); x->plain = NULL; c->sb_args = x; } /* (scope, parameter) -> "some call site hands that parameter a shared handle". Evidence from the CALLER, which the rules around it do not have. The propagation runs param -> argument: a parameter that is byref, or already a handle, pulls its call sites' locals into the shared set. Nothing ran the other way, so a parameter that is only RETAINED -- never mutated, in a class that never mutates the slot -- stayed a value while the argument was a handle, and the call site handed it sp_str_concat(cstr, "") instead. The holder walked away with a snapshot and never saw a mutation made through any other alias: class Holder; def initialize(b) @b = b end; def at(i) @b.getbyte(i) end; end class Poker; def initialize(b) @b = b end; def poke(i,v) @b.setbyte(i,v) end; end s = +"abcd"; h = Holder.new(s); pk = Poker.new(s) pk.poke(2, 7); h.at(2) # 7 in Ruby, the old byte here Poker's parameter is a handle (its slot is mutated) and that made `s` one; Holder's had no evidence of its own. The argument being a handle IS the evidence -- what a handle is passed to is a handle, the call-site twin of the rule below. Built ONCE per pass, not per candidate. The question is asked for every ivar write that stores a parameter, and answering each by walking the call nodes made the pass O(writes x nodes): a 15k-line program whose classes mostly retain a string parameter took 3.1x as long to compile. One walk fills every slot, and the answers move only between passes anyway, as slots promote. */ typedef struct HandleArgTab { int *off; /* scope -> base into `bit`, or -1 when it has no params */ unsigned char *bit; int ok; /* 0 when allocation failed: fall back to no evidence */ /* scope -> its call sites, as a head/next chain over call node ids. The byref pass below asked `does call u target scope mi2?` for every node and every handle parameter, which is O(handle params x nodes) -- fine while handle parameters were rare, and the dominant cost once a whole program's retaining classes became handles. The same walk that fills `bit` records the chain, so both passes read it instead. */ int *head; /* scope -> first edge, or -1 */ int *enext; /* edge -> next edge for the same scope */ int *enode; /* edge -> the call node */ } HandleArgTab; enum { HA_HANDLE = 1, HA_STRING_KNOWN = 2, HA_STRING_POSSIBLE = 4 }; static void handle_arg_tab_init(Compiler *c, HandleArgTab *t) { const NodeTable *nt = c->nt; t->off = NULL; t->bit = NULL; t->head = NULL; t->enext = NULL; t->enode = NULL; t->ok = 0; if (c->nscopes <= 0) return; size_t maxe = (size_t)(nt->count > 0 ? nt->count : 1) * 2; t->off = (int *)malloc(sizeof(int) * (size_t)c->nscopes); t->head = (int *)malloc(sizeof(int) * (size_t)c->nscopes); t->enext = (int *)malloc(sizeof(int) * maxe); t->enode = (int *)malloc(sizeof(int) * maxe); if (!t->off || !t->head || !t->enext || !t->enode) { free(t->off); free(t->head); free(t->enext); free(t->enode); t->off = NULL; t->head = NULL; t->enext = NULL; t->enode = NULL; return; } for (int i = 0; i < c->nscopes; i++) t->head[i] = -1; int total = 0; for (int i = 0; i < c->nscopes; i++) { int np = c->scopes[i].nparams; if (np <= 0) { t->off[i] = -1; continue; } t->off[i] = total; total += np; } t->bit = total > 0 ? (unsigned char *)calloc((size_t)total, 1) : NULL; if (total > 0 && !t->bit) { free(t->off); free(t->head); free(t->enext); free(t->enode); t->off = NULL; t->head = NULL; t->enext = NULL; t->enode = NULL; return; } t->ok = 1; int ne = 0; ACallTargets tg = { NULL, 0, 0, -1, 0 }; for (int u = 0; u < nt->count; u++) { if (nt_kind(nt, u) != NK_CallNode) continue; an_call_targets_of(c, u, &tg); for (int k = 0; k < tg.n; k++) { int mi = tg.v[k]; /* only a parameter's chain is ever read, and a poly receiver's call reaches every method of its name, most of them often taking none */ if (mi < 0 || mi >= c->nscopes || c->scopes[mi].nparams <= 0) continue; /* a call a poly receiver makes is an edge to every method of its name, so the edges can outnumber the two per node reserved above */ if ((size_t)ne == maxe) { size_t nmax = maxe * 2; int *nn = (int *)realloc(t->enext, sizeof(int) * nmax); if (nn) t->enext = nn; int *nd = nn ? (int *)realloc(t->enode, sizeof(int) * nmax) : NULL; if (nd) t->enode = nd; if (!nn || !nd) { t->ok = 0; free(tg.v); return; } maxe = nmax; } t->enode[ne] = u; t->enext[ne] = t->head[mi]; t->head[mi] = ne; ne++; if (t->off[mi] < 0 || !t->bit) continue; int np = c->scopes[mi].nparams; for (int pj = 0; pj < np; pj++) { int a = arg_layout_param_node(c, &c->scopes[mi], u, pj, NULL); if (a >= 0 && an_arg_hands_handle(c, a)) t->bit[t->off[mi] + pj] = HA_HANDLE; } } } free(tg.v); } static void handle_arg_tab_free(HandleArgTab *t) { free(t->off); free(t->bit); free(t->head); free(t->enext); free(t->enode); t->off = NULL; t->bit = NULL; t->head = NULL; t->enext = NULL; t->enode = NULL; t->ok = 0; } /* Qualifiers flow in the opposite direction to byref inference: from a setjmp-live caller slot into the parameter borrowing it. Codegen seeds the slots using its existing selective volatility policy. Reuse the resolved call-site chains, including keywords, overrides and splices. */ static int lend_volatile_slot(Compiler *c, int arg, LocalVar *dst) { if (!dst || dst->type != TY_STRING || dst->borrowed_volatile || arg < 0 || nt_kind(c->nt, arg) != NK_LocalVariableReadNode) return 0; Scope *s = comp_scope_of(c, arg); LocalVar *src = s ? scope_local(s, nt_str(c->nt, arg, "name")) : NULL; if (!src || !src->borrowed_volatile) return 0; dst->borrowed_volatile = 1; return 1; } void propagate_borrowed_volatile(Compiler *c) { const NodeTable *nt = c->nt; HandleArgTab sites; handle_arg_tab_init(c, &sites); if (!sites.ok) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } ACallerBlocks blocks = {0}; an_caller_blocks_build(c, &blocks); int changed = 1; while (changed) { changed = 0; for (int mi = 1; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams; j++) { LocalVar *p = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!p || p->type != TY_STRING) continue; for (int e = sites.head[mi]; e >= 0; e = sites.enext[e]) { int u = sites.enode[e], blk = nt_ref(nt, u, "block"); if (!p->byref_out && !(m->yields && an_inline_param_lent(c, mi, j, blk))) continue; changed |= lend_volatile_slot(c, arg_layout_param_node(c, m, u, j, NULL), p); } /* Byref signatures agree per name group, including override arms. */ if (p->byref_out && p->borrowed_volatile) for (int k = an_any_scope_by_name(c, m->name); k >= 0; k = an_same_name_next(c, k)) { Scope *other = &c->scopes[k]; LocalVar *q = j < other->nparams && other->pnames[j] ? scope_local(other, other->pnames[j]) : NULL; if (q && q->byref_out && !q->borrowed_volatile) { q->borrowed_volatile = 1; changed = 1; } } } } for (int u = 0; u < nt->count; u++) { NodeKind kind = nt_kind(nt, u); Scope *s = comp_scope_of(c, u); if (!s) continue; if (kind == NK_SuperNode || kind == NK_ForwardingSuperNode) { /* a super in no class, a top-level def's or one a rewrite left outside the tree, has no target */ int mi = s->class_id >= 0 ? a_super_target(c, s) : -1; if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams; j++) { LocalVar *p = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!p || !p->byref_out) continue; if (kind == NK_SuperNode) changed |= lend_volatile_slot(c, arg_layout_param_node(c, m, u, j, NULL), p); else { int k = zsuper_param_source(c, s, m, j); LocalVar *src = k >= 0 ? scope_local(s, s->pnames[k]) : NULL; if (src && src->borrowed_volatile && !p->borrowed_volatile) { p->borrowed_volatile = 1; changed = 1; } } } continue; } int exec = kind == NK_CallNode && nt_str(nt, u, "name") && sp_streq(nt_str(nt, u, "name"), "instance_exec"); if (kind != NK_YieldNode && !exec && !(kind == NK_CallNode && an_yield_like(c, u, s))) continue; int aa = nt_ref(nt, u, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; int si = (int)(s - c->scopes); for (int e = exec ? -1 : blocks.head[si]; exec || e >= 0; e = blocks.next[e]) { int blk = exec ? nt_ref(nt, u, "block") : blocks.blk[e]; if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode) { Scope *bs = comp_scope_of(c, blk); for (int k = 0; k < ac; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) break; if (nt_kind(nt, av[k]) == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, av[k], "elements", &en); for (int i = 0; i < en; i++) { int v; const char *key = dyn_kw_elem_key(c, el[i], &v); const char *kp = key ? an_block_kw_param(c, blk, key) : NULL; LocalVar *p = kp && bs ? scope_local(bs, kp) : NULL; if (key && an_block_kw_lent(c, blk, key)) changed |= lend_volatile_slot(c, v, p); } } else { const char *bp = block_param_at(c, blk, k, call_plain_argc(c, u)); LocalVar *p = bp && bs ? scope_local(bs, bp) : NULL; if (an_block_param_lent(c, blk, k, call_plain_argc(c, u))) changed |= lend_volatile_slot(c, av[k], p); } } } if (exec) break; } } } an_caller_blocks_free(&blocks); handle_arg_tab_free(&sites); } static int handle_arg_tab_get(const HandleArgTab *t, int mi, int pj) { if (!t->ok || !t->bit || mi < 0 || pj < 0 || t->off[mi] < 0) return 0; return (t->bit[t->off[mi] + pj] & HA_HANDLE) != 0; } static int promote_params_stored_in_shared_ivars(Compiler *c, const HandleArgTab *hat) { const NodeTable *nt = c->nt; int changed = 0; /* --share-strings: the one rule decides first (#6765). The facts it builds ask whether a boxed receiver of a String mutator can be a String (an_recv_may_be_string): with the caller table in hand a parameter is bounded by what its callers pass, as it is at the seal, so an accumulator handed down a chain of writers (ffi's `keep`, an Array at every call) is no String's mutation (#8500, #8501). */ struct HandleArgTab *sv_args = c->share_args; if (hat && hat->ok) c->share_args = (struct HandleArgTab *)hat; changed |= share_default_apply(c, 0); c->share_args = sv_args; for (int w = 0; w < nt->count; w++) { if (nt_kind(nt, w) != NK_InstanceVariableWriteNode) continue; int wv = nt_ref(nt, w, "value"); if (wv < 0 || nt_kind(nt, wv) != NK_LocalVariableReadNode) continue; const char *ivname = nt_str(nt, w, "name"); const char *lname = nt_str(nt, wv, "name"); if (!ivname || !lname) continue; Scope *ms = comp_scope_of(c, wv); if (!ms) continue; LocalVar *pp = scope_local(ms, lname); /* a parameter only: a plain local written from an ivar is the alias rule above, and a block parameter binds per iteration rather than per call */ if (!pp || !pp->is_param || pp->is_block_param) continue; if (an_param_idx(ms, lname) < 0) continue; int icid = comp_ivar_owner(c, w); if (icid < 0) continue; /* The slot has to be one that is mutated in place; without that there is no aliasing to preserve and the value representation stays cheaper. Kind -1 is the AMBIGUOUS evidence -- `[]=`, `insert`, `slice!`, `setbyte` and the bang forms -- which names Array's and Hash's methods as much as String's, so on its own it proves nothing. Once the slot's own type says String the ambiguity is settled, and a splice is an in-place mutation like any other. */ { int mk = strbuf_ivar_mut_kind(c, icid, ivname); int ivx0 = comp_ivar_index(&c->classes[icid], ivname); TyKind ivt0 = ivx0 >= 0 ? c->classes[icid].ivar_types[ivx0] : TY_UNKNOWN; /* A slot ANOTHER rule already promoted needs no evidence of its own: being a shared handle is the conclusion the evidence was for. The mutation census keys on calls whose receiver is an ivar read, so a slot mutated only from OUTSIDE its class -- `d.s << "x"` through the reader -- records nothing there, and the parameter written into it was left a value while the ivar itself was a handle (#4363). */ int already = ivx0 >= 0 && c->classes[icid].ivar_str_shared[ivx0]; /* And the converse: the SLOT BEING WRITTEN is evidence too, once what is written into it is already a handle. `def initialize(s) @one = s; @two = s end` with only @one mutated made the parameter a handle through @one, and @two -- having no mutator of its own -- kept a value copy, so the second name did not see the mutation and was not the same object. What a handle is assigned to is a handle (#4363). */ int src_is_handle = pp->type == TY_STRBUF && pp->str_shared; /* And from the other side of the call: the ARGUMENT is already a handle. Without this the chain only ever ran outwards from a slot that could show its own mutation, so a retaining class that mutates nothing -- the reader half of an aliased pair -- was handed a copy. */ int arg_is_handle = handle_arg_tab_get(hat, (int)(ms - c->scopes), an_param_idx(ms, lname)); if (!already && !src_is_handle && !arg_is_handle && mk != 1 && !(mk == -1 && (ivt0 == TY_STRING || ivt0 == TY_STRBUF))) continue; } if (pp->type != TY_UNKNOWN && pp->type != TY_STRING && pp->type != TY_STRBUF && pp->type != TY_POLY) continue; if (strbuf_promote_ivar(c, icid, ivname)) changed = 1; { ClassInfo *ci2 = &c->classes[icid]; int iv2 = comp_ivar_index(ci2, ivname); if (iv2 < 0 || !ci2->ivar_str_shared[iv2]) continue; } if (pp->type != TY_POLY && (pp->type != TY_STRBUF || !pp->str_shared)) { lv_set_type(pp, TY_STRBUF); { pp->str_shared = 1; comp_touch_scope(pp->owner); } { pp->byref_out = 0; comp_touch_scope(pp->owner); } changed = 1; } } return changed; } /* Does CallNode `u` statically call scope `mi2`? The byref machinery resolves a callee by program-unique name, which is exactly what a constructor is not: every class has an `initialize`, so the reported shape -- a string handed to `K.new` and retained in an ivar -- had no call site the propagation could find (#4363). `K.new(...)` resolves through the constant receiver instead. Guarded on the whole program rather than per class: if any `new` in the program goes through a receiver that is not a constant, some class is instantiated by a class object we cannot pin, and a parameter whose C type we are about to change could be reached through it. */ /* Could a `new` whose receiver cannot be pinned construct class `cid`? A dynamic `k.new(...)` is emitted as a switch over the receiver's class id with an arm for every class whose `initialize` accepts the call's shape, and that arm passes each argument in the parameter's own C type. A class the switch can land on therefore has to keep the C type the arm was written against, which is why promoting its parameter to a handle is refused. The question used to be asked of the whole PROGRAM: one `new` anywhere with a receiver that is not a constant switched constructor resolution off for every class at once. That is far wider than the hazard. `k.new` with no arguments cannot construct a class whose `initialize` requires one, so a single dynamic no-argument `new` -- of an unrelated class, in an unrelated file -- cost every retained string in the program its handle and silently brought back the copy this machinery exists to prevent. Asked per class, an arm can only reach a class whose initialize accepts the shape. An argument list the analyzer cannot count (a splat, or keywords) still puts every class at risk, as before. */ static int an_class_dynamic_new_risk(Compiler *c, int cid) { const NodeTable *nt = c->nt; static signed char *risk = NULL; static int risk_n = -1, risk_count = -1; if (cid < 0 || cid >= c->nclasses) return 1; if (!risk || risk_n != c->nclasses || risk_count != nt->count) { free(risk); risk = (signed char *)calloc((size_t)(c->nclasses > 0 ? c->nclasses : 1), 1); if (!risk) { risk_n = -1; risk_count = -1; return 1; } risk_n = c->nclasses; risk_count = nt->count; /* The two walks are separate on purpose. Asking every class about every unpinnable `new` is O(calls x classes) with a method lookup inside, and a program with a thousand classes and a thousand such calls paid 1.2x the compile time for an answer that depends only on the SHAPES seen. Collect the shapes first -- there are a handful, whatever the program does -- then ask each class once. */ unsigned long long seen = 0; /* bit k: some unpinnable `new` passes k */ int seen_any = 0; /* a shape we cannot count, or one over 63 */ /* A receiverless `new` inside `def self.m` is `self.new`. It constructs the enclosing class -- or a descendant, when the class method is inherited -- and never an unrelated one, so its shape does not belong in the free-floating census. Counted there, its ARITY alone cost every class that takes that many parameters: a five-argument `new(a, b, c, d, e)` in one class's own factory method put an unrelated five-parameter class out of reach of the handle-argument evidence, and a mutable String that class retained was then copied at the call, so the caller's later writes never reached it. Held per owner and folded into that owner's subtree below. */ unsigned long long *selfseen = (unsigned long long *)calloc( (size_t)(c->nclasses > 0 ? c->nclasses : 1), sizeof(unsigned long long)); unsigned char *selfany = (unsigned char *)calloc( (size_t)(c->nclasses > 0 ? c->nclasses : 1), 1); for (int u = 0; u < nt->count; u++) { if (nt_kind(nt, u) != NK_CallNode) continue; const char *un = nt_str(nt, u, "name"); if (!un || !sp_streq(un, "new")) continue; int rc = nt_ref(nt, u, "receiver"); if (rc >= 0 && nt_kind(nt, rc) == NK_ConstantReadNode) continue; /* pinned */ int selfowner = -1; if (rc < 0 && selfseen && selfany) { Scope *csc = comp_scope_of(c, u); if (csc && csc->is_cmethod && csc->class_id >= 0) selfowner = csc->class_id; } int argsN = nt_ref(nt, u, "arguments"); int argc = 0; const int *argv = argsN >= 0 ? nt_arr(nt, argsN, "arguments", &argc) : NULL; int npos = 0, uncountable = 0; for (int a = 0; a < argc && argv; a++) { NodeKind ak = nt_kind(nt, argv[a]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode) { uncountable = 1; break; } if (ak == NK_BlockArgumentNode) continue; npos++; } if (selfowner >= 0) { if (uncountable || npos >= 64) selfany[selfowner] = 1; else selfseen[selfowner] |= 1ULL << npos; } else if (uncountable || npos >= 64) seen_any = 1; else seen |= 1ULL << npos; } /* push each owner's `self.new` shapes down its subtree, once, so the per-class test below is a lookup rather than an ancestor walk */ int self_any_seen = 0; if (selfseen && selfany) { for (int oc = 0; oc < c->nclasses; oc++) { if (!selfseen[oc] && !selfany[oc]) continue; self_any_seen = 1; int nd = 0; const int *ds = comp_descendants(c, oc, &nd); for (int d = 0; d < nd && ds; d++) { selfseen[ds[d]] |= selfseen[oc]; selfany[ds[d]] |= selfany[oc]; } } } if (seen_any || seen || self_any_seen) { for (int ci = 0; ci < c->nclasses; ci++) { if (seen_any || (selfany && selfany[ci])) { risk[ci] = 1; continue; } /* the initialize `new` runs: an inherited one for a class with none of its own, as an_call_targets_of ties it (a subclass passed MyFragment.new(h) beside an unpinnable Base::Fragment.new(doc)) */ int mi = comp_method_in_chain(c, ci, "initialize", NULL); if (mi < 0) continue; /* no initialize: no parameter to protect */ int pn = (mi < c->nscopes && c->scopes[mi].def_node >= 0) ? nt_ref(nt, c->scopes[mi].def_node, "parameters") : -1; int nreq = 0, nopt = 0, npost = 0, rest = -1; if (pn >= 0) { nt_arr(nt, pn, "requireds", &nreq); nt_arr(nt, pn, "optionals", &nopt); /* the required parameters AFTER the optionals -- `def initialize(n = 0, b)` takes 1..2, not 0..1. Counting only the leading requireds put the class out of reach of a two-argument `k.new`, the parameter was promoted, and the arm codegen then wanted no longer matched: the dynamic call fell through to the default and raised NoMethodError where it had merely been wrong before. emit_class_value_new_kw counts all three, and these two have to agree. */ nt_arr(nt, pn, "posts", &npost); rest = nt_ref(nt, pn, "rest"); } nreq += npost; if (nreq >= 64) continue; /* no countable shape reaches it */ unsigned long long accept; if (rest >= 0) accept = ~0ULL << nreq; /* nreq or more */ else { int hi = nreq + nopt; if (hi > 63) hi = 63; accept = (hi - nreq >= 63) ? (~0ULL << nreq) : (((1ULL << (hi - nreq + 1)) - 1) << nreq); } if ((seen | (selfseen ? selfseen[ci] : 0ULL)) & accept) risk[ci] = 1; } } free(selfseen); free(selfany); } return risk[cid] != 0; } static int an_call_targets_scope(Compiler *c, int u, int mi2, Scope *m2) { const NodeTable *nt = c->nt; const char *un = nt_str(nt, u, "name"); if (!un) return 0; int is_new = sp_streq(un, "new"), named = sp_streq(un, m2->name); /* only an alias reaches a scope under another name */ int ae = is_new ? -1 : an_alias_first(c, un), aliased = ae >= 0; int byname = named ? an_unique_scope_by_name(c, un) : -1; if (named && byname == mi2 && cplan_user_fresh(c, u)->mi == mi2 && !an_recv_builtin_without(c, u, un)) return 1; /* a name more than one method defines, or an alias: the targets the call itself resolves to, as an_call_targets_of lists them */ if (!is_new && ((named && byname < 0) || aliased)) { ACallTargets tg = { NULL, 0, 0, mi2, 0 }; an_call_targets_nonunique(c, u, un, &tg, ae); if (tg.hit) return 1; } if (!sp_streq(un, "new") || !sp_streq(m2->name, "initialize")) return 0; if (m2->class_id < 0 || m2->is_cmethod) return 0; int rc = nt_ref(nt, u, "receiver"); if (rc < 0 || nt_kind(nt, rc) != NK_ConstantReadNode) return 0; const char *cn = nt_str(nt, rc, "name"); int cid = cn ? comp_class_index(c, cn) : -1; if (cid < 0) return 0; /* the same per-class question an_call_targets_of asks, at the same point: the two answer for the same set or the index and the predicate diverge */ if (an_class_dynamic_new_risk(c, cid)) return 0; return comp_method_in_chain(c, cid, "initialize", NULL) == mi2; } /* The parameter twin of narrow_locals_from_arrays. An array's element type is only known HERE, after the fixpoint: while it was still TY_POLY_ARRAY every read of it answered poly. narrow_locals_from_arrays repairs the local that took that answer -- `b = arr[i]` becomes the element type once `arr` narrows. A parameter handed the same expression, `f(arr[i])`, had no such repair: it bound the poly, the later narrowing never reached it, and the callee boxed every use of a value the caller holds unboxed. `@col[q]` passed to `write_column(wcol)` made `wcol * 4` an sp_poly_mul with a boxed argument and a GC root slot, beside a local holding that same element as a plain sp_int. Sound for the same reason the binding that produced the poly was: the parameter is re-derived from the arguments at the call sites infer_param_types itself binds from. The extra condition is that the parameter is not WRITTEN in the callee -- a write is a contribution only the fixpoint saw, and re-deriving from the arguments alone would drop it. Every argument must be an index read of an array that narrowed to the SAME element type, as the local rule insists of every write. */ static int narrow_params_from_arrays(Compiler *c) { const NodeTable *nt = c->nt; if (c->nscopes <= 0) return 0; int *off = (int *)malloc(sizeof(int) * (size_t)c->nscopes); if (!off) return 0; int total = 0; for (int i = 0; i < c->nscopes; i++) { int np = c->scopes[i].nparams; if (np <= 0) { off[i] = -1; continue; } off[i] = total; total += np; } if (total <= 0) { free(off); return 0; } TyKind *elem = (TyKind *)malloc(sizeof(TyKind) * (size_t)total); signed char *ok = (signed char *)malloc((size_t)total); signed char *saw = (signed char *)calloc((size_t)total, 1); if (!elem || !ok || !saw) { free(off); free(elem); free(ok); free(saw); return 0; } for (int i = 0; i < total; i++) { elem[i] = TY_UNKNOWN; ok[i] = 1; } /* Names reached by a route the call scan cannot see. `super` reaches the parent's method of the SAME name with no CallNode naming it, and the runtime protocols (and an extension entry) call with arguments no site in the program supplies. Narrowing such a parameter from the calls that ARE visible declares an ABI the other route does not honour -- and for `initialize`, which resolves through `K.new` rather than by unique name, that is reachable: a subclass whose `super(s)` passes a String to a parameter narrowed to sp_int from `Holder.new(@col[q])` does not compile. An alias is such a route too: a call by the alias's name finds no scope of that name below, so `alias on_b on_a` narrowed on_a's parameter to the class its own callers pass, and `on_b(other)` was refused. */ ANameHash supern; memset(&supern, 0, sizeof supern); for (int u = 0; u < nt->count; u++) { NodeKind sk = nt_kind(nt, u); if (sk != NK_SuperNode && sk != NK_ForwardingSuperNode) continue; Scope *ss = comp_scope_of(c, u); if (ss && ss->name && !anh_has(&supern, ss->name)) anh_add(&supern, ss->name); } for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cls = &c->classes[ci]; for (int a = 0; a < cls->naliases; a++) { const char *ao = cls->alias_old[a]; if (ao && !anh_has(&supern, ao)) anh_add(&supern, ao); } } /* The unique scope of each method name, hashed once: resolving it per call site scanned every scope, and a pass over every call site grew quadratically with the program (scale-test). */ int nb = 1; while (nb < 2 * c->nscopes + 2) nb <<= 1; int *hs = (int *)malloc(sizeof(int) * (size_t)nb); /* scope index, -1 empty */ int *hv = (int *)malloc(sizeof(int) * (size_t)nb); /* unique scope or -1 */ if (!hs || !hv) { free(hs); free(hv); free(off); free(elem); free(ok); free(saw); return 0; } for (int i = 0; i < nb; i++) hs[i] = -1; for (int i = 1; i < c->nscopes; i++) { const char *sn = c->scopes[i].name; if (!sn) continue; unsigned h = 2166136261u; for (const char *q = sn; *q; q++) { h ^= (unsigned char)*q; h *= 16777619u; } for (unsigned k = h & (unsigned)(nb - 1);; k = (k + 1) & (unsigned)(nb - 1)) { if (hs[k] < 0) { hs[k] = i; hv[k] = i; break; } if (sp_streq(c->scopes[hs[k]].name, sn)) { hv[k] = -1; break; } } } for (int u = 0; u < nt->count; u++) { if (nt_kind(nt, u) != NK_CallNode) continue; int tgt[2], ntg = 0; { const char *un = nt_str(nt, u, "name"); int byname = -1; if (un) { unsigned h = 2166136261u; for (const char *q = un; *q; q++) { h ^= (unsigned char)*q; h *= 16777619u; } for (unsigned k = h & (unsigned)(nb - 1); hs[k] >= 0; k = (k + 1) & (unsigned)(nb - 1)) if (sp_streq(c->scopes[hs[k]].name, un)) { byname = hv[k]; break; } } /* the by-name arm here; a `K.new` call also takes the constructor arm an_call_targets_of resolves (only `new` calls reach its scan) */ if (un && sp_streq(un, "new")) { static ACallTargets ctg; an_call_targets_of(c, u, &ctg); for (int k = 0; k < ctg.n && ntg < 2; k++) tgt[ntg++] = ctg.v[k]; } else if (byname >= 0) tgt[ntg++] = byname; } for (int t = 0; t < ntg; t++) { int mi = tgt[t]; if (mi < 0 || mi >= c->nscopes || off[mi] < 0) continue; int np = c->scopes[mi].nparams; int argsN = nt_ref(nt, u, "arguments"); int argc = 0; const int *argv = argsN >= 0 ? nt_arr(nt, argsN, "arguments", &argc) : NULL; ArgLayout L; call_layout(c, &c->scopes[mi], argv, argc, &L); for (int pj = 0; pj < np; pj++) { int slot = off[mi] + pj; if (!ok[slot]) continue; /* a call site that does not hand this parameter an argument as written says nothing this pass can read (a default, a splat, the gather, the keyword hash): give the slot up */ int a = layout_plain_arg(c, &c->scopes[mi], argv, &L, pj); if (a < 0) { ok[slot] = 0; continue; } NodeKind ak = nt_kind(nt, a); /* A local holding the element is the same evidence one step on: narrow_locals_from_arrays has just given it the element type, and `w = arr[i]; f(w)` is how a caller's own repair reads. Taken from the slot rather than infer_type, whose node cache still answers the poly this pass exists to undo. */ if (ak == NK_LocalVariableReadNode) { const char *ln = nt_str(nt, a, "name"); Scope *ls = ln ? comp_scope_of(c, a) : NULL; LocalVar *lv = ls ? scope_local(ls, ln) : NULL; TyKind lt = lv ? lv->type : TY_UNKNOWN; /* only a type an ELEMENT READ can hand back -- the kinds `ec` above produces. Any concrete type is not enough: TY_STRBUF is a storage refinement a mutable-string local carries without coming from an element, and narrowing the parameter to it declares an sp_String * where the ordinary local read hands a const char *. */ if (!lv || !(lt == TY_INT || lt == TY_FLOAT || lt == TY_STRING || lt == TY_INT_ARRAY || lt == TY_FLOAT_ARRAY || ty_is_object(lt))) { ok[slot] = 0; continue; } if (!saw[slot]) { elem[slot] = lt; saw[slot] = 1; } else if (elem[slot] != lt) ok[slot] = 0; continue; } if (ak != NK_CallNode) { ok[slot] = 0; continue; } const char *cn = nt_str(nt, a, "name"); int crecv = nt_ref(nt, a, "receiver"); int can = 0; { int ca = nt_ref(nt, a, "arguments"); if (ca >= 0) nt_arr(nt, ca, "arguments", &can); } int idx_op = cn && (is_element_at_alias(cn)) && can == 1; int end_op = cn && (is_endpoint_query(cn)) && can == 0; if ((!idx_op && !end_op) || crecv < 0) { ok[slot] = 0; continue; } TyKind rt = infer_type(c, crecv); TyKind ec = ty_is_obj_array(rt) ? ty_object(ty_obj_array_class(rt)) : (rt == TY_INT_ARRAY_ARRAY) ? TY_INT_ARRAY : (rt == TY_FLOAT_ARRAY_ARRAY) ? TY_FLOAT_ARRAY : (rt == TY_INT_ARRAY || rt == TY_FLOAT_ARRAY || rt == TY_STR_ARRAY) ? ty_array_elem(rt) : TY_UNKNOWN; if (ec == TY_UNKNOWN) { ok[slot] = 0; continue; } if (!saw[slot]) { elem[slot] = ec; saw[slot] = 1; } else if (elem[slot] != ec) ok[slot] = 0; } arg_layout_free(&L); } } int any = 0; for (int mi = 0; mi < c->nscopes; mi++) { if (off[mi] < 0) continue; Scope *m = &c->scopes[mi]; for (int pj = 0; pj < m->nparams; pj++) { int slot = off[mi] + pj; if (!ok[slot] || !saw[slot] || elem[slot] == TY_UNKNOWN) continue; if (!m->pnames[pj]) continue; if (m->name && (method_name_implicitly_invoked(m->name) || anh_has(&supern, m->name))) continue; if (m->is_ext_entry) continue; LocalVar *p = scope_local(m, m->pnames[pj]); if (!p || p->type != TY_POLY) continue; if (p->is_block_param || p->rbs_seeded || p->poly_dispatch_widened) continue; if (m->body >= 0 && blkp_name_written(c, m->body, p->name)) continue; lv_set_type(p, elem[slot]); p->oa_pin = elem[slot]; any = 1; } } anh_free(&supern); free(off); free(elem); free(ok); free(saw); free(hs); free(hv); return any; } /* `obj.reader.equal?(x)` and `x.equal?(obj.reader)` ask whether two names are one object, and a reader that hands out a reading of the slot answers no for an object that IS shared. The in-fixpoint rule beside the container stores marks the argument side, but it cannot see a slot promoted after the fixpoint -- a parameter retained in an ivar becomes a handle below -- so the demand is made again once every promotion has settled. It goes in strbuf_handle_demand rather than strbuf_box because the two mean different things here: strbuf_box is read as the node's TYPE as well, and the emitters pick their arm from the receiver's type, so marking a receiver moves `equal?` off the String surface that answers it. This says only "hand out the handle" (#4363). */ /* A reader call whose result is only READ hands out the live handle. Runs after the handle promotion has converged, because it asks whether the backing slot ended up a shared handle -- it promotes nothing itself, so it changes no representation and no answer, only whether the read copies. */ static int mark_reader_read_only_operands(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; /* External reader READ (`ctx.buf.getbyte(i)`): the shape above with a read-only accessor in place of the mutator. The unmarked read hands out a fresh copy of the WHOLE string to answer one byte, so a per-row scan is O(len) per row -- a 200 KB buffer read 20_000 times copied 4 GB to read 20_000 bytes. The accessor reads through sp_String_cstr and keeps no pointer past the call, so the live handle serves it. This rule promotes NOTHING: it only applies where the backing slot is already a shared handle, so it changes no representation and no answer, only the copy. Where the slot is a plain value the read is already a direct one. */ for (int ru = 0; ru < nt->count; ru++) { if (nt_kind(nt, ru) != NK_CallNode) continue; const char *run = nt_str(nt, ru, "name"); if (!run || !an_str_read_only_accessor(run)) continue; int rrecv2 = nt_ref(nt, ru, "receiver"); if (rrecv2 < 0 || nt_kind(nt, rrecv2) != NK_CallNode) continue; if (c->strbuf_box[rrecv2] || c->strbuf_read_raw[rrecv2]) continue; if (nt_ref(nt, rrecv2, "block") >= 0) continue; { int ra = nt_ref(nt, rrecv2, "arguments"); int rac = 0; if (ra >= 0) nt_arr(nt, ra, "arguments", &rac); if (rac != 0) continue; } int robj = nt_ref(nt, rrecv2, "receiver"); if (robj < 0) continue; TyKind robt = infer_type(c, robj); if (!ty_is_object(robt)) continue; char ivb5[300]; int defc5 = ty_object_class(robt); const char *ivn5 = an_reader_ivar_of(c, rrecv2, &defc5, ivb5, sizeof ivb5); if (!ivn5 || defc5 < 0) continue; int iv5 = comp_ivar_index(&c->classes[defc5], ivn5); if (iv5 < 0 || !c->classes[defc5].ivar_str_shared[iv5]) continue; c->strbuf_read_raw[rrecv2] = 1; changed = 1; } /* The same reader, aliased into a local that is only READ (`bytes = ctx.bin_bytes`, then `bytes.getbyte(i)` in a loop). Nothing is mutated, so the carry-back rule does not fire, and the assignment took a full copy of the String -- O(len) once per call, which in a per-feature scan over a large buffer is most of the run. Held as the handle instead, every read of the local lowers to sp_String_cstr on the live buffer and costs nothing. Aliasing a reader's String is what CRuby does here anyway; the copy was the divergence. Like the rule above this promotes NOTHING -- it applies only where the slot is already a shared handle -- and it has to run HERE, after the promotion fixpoint, because that is when the slot's answer is known. */ for (int w7 = 0; w7 < nt->count; w7++) { if (nt_kind(nt, w7) != NK_LocalVariableWriteNode) continue; int v7 = nt_ref(nt, w7, "value"); if (v7 < 0 || nt_kind(nt, v7) != NK_CallNode) continue; if (c->strbuf_box[v7] || c->strbuf_handle_demand[v7]) continue; if (nt_ref(nt, v7, "block") >= 0) continue; { int a7 = nt_ref(nt, v7, "arguments"); int ac7 = 0; if (a7 >= 0) nt_arr(nt, a7, "arguments", &ac7); if (ac7 != 0) continue; } int rr7 = nt_ref(nt, v7, "receiver"); if (rr7 < 0) continue; TyKind rt7 = infer_type(c, rr7); if (!ty_is_object(rt7)) continue; const char *tn7 = nt_str(nt, w7, "name"); Scope *ws7 = comp_scope_of(c, w7); if (!tn7 || !ws7) continue; char ivb7[300]; int defc7 = ty_object_class(rt7); const char *ivn7 = an_reader_ivar_of(c, v7, &defc7, ivb7, sizeof ivb7); if (!ivn7 || defc7 < 0) continue; { int iv7 = comp_ivar_index(&c->classes[defc7], ivn7); if (iv7 < 0 || !c->classes[defc7].ivar_str_shared[iv7]) continue; } LocalVar *tv7 = scope_local(ws7, tn7); if (!tv7 || tv7->is_param) continue; if (tv7->type != TY_STRING) continue; /* READ-only means read-only: a local this scope mutates belongs to the carry-back rule above, which asks what the mutator is before handing it a handle. A String-typed local settles the ambiguous kinds, so that rule claims every mutated alias reachable today -- but this rule must not be the one deciding, or tightening that rule later would silently hand a disqualified mutation the handle path. */ if (strbuf_mut_kind(c, tn7, ws7) != 0) continue; if (!strbuf_slot_eligible_shape(c, tn7, ws7, tv7)) continue; /* the read now yields the handle, so the node says so: left a String, the local's write took the handle for a const char * and wrapped it in a fresh one (test/reader_alias_takes_handle) */ comp_set_strbuf_box(c, v7, 1); comp_sn_retype(c, v7, TY_STRBUF); lv_set_type(tv7, TY_STRBUF); { tv7->str_shared = 1; comp_touch_scope(tv7->owner); } changed = 1; } return changed; } /* A user method's parameter that only reads a String for the length of the call takes the live buffer of a shared handle handed to it (#7482). The handle's read face is a full copy, made so that no `const char *` can outlive the bytes it points into: a growth reallocates them. A 31 MB byte matrix passed to a read-only method once per row was copied 62,710 times per pass. The copy is only needed where something could see the difference, and here nothing can: - the parameter keeps nothing: its every use is the receiver of an accessor that reads through the pointer and keeps none (an_str_read_only_accessor; a match would keep it in MatchData); - nothing changes the String while the callee runs: the callee is quiet (param_borrow_loud), and every other operand of the call is a plain read, so nothing between the borrow and the call runs either; - no Ruby code runs inside the callee that it does not call (an_ruby_runs_unseen): a program with a thread, a fiber, a signal handler or an ffi_callback borrows nothing; - the parameter is a `const char *` slot, so the borrowed pointer is read exactly as the copy would have been. This makes nothing shared that was not: it applies only where the slot is already a shared handle, and only changes whether its read copies, as mark_reader_read_only_operands does for a reader's builtin accessor. */ /* A type whose builtin methods run no user code and keep no String pointer they are handed: a scalar, a String, and the typed containers of those. */ static int param_borrow_plain_ty(TyKind t) { switch (t) { case TY_INT: case TY_BIGINT: case TY_FLOAT: case TY_BOOL: case TY_SYMBOL: case TY_NIL: case TY_VOID: case TY_STRING: case TY_STRBUF: case TY_RANGE: case TY_FLOAT_RANGE: case TY_STR_RANGE: case TY_INT_ARRAY: case TY_FLOAT_ARRAY: case TY_STR_ARRAY: case TY_INT_ARRAY_ARRAY: case TY_FLOAT_ARRAY_ARRAY: case TY_STR_INT_HASH: case TY_INT_INT_HASH: case TY_STR_STR_HASH: case TY_INT_STR_HASH: case TY_REGEX: case TY_MATCHDATA: case TY_TIME: case TY_COMPLEX: case TY_RATIONAL: case TY_IO: return 1; default: return 0; } } static int param_borrow_scalar_ty(TyKind t) { return t == TY_INT || t == TY_FLOAT || t == TY_BOOL || t == TY_SYMBOL || t == TY_NIL; } /* Is call n an attr_reader's read of an object's field (`node.off`)? It reads the slot and runs nothing, unless a class below the receiver's defines a method of the name, which the receiver may be. */ static int param_borrow_attr_read(Compiler *c, int n) { const NodeTable *nt = c->nt; if (nt_ref(nt, n, "block") >= 0) return 0; int args = nt_ref(nt, n, "arguments"), argc = 0; if (args >= 0) nt_arr(nt, args, "arguments", &argc); int recv = nt_ref(nt, n, "receiver"); const char *name = nt_str(nt, n, "name"); if (argc != 0 || recv < 0 || !name || !ty_is_object(c->ntype[recv])) return 0; int cid = ty_object_class(c->ntype[recv]), defc = cid; if (cid < 0 || cid >= c->nclasses || !comp_reader_in_chain(c, cid, name, &defc)) return 0; if (call_target(c, n, cid, name, 0, NULL) >= 0) return 0; int nd = 0; const int *ds = comp_descendants(c, cid, &nd); for (int d = 0; d < nd && ds; d++) if (call_target(c, n, ds[d], name, CT_OWN, NULL) >= 0) return 0; return 1; } /* Is call n an element read of a typed Array (`w[0]`, a row of an Array of objects)? It runs nothing and keeps no String pointer. */ static int param_borrow_elem_read(Compiler *c, int n) { const NodeTable *nt = c->nt; const char *name = nt_str(nt, n, "name"); int recv = nt_ref(nt, n, "receiver"); if (!name || !sp_streq(name, "[]") || recv < 0 || nt_ref(nt, n, "block") >= 0) return 0; int args = nt_ref(nt, n, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; TyKind rt = c->ntype[recv]; if (argc != 1 || c->ntype[argv[0]] != TY_INT) return 0; return rt == TY_INT_ARRAY || rt == TY_FLOAT_ARRAY || rt == TY_STR_ARRAY || rt == TY_INT_ARRAY_ARRAY || rt == TY_FLOAT_ARRAY_ARRAY || ty_is_obj_array(rt); } /* Is call n `Array.new(...)` or `Hash.new(...)` of the builtin class (one the program gives no initialize of its own), with no block handed on as a value? A fresh container reaches no String the caller holds and runs no code but a literal block's, which the walk sees. */ static int param_borrow_builtin_new(Compiler *c, int n) { const NodeTable *nt = c->nt; const char *name = nt_str(nt, n, "name"); int recv = nt_ref(nt, n, "receiver"), blk = nt_ref(nt, n, "block"); if (!name || !sp_streq(name, "new") || recv < 0 || nt_kind(nt, recv) != NK_ConstantReadNode) return 0; if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode) return 0; const char *cn = nt_str(nt, recv, "name"); if (!cn || (!sp_streq(cn, "Array") && !sp_streq(cn, "Hash"))) return 0; int cid = comp_class_index(c, cn); return cid < 0 || comp_method_in_class(c, cid, "initialize") < 0; } /* The user methods call n reaches (its plan's method and, for a switch, every member), into out; 0 for a builtin, -1 when the switch has more members than out can hold (none of them is checked). */ static int param_borrow_targets(Compiler *c, int n, int *out, int cap) { const CallPlan *p = cplan_user_fresh(c, n); if (p->mi < 0 || p->dispatch == CP_REFUSE) return 0; CallPlan plan = *p; int k = 0; out[k++] = plan.mi; if (plan.dispatch >= CP_SWITCH) { const char *name = c->scopes[plan.mi].name; PolyBlockMembers blk; blk.state = 0; for (int s = 0; s < c->nscopes; s++) if (s != plan.mi && c->scopes[s].name && name && sp_streq(c->scopes[s].name, name) && cplan_virtual_member(c, n, &plan, s, &blk)) { if (k == cap) return -1; /* more targets than can be checked: no borrow */ out[k++] = s; } } return k; } /* Could node n, run inside a callee, change a String, drop the last name of one, or run code this cannot see? A user call, a yield, a super, a proc, a reflective call, a builtin on or handed a value that can reach user code (an object's to_s, ==, hash), an in-place String change, a write of a variable that can hold a String. Deliberately coarse: a callee that does any of it keeps the copy. */ static int param_borrow_loud_ex(Compiler *c, int n, int *tg, int *ntg); /* ... a user call is not loud by itself when its targets resolve: they go into tg (*ntg of them, at most 64), and the call is as loud as the loudest of them (settled over the call graph by the caller). */ static int param_borrow_loud_ex(Compiler *c, int n, int *tg, int *ntg) { const NodeTable *nt = c->nt; *ntg = 0; switch (nt_kind(nt, n)) { case NK_YieldNode: case NK_SuperNode: case NK_ForwardingSuperNode: case NK_LambdaNode: case NK_ClassVariableWriteNode: case NK_ClassVariableOrWriteNode: case NK_ClassVariableAndWriteNode: case NK_ClassVariableOperatorWriteNode: case NK_ClassVariableTargetNode: case NK_InstanceVariableTargetNode: case NK_GlobalVariableTargetNode: return 1; case NK_InstanceVariableWriteNode: case NK_InstanceVariableOrWriteNode: case NK_InstanceVariableAndWriteNode: case NK_InstanceVariableOperatorWriteNode: { const char *nm = nt_str(nt, n, "name"); Scope *s = comp_scope_of(c, n); int cid = s && s->class_id >= 0 ? s->class_id : c->toplevel_class; int iv = nm && cid >= 0 ? comp_ivar_index(&c->classes[cid], nm) : -1; return iv < 0 || !param_borrow_scalar_ty(c->classes[cid].ivar_types[iv]); } case NK_GlobalVariableWriteNode: case NK_GlobalVariableOrWriteNode: case NK_GlobalVariableAndWriteNode: case NK_GlobalVariableOperatorWriteNode: { const char *nm = nt_str(nt, n, "name"); LocalVar *gv = nm ? comp_gvar(c, nm[0] == '$' ? nm + 1 : nm) : NULL; return !gv || !param_borrow_scalar_ty(gv->type); } case NK_EmbeddedStatementsNode: { /* `#{x}` calls x.to_s */ int st = nt_ref(nt, n, "statements"); int bn = 0; const int *bv = st >= 0 ? nt_arr(nt, st, "body", &bn) : NULL; return bn > 0 && !param_borrow_plain_ty(c->ntype[bv[bn - 1]]); } case NK_CallNode: { const char *name = nt_str(nt, n, "name"); if (!name) return 1; int k = param_borrow_targets(c, n, tg, 64); if (k < 0) return 1; *ntg = k; /* a builtin Array or Hash made fresh (its arguments still plain) */ int fresh = k == 0 && param_borrow_builtin_new(c, n); if (is_opaque_reaching_call(name) && !fresh) return 1; int recv = nt_ref(nt, n, "receiver"); TyKind rt = recv >= 0 ? c->ntype[recv] : TY_VOID; if (k == 0 && (param_borrow_attr_read(c, n) || param_borrow_elem_read(c, n))) return 0; if (recv >= 0 && !param_borrow_plain_ty(rt) && rt != TY_CLASS) return 1; if ((rt == TY_STRING || rt == TY_STRBUF) && sp_str_mutator(name, 0)) return 1; int blk = nt_ref(nt, n, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode) return 1; int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; /* IO#read(n, buf) and its kin fill the buffer in place */ if (rt == TY_IO && argc >= 2) return 1; /* a user method's arguments are only bound: what it does with an object it is handed, its own walk sees (and its loudness is this call's) */ for (int i = 0; i < argc && k == 0; i++) if (!param_borrow_plain_ty(c->ntype[argv[i]])) return 1; return 0; } default: return 0; } } /* An operand evaluated beside the borrowed argument: a variable read, a literal, scalar arithmetic over those, an element read of a typed Array, or an attr_reader's read of a field. */ static int param_borrow_plain_operand(Compiler *c, int n) { const NodeTable *nt = c->nt; if (n < 0) return 1; switch (nt_kind(nt, n)) { case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: case NK_GlobalVariableReadNode: case NK_ConstantReadNode: case NK_SelfNode: case NK_IntegerNode: case NK_FloatNode: case NK_StringNode: case NK_SymbolNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: return 1; case NK_CallNode: { int recv = nt_ref(nt, n, "receiver"); { int tg0[64]; if ((param_borrow_elem_read(c, n) || param_borrow_attr_read(c, n)) && param_borrow_targets(c, n, tg0, 64) == 0) { int a0 = nt_ref(nt, n, "arguments"), ac0 = 0; const int *av0 = a0 >= 0 ? nt_arr(nt, a0, "arguments", &ac0) : NULL; return param_borrow_plain_operand(c, recv) && (ac0 == 0 || param_borrow_plain_operand(c, av0[0])); } } TyKind rt = recv >= 0 ? c->ntype[recv] : TY_VOID; if ((rt != TY_INT && rt != TY_FLOAT) || nt_ref(nt, n, "block") >= 0) return 0; int tg[64]; if (param_borrow_targets(c, n, tg, 64) != 0 || !param_borrow_plain_operand(c, recv)) return 0; int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; for (int i = 0; i < argc; i++) if (!param_borrow_plain_operand(c, argv[i])) return 0; return 1; } default: return 0; } } /* Is argument node a a read of a String slot held as the shared handle, which reads out through the copying face? */ static int param_borrow_handle_read(Compiler *c, int a) { const NodeTable *nt = c->nt; if (c->strbuf_box[a] || c->strbuf_handle_demand[a] || c->strbuf_read_raw[a]) return 0; if (nt_kind(nt, a) == NK_LocalVariableReadNode) { const char *ln = nt_str(nt, a, "name"); LocalVar *lv = ln ? scope_local(comp_scope_of(c, a), ln) : NULL; return lv && lv->type == TY_STRBUF && lv->str_shared && repr_of_slot(c, lv).kind == RK_STRBUF; } if (nt_kind(nt, a) == NK_InstanceVariableReadNode) { const char *nm = nt_str(nt, a, "name"); Scope *s = comp_scope_of(c, a); /* a class method's @x is the class's own ivar, another slot */ if (!nm || !s || s->is_cmethod) return 0; int cid = s->class_id >= 0 ? s->class_id : c->toplevel_class; int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], nm) : -1; return iv >= 0 && c->classes[cid].ivar_types[iv] == TY_STRBUF && c->classes[cid].ivar_str_shared[iv]; } return 0; } /* Can Ruby code run inside a method at a point where the method makes no call? Another thread's body can, while the method sleeps or waits on IO; so can the code that resumed a fiber the method yields, a Signal.trap handler (run inside the C signal handler, at any instruction) and an ffi_callback, which a C function may call back from one it was handed earlier (finalizers run through one). Any of them can grow a String the method holds the live buffer of, and the buffer is freed under it: a quiet method then reads freed memory. The program-wide answer is deliberately coarse: such programs keep the copy. */ static int an_ruby_runs_unseen(Compiler *c) { if (c->n_ffi_callbacks > 0) return 1; const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, n) { int recv = nt_ref(nt, n, "receiver"); const char *rn = NULL; if (recv >= 0) { NodeKind rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) continue; if (!(rn = nt_str(nt, recv, "name"))) continue; } if (is_async_code_entry(rn, nt_str(nt, n, "name"))) return 1; } return 0; } /* Is local p of method m a parameter that may take a borrowed buffer, as far as its own slot goes? */ static int param_borrow_slot_ok(const LocalVar *p) { return p->is_param && !p->is_block_param && !p->is_cell && !p->cell_outlives && !p->byref_out && !p->str_shared && !p->rbs_seeded && p->type == TY_STRING; } /* The parameter of target method m that argument a of call n binds, or NULL (a rest, a keyword rest, a spread or no parameter at all). */ static LocalVar *param_borrow_bound(Compiler *c, Scope *m, int n, int a) { int spread = -1, pj = -1; for (int j = 0; j < m->nparams && pj < 0; j++) if (arg_layout_param_node(c, m, n, j, &spread) == a) pj = j; return pj >= 0 && m->pnames[pj] && pj != m->rest_idx && pj != m->kwrest_idx ? scope_local(m, m->pnames[pj]) : NULL; } /* Does call n hand its arguments over with nothing evaluated beside them that could run code: no block, its targets known, every operand plain? */ static int param_borrow_plain_call(Compiler *c, int n, int *tg, int *ntg) { const NodeTable *nt = c->nt; *ntg = 0; if (nt_ref(nt, n, "block") >= 0) return 0; int k = param_borrow_targets(c, n, tg, 64); if (k <= 0 || !param_borrow_plain_operand(c, nt_ref(nt, n, "receiver"))) return 0; int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; for (int i = 0; i < argc; i++) if (!param_borrow_handle_read(c, argv[i]) && !param_borrow_plain_operand(c, argv[i])) return 0; *ntg = k; return 1; } /* A method is loud when it does something loud itself or calls a method that is (a least fixpoint over the call graph: a quiet cycle stays quiet). A parameter takes a borrowed buffer when its slot can, its method is quiet, and its every use reads through it (an accessor's receiver) or hands it on to a parameter that takes one too (a greatest fixpoint: a cycle of hand-offs that reads nothing else keeps the borrow). #9229 */ static int mark_param_read_only_operands(Compiler *c) { const NodeTable *nt = c->nt; if (an_ruby_runs_unseen(c)) return 0; int ns = c->nscopes; unsigned char *loud = calloc((size_t)(ns > 0 ? ns : 1), 1); int **other = calloc((size_t)(ns > 0 ? ns : 1), sizeof(int *)); unsigned char **safe = calloc((size_t)(ns > 0 ? ns : 1), sizeof(unsigned char *)); int marked = 0; /* the call graph's edges (caller scope, callee scope), and the hand-offs (scope, local, call, argument) */ int ne = 0, ce = 0, *ef = NULL, *et = NULL; int nh = 0, chh = 0, *hs = NULL, *hl = NULL, *hn = NULL, *ha = NULL; for (int s = 0; s < ns; s++) if (c->scopes[s].def_node >= 0) { other[s] = calloc((size_t)(c->scopes[s].nlocals + 1), sizeof(int)); safe[s] = calloc((size_t)(c->scopes[s].nlocals + 1), 1); } /* one pass: which methods are loud by themselves and whom they call, and how many uses of each local are anything but an accessor's receiver or a hand-off (a block shares its method's scope) */ for (int n = 0; n < nt->count; n++) { int si = c->nscope[n]; if (si < 0 || si >= ns || !other[si]) continue; if (!loud[si]) { int tg[64], ntg = 0; if (param_borrow_loud_ex(c, n, tg, &ntg)) loud[si] = 1; else for (int t = 0; t < ntg; t++) { if (ne == ce) { ce = ce ? ce * 2 : 64; ef = realloc(ef, (size_t)ce * sizeof(int)); et = realloc(et, (size_t)ce * sizeof(int)); } ef[ne] = si; et[ne] = tg[t]; ne++; } } NodeKind k = nt_kind(nt, n); const char *ln = NULL; int weight = 0; if (k == NK_LocalVariableReadNode) { ln = nt_str(nt, n, "name"); weight = 1; } else if (comp_is_local_write(k)) { ln = nt_str(nt, n, "name"); weight = 1 << 20; } else if (k == NK_CallNode) { int recv = nt_ref(nt, n, "receiver"); const char *nm = nt_str(nt, n, "name"); if (recv >= 0 && nt_kind(nt, recv) == NK_LocalVariableReadNode && nm && an_str_read_only_accessor(nm) && nt_ref(nt, n, "block") < 0) { ln = nt_str(nt, recv, "name"); weight = -1; } int tg[64], ntg = 0; if (param_borrow_plain_call(c, n, tg, &ntg)) { int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; for (int i = 0; i < argc; i++) { if (nt_kind(nt, argv[i]) != NK_LocalVariableReadNode) continue; LocalVar *a = scope_local(&c->scopes[si], nt_str(nt, argv[i], "name")); if (!a || !a->is_param) continue; if (nh == chh) { chh = chh ? chh * 2 : 64; hs = realloc(hs, (size_t)chh * sizeof(int)); hl = realloc(hl, (size_t)chh * sizeof(int)); hn = realloc(hn, (size_t)chh * sizeof(int)); ha = realloc(ha, (size_t)chh * sizeof(int)); } int li = (int)(a - c->scopes[si].locals); hs[nh] = si; hl[nh] = li; hn[nh] = n; ha[nh] = argv[i]; nh++; other[si][li]--; } } } LocalVar *lv = ln ? scope_local(&c->scopes[si], ln) : NULL; if (lv) other[si][lv - c->scopes[si].locals] += weight; } for (int changed = 1; changed; ) { changed = 0; for (int e = 0; e < ne; e++) if (loud[et[e]] && !loud[ef[e]]) { loud[ef[e]] = 1; changed = 1; } } for (int s = 0; s < ns; s++) { if (!safe[s] || loud[s]) continue; Scope *m = &c->scopes[s]; for (int j = 0; j < m->nlocals; j++) safe[s][j] = other[s][j] == 0 && param_borrow_slot_ok(&m->locals[j]); } for (int changed = 1; changed; ) { changed = 0; for (int h = 0; h < nh; h++) { if (!safe[hs[h]][hl[h]]) continue; int tg[64], ntg = param_borrow_targets(c, hn[h], tg, 64), ok = ntg > 0; for (int t = 0; t < ntg && ok; t++) { Scope *m = &c->scopes[tg[t]]; LocalVar *p = safe[tg[t]] ? param_borrow_bound(c, m, hn[h], ha[h]) : NULL; ok = p && safe[tg[t]][p - m->locals]; } if (!ok) { safe[hs[h]][hl[h]] = 0; changed = 1; } } } NT_FOREACH_KIND(nt, NK_CallNode, n) { int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; int any = 0; for (int i = 0; i < argc && !any; i++) any = param_borrow_handle_read(c, argv[i]); if (!any) continue; int tg[64], ntg = 0; if (!param_borrow_plain_call(c, n, tg, &ntg)) continue; for (int i = 0; i < argc; i++) { if (!param_borrow_handle_read(c, argv[i])) continue; int ok = 1; for (int t = 0; t < ntg && ok; t++) { Scope *m = &c->scopes[tg[t]]; LocalVar *p = safe[tg[t]] ? param_borrow_bound(c, m, n, argv[i]) : NULL; ok = p && safe[tg[t]][p - m->locals]; } if (ok) { c->strbuf_read_raw[argv[i]] = 1; marked++; } } } for (int s = 0; s < ns; s++) { free(other[s]); free(safe[s]); } free(other); free(safe); free(loud); free(ef); free(et); free(hs); free(hl); free(hn); free(ha); return marked; } static int mark_reader_identity_operands(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; for (int w = 0; w < nt->count; w++) { if (nt_kind(nt, w) != NK_CallNode) continue; const char *nm = nt_str(nt, w, "name"); if (!nm || !is_identity_query(nm)) continue; int pair = is_equality_name(nm); int recv = nt_ref(nt, w, "receiver"); int a = nt_ref(nt, w, "arguments"); int ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (recv < 0 || ac != pair) continue; for (int side = pair ? 0 : 1; side < 2; side++) { int opnd = side == 0 ? av[0] : recv; /* A call answering a handle uses the same demand as a reader. Its settled return fact is the one the return route picks up. Replace an earlier box mark too: its storage type lets operand ordering mistake the call for a fresh String to wrap. */ if (opnd >= 0 && !c->strbuf_handle_demand[opnd] && repr_call_returns_handle(c, opnd) && cplan_user_fresh(c, w)->dispatch == CP_NONE) { comp_set_strbuf_box(c, opnd, 0); comp_sn_retype(c, opnd, TY_STRING); c->strbuf_handle_demand[opnd] = 1; /* Boxing a parenthesized operand reads its inner value directly. */ int inner = nt_paren_single(c->nt, opnd); if (inner != opnd) { comp_set_strbuf_box(c, inner, 0); comp_sn_retype(c, inner, TY_STRING); c->strbuf_handle_demand[inner] = 1; } changed = 1; continue; } if (opnd < 0 || c->strbuf_box[opnd] || c->strbuf_handle_demand[opnd]) continue; /* a local the handle pass above made shared: its read is marked the way the in-fixpoint rule marks one shared by then */ if (side == 0 && nt_kind(nt, opnd) == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, opnd, "name"); Scope *vs = vn ? comp_scope_of(c, opnd) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (lv && lv->str_shared && lv->type == TY_STRBUF) { comp_set_strbuf_box(c, opnd, 1); comp_sn_retype(c, opnd, TY_STRBUF); changed = 1; } continue; } char ivb[300]; int defc = -1; /* A boxed reader's plan can give the same handle answer as a statically resolved field. Its demand takes the boxed handle. */ if (repr_boxed_reader_handle(c, opnd)) { c->strbuf_handle_demand[opnd] = 1; changed = 1; continue; } const char *ivn = an_reader_ivar_of(c, opnd, &defc, ivb, sizeof ivb); if (!ivn || defc < 0) continue; int iv = comp_ivar_index(&c->classes[defc], ivn); if (iv < 0 || !c->classes[defc].ivar_str_shared[iv]) continue; /* (not where the receiver's class answers the name with a def of a class other than the reader's, a subclass's over the reader) */ { int orv = nt_ref(nt, opnd, "receiver"), mdc = -1; TyKind ort = orv >= 0 ? comp_ntype(c, orv) : TY_UNKNOWN; int ocid = ty_is_object(ort) ? ty_object_class(ort) : -1; if (ocid >= 0 && ocid < c->nclasses && comp_resolve_member(c, ocid, nt_str(nt, opnd, "name"), 0, &mdc, NULL) == SP_MEMBER_METHOD && mdc != defc) continue; } c->strbuf_handle_demand[opnd] = 1; changed = 1; } } return changed; } /* Can POLY variable `vn` of scope `vs` hold a String? Only then can lifting its read into the handle (poly_strbuf_lift) change anything, so a variable that only ever holds Arrays, Hashes or numbers -- an accumulator handed to a method that pushes onto it -- keeps its plain read and its C. Answers 1 whenever the values cannot all be seen: a block's parameter, a captured local, a parameter whose callers the table does not list (hat NULL), a multiple assignment or an operator write, and a chain deeper than a few variables. */ static int poly_var_may_hold_string(Compiler *c, const HandleArgTab *hat, const char *vn, Scope *vs, int depth, const PolyLits *lits); static int poly_lit_may_be_string(Compiler *c, const HandleArgTab *hat, int v, int depth, const PolyLits *lits); static int poly_value_may_be_string(Compiler *c, const HandleArgTab *hat, int v, int depth, const PolyLits *lits) { const NodeTable *nt = c->nt; if (lits) { v = nt_paren_single(nt, v); int got = v >= 0 ? poly_lit_may_be_string(c, hat, v, depth, lits) : -1; if (got >= 0) return got; } if (v < 0) return 1; TyKind t = comp_ntype(c, v); if (t == TY_UNKNOWN) t = infer_type(c, v); if (t == TY_STRING || t == TY_STRBUF || t == TY_UNKNOWN) return 1; if (t != TY_POLY) return 0; if (nt_kind(nt, v) != NK_LocalVariableReadNode) return 1; const char *rn = nt_str(nt, v, "name"); Scope *rs = rn ? comp_scope_of(c, v) : NULL; return !rs || poly_var_may_hold_string(c, hat, rn, rs, depth + 1, lits); } /* With `lits` (the share facts' question, an_recv_may_be_string, and their index of the variables literal blocks bind), a value is also read by its form (poly_lit_may_be_string): a literal of another class is no String whatever its type settled as (an untyped method's `acc = []`), and a conditional is one of its arms (`c ? [0] : 1`). The lifts ask without it, and their answers and C stay as they were. The arms are read through the iterator: an arm's last statement, through an `else` and parentheses, and a missing arm answers nil. */ typedef struct { Compiler *c; const HandleArgTab *hat; int depth; const PolyLits *lits; } PolyArms; static int poly_arm_may_be_string(const VpLeaf *lf, void *ud) { const PolyArms *a = ud; if (lf->kind == VPK_NIL) return 0; if (lf->kind == VPK_GAVE_UP) return 1; return poly_value_may_be_string(a->c, a->hat, lf->node, a->depth, a->lits); } /* 0 or 1 for a literal or a conditional, -1 for any other form */ static int poly_lit_may_be_string(Compiler *c, const HandleArgTab *hat, int v, int depth, const PolyLits *lits) { const NodeTable *nt = c->nt; switch (nt_kind(nt, v)) { case NK_ArrayNode: case NK_HashNode: case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: case NK_RangeNode: case NK_LambdaNode: case NK_RegularExpressionNode: return 0; case NK_IfNode: case NK_UnlessNode: case NK_OrNode: case NK_AndNode: { PolyArms a = { c, hat, depth, lits }; return for_each_value_position(nt, v, VP_AND_LEFT | VPS_IF | VPS_UNLESS | VPS_LOGIC, poly_arm_may_be_string, &a); } default: return -1; } } static int poly_var_may_hold_string(Compiler *c, const HandleArgTab *hat, const char *vn, Scope *vs, int depth, const PolyLits *lits) { const NodeTable *nt = c->nt; LocalVar *lv = vn && vs ? scope_local(vs, vn) : NULL; /* with `lits`, how a block or a lambda binds it (PolyLits.bound): one bound only as Enumerator.new's yielder is no String */ int bound = lv && lits && !lv->is_param ? lits->bound(lits->ctx, c, (int)(vs - c->scopes), vn) : 0; if (!lv || (!lits && depth > 4) || lv->is_cell || (lv->is_block_param && bound != 1) || bound == 2) return 1; /* a variable already being asked about further up the chain (a method handing its parameter to itself) adds no value of its own */ static LocalVar *asking[10]; for (int k = 0; k < depth && k < 10; k++) if (asking[k] == lv) return 0; /* With literals, recognize a cycle before the conservative depth limit, which is deeper than the lifts': an accumulator handed down a chain of writers (ffi's `keep`, through six methods) is still an Array at its end */ if (depth > 8) return 1; asking[depth] = lv; int si = (int)(vs - c->scopes); if (lv->is_param) { int pj = an_param_idx(vs, vn); if (pj < 0 || !hat || !hat->ok || !vs->name) return 1; if (vs->pdefault && vs->pdefault[pj] >= 0 && poly_value_may_be_string(c, hat, vs->pdefault[pj], depth, lits)) return 1; for (int e = hat->head[si]; e >= 0; e = hat->enext[e]) { int sp = -1; int a = lits ? arg_layout_param_source(c, vs, hat->enode[e], pj, &sp) : arg_layout_param_node(c, vs, hat->enode[e], pj, &sp); if (a < 0 || sp >= 0 || poly_value_may_be_string(c, hat, a, depth, lits)) return 1; } } int seen = 0; for (int w = comp_lvw_first_sc(c, si, vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; NodeKind wk = nt_kind(nt, w); if (wk != NK_LocalVariableWriteNode && wk != NK_LocalVariableOrWriteNode && wk != NK_LocalVariableAndWriteNode) return 1; if (poly_value_may_be_string(c, hat, nt_ref(nt, w, "value"), depth, lits)) return 1; seen = 1; } /* with `lits`, a variable no write or yielder binds is bound some other way */ return lits && !lv->is_param && !seen && bound != 1; } /* Drop the rule's lifts of a variable that cannot hold a String: the final caller table answers once for all its reads, through the existing variable-site index. */ static void strbuf_reconcile_lifted_reads(Compiler *c, const HandleArgTab *hat) { const NodeTable *nt = c->nt; for (int si = 0; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; for (int li = 0; li < s->nlocals; li++) { const char *vn = s->locals[li].name; int may_str = -1; for (int e = comp_vsite_first(c, VS_READ, NK_LocalVariableReadNode, vn, si); e >= 0; e = comp_vsite_next(c, e)) { int r = comp_vsite_var(c, e); if (nt_kind(nt, r) != NK_LocalVariableReadNode || comp_scope_of(c, r) != s || !sp_streq(nt_str(nt, r, "name"), vn) || !c->poly_strbuf_lift[r]) continue; if (may_str < 0) may_str = poly_var_may_hold_string(c, hat, vn, s, 0, NULL); if (may_str) break; c->poly_strbuf_lift[r] = 0; } } } } /* The instance variable receiver r reads, as its node names it (with the `@`), or NULL: an ivar's read, or an argument-less call on the current instance or on a receiver of a known class that every class it can be -- the class and each subclass, which a singleton method or an extended module also makes -- answers with one and the same reader: an attribute reader, or a method with no `return` whose value is an ivar's read or its memo (`def x = @x`, `@x ||= v`). A Struct's or a Data's member is set by its constructor, not by a write, and a native class's attribute is its binding's, so neither is one; nor is a module's self, which any includer's own method may answer. The name is left in buf; one that does not fit is no answer (NULL). */ static const char *poly_recv_ivar(Compiler *c, int r, char *buf, size_t cap) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, r); if (k == NK_InstanceVariableReadNode) { int cid = comp_ivar_owner(c, r); const char *in = nt_str(nt, r, "name"); if (!in || cid < 0 || cid >= c->nclasses || c->classes[cid].is_struct || c->classes[cid].is_data || c->classes[cid].is_native_class) return NULL; return snprintf(buf, cap, "%s", in) < (int)cap ? buf : NULL; } const char *nm = nt_str(nt, r, "name"); if (k != NK_CallNode || !nm || nt_ref(nt, r, "arguments") >= 0 || nt_ref(nt, r, "block") >= 0) return NULL; int recv = nt_paren_single(nt, nt_ref(nt, r, "receiver")), cid = -1; if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *s = comp_scope_of(c, r); if (!s || s->is_cmethod) return NULL; cid = s->class_id; } else if (ty_is_object(comp_ntype(c, recv))) cid = ty_object_class(comp_ntype(c, recv)); if (cid < 0 || cid >= c->nclasses || comp_class_is_module(c, &c->classes[cid])) return NULL; /* A subclass's alias can name another attribute of the same class (`alias_method :x, :y` under the class of both readers): the reader is the defining class's either way, so the attribute each class reads is compared by the name its aliases resolve to. */ const char *an = comp_resolve_alias(c, cid, nm); int kind = SP_MEMBER_NONE, dc = -1, mi = -1; for (int j = 0; j < c->nclasses; j++) { int a = j; while (a >= 0 && a != cid) a = c->classes[a].parent; if (a != cid) continue; int jd = -1, jm = -1; int jk = comp_resolve_member(c, j, nm, 0, &jd, &jm); if (jk == SP_MEMBER_NONE || jd < 0 || jd >= c->nclasses) return NULL; if (j == cid) { kind = jk; dc = jd; mi = jm; } else if (jk != kind || jd != dc || (jk == SP_MEMBER_METHOD && jm != mi) || (jk == SP_MEMBER_ATTR && !sp_streq(comp_resolve_alias(c, j, nm), an))) return NULL; } if (kind == SP_MEMBER_NONE || c->classes[dc].is_struct || c->classes[dc].is_data || c->classes[dc].is_native_class) return NULL; if (kind == SP_MEMBER_ATTR) return snprintf(buf, cap, "@%s", an) < (int)cap ? buf : NULL; /* (a `return` elsewhere in the method answers another value) */ if (mi < 0 || mi >= c->nscopes || comp_ret_first(c, mi) >= 0) return NULL; const char *ivn = an_memo_reader_ivar(c, mi); int last = scope_body_last(c, mi); if (!ivn && last >= 0 && nt_kind(nt, last) == NK_InstanceVariableReadNode) ivn = nt_str(nt, last, "name"); if (!ivn) return NULL; return snprintf(buf, cap, "%s", ivn) < (int)cap ? buf : NULL; } int an_value_may_be_string(Compiler *c, int v, const PolyLits *lits) { return poly_value_may_be_string(c, lits ? c->share_args : NULL, v, 0, lits); } /* --share-strings (#6765): can receiver r, a boxed or untyped value, be a String? A variable answers by its values (poly_var_may_hold_string), a literal or a conditional by its form, an instance variable's read or an attribute reader's call by what is written to the ivar (PolyLits.ivar, poly_recv_ivar); a parameter, a block's parameter, a captured local and any other value may be one. At seal, the final caller table also bounds parameters, using the same argument walk. */ int an_recv_may_be_string(Compiler *c, int r, const PolyLits *lits) { const NodeTable *nt = c->nt; r = nt_paren_single(nt, r); if (r < 0) return 1; if (nt_kind(nt, r) != NK_LocalVariableReadNode) { char ivb[300]; const char *iv = lits && lits->ivar ? poly_recv_ivar(c, r, ivb, sizeof ivb) : NULL; if (iv) return lits->ivar(lits->ctx, c, iv); return poly_lit_may_be_string(c, lits ? c->share_args : NULL, r, 0, lits) != 0; } const char *vn = nt_str(nt, r, "name"); Scope *vs = vn ? comp_scope_of(c, r) : NULL; const HandleArgTab *hat = lits ? c->share_args : NULL; int pj = hat && hat->ok && vs ? an_param_idx(vs, vn) : -1; unsigned char *memo = pj >= 0 && hat->off[vs - c->scopes] >= 0 ? &hat->bit[hat->off[vs - c->scopes] + pj] : NULL; if (memo && (*memo & HA_STRING_KNOWN)) return (*memo & HA_STRING_POSSIBLE) != 0; /* Only a completed outer query is cached: a nested cycle's no-value answer depends on the callers still being visited above it. */ int may = !vs || poly_var_may_hold_string(c, hat, vn, vs, 0, lits); if (memo) *memo |= HA_STRING_KNOWN | (may ? HA_STRING_POSSIBLE : 0); return may; } /* Is POLY variable `vn` of scope `vs` appended to in place: the receiver of a String mutator, or read where it is lifted into the handle for a parameter appended to (poly_strbuf_lift, the reads `lifted` collects)? */ static int poly_var_appended(Compiler *c, SbMutTab *lifted, const char *vn, Scope *vs) { if (strbuf_any_str_mut(c, vn, vs)) return 1; signed char *v = sb_mut_tab_slot(lifted, vn, (int)(vs - c->scopes), 0); return v && *v == 1; } /* Where an ivar, class or global variable node `id` named `nm` lives, as a key: an ivar is its class's, an instance's or (in a class method) the class object's; a class variable is its owner's, found as cvar_slot finds the class (a class body's own, else Toplevel) and up the chain to the class that declares it; a global is the one. -1 when there is none. */ static int poly_store_place(Compiler *c, int id, NodeKind rk, const char *nm) { if (rk == NK_GlobalVariableReadNode || rk == NK_GlobalVariableWriteNode) return 0; Scope *s = comp_scope_of(c, id); int cid = s ? s->class_id : -1; if (rk == NK_InstanceVariableReadNode || rk == NK_InstanceVariableWriteNode) return cid < 0 ? -1 : cid * 2 + (s->is_cmethod ? 1 : 0); if (cid < 0 && c->node_cbody && id < c->node_cap) cid = c->node_cbody[id]; if (cid < 0) cid = c->toplevel_class; if (cid < 0 || cid >= c->nclasses) return -1; return comp_cvar_owner(c, cid, nm); } /* The variables of read kind `rk` the program appends to in place -- a read of one is a String mutator's receiver, or is lifted for a parameter appended to (lift_poly_read) -- as (name, poly_store_place) pairs, found in one walk of the reads and one of the calls, not one per store. */ typedef struct { const char **nm; int *place; int n, cap; } PolyStoreApp; static void poly_store_app_add(PolyStoreApp *t, const char *nm, int place) { while (*nm == '@') nm++; for (int i = 0; i < t->n; i++) if (t->place[i] == place && sp_streq(t->nm[i], nm)) return; if (t->n == t->cap) { t->cap = t->cap ? t->cap * 2 : 8; t->nm = realloc(t->nm, sizeof(char *) * (size_t)t->cap); t->place = realloc(t->place, sizeof(int) * (size_t)t->cap); } t->nm[t->n] = nm; t->place[t->n] = place; t->n++; } static int sa_returned_args_t(Compiler *c, int call, int *out, int cap, int poly_args); /* The poly_store_place of the ivar an attr_reader / attr_writer call (`box`, `self.box = v`, `o.box`) reaches, or -1: a call on the current instance or on a receiver of a known class, whose name is an attribute of it. */ static int poly_attr_place(Compiler *c, int call, const char *base, int want_write) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, call, "receiver"), cid = -1; if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *s = comp_scope_of(c, call); if (!s || s->is_cmethod) return -1; cid = s->class_id; } else { TyKind t = comp_ntype(c, recv); if (!ty_is_object(t)) return -1; cid = ty_object_class(t); } int dc = -1, mi = -1; if (cid < 0 || comp_resolve_member(c, cid, base, want_write, &dc, &mi) != SP_MEMBER_ATTR || dc < 0) return -1; return dc * 2; } static void poly_store_app_build(Compiler *c, NodeKind rk, PolyStoreApp *t) { const NodeTable *nt = c->nt; t->n = 0; NT_FOREACH_KIND(nt, rk, r) { const char *rn = nt_str(nt, r, "name"); if (rn && c->poly_strbuf_lift[r] && comp_ntype(c, r) == TY_POLY) poly_store_app_add(t, rn, poly_store_place(c, r, rk, rn)); } NT_FOREACH_KIND(nt, NK_CallNode, u) { int r = nt_ref(nt, u, "receiver"); if (r < 0 || !an_str_mutator_name(nt_str(nt, u, "name"))) continue; const char *rn = nt_str(nt, r, "name"); if (nt_kind(nt, r) == rk) { if (rn && comp_ntype(c, r) == TY_POLY) poly_store_app_add(t, rn, poly_store_place(c, r, rk, rn)); } /* `box << y` through an attribute reader of the instance variable */ else if (rk == NK_InstanceVariableReadNode && nt_kind(nt, r) == NK_CallNode && rn && nt_ref(nt, r, "arguments") < 0 && nt_ref(nt, r, "block") < 0 && comp_ntype(c, r) == TY_POLY) { int pl = poly_attr_place(c, r, rn, 0); if (pl >= 0) poly_store_app_add(t, rn, pl); } } } static int poly_store_appended(const PolyStoreApp *t, const char *in, int wp) { while (*in == '@') in++; for (int i = 0; i < t->n; i++) if (t->place[i] == wp && sp_streq(t->nm[i], in)) return 1; return 0; } /* Lift read `a` of a POLY variable that can hold a String (poly_strbuf_lift); a method's own parameter read so is appended to as well, and its callers are pulled in on the next round (convert_byref_handle_params). */ static int lift_poly_read(Compiler *c, const HandleArgTab *hat, SbMutTab *lifted, int a) { const NodeTable *nt = c->nt; if (a < 0 || c->poly_strbuf_lift[a]) return 0; /* A POLY ivar is lifted the same way, its slot stored back (emit_expr's ivar read); a value type's ivar is a field of a struct copy, with no slot the caller would see */ if (nt_kind(nt, a) == NK_InstanceVariableReadNode) { Scope *as = comp_scope_of(c, a); if (!as || comp_ntype(c, a) != TY_POLY) return 0; if (as->class_id >= 0 && !as->is_cmethod && comp_ty_value_obj(c, ty_object(as->class_id))) return 0; c->poly_strbuf_lift[a] = 1; return 1; } if (nt_kind(nt, a) != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->type != TY_POLY || !poly_var_may_hold_string(c, hat, vn, vs, 0,NULL)) return 0; c->poly_strbuf_lift[a] = 1; if (lifted) sb_mut_tab_note(lifted, vn, (int)(vs - c->scopes), 1); if (lv->is_param && !lv->is_block_param && an_param_idx(vs, vn) >= 0) lv->poly_lift |= POLY_LIFT_APPENDED; return 1; } /* lift_poly_read for a container store (strbuf_demand_store_leaf), which runs ahead of the call-site tables */ static int lift_poly_read_store(Compiler *c, int a) { return lift_poly_read(c, NULL, NULL, a); } /* Is call `n` in yielding method `m` a call of the method's own `&b` (`b.call(v)`, `b.(v)`, `b[v]`, `b.yield(v)`), which is spliced as a yield? */ static int poly_spliced_block_call(Compiler *c, Scope *m, int n) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"); if (!nm || r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode || !m->blk_param) return 0; if (!is_call_or_yield(nm)) return 0; const char *rn = nt_str(nt, r, "name"); return rn && m->blk_param[0] && sp_streq(rn, m->blk_param); } /* --share-strings: the String a boxed to_s answers is the one its receiver holds. For a parameter receiver every call site has to pass a String no other name holds, or one the lift can make a handle in the caller. A call answering its String argument (`b.itself`, a lambda's value), or a variable that was given such a value, is boxed as bytes, and the callee's lift would copy them. So the rule looks one level only: an argument is a value that cannot be a String, a fresh String (a literal, `+"lit"`), a reader over a shared slot, a local whose every write is one of those, or a poly ivar whose every write passes this rule. A parameter, or a local written from a call or another variable, keeps the refusal. The refusal and the lift ask the same question. */ static int boxed_to_s_fresh(Compiler *c, int a) { const NodeTable *nt = c->nt; a = nt_paren_single(nt, a); if (a < 0) return 0; NodeKind k = nt_kind(nt, a); if (k == NK_StringNode || k == NK_InterpolatedStringNode) return 1; return k == NK_CallNode && is_unary_plus(nt_str(nt, a, "name")) && nt_ref(nt, a, "arguments") < 0 && nt_ref(nt, a, "block") < 0 && boxed_to_s_fresh(c, nt_ref(nt, a, "receiver")); } static int boxed_to_s_arg_ok(Compiler *c, const HandleArgTab *hat, int a, int ivar_depth); /* A write's value: fresh, or no String */ static int boxed_to_s_value_ok(Compiler *c, const HandleArgTab *hat, int v) { return v >= 0 && (boxed_to_s_fresh(c, v) || !poly_value_may_be_string(c, hat, v, 0, NULL)); } static int boxed_to_s_arg_ok(Compiler *c, const HandleArgTab *hat, int a, int ivar_depth) { const NodeTable *nt = c->nt; a = nt_paren_single(nt, a); if (a < 0) return 0; if (boxed_to_s_value_ok(c, hat, a)) return 1; NodeKind k = nt_kind(nt, a); if (k == NK_CallNode) return an_arg_hands_handle(c, a); if (k == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->is_param || lv->is_block_param) return 0; int writes = 0; for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs || !sp_streq(nt_str(nt, w, "name"), vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode || !boxed_to_s_value_ok(c, hat, nt_ref(nt, w, "value"))) return 0; writes++; } return writes > 0; } if (k == NK_InstanceVariableReadNode && !ivar_depth && comp_ntype(c, a) == TY_POLY) { const char *in = nt_str(nt, a, "name"); int cid = in ? comp_ivar_owner(c, a) : -1, writes = 0; if (cid < 0) return 0; NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, w) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, in) || comp_ivar_owner(c, w) != cid) continue; if (!boxed_to_s_arg_ok(c, hat, nt_ref(nt, w, "value"), 1)) return 0; writes++; } /* any other kind of write (`@v ||= x`, `@v += x`) is not looked at */ static const NodeKind other[] = { NK_InstanceVariableOrWriteNode, NK_InstanceVariableAndWriteNode, NK_InstanceVariableOperatorWriteNode, NK_InstanceVariableTargetNode }; for (size_t i = 0; i < sizeof other / sizeof *other; i++) NT_FOREACH_KIND(nt, other[i], w) { const char *wn = nt_str(nt, w, "name"); if (wn && sp_streq(wn, in)) return 0; } return writes > 0; } return 0; } static int boxed_to_s_recv_ok(Compiler *c, const HandleArgTab *hat, int r) { const NodeTable *nt = c->nt; const char *vn = nt_kind(nt, r) == NK_LocalVariableReadNode ? nt_str(nt, r, "name") : NULL; Scope *vs = vn ? comp_scope_of(c, r) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv) return 0; /* a local receiver is judged by its writes, as an argument is */ if (!lv->is_param) return boxed_to_s_arg_ok(c, hat, r, 0); int pj = an_param_idx(vs, vn), si = (int)(vs - c->scopes); if (pj < 0 || lv->is_block_param || !hat || !hat->ok || hat->head[si] < 0) return 0; /* a parameter written from something else inside the method is no longer the caller's String; one rebound from a call on itself (`o = o.to_s`, `o = o.dup`) is read where it is rebound */ for (int w = comp_lvw_first_sc(c, si, vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs || !sp_streq(nt_str(nt, w, "name"), vn)) continue; int wv = nt_kind(nt, w) == NK_LocalVariableWriteNode ? nt_paren_single(nt, nt_ref(nt, w, "value")) : -1; int wr = wv >= 0 && nt_kind(nt, wv) == NK_CallNode ? nt_paren_single(nt, nt_ref(nt, wv, "receiver")) : -1; if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode || !sp_streq(nt_str(nt, wr, "name"), vn)) return 0; } if (vs->pdefault && vs->pdefault[pj] >= 0 && !boxed_to_s_arg_ok(c, hat, vs->pdefault[pj], 0)) return 0; for (int e = hat->head[si]; e >= 0; e = hat->enext[e]) { int sp = -1; int a = arg_layout_param_node(c, vs, hat->enode[e], pj, &sp); if (sp >= 0 || (a < 0 && !(vs->pdefault && vs->pdefault[pj] >= 0)) || (a >= 0 && !boxed_to_s_arg_ok(c, hat, a, 0))) return 0; } return 1; } /* A POLY variable holds a plain String as a value, so a second name for it is a copy: `y = x` copied the box, and `sp_poly_shl` on either name answered a new String the other never saw, as did a callee's append through either. The same held for a String a yielding method's POLY parameter yields to a block that appends to it (#6475 lifted a POLY variable only where a call hands it to an appending parameter). The source of an alias write is lifted into the handle where either name is appended to in place, and so is the argument a yielding method binds to a parameter it yields to such a block, so that both names hold the one String. A lifted read makes its variable appended in turn, which carries the rule along a chain of aliases. */ static int lift_poly_alias_reads(Compiler *c, const HandleArgTab *hat) { const NodeTable *nt = c->nt; int changed = 0, nr = 0; nt_nodes_of_kind(nt, NK_LocalVariableReadNode, &nr); SbMutTab lifted; sb_mut_tab_init(&lifted, nr); NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, r) { if (!c->poly_strbuf_lift[r]) continue; const char *rn = nt_str(nt, r, "name"); Scope *rs = rn ? comp_scope_of(c, r) : NULL; if (rs) sb_mut_tab_note(&lifted, rn, (int)(rs - c->scopes), 1); } static const NodeKind wkinds[3] = { NK_LocalVariableWriteNode, NK_LocalVariableOrWriteNode, NK_LocalVariableAndWriteNode }; for (int round = 1; round; ) { round = 0; /* `y = x`, `y ||= x`, and a chain `z = y = x`, whose every name holds the value of the innermost read */ for (int wk = 0; wk < 3; wk++) NT_FOREACH_KIND(nt, wkinds[wk], w) { int v = w, app = 0, poly = 1; while (poly && v >= 0) { NodeKind vk = nt_kind(nt, v); if (vk == NK_ParenthesesNode) { int pb = nt_ref(nt, v, "body"), bn = 0; const int *bb = pb >= 0 ? nt_arr(nt, pb, "body", &bn) : NULL; v = bn == 1 ? bb[0] : -1; continue; } /* With the sharing rule, +x aliases a mutable String. The emitted sp_poly_uplus still copies a frozen receiver after it is lifted. */ if (vk == NK_CallNode && nt_str(nt, v, "name") && is_unary_plus(nt_str(nt, v, "name")) && nt_ref(nt, v, "arguments") < 0) { v = nt_ref(nt, v, "receiver"); continue; } if (vk == NK_InstanceVariableReadNode && v != w) { poly = comp_ntype(c, v) == TY_POLY; break; } /* `y = id(x)`: a method that answers its argument as it is hands y the String x holds (a single such argument) */ if (vk == NK_CallNode && v != w) { int ra[2]; if (sa_returned_args_t(c, v, ra, 2, 1) == 1 && nt_kind(nt, ra[0]) == NK_LocalVariableReadNode) { v = ra[0]; continue; } v = -1; break; } if (vk != NK_LocalVariableReadNode && (v != w && vk != NK_LocalVariableWriteNode)) { v = -1; break; } const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; LocalVar *vl = vs ? scope_local(vs, vn) : NULL; poly = vl && vl->type == TY_POLY; if (poly && poly_var_appended(c, &lifted, vn, vs)) app = 1; if (vk == NK_LocalVariableReadNode) break; v = nt_ref(nt, v, "value"); } if (!poly || !app || v < 0 || v == w) continue; if (lift_poly_read(c, hat, &lifted, v)) round = changed = 1; } /* `s = o.to_s` where the program appends to s in place: a boxed to_s answers the String in o's box as the handle (repr_boxed_to_s_operand), so o is lifted into the handle as `s = o` would lift it, and a parameter o pulls its callers in. A plain box would answer a copy. */ NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, w) { int v = nt_paren_single(nt, nt_ref(nt, w, "value")); const char *wn = nt_str(nt, w, "name"); int r = v >= 0 && nt_kind(nt, v) == NK_CallNode ? nt_ref(nt, v, "receiver") : -1; if (r < 0 || !wn || nt_kind(nt, r) != NK_LocalVariableReadNode || !is_to_s_name(nt_str(nt, v, "name")) || nt_ref(nt, v, "block") >= 0 || call_plain_argc(c, v) != 0 || !strbuf_any_str_mut(c, wn, comp_scope_of(c, w)) || !boxed_to_s_recv_ok(c, hat, r)) continue; if (lift_poly_read(c, hat, &lifted, r)) round = changed = 1; } /* `@s = x` (or `@@s`, `$s`) where the program appends to that variable (`@s << y`, or a read of it lifted for a parameter appended to): the variable is another name for x's String, as `y = x` is, so x is lifted and a parameter x pulls its callers in. Left as a copy, the append landed in the variable's String only, the caller's never saw it, and nothing refused. */ static const NodeKind skinds[3][2] = { { NK_InstanceVariableWriteNode, NK_InstanceVariableReadNode }, { NK_ClassVariableWriteNode, NK_ClassVariableReadNode }, { NK_GlobalVariableWriteNode, NK_GlobalVariableReadNode } }; for (int sk = 0; sk < 3; sk++) { PolyStoreApp app_tab = { NULL, NULL, 0, 0 }; int built = 0; NT_FOREACH_KIND(nt, skinds[sk][0], w) { int v = nt_ref(nt, w, "value"); const char *in = nt_str(nt, w, "name"); if (v < 0 || !in || nt_kind(nt, v) != NK_LocalVariableReadNode || c->poly_strbuf_lift[v]) continue; int wp = poly_store_place(c, w, skinds[sk][0], in); if (wp < 0) continue; if (!built) { poly_store_app_build(c, skinds[sk][1], &app_tab); built = 1; } if (!poly_store_appended(&app_tab, in, wp)) continue; if (lift_poly_read(c, hat, &lifted, v)) round = changed = 1; } free(app_tab.nm); free(app_tab.place); } /* `self.box = x` through an attr_writer, where the program appends to the attribute (`box << y`): the instance variable is another name for x's String, as in the stores above */ { PolyStoreApp app_tab = { NULL, NULL, 0, 0 }; int built = 0; NT_FOREACH_KIND(nt, NK_CallNode, w) { const char *wn = nt_str(nt, w, "name"); char base[256]; if (!setter_base_name(wn, base, sizeof base)) continue; int wa = nt_ref(nt, w, "arguments"), wc = 0; const int *wv = wa >= 0 ? nt_arr(nt, wa, "arguments", &wc) : NULL; if (wc != 1 || nt_kind(nt, wv[0]) != NK_LocalVariableReadNode || c->poly_strbuf_lift[wv[0]]) continue; int wp = poly_attr_place(c, w, base, 1); if (wp < 0) continue; if (!built) { poly_store_app_build(c, NK_InstanceVariableReadNode, &app_tab); built = 1; } if (!poly_store_appended(&app_tab, base, wp)) continue; if (lift_poly_read(c, hat, &lifted, wv[0])) round = changed = 1; } free(app_tab.nm); free(app_tab.place); } /* `def yl(v) = yield(v)` called `yl(x) { |t| t << s }`: the block's parameter is another name for the caller's variable. A `b.call(v)` on the method's own `&b` is spliced as the yield is. */ for (int pass = 0; pass < 2; pass++) NT_FOREACH_KIND(nt, pass ? NK_CallNode : NK_YieldNode, y) { Scope *m = comp_scope_of(c, y); if (!m || !m->name || !m->yields || !hat->ok) continue; if (pass && !poly_spliced_block_call(c, m, y)) continue; int mi = (int)(m - c->scopes); int aa = nt_ref(nt, y, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode) break; if (ak != NK_LocalVariableReadNode) continue; int pj = an_param_idx(m, nt_str(nt, av[k], "name")); LocalVar *q = pj >= 0 ? scope_local(m, m->pnames[pj]) : NULL; if (!q || !q->is_param || q->is_block_param || q->type != TY_POLY) continue; for (int e = hat->head[mi]; e >= 0; e = hat->enext[e]) { int u = hat->enode[e], blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; const char *bp = block_param_name(c, blk, k); Scope *bs = bp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, bp) : NULL; if (!t || t->type != TY_POLY || !poly_var_appended(c, &lifted, bp, bs)) continue; if (lift_poly_read(c, hat, &lifted, arg_layout_param_node(c, m, u, pj, NULL))) round = changed = 1; } } } } sb_mut_tab_free(&lifted); return changed; } int spread_string_reads(Compiler *c, Scope *m, int call, int pj, int *out, int *direct, int cap); static int dyn_pull_arg(Compiler *c, int a, int mark_read); static int fwd_poly_param_handed_on(Compiler *c, int mi, int pj, int depth); /* Local `vn` of scope `vs` lent to byref slots by calls in that scope: each such parameter, with every method of its name, takes the shared handle instead (a slot cannot carry it). Answers 1 when it changed any. */ static int an_unlend_local(Compiler *c, const char *vn, Scope *vs) { const NodeTable *nt = c->nt; int changed = 0; if (!vn || !vs) return 0; int vsi = (int)(vs - c->scopes); for (int u = comp_scall_first(c, vsi); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || comp_scope_of(c, u) != vs) continue; if (an_call_recv_builtin(c, u)) continue; /* lends to no method of the program's */ int mi = an_any_scope_by_name(c, nt_str(nt, u, "name")); if (mi < 0) continue; for (int j = 0; j < c->scopes[mi].nparams; j++) { if (!comp_byref_param(c, &c->scopes[mi], j)) continue; int a2 = arg_layout_param_node(c, &c->scopes[mi], u, j, NULL); const char *an2 = a2 >= 0 && nt_kind(nt, a2) == NK_LocalVariableReadNode ? nt_str(nt, a2, "name") : NULL; if (!an2 || !sp_streq(an2, vn)) continue; const char *gname = c->scopes[mi].name; for (int m = 1; m < c->nscopes; m++) { Scope *mk = &c->scopes[m]; if (!mk->name || !sp_streq(mk->name, gname) || j >= mk->nparams) continue; LocalVar *q = mk->pnames[j] ? scope_local(mk, mk->pnames[j]) : NULL; if (!q || !q->byref_out) continue; { q->byref_out = 0; comp_touch_scope(q->owner); } { q->is_cell = 0; comp_touch_scope(q->owner); } lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } changed = 1; } } } return changed; } /* Plain local `vn` of scope `vs` handed to a parameter that is the shared handle: the local takes the handle too, so the callee's appends are its String's. Answers 1 when it changed anything. */ static int pull_local_into_handle(Compiler *c, LocalVar *alv, const char *vn, Scope *vs) { int changed = 0; /* already the handle: nothing below changes, and the shape check walks the scope's calls, once per call site and per round */ if (!alv || (alv->type == TY_STRBUF && alv->str_shared)) return 0; /* Also lent to a byref slot (`app(s)` beside this call): a slot cannot carry the handle, so that parameter takes the handle instead, with its whole name group (an_unlend_local), as at a dynamic call (dyn_pull_arg). Left lent, the local stayed a plain String, which this call took a copy of. */ if (!strbuf_slot_eligible_shape(c, vn, vs, alv) && an_unlend_local(c, vn, vs)) changed = 1; if (!strbuf_slot_eligible_shape(c, vn, vs, alv)) return changed; if (alv->type != TY_UNKNOWN && alv->type != TY_STRING && alv->type != TY_STRBUF && alv->type != TY_POLY) return changed; if (alv->type != TY_POLY && (alv->type != TY_STRBUF || !alv->str_shared)) { lv_set_type(alv, TY_STRBUF); { alv->str_shared = 1; comp_touch_scope(alv->owner); } changed = 1; } return changed; } static int convert_byref_handle_params(Compiler *c, const HandleArgTab *hat) { const NodeTable *nt = c->nt; int changed = 0; /* the call-site chain is what makes this pass affordable; with no table there is nothing to walk, and promoting nothing is the safe answer */ if (!hat->ok) return 0; ALocalAliases aliases; an_local_aliases_build(c, &aliases); /* byref -> handle parameter conversion: a shared handle passed into a string-mutating (byref) parameter converts that parameter to the handle representation -- byref's const char** slot cannot carry the handle, so the mutation silently landed in a discarded copy. Once converted, every other call site's plain-local argument joins the shared set (so its own caller still observes the mutation, as with byref). */ for (int mi2 = 1; mi2 < c->nscopes; mi2++) { Scope *m2 = &c->scopes[mi2]; if (!m2->name || m2->nparams <= 0) continue; for (int pj = 0; pj < m2->nparams; pj++) { if (!m2->pnames[pj]) continue; LocalVar *pp = scope_local(m2, m2->pnames[pj]); if (!pp) continue; int is_handle = (pp->is_param && pp->type == TY_STRBUF && pp->str_shared); /* A POLY parameter the callee mutates in place takes the same pull: the call sites disagreed on the argument's shape (a reader here, a hash element there), so inference widened the parameter instead of making it a handle. A handle arriving at one call site still means every other call site has to hand one over -- boxed as a plain string, the plain local was copied and the caller never saw the append. */ int poly_mut = (pp->is_param && pp->type == TY_POLY && ((pp->poly_lift & POLY_LIFT_APPENDED) || an_param_mutated_in_place(c, mi2, pj) || an_poly_param_yielded_lent(c, mi2, pj) || fwd_poly_param_handed_on(c, mi2, pj, 0))); if (poly_mut && !(pp->poly_lift & POLY_LIFT_APPENDED)) { pp->poly_lift |= POLY_LIFT_APPENDED; changed = 1; } /* A String parameter the callee mutates that inference typed from a handle argument (the copy-on-read refinement, not the handle): it never passed through the byref ABI this pass converts, so it is converted the same way here -- read as a copy, the appends to a keyword's handle stayed in the callee's. */ int strbuf_mut = (pp->is_param && pp->type == TY_STRBUF && !pp->str_shared && an_param_mutated_in_place(c, mi2, pj)); /* --share-strings: a by-value String parameter the callee mutates (a name group that could not lend its slot) is a copy, which a handle argument must not be handed: it converts as a lent one does */ if (!strbuf_mut && pp->is_param && !pp->is_block_param && !pp->byref_out && pp->type == TY_STRING && an_param_mutated_in_place(c, mi2, pj)) strbuf_mut = 1; if (!pp->byref_out && !is_handle && !poly_mut && !strbuf_mut) continue; /* one pass over this method's call sites: detect a handle arg, and (once converted) pull plain-local args into the shared set */ /* A POLY parameter the callee mutates in place boxes whatever it is handed, and a plain String boxed is a copy: every call site's String local has to hand the handle over, whether or not another already does -- `m(v)` into `def m(p) = p << "x"` widened by `m([])`, or by the boxed elements `m(*[], v)` gathers, lost the append. */ int saw_handle = is_handle || poly_mut; for (int e = hat->head[mi2]; e >= 0; e = hat->enext[e]) { int u = hat->enode[e]; int ua = arg_layout_param_node(c, m2, u, pj, NULL); if (ua < 0) { /* filled from a splat's elements or gathered into the rest: a String there that is the handle makes the parameter one, since a lent slot cannot ride in the Array the call builds */ int sr[32], sd[32]; int ns = spread_string_reads(c, m2, u, pj, sr, sd, 32); for (int i = 0; i < ns && !saw_handle; i++) if (an_arg_is_shared_handle(c, sr[i])) saw_handle = 1; continue; } if (an_arg_is_shared_handle(c, ua)) saw_handle = 1; /* an ALIASED plain-local argument also demands the handle: the callee's mutation must stay visible through the caller's alias (byref reassigns only the one slot) */ else if (nt_kind(nt, ua) == NK_LocalVariableReadNode) { const char *avn = nt_str(nt, ua, "name"); Scope *avs = avn ? comp_scope_of(c, ua) : NULL; LocalVar *alv0 = avs ? scope_local(avs, avn) : NULL; /* shape check WITHOUT the byref-arg clause: being this byref param's argument is exactly the situation we are converting */ if (alv0 && !alv0->is_param && !alv0->is_cell && (alv0->type == TY_STRING || alv0->type == TY_UNKNOWN || alv0->type == TY_STRBUF) && an_local_has_alias(c, &aliases, avn, avs)) saw_handle = 1; /* A growable buffer that is not (yet) the shared handle -- an append accumulator, or a parameter inference typed from a handle argument -- is an sp_String * all the same: it has no const char * slot to lend, so the call lent a temp of its bytes and the callee's append stayed there. */ else if (alv0 && !alv0->is_block_param && alv0->type == TY_STRBUF) saw_handle = 1; } } if (!saw_handle) continue; if (pp->byref_out || strbuf_mut) { { pp->byref_out = 0; comp_touch_scope(pp->owner); } { pp->is_cell = 0; comp_touch_scope(pp->owner); } lv_set_type(pp, TY_STRBUF); { pp->str_shared = 1; comp_touch_scope(pp->owner); } changed = 1; } /* pull the remaining plain-local args into the shared set */ for (int e = hat->head[mi2]; e >= 0; e = hat->enext[e]) { int u = hat->enode[e]; int an2 = arg_layout_param_node(c, m2, u, pj, NULL); if (an2 < 0) { int sr[32], sd[32]; int ns = spread_string_reads(c, m2, u, pj, sr, sd, 32); for (int i = 0; i < ns; i++) if (dyn_pull_arg(c, sr[i], sd[i])) changed = 1; continue; } if (nt_kind(nt, an2) == NK_LocalVariableReadNode) { const char *vn2 = nt_str(nt, an2, "name"); Scope *vs2 = vn2 ? comp_scope_of(c, an2) : NULL; LocalVar *alv = vs2 ? scope_local(vs2, vn2) : NULL; /* A POLY variable -- a local, a parameter, a block's parameter -- holds a boxed value, and a plain String in the box is a copy: the callee appended to its own and the caller's variable kept the old String (#6179). Its read is lifted instead: a plain String it holds becomes the handle and is stored back before the call (sp_poly_strbuf_lift), so both sides hold the one String. A method's own POLY parameter handed on this way is appended to as much as one it appends to itself, so its callers are pulled in on the next round (poly_lift). */ if (alv && alv->type == TY_POLY) { if (c->poly_strbuf_lift[an2] || !poly_var_may_hold_string(c, hat, vn2, vs2, 0,NULL)) continue; c->poly_strbuf_lift[an2] = 1; changed = 1; if (alv->is_param && !alv->is_block_param && an_param_idx(vs2, vn2) >= 0 && !(alv->poly_lift & POLY_LIFT_APPENDED)) { alv->poly_lift |= POLY_LIFT_APPENDED; changed = 1; } continue; } /* The argument is this method's OWN parameter, being passed on: `def via(k, y) k.take(y) end`. It has to become a handle for the same reason the callee's did, and then this loop's next round reaches via's own call sites, so the chain carries however deep it runs. strbuf_slot_eligible_shape turns parameters away -- that is what defers them to this pass -- so the shape check does not apply to them here (#4363). */ if (alv && alv->is_param && !alv->is_block_param && an_param_idx(vs2, vn2) >= 0 && (alv->type == TY_UNKNOWN || alv->type == TY_STRING || alv->type == TY_STRBUF)) { if (alv->type != TY_STRBUF || !alv->str_shared) { /* a lent slot's cell goes with it, as above */ if (alv->byref_out) { alv->is_cell = 0; comp_touch_scope(alv->owner); } lv_set_type(alv, TY_STRBUF); { alv->str_shared = 1; comp_touch_scope(alv->owner); } { alv->byref_out = 0; comp_touch_scope(alv->owner); } changed = 1; } continue; } if (pull_local_into_handle(c, alv, vn2, vs2)) changed = 1; } else if (nt_kind(nt, an2) == NK_InstanceVariableReadNode && comp_ntype(c, an2) == TY_POLY) { /* a POLY ivar boxes a plain String as a copy too: its read is lifted as a POLY local's is */ if (lift_poly_read(c, hat, NULL, an2)) changed = 1; } else if (nt_kind(nt, an2) == NK_InstanceVariableReadNode) { const char *vn2 = nt_str(nt, an2, "name"); int cid2 = vn2 ? comp_ivar_owner(c, an2) : -1; if (cid2 >= 0 && strbuf_ivar_mut_kind(c, cid2, vn2) >= 0) if (strbuf_promote_ivar(c, cid2, vn2)) changed = 1; /* into a boxed parameter, or a handle one a gathering call binds out of an Array of boxes, the read boxes the handle itself (its mark, as a container store's); unmarked, the box held a copy */ int iv4 = cid2 >= 0 ? comp_ivar_index(&c->classes[cid2], vn2) : -1; if ((pp->type == TY_POLY || (pp->type == TY_STRBUF && pp->str_shared)) && iv4 >= 0 && c->classes[cid2].ivar_str_shared[iv4] && !c->strbuf_box[an2]) { comp_set_strbuf_box(c, an2, 1); comp_sn_retype(c, an2, TY_STRBUF); changed = 1; } } /* a global holding the handle (--share-strings) hands it over as an ivar's read does */ else if (repr_static_read_kind(nt_kind(nt, an2)) && repr_static_share(c, an2) && (pp->type == TY_POLY || repr_of_slot(c, pp).share) && !c->strbuf_box[an2]) { comp_set_strbuf_box(c, an2, 1); comp_sn_retype(c, an2, TY_STRBUF); changed = 1; } /* `K.new(obj.reader)`: the reader has to hand out the HANDLE, or the new holder and `obj` walk away with two strings. The P5 rule makes exactly this mark, but only ever looked at a local write. Marking alone is not enough here: this pass runs after the node-type cache is finalized, and comp_ntype answers from the cache, so the caller would build a `const char *` temp out of an `sp_String *`. The cached type is corrected with the mark, which is what makes the two agree (#4363). */ else if (nt_kind(nt, an2) == NK_CallNode) { char ivbuf2[300]; int defc2 = -1; const char *ivn2 = an_reader_ivar_of(c, an2, &defc2, ivbuf2, sizeof ivbuf2); if (ivn2 && defc2 >= 0 && strbuf_ivar_mut_kind(c, defc2, ivn2) >= 0) { if (strbuf_promote_ivar(c, defc2, ivn2)) changed = 1; int iv3 = comp_ivar_index(&c->classes[defc2], ivn2); if (iv3 >= 0 && c->classes[defc2].ivar_str_shared[iv3] && !c->strbuf_box[an2]) { comp_set_strbuf_box(c, an2, 1); comp_sn_retype(c, an2, TY_STRBUF); changed = 1; } } } } } } /* A `super` is a call site the table does not list: the parameter it hands on and the parent's parameter it lands in are one String, so a handle on either side makes both handles. Left a lent slot, the parent took the handle's copy, and a bare `super` from a method whose caller passed a handle lost the parent's appends. */ for (int pass = 0; pass < 2; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int q = comp_kind_first(c, sk); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != sk) continue; Scope *s = comp_scope_of(c, q); if (!s || !s->name || s->class_id < 0) continue; int mi = a_super_target(c, s); if (mi < 0) continue; Scope *pm = &c->scopes[mi]; for (int j = 0; j < pm->nparams; j++) { int pi = -1, an = -1; if (pass) pi = zsuper_param_source(c, s, pm, j); else { an = arg_layout_param_node(c, pm, q, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode) pi = an_param_idx(s, nt_str(nt, an, "name")); else an = -1; } if (!pm->pnames[j]) continue; /* A POLY parent parameter appended to in place takes a POLY variable the super hands it the way a call's does: the read is lifted into the handle (a bare super lifts this method's parameter itself), and a parameter of this method handed on is appended to as well, so its callers are pulled in on the next round. */ LocalVar *pdst = scope_local(pm, pm->pnames[j]); if (pdst && pdst->is_param && pdst->type == TY_POLY && (pdst->poly_lift & POLY_LIFT_APPENDED)) { const char *vn3 = pass ? (pi >= 0 ? s->pnames[pi] : NULL) : (an >= 0 ? nt_str(nt, an, "name") : NULL); Scope *vs3 = pass ? s : (an >= 0 ? comp_scope_of(c, an) : NULL); LocalVar *v3 = vn3 && vs3 ? scope_local(vs3, vn3) : NULL; if (v3 && v3->type == TY_POLY && poly_var_may_hold_string(c, hat, vn3, vs3, 0,NULL)) { if (!pass && !c->poly_strbuf_lift[an]) { c->poly_strbuf_lift[an] = 1; changed = 1; } if (pass && v3->is_param && !(v3->poly_lift & POLY_LIFT_ZSUPER)) { v3->poly_lift |= POLY_LIFT_ZSUPER; changed = 1; } if (v3->is_param && !v3->is_block_param && an_param_idx(vs3, vn3) >= 0 && !(v3->poly_lift & POLY_LIFT_APPENDED)) { v3->poly_lift |= POLY_LIFT_APPENDED; changed = 1; } } } /* a plain local `super(v)` hands the parent's handle parameter is pulled in as a call's argument is */ if (pi < 0 && an >= 0 && pdst && pdst->is_param && pdst->type == TY_STRBUF && pdst->str_shared && !pdst->byref_out) { const char *vn3 = nt_str(nt, an, "name"); Scope *vs3 = comp_scope_of(c, an); LocalVar *v3 = vs3 ? scope_local(vs3, vn3) : NULL; if (v3 && !v3->is_param && !v3->is_block_param && pull_local_into_handle(c, v3, vn3, vs3)) changed = 1; } if (pi < 0) continue; LocalVar *dst = scope_local(pm, pm->pnames[j]), *src = scope_local(s, s->pnames[pi]); if (!dst || !src || !dst->is_param || !src->is_param || src->is_block_param) continue; int dh = dst->type == TY_STRBUF && dst->str_shared; int sh = src->type == TY_STRBUF && src->str_shared; if (dh == sh) continue; LocalVar *o = dh ? src : dst; if (o->type != TY_STRING && o->type != TY_STRBUF) continue; if (o->byref_out) { { o->byref_out = 0; comp_touch_scope(o->owner); } { o->is_cell = 0; comp_touch_scope(o->owner); } } lv_set_type(o, TY_STRBUF); { o->str_shared = 1; comp_touch_scope(o->owner); } changed = 1; } } } /* A name group lends the slot as one (an_byref_promote_group), so it leaves the slot as one too. The rules above turn a single method's parameter into the handle -- the one a POLY parameter's pull reaches -- and left its same-named siblings lending theirs. A dispatch over the group then hoisted the argument in one ABI for both: a bare `show_into(io)` in Base, overridden by a Sub whose parameter became the handle, handed the Sub the lent `const char **` and the C build stopped. Every lending member at that index takes the handle, and the next round pulls its callers in (#6065). */ /* one pass collects, per name, the indices some member takes as the handle; a second converts the lending members at those indices */ ANameHash hnames; memset(&hnames, 0, sizeof hnames); unsigned *hbits = NULL; int hcap = 0; for (int k = 1; k < c->nscopes; k++) { Scope *mk = &c->scopes[k]; if (!mk->name) continue; unsigned bits = 0; for (int j = 0; j < mk->nparams && j < 32; j++) { LocalVar *q = mk->pnames[j] ? scope_local(mk, mk->pnames[j]) : NULL; if (q && q->is_param && q->type == TY_STRBUF && q->str_shared && !q->byref_out) bits |= 1u << j; } if (!bits) continue; int h = anh_find(&hnames, mk->name); if (h < 0) { if (hnames.n == hcap) { hcap = hcap ? hcap * 2 : 64; unsigned *nb = (unsigned *)realloc(hbits, sizeof(unsigned) * (size_t)hcap); if (!nb) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } hbits = nb; } anh_add(&hnames, mk->name); h = hnames.n - 1; hbits[h] = 0; } hbits[h] |= bits; } for (int k = 1; hnames.n > 0 && k < c->nscopes; k++) { Scope *mk = &c->scopes[k]; if (!mk->name || mk->nparams <= 0) continue; int h = -1; for (int j = 0; j < mk->nparams && j < 32; j++) { LocalVar *q = mk->pnames[j] ? scope_local(mk, mk->pnames[j]) : NULL; if (!q || !q->byref_out) continue; if (h < 0 && (h = anh_find(&hnames, mk->name)) < 0) break; if (!(hbits[h] & (1u << j))) continue; { q->byref_out = 0; comp_touch_scope(q->owner); } { q->is_cell = 0; comp_touch_scope(q->owner); } lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } changed = 1; } } an_local_aliases_free(&aliases); anh_free(&hnames); free(hbits); return changed; } /* ---- A String handed to a proc, a lambda or a Method (#6179) ---------- `f.call(s)`, `f.(s)`, `f[s]`, `f === s` and `m.call(s)` go through one type-erased entry: the caller boxes each argument into _sp_proc_poly_args and lays its plain value beside it in the sp_int slot, and the target reads whichever its parameter's type wants. A plain String went over as its bytes either way, so a target that appended to it grew a copy, and the caller's String never changed where CRuby changes the one object both names hold. A String variable handed to such a call becomes the shared handle (#3227) when a target the call can reach appends to the parameter it binds; the call then boxes the handle, and the target's parameter reads the box and appends to it in place. The question is asked per target, not per program, as Matz ruled on #6179: a proc in a hot loop that only reads its String keeps the plain `const char *`, and so does every caller of it. A call's targets are what its receiver can hold: - a proc literal (`->`, `lambda {}`, `proc {}`, `Proc.new {}`) or a block a method keeps as `&blk`: the body appends to the parameter (`<<`, concat, insert, a `!` method, ...) or hands it on to a user method whose parameter there is lent, the handle, or appended to; - `method(:m)` / `obj.method(:m)` / `public_method`, and `to_proc` of one: the method it names; - a local: every value written to it; a method's `&blk`: the blocks its call sites pass. Anything else is unknown, and an unknown target counts as appending only when the program has some target that appends at all, so a program with none keeps its C. */ #define DYN_ARGS 16 /* the proc-call ABI's cap (sp_int[16]) */ #define DYN_DONE 0x80000000u /* memo entry computed this pass */ /* Per pass, what each proc literal, kept block and method does with its positional parameters: bits 0-15 appended, bits 16-29 read for more than their bytes (kept, returned, handed on; a position past 13 always counts as kept). The answers depend on parameter types the fixpoint is still settling, so the memo is thrown away at the start of every pass; within one pass each body is walked once, however many call sites reach it. */ static struct { unsigned *lit; /* per node: a proc literal or a block */ unsigned *meth; /* per scope: a method */ unsigned *blk; /* per scope: the blocks its call sites pass as `&blk` */ int nlit, nscope; int any; /* -1 not asked yet, else "some target appends" */ ANameHash bnames; /* call names that pass a block, and their calls */ int *bhead, *bnext, bcap, bn, bbuilt; int *bnode; ANameHash snames; /* method names, and the scopes of each (snext chains) */ int *shead, *snext, sbuilt; int fresh; /* 0 once analysis has changed types since the memo filled */ unsigned *ctor; /* per scope: an initialize's appended and kept parameters (ctor_append_bits) */ int ctor_any; /* -1 not asked yet, else "some initialize parameter is the handle" */ int kw_any; /* -1 not asked yet, else "some target appends to a keyword parameter" */ /* dyn_may_callable's memos: 0 not asked, 1 never a proc, 2 may be, 3 being asked */ unsigned char *callable; /* per node */ unsigned char *pcall; /* per scope and positional parameter */ unsigned char *rcall; /* per scope: what it returns */ /* and its indexes, built once per pass: every call by name, every return by scope, the names a Symbol or String argument spells, and whether the program reaches methods by a name it computes */ ANameHash cnames; int *chead, *cnext, *rhead, *rnext, ccap, ibuilt, icount, open_names, returns_open; ANameHash lnames; unsigned char *litpost; /* per node: 0 not asked, 1 no post appended, 2 a post appended (dyn_lit_post_app) */ unsigned char *blkpost; /* per scope: the same for the blocks its call sites pass as `&blk` */ /* dyn_call_target_mi's memo: per call, its method and the edit count it was asked at (+1, 0 for none), kept while the table and the scopes are the ones it was filled for */ int *tgt; unsigned long *tgt_at; unsigned tgt_ver; int tgt_count, tgt_nscopes; } g_dyn; static void dyn_memo_reset(Compiler *c) { free(g_dyn.lit); free(g_dyn.meth); free(g_dyn.blk); free(g_dyn.ctor); free(g_dyn.callable); free(g_dyn.pcall); free(g_dyn.rcall); anh_free(&g_dyn.cnames); anh_free(&g_dyn.lnames); free(g_dyn.chead); free(g_dyn.cnext); free(g_dyn.rhead); free(g_dyn.rnext); free(g_dyn.litpost); free(g_dyn.blkpost); anh_free(&g_dyn.bnames); free(g_dyn.bhead); free(g_dyn.bnext); free(g_dyn.bnode); anh_free(&g_dyn.snames); free(g_dyn.shead); free(g_dyn.snext); free(g_dyn.tgt); free(g_dyn.tgt_at); g_dyn.tgt = NULL; g_dyn.tgt_at = NULL; memset(&g_dyn, 0, sizeof g_dyn); g_dyn.nlit = c->nt->count; g_dyn.nscope = c->nscopes; g_dyn.lit = (unsigned *)calloc((size_t)g_dyn.nlit + 1, sizeof(unsigned)); g_dyn.meth = (unsigned *)calloc((size_t)g_dyn.nscope + 1, sizeof(unsigned)); g_dyn.blk = (unsigned *)calloc((size_t)g_dyn.nscope + 1, sizeof(unsigned)); g_dyn.ctor = (unsigned *)calloc((size_t)g_dyn.nscope + 1, sizeof(unsigned)); g_dyn.callable = (unsigned char *)calloc((size_t)g_dyn.nlit + 1, 1); g_dyn.pcall = (unsigned char *)calloc(((size_t)g_dyn.nscope + 1) * DYN_ARGS, 1); g_dyn.rcall = (unsigned char *)calloc((size_t)g_dyn.nscope + 1, 1); g_dyn.litpost = (unsigned char *)calloc((size_t)g_dyn.nlit + 1, 1); g_dyn.blkpost = (unsigned char *)calloc((size_t)g_dyn.nscope + 1, 1); if (!g_dyn.lit || !g_dyn.meth || !g_dyn.blk || !g_dyn.ctor || !g_dyn.callable || !g_dyn.pcall || !g_dyn.rcall || !g_dyn.litpost || !g_dyn.blkpost) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_dyn.any = -1; g_dyn.ctor_any = -1; g_dyn.kw_any = -1; g_dyn.fresh = 1; } /* an_call_target_mi, kept per call: what it reads (the call's name and receiver, the receiver's type, the methods and scopes) stays while no type moved (comp_ty_edits), the table is the one it was asked of and no scope was added. A yield of a spliced method asks it of every call of the method's name, once per yield: at 400 classes each with its own yielding method and calls, a quarter of a million asks of 800 answers. */ static int dyn_call_target_mi(Compiler *c, int u) { const NodeTable *nt = c->nt; if (u < 0 || u >= nt->count) return an_call_target_mi(c, u); if (!g_dyn.tgt || g_dyn.tgt_count != nt->count || g_dyn.tgt_ver != nt->version || g_dyn.tgt_nscopes != c->nscopes) { free(g_dyn.tgt); free(g_dyn.tgt_at); g_dyn.tgt = malloc(sizeof(int) * (size_t)nt->count); g_dyn.tgt_at = calloc((size_t)nt->count, sizeof(unsigned long)); if (!g_dyn.tgt || !g_dyn.tgt_at) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_dyn.tgt_count = nt->count; g_dyn.tgt_ver = nt->version; g_dyn.tgt_nscopes = c->nscopes; } if (g_dyn.tgt_at[u] == comp_ty_edits + 1) return g_dyn.tgt[u]; int mi = an_call_target_mi(c, u); g_dyn.tgt[u] = mi; g_dyn.tgt_at[u] = comp_ty_edits + 1; return mi; } /* A proc-creating node: a lambda literal, or proc / lambda / Proc.new with a literal block. */ static int dyn_is_proc_literal(Compiler *c, int n) { const NodeTable *nt = c->nt; if (n < 0) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_LambdaNode) return 1; if (k != NK_CallNode) return 0; const char *nm = nt_str(nt, n, "name"); int b = nt_ref(nt, n, "block"); if (!nm || b < 0 || nt_kind(nt, b) != NK_BlockNode) return 0; int r = nt_ref(nt, n, "receiver"); if (r < 0) return is_proc_constructor(nm); return sp_streq(nm, "new") && nt_kind(nt, r) == NK_ConstantReadNode && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), "Proc"); } /* The lambda desugar_block_capture_wrap wraps an inlined block's body in, `{ |a| ->(__cap_a) { body }.call(a) }`, so a proc the body makes captures a fresh cell per iteration: Spinel's own, called where it is made, with the block's own parameter. It is no dynamic call of the program's and no target one can reach: a String in it binds as it did before the wrap (#6179 leaves it alone; sharing it is the block parameter's question). */ static int dyn_cap_wrapper(Compiler *c, int n) { return n >= 0 && nt_kind(c->nt, n) == NK_LambdaNode && nt_int(c->nt, n, "cap_iife", 0); } /* A String method that answers a new value and keeps nothing of its receiver: a parameter only ever the receiver of these (or printed, or interpolated) is read for its bytes alone, so the caller may hand it the live bytes of a handle rather than a copy. Anything not listed counts as keeping it (`to_s`, `itself` and `freeze` answer the receiver itself). With a Regexp argument (rx) all but `match?` leave the receiver's bytes in the match globals (`$~`, the captures), which read them after the String grows: such a call keeps its receiver. */ static int dyn_pure_read_name(const char *n, int rx) { static const char *const R[] = { "size", "length", "bytesize", "empty?", "==", "!=", "eql?", "<=>", "=~", "match?", "start_with?", "end_with?", "include?", "index", "rindex", "count", "getbyte", "ord", "upcase", "downcase", "capitalize", "swapcase", "reverse", "strip", "lstrip", "rstrip", "chomp", "chop", "chars", "bytes", "lines", "split", "to_i", "to_f", "to_sym", "hash", "inspect", "+", "*", "%", "[]", "slice", "center", "ljust", "rjust", "tr", "delete", "squeeze", "sub", "gsub", "scan", "unpack", "unpack1", "sum", "casecmp", "casecmp?", "ascii_only?", "valid_encoding?", "each_char", "each_byte", "each_line", "hex", "oct", NULL }; static const char *const NOGLOBALS[] = { "match?", NULL }; for (int i = 0; n && R[i]; i++) { if (!sp_streq(n, R[i])) continue; for (int g = 0; rx && NOGLOBALS[g]; g++) if (sp_streq(n, NOGLOBALS[g])) rx = 0; return !rx; } return 0; } static int dyn_name_at(const char **pn, int np, const char *vn) { for (int k = 0; vn && k < np; k++) if (pn[k] && sp_streq(pn[k], vn)) return k; return -1; } /* Which of the names pn[0..np) a body appends to (*app) and reads for more than its bytes (*kept). A name handed on to a user method whose parameter there is lent, the handle or appended to counts as appended too: the method appends to the one String. */ static int dyn_scopes_named(Compiler *c, const char *nm); /* Is parameter j of method mi lent, the handle, or appended to? */ static int dyn_param_appended(Compiler *c, int mi, int j) { Scope *m = &c->scopes[mi]; LocalVar *q = j < m->nparams && m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; return q && (q->byref_out || (q->type == TY_STRBUF && q->str_shared) || an_param_mutated_in_place(c, mi, j) || (q->type == TY_POLY && (q->poly_lift & POLY_LIFT_APPENDED))); } /* Does call `call` into method mi bind argument `arg` to a parameter the method appends to? The argument is placed by the call's own layout (arg_layout_param_node), so a splat or a keyword ahead of it does not shift it onto another parameter. An argument the layout cannot place (it rides a splat's positions) counts when any parameter of the method is appended to. */ static int dyn_scope_appends_arg(Compiler *c, int mi, int call, int arg) { Scope *m = &c->scopes[mi]; int any = 0; for (int j = 0; j < m->nparams; j++) { int an = arg_layout_param_node(c, m, call, j, NULL); if (an == arg) return dyn_param_appended(c, mi, j); if (!any && dyn_param_appended(c, mi, j)) any = 1; } const NodeTable *nt = c->nt; int a = nt_ref(nt, call, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; return nt_argv_has_splat(nt, av, ac) ? any : 0; } /* Does a call in a proc's or method's body hand the argument `arg` on to a user method that appends to the parameter it binds? The callee is the one the receiver names: self's class for a bare call, an object's class, a class constant's class method. A receiver the analysis cannot pin (a boxed value, a method's value) may be any method of the name, and counts when one of them appends to what the argument binds; a read-only method of the name does not. A typed builtin receiver is not a user method's. */ static int dyn_call_hands_on(Compiler *c, int call, const char *un, int ur, int arg) { const NodeTable *nt = c->nt; int mi = -1; if (ur < 0 || nt_kind(nt, ur) == NK_SelfNode) { Scope *encl = comp_scope_of(c, call); if (encl && encl->class_id >= 0) mi = call_target(c, call, encl->class_id, un, encl->is_cmethod ? CT_CMETHOD : 0, NULL); } else { NodeKind rk = nt_kind(nt, ur); TyKind rt = comp_ntype(c, ur); int ci = (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) && nt_str(nt, ur, "name") ? const_node_class_index(c, ur) : -1; if (ci >= 0) mi = call_target(c, call, ci, un, CT_CMETHOD, NULL); else if (ty_is_object(rt)) mi = call_target(c, call, ty_object_class(rt), un, 0, NULL); else if (rt != TY_POLY && rt != TY_UNKNOWN) return 0; } if (mi >= 0) return dyn_scope_appends_arg(c, mi, call, arg); for (int m = dyn_scopes_named(c, un); m >= 0; m = g_dyn.snext[m]) if (dyn_scope_appends_arg(c, m, call, arg)) return 1; return 0; } /* An index assignment (`x[i] = v`) that cannot be a String's. `[]=` is on the String-mutator table because `s[0] = "a"` rewrites a String, but the name is every container's too, and a block parameter carries no type to ask: a kept block doing `acc[:k] = 1` on a Hash was counted as "appends to the String it is handed", and from then on every unresolved String `yield` in the program was refused -- including ones in the bundled net/http -- for a String nothing ever touched. String#[]= takes an Integer, Range, String or Regexp index and a String value, so a Symbol index, or a value that is written as a literal of another class, says the receiver is not a String. */ static int dyn_index_write_not_string(Compiler *c, int node) { const NodeTable *nt = c->nt; int a = nt_ref(nt, node, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac < 2) return 0; for (int i = 0; i < ac - 1; i++) if (nt_kind(nt, av[i]) == NK_SymbolNode) return 1; switch (nt_kind(nt, av[ac - 1])) { case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_TrueNode: case NK_FalseNode: case NK_NilNode: case NK_ArrayNode: case NK_HashNode: return 1; default: break; } /* a value the inference already settled as a non-String scalar */ TyKind vt = comp_ntype(c, av[ac - 1]); if (vt == TY_INT || vt == TY_FLOAT || vt == TY_BOOL) return 1; /* the receiver itself, when the inference knows it for a container: the memo of `each_with_object({}) { |pair, result| result[k] = v }` is a Hash whatever `k` and `v` are */ int ur = nt_ref(nt, node, "receiver"); TyKind rt = ur >= 0 ? comp_ntype(c, ur) : TY_UNKNOWN; return ty_is_hash(rt) || ty_is_array(rt); } /* `recv.each_with_object()` whose memo is spelled as a container: a Hash or Array literal, or `Hash.new` / `Array.new` (with or without arguments or a default block). */ static int dyn_memo_is_container(Compiler *c, int call, const char *nm) { const NodeTable *nt = c->nt; /* by the time this runs the call may already be the Enumerable desugar (`__enum_each_with_object__N(recv, memo)`); the memo is the last argument either way */ if (!nm || (!sp_streq(nm, "each_with_object") && strncmp(nm, "__enum_each_with_object", 23) != 0)) return 0; int a = nt_ref(nt, call, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac < 1) return 0; int memo = av[ac - 1]; NodeKind k = nt_kind(nt, memo); if (k == NK_HashNode || k == NK_ArrayNode) return 1; if (k != NK_CallNode) return 0; const char *mn = nt_str(nt, memo, "name"); int r = nt_ref(nt, memo, "receiver"); if (!mn || !sp_streq(mn, "new") || r < 0 || nt_kind(nt, r) != NK_ConstantReadNode) return 0; const char *cn = nt_str(nt, r, "name"); return cn && (sp_streq(cn, "Hash") || sp_streq(cn, "Array")); } static void dyn_body_scan(Compiler *c, int node, const char **pn, int np, unsigned *app, unsigned *kept) { const NodeTable *nt = c->nt; if (node < 0) return; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return; if (k == NK_LocalVariableReadNode) { int j = dyn_name_at(pn, np, nt_str(nt, node, "name")); if (j >= 0) *kept |= 1u << j; return; } /* `"#{t}"` copies the bytes */ if (k == NK_EmbeddedStatementsNode) { int st = nt_ref(nt, node, "statements"), sn = 0; const int *sv = st >= 0 ? nt_arr(nt, st, "body", &sn) : NULL; if (sn == 1 && nt_kind(nt, sv[0]) == NK_LocalVariableReadNode && dyn_name_at(pn, np, nt_str(nt, sv[0], "name")) >= 0) return; } int skip_recv = -1; if (k == NK_CallNode) { const char *un = nt_str(nt, node, "name"); int ur = nt_ref(nt, node, "receiver"); /* `(+t) << x` appends to t: unary plus answers the receiver itself unless frozen */ int up = nt_paren_single(nt, ur); if (up >= 0 && up != ur && nt_kind(nt, up) == NK_CallNode && nt_str(nt, up, "name") && sp_streq(nt_str(nt, up, "name"), "+@") && nt_ref(nt, up, "receiver") >= 0) ur = nt_ref(nt, up, "receiver"); else if (ur >= 0 && nt_kind(nt, ur) == NK_CallNode && nt_str(nt, ur, "name") && sp_streq(nt_str(nt, ur, "name"), "+@") && nt_ref(nt, ur, "receiver") >= 0) ur = nt_ref(nt, ur, "receiver"); int j = ur >= 0 && nt_kind(nt, ur) == NK_LocalVariableReadNode ? dyn_name_at(pn, np, nt_str(nt, ur, "name")) : -1; if (j >= 0 && un && sp_streq(un, "[]=") && dyn_index_write_not_string(c, node)) skip_recv = ur; else if (j >= 0 && un && an_str_mutator_name(un)) { *app |= 1u << j; skip_recv = ur; } int a = nt_ref(nt, node, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int rx = 0; for (int i = 0; i < ac; i++) rx |= nt_kind(nt, av[i]) == NK_RegularExpressionNode || nt_kind(nt, av[i]) == NK_InterpolatedRegularExpressionNode || comp_ntype(c, av[i]) == TY_REGEX; if (j >= 0 && dyn_pure_read_name(un, rx)) skip_recv = ur; for (int i = 0; i < ac; i++) { if (nt_kind(nt, av[i]) != NK_LocalVariableReadNode) continue; int ji = dyn_name_at(pn, np, nt_str(nt, av[i], "name")); if (ji < 0) continue; /* printed: the bytes are all that is read */ if (ur < 0 && un && (is_text_print(un))) continue; *kept |= 1u << ji; if (un && dyn_call_hands_on(c, node, un, ur, av[i])) *app |= 1u << ji; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) { int ch = nt_ref_at(nt, node, i); if (ch == skip_recv || ch == a) continue; dyn_body_scan(c, ch, pn, np, app, kept); } /* the arguments, but for the ones settled above */ for (int i = 0; i < ac; i++) { if (nt_kind(nt, av[i]) == NK_LocalVariableReadNode && dyn_name_at(pn, np, nt_str(nt, av[i], "name")) >= 0) continue; dyn_body_scan(c, av[i], pn, np, app, kept); } return; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) dyn_body_scan(c, nt_ref_at(nt, node, i), pn, np, app, kept); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) dyn_body_scan(c, ids[j], pn, np, app, kept); } } const char *proc_param_name(Compiler *c, int create, int idx); int proc_opt_count(Compiler *c, int create); int proc_post_count(Compiler *c, int create); int proc_has_rest(Compiler *c, int create); /* The locals of scope `sc` that a write under `node` makes another name for local `vn` (`t = vn`, `t = (vn << x)`), and that are never another String (an_local_pure_alias_of): up to `cap` into `out`. */ static int dyn_alias_writes(Compiler *c, int node, Scope *sc, const char *vn, const char **out, int cap, int n) { const NodeTable *nt = c->nt; if (node < 0 || n >= cap) return n; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return n; if (k == NK_LocalVariableWriteNode && comp_scope_of(c, node) == sc) { int sv = an_strbuf_alias_source(c, nt_ref(nt, node, "value")); const char *wn = nt_str(nt, node, "name"); if (sv >= 0 && wn && nt_str(nt, sv, "name") && sp_streq(nt_str(nt, sv, "name"), vn) && !sp_streq(wn, vn) && an_local_pure_alias_of(c, (int)(sc - c->scopes), wn, vn, 0)) out[n++] = wn; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) n = dyn_alias_writes(c, nt_ref_at(nt, node, i), sc, vn, out, cap, n); int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, node, i, &m); for (int j = 0; j < m; j++) n = dyn_alias_writes(c, ids[j], sc, vn, out, cap, n); } return n; } static int dyn_lit_params(Compiler *c, int lit) { if (nt_kind(c->nt, lit) != NK_BlockNode) return a_proc_params_node(c, lit); int bp = nt_ref(c->nt, lit, "parameters"); return bp >= 0 && nt_kind(c->nt, bp) == NK_BlockParametersNode ? nt_ref(c->nt, bp, "parameters") : -1; } /* The parameter call position k of a proc literal or a block binds whatever the count, or NULL: a required one, or an optional after them unless required parameters follow (as dyn_method_nreq counts a method's). The requireds alone left `proc { |t = nil| t << x }` appending to a copy. */ static const char *dyn_lit_param_name(Compiler *c, int lit, int k) { const char *pn = proc_param_name(c, lit, k); if (pn) return pn; const NodeTable *nt = c->nt; int pnode = dyn_lit_params(c, lit); int rn = 0, on = 0, sn = 0; if (pnode < 0) return NULL; nt_arr(nt, pnode, "requireds", &rn); const int *ov = nt_arr(nt, pnode, "optionals", &on); nt_arr(nt, pnode, "posts", &sn); return sn == 0 && k >= rn && k - rn < on ? nt_str(nt, ov[k - rn], "name") : NULL; } /* The memo entry of a proc literal or a block: its positional parameters that a position binds whatever the count (dyn_lit_param_name). */ static unsigned dyn_lit_bits(Compiler *c, int lit) { if (lit < 0 || lit >= g_dyn.nlit) return 0; if (g_dyn.lit[lit] & DYN_DONE) return g_dyn.lit[lit]; const char *pn[DYN_ARGS]; int np = 0; while (np < DYN_ARGS && (pn[np] = dyn_lit_param_name(c, lit, np))) np++; unsigned app = 0, kept = 0; int body = nt_kind(c->nt, lit) == NK_BlockNode ? nt_ref(c->nt, lit, "body") : a_proc_body(c, lit); dyn_body_scan(c, body, pn, np, &app, &kept); /* An optional whose default is a required parameter (`->(x, y = x)`) is that parameter's String when the call omits it, so what the body does to it is done to the required one (promote_default_alias_params). */ const NodeTable *nt = c->nt; int pnode = dyn_lit_params(c, lit); int on = 0; const int *ov = pnode >= 0 ? nt_arr(nt, pnode, "optionals", &on) : NULL; for (int o = 0; o < on; o++) { int dv = nt_ref(nt, ov[o], "value"); int k = dv >= 0 && nt_kind(nt, dv) == NK_LocalVariableReadNode ? dyn_name_at(pn, np, nt_str(nt, dv, "name")) : -1; const char *on1[1] = { nt_str(nt, ov[o], "name") }; if (k < 0 || !on1[0]) continue; unsigned oapp = 0, okept = 0; dyn_body_scan(c, body, on1, 1, &oapp, &okept); if (oapp) app |= 1u << k; if (okept) kept |= 1u << k; } /* A local that names a parameter (`t = x`, or a chain through one) is that parameter's String: an append through it is one to the parameter, as an_param_mutated_in_place counts it for a method's */ for (int k = 0; k < np; k++) { if (app & (1u << k)) continue; const char *an[8]; int na = 0; an[na++] = pn[k]; for (int i = 0; i < na && !(app & (1u << k)); i++) { const char *wn[8]; int nw = dyn_alias_writes(c, body, comp_scope_of(c, lit), an[i], wn, 8, 0); for (int j = 0; j < nw && na < 8; j++) { int seen = 0; for (int q = 0; q < na && !seen; q++) seen = sp_streq(an[q], wn[j]); if (seen) continue; an[na++] = wn[j]; const char *on1[1] = { wn[j] }; unsigned aapp = 0, akept = 0; dyn_body_scan(c, body, on1, 1, &aapp, &akept); if (aapp) { app |= 1u << k; break; } } } } g_dyn.lit[lit] = DYN_DONE | (app & 0xffffu) | ((kept & 0x3fffu) << 16); return g_dyn.lit[lit]; } /* A post parameter of a proc literal or a block (`|*q, t|`, `|n = 0, t|`) binds by the call's count, not by a position of its own, so the entry above leaves it out. The post call position k binds in a call of n plain arguments (sp_proc_fill's distribution), or NULL. */ static const char *dyn_lit_post_name(Compiler *c, int lit, int k, int n) { const NodeTable *nt = c->nt; int pnode = dyn_lit_params(c, lit); if (pnode < 0 || k >= n) return NULL; int rn = 0, on = 0, sn = 0; nt_arr(nt, pnode, "requireds", &rn); nt_arr(nt, pnode, "optionals", &on); const int *sv = nt_arr(nt, pnode, "posts", &sn); if (sn == 0) return NULL; int ot, ps; block_fill(rn, on, sn, nt_ref(nt, pnode, "rest") >= 0, n, &ot, &ps); return k >= ps && k < ps + sn ? nt_str(nt, sv[k - ps], "name") : NULL; } /* Does the body of literal `lit` append to its parameter `pn`? */ static int dyn_lit_name_appended(Compiler *c, int lit, const char *pn) { const NodeTable *nt = c->nt; const char *on1[1] = { pn }; unsigned app = 0, kept = 0; int body = nt_kind(nt, lit) == NK_BlockNode ? nt_ref(nt, lit, "body") : a_proc_body(c, lit); dyn_body_scan(c, body, on1, 1, &app, &kept); return app != 0; } /* Does literal `lit` append to any of its post parameters? Memoized per pass, as its entry is. */ static int dyn_lit_post_app(Compiler *c, int lit) { const NodeTable *nt = c->nt; if (lit < 0 || lit >= g_dyn.nlit) return 0; if (g_dyn.litpost[lit]) return g_dyn.litpost[lit] == 2; int pnode = dyn_lit_params(c, lit); int sn = 0; const int *sv = pnode >= 0 ? nt_arr(nt, pnode, "posts", &sn) : NULL; int app = 0; for (int i = 0; i < sn && !app; i++) { const char *pn = nt_str(nt, sv[i], "name"); if (pn) app = dyn_lit_name_appended(c, lit, pn); } g_dyn.litpost[lit] = app ? 2 : 1; return app; } /* A method's leading positional parameters that call position k binds for k below this count, whatever the count: the required ones, and the optionals after them unless required parameters follow (`def f(a = 1, b)` or `def f(a = 1, *r, b)` hands the last argument to b first, so a short call moves the lead argument onto it). An optional counted only the required ones, and `def fill(s = nil)` reached through `method(:fill)` or `bind_call` appended to a copy (#6179). */ static int dyn_method_nreq(Compiler *c, Scope *m) { if (m->def_node < 0) return 0; const NodeTable *nt = c->nt; int pn = -1; if (nt_kind(nt, m->def_node) == NK_DefNode) pn = nt_ref(nt, m->def_node, "parameters"); else { /* a define_method body's scope hangs off the call: its block's parameters, requireds then optionals (collect_scopes); any other scope counts its own required ones */ int b = nt_ref(nt, m->def_node, "block"); if (b < 0 || nt_kind(nt, b) != NK_BlockNode) return m->nrequired < m->nparams ? m->nrequired : m->nparams; int bp = nt_ref(nt, b, "parameters"); int inner = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; pn = inner >= 0 ? inner : bp; } int rn = 0, on = 0, sn = 0; if (pn >= 0) { nt_arr(nt, pn, "requireds", &rn); nt_arr(nt, pn, "optionals", &on); nt_arr(nt, pn, "posts", &sn); } int n = sn > 0 ? rn : rn + on; return n < m->nparams ? n : m->nparams; } static unsigned dyn_meth_bits(Compiler *c, int mi) { if (mi < 0 || mi >= g_dyn.nscope) return 0; if (g_dyn.meth[mi] & DYN_DONE) return g_dyn.meth[mi]; Scope *m = &c->scopes[mi]; int np = dyn_method_nreq(c, m); if (np > DYN_ARGS) np = DYN_ARGS; unsigned app = 0, kept = 0; const char *pn[DYN_ARGS]; for (int j = 0; j < np; j++) { pn[j] = m->pnames[j]; LocalVar *q = pn[j] ? scope_local(m, pn[j]) : NULL; if (q && (q->byref_out || (q->type == TY_STRBUF && q->str_shared) || an_param_mutated_in_place(c, mi, j) || (q->type == TY_POLY && (q->poly_lift & POLY_LIFT_APPENDED)))) app |= 1u << j; } dyn_body_scan(c, m->body, pn, np, &app, &kept); g_dyn.meth[mi] = DYN_DONE | (app & 0xffffu) | ((kept & 0x3fffu) << 16); return g_dyn.meth[mi]; } /* Keyword arguments (`f.call(k1: s)`): the keyword parameter `key` names. A keyword matches by name, so a target's answer is the parameter of that name: appended (bit 0) and read for more than its bytes (bit 1), 0 when the target has no such keyword. A block's keyword may carry the shadow rename's suffix (#3679); the key is the name the program wrote. */ static int dyn_kw_name_is(const Compiler *c, const char *pn, const char *key) { if (!pn || !key) return 0; size_t n = block_param_is_renamed(c, pn) ? block_param_written_len(c, pn) : strlen(pn); return strlen(key) == n && !strncmp(pn, key, n); } static unsigned dyn_lit_kw_bits(Compiler *c, int lit, const char *key) { const NodeTable *nt = c->nt; int pnode = dyn_lit_params(c, lit); int kn = 0; const int *kv = pnode >= 0 ? nt_arr(nt, pnode, "keywords", &kn) : NULL; for (int i = 0; i < kn; i++) { const char *pn[1] = { nt_str(nt, kv[i], "name") }; if (!dyn_kw_name_is(c, pn[0], key)) continue; unsigned app = 0, kept = 0; int body = nt_kind(nt, lit) == NK_BlockNode ? nt_ref(nt, lit, "body") : a_proc_body(c, lit); dyn_body_scan(c, body, pn, 1, &app, &kept); return (app ? 1u : 0u) | (kept ? 2u : 0u); } return 0; } /* The same for method `mi`'s keyword parameter `key`; *j_out its index. */ int callee_param_is_declared_kwarg(Compiler *c, Scope *m, const char *name); static unsigned dyn_meth_kw_bits(Compiler *c, int mi, const char *key, int *j_out) { Scope *m = &c->scopes[mi]; if (j_out) *j_out = -1; for (int j = 0; j < m->nparams; j++) { if (!m->pnames[j] || !sp_streq(m->pnames[j], key ? key : "") || !callee_param_is_declared_kwarg(c, m, m->pnames[j])) continue; if (j_out) *j_out = j; unsigned app = 0, kept = 0; LocalVar *q = scope_local(m, m->pnames[j]); if (q && (q->byref_out || (q->type == TY_STRBUF && q->str_shared) || an_param_mutated_in_place(c, mi, j))) app = 1; const char *pn[1] = { m->pnames[j] }; dyn_body_scan(c, m->body, pn, 1, &app, &kept); return (app ? 1u : 0u) | (kept ? 2u : 0u); } return 0; } /* Does any proc literal, kept block or Method-named method append to a keyword parameter? What an unknown target answers for a keyword, as dyn_any_appender does for a position: a program with none keeps its C. */ static int dyn_lit_kw_any(Compiler *c, int lit) { const NodeTable *nt = c->nt; int pnode = dyn_lit_params(c, lit); int kn = 0; const int *kv = pnode >= 0 ? nt_arr(nt, pnode, "keywords", &kn) : NULL; for (int i = 0; i < kn; i++) { const char *pn = nt_str(nt, kv[i], "name"); char key[128]; size_t len = pn ? (block_param_is_renamed(c, pn) ? block_param_written_len(c, pn) : strlen(pn)) : 0; if (!pn || len >= sizeof key) continue; memcpy(key, pn, len); key[len] = 0; if (dyn_lit_kw_bits(c, lit, key) & 1u) return 1; } return 0; } static int dyn_block_targets(Compiler *c, int n, int *out); static int dyn_any_kw_appender(Compiler *c) { const NodeTable *nt = c->nt; if (g_dyn.kw_any >= 0) return g_dyn.kw_any; g_dyn.kw_any = 0; int *tg = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int n = comp_kind_first(c, NK_LambdaNode); n >= 0 && !g_dyn.kw_any; n = comp_kind_next(c, n)) if (nt_kind(nt, n) == NK_LambdaNode && !dyn_cap_wrapper(c, n) && dyn_lit_kw_any(c, n)) g_dyn.kw_any = 1; for (int n = comp_kind_first(c, NK_CallNode); n >= 0 && !g_dyn.kw_any; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode) continue; const char *nm = nt_str(nt, n, "name"); if (!nm) continue; int b = nt_ref(nt, n, "block"); if (dyn_is_proc_literal(c, n)) { if (dyn_lit_kw_any(c, b)) g_dyn.kw_any = 1; continue; } if (b >= 0 && nt_kind(nt, b) == NK_BlockNode) { /* the methods the block reaches, `new`'s initialize included (as dyn_any_appender asks it) */ if (!dyn_lit_kw_any(c, b)) continue; int nk = dyn_block_targets(c, n, tg); for (int e = 0; e < nk && !g_dyn.kw_any; e++) { Scope *m = &c->scopes[tg[e]]; if (m->blk_param && m->blk_param[0] && !m->yields) g_dyn.kw_any = 1; } continue; } if (!sp_streq(nm, "method") && !sp_streq(nm, "public_method")) continue; for (int mi = dyn_scopes_named(c, method_sym_arg(c, n)); mi >= 0 && !g_dyn.kw_any; mi = g_dyn.snext[mi]) { Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams && !g_dyn.kw_any; j++) if (callee_param_is_declared_kwarg(c, m, m->pnames[j]) && (dyn_meth_kw_bits(c, mi, m->pnames[j], NULL) & 1u)) g_dyn.kw_any = 1; } } free(tg); return g_dyn.kw_any; } /* The calls that pass a block, by name, built once per pass the first time a method's `&blk` is asked about. */ static void dyn_blk_index(Compiler *c) { const NodeTable *nt = c->nt; if (g_dyn.bbuilt) return; g_dyn.bbuilt = 1; for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode || nt_ref(nt, n, "block") < 0) continue; const char *nm = nt_str(nt, n, "name"); if (!nm) continue; int h = anh_find(&g_dyn.bnames, nm); if (h < 0) { anh_add(&g_dyn.bnames, nm); h = g_dyn.bnames.n - 1; g_dyn.bhead = (int *)realloc(g_dyn.bhead, sizeof(int) * (size_t)g_dyn.bnames.n); if (!g_dyn.bhead) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_dyn.bhead[h] = -1; } if (g_dyn.bn == g_dyn.bcap) { g_dyn.bcap = g_dyn.bcap ? g_dyn.bcap * 2 : 64; g_dyn.bnext = (int *)realloc(g_dyn.bnext, sizeof(int) * (size_t)g_dyn.bcap); g_dyn.bnode = (int *)realloc(g_dyn.bnode, sizeof(int) * (size_t)g_dyn.bcap); if (!g_dyn.bnext || !g_dyn.bnode) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } g_dyn.bnode[g_dyn.bn] = n; g_dyn.bnext[g_dyn.bn] = g_dyn.bhead[h]; g_dyn.bhead[h] = g_dyn.bn++; } } /* The methods call `n` hands its block to, into out[] (room for every scope and one more): the one its name names, and for a `new` the initialize methods it can reach (ctor_call_targets). Looked up by its name alone, a block `Agg.new { |t| t << x }` hands to an initialize that keeps it as `&b` found nothing and was never taken for a kept block: its parameter kept the value ABI, the program counted no appender, and `@b.call(s)` later appended to a copy. */ static int dyn_block_targets(Compiler *c, int n, int *out) { const char *nm = nt_str(c->nt, n, "name"); if (!nm) return 0; int k = 0; if (sp_streq(nm, "new")) { int first, boxed; k = ctor_call_targets(c, n, &first, &boxed, out, c->nscopes); } /* the method the name names, a `def new(&b)` on an object included -- none for a builtin value's call (an_call_recv_builtin) */ int mi = an_call_recv_builtin(c, n) ? -1 : an_any_scope_by_name(c, nm); for (int e = 0; e < k && mi >= 0; e++) if (out[e] == mi) mi = -1; if (mi >= 0) out[k++] = mi; return k; } /* Is scope mi one of them? */ static int dyn_block_reaches(Compiler *c, int n, int mi) { const char *nm = nt_str(c->nt, n, "name"); if (!nm || !c->scopes[mi].name) return 0; if (!sp_streq(nm, "new")) return sp_streq(nm, c->scopes[mi].name); int *tg = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int k = dyn_block_targets(c, n, tg), hit = 0; for (int e = 0; e < k && !hit; e++) hit = tg[e] == mi; free(tg); return hit; } /* The blocks a method's call sites pass as its `&blk`: the OR of their entries, with DYN_OPEN set when one passes something that is not a literal block, or none is found. An initialize's call sites are the `new` calls that reach it. */ #define DYN_OPEN 0x40000000u static void dyn_reach_value(Compiler *c, int v, int k, int depth, DynReach *r); /* Can call `n` not reach method mi of a user class? Its receiver is an Array or a Hash, which has the method only if Array or Hash is reopened or a module adds it: `@parts.each(&blk)` inside `def each(&blk)` is Array#each, not a call site of the method, and its `&blk` is not a block it is passed. */ static int dyn_site_misses(Compiler *c, int n, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; int recv0 = nt_ref(nt, n, "receiver"); NodeKind rk = recv0 >= 0 ? nt_kind(nt, recv0) : NK_SelfNode; int by_const = rk == NK_ConstantReadNode || rk == NK_ConstantPathNode; /* a receiver of a known class reaches only the method its class or a subclass answers with */ if (rk != NK_SelfNode && !by_const && ty_is_object(comp_ntype(c, recv0))) { int rc = ty_object_class(comp_ntype(c, recv0)); if (rc >= 0 && m->name) { if (m->is_cmethod) return 1; for (int k = 0; k < c->nclasses; k++) if (is_descendant(c, k, rc) && comp_method_in_chain(c, k, m->name, NULL) == mi) return 0; return 1; } } /* a class value never reaches an instance method of a class */ if (by_const && comp_ntype(c, recv0) == TY_CLASS && !m->is_cmethod && m->class_id >= 0 && c->classes[m->class_id].def_node >= 0 && nt_kind(nt, c->classes[m->class_id].def_node) == NK_ClassNode) return 1; if (m->is_cmethod || m->class_id < 0) return 0; ClassInfo *ci = &c->classes[m->class_id]; if (ci->def_node < 0 || nt_kind(nt, ci->def_node) != NK_ClassNode || is_builtin_class_name(ci->name)) return 0; int recv = nt_ref(nt, n, "receiver"); if (recv < 0) return 0; TyKind rt = comp_ntype(c, recv); return ty_is_array(rt) || ty_is_hash(rt); } /* Does a `method(:nm)` or `instance_method(:nm)` literal name the method? Its block then comes through `call`, `bind_call` or `to_proc`, which the by-name index of call sites does not see. */ static int dyn_named_by_method_obj(Compiler *c, const char *nm) { const NodeTable *nt = c->nt; for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode) continue; const char *cn = nt_str(nt, n, "name"); if (!cn || !(sp_streq(cn, "method") || sp_streq(cn, "public_method") || sp_streq(cn, "instance_method") || sp_streq(cn, "public_instance_method"))) continue; const char *sym = method_sym_arg(c, n); if (!sym || sp_streq(sym, nm)) return 1; } return 0; } static unsigned dyn_blk_bits(Compiler *c, int mi) { const NodeTable *nt = c->nt; if (mi < 0 || mi >= g_dyn.nscope) return DYN_OPEN; if (g_dyn.blk[mi] & DYN_DONE) return g_dyn.blk[mi]; /* a proc form (`m#pf`) is passed the blocks of the method it copies */ if (c->scopes[mi].is_proc_form) { int si = proc_form_source(c, mi); g_dyn.blk[mi] = DYN_DONE | DYN_OPEN; if (si < 0) return g_dyn.blk[mi]; g_dyn.blk[mi] = dyn_blk_bits(c, si); g_dyn.blkpost[mi] = g_dyn.blkpost[si]; return g_dyn.blk[mi]; } dyn_blk_index(c); unsigned bits = 0; int any = 0; const char *cn = c->scopes[mi].name; int ctor = cn && sp_streq(cn, "initialize") && !c->scopes[mi].is_cmethod; int h = cn ? anh_find(&g_dyn.bnames, ctor ? "new" : cn) : -1; /* open while the sites are walked: a `&blk` that leads back here stops */ g_dyn.blk[mi] = DYN_DONE | DYN_OPEN; if (cn && dyn_named_by_method_obj(c, cn)) return g_dyn.blk[mi]; for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int b = nt_ref(nt, g_dyn.bnode[e], "block"); if (ctor && !dyn_block_reaches(c, g_dyn.bnode[e], mi)) continue; if (!ctor && dyn_site_misses(c, g_dyn.bnode[e], mi)) continue; any = 1; /* `&pr`: what the procs it can hold do with each position */ if (nt_kind(nt, b) == NK_BlockArgumentNode && nt_ref(nt, b, "expression") >= 0) { for (int k = 0; k < DYN_ARGS && !(bits & DYN_OPEN); k++) { DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, nt_ref(nt, b, "expression"), k, 1, &r); if (r.unknown) bits |= DYN_OPEN; if (r.app) bits |= 1u << k; /* past 14 a position is kept anyway (dyn_fold) */ if (r.keeps && k < 14) bits |= 1u << (16 + k); } continue; } if (nt_kind(nt, b) != NK_BlockNode) { bits |= DYN_OPEN; continue; } unsigned lb = dyn_lit_bits(c, b); bits |= lb & 0x3fffffffu; if (dyn_lit_post_app(c, b)) g_dyn.blkpost[mi] = 2; } /* a method nothing calls is passed no block */ if (!any && c->scopes[mi].reachable) bits |= DYN_OPEN; g_dyn.blk[mi] = DYN_DONE | bits; return g_dyn.blk[mi]; } /* The first scope named `nm` (the rest follow g_dyn.snext), or -1: the scopes a `method(:nm)` literal can name, indexed once per pass rather than scanned per literal. */ static int dyn_scopes_named(Compiler *c, const char *nm) { if (!nm) return -1; if (!g_dyn.sbuilt) { g_dyn.sbuilt = 1; g_dyn.snext = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); g_dyn.shead = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!g_dyn.snext || !g_dyn.shead) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = c->nscopes - 1; i >= 1; i--) { const char *sn = c->scopes[i].name; g_dyn.snext[i] = -1; if (!sn) continue; int h = anh_find(&g_dyn.snames, sn); if (h < 0) { anh_add(&g_dyn.snames, sn); h = g_dyn.snames.n - 1; g_dyn.shead[h] = -1; } g_dyn.snext[i] = g_dyn.shead[h]; g_dyn.shead[h] = i; } } int h = anh_find(&g_dyn.snames, nm); return h >= 0 ? g_dyn.shead[h] : -1; } /* Is every method named nm one whose container return seed keeps the box and checks it (Scope.ret_rbs_cont), with no builtin and no attribute reader of the name? A call of it on any receiver then answers a Hash or an Array, or raises. */ static int dyn_rbs_cont_name(Compiler *c, const char *nm) { if (!nm || builtin_method_known_any(nm) || builtin_object_method_known(nm)) return 0; for (int k = 0; k < c->nclasses; k++) if (comp_reader_in_chain(c, k, nm, NULL)) return 0; int any = 0; for (int mi = dyn_scopes_named(c, nm); mi >= 0; mi = g_dyn.snext[mi]) { if (!c->scopes[mi].ret_rbs_cont) return 0; any = 1; } return any; } /* Is there any target at all that appends to a String parameter: a proc literal, a block kept as `&blk`, a method a `method(:m)` names? What an unknown target answers (see above). */ static int dyn_lit_rest_takes(Compiler *c, int lit, int k); static int dyn_any_appender(Compiler *c) { const NodeTable *nt = c->nt; if (g_dyn.any >= 0) return g_dyn.any; g_dyn.any = 0; /* a proc literal or a block appending to its rest's elements */ for (int n = comp_kind_first(c, NK_LambdaNode); n >= 0 && !g_dyn.any; n = comp_kind_next(c, n)) if (nt_kind(nt, n) == NK_LambdaNode && !dyn_cap_wrapper(c, n) && dyn_lit_rest_takes(c, n, DYN_ARGS)) g_dyn.any = 1; for (int n = comp_kind_first(c, NK_BlockNode); n >= 0 && !g_dyn.any; n = comp_kind_next(c, n)) if (nt_kind(nt, n) == NK_BlockNode && dyn_lit_rest_takes(c, n, DYN_ARGS)) g_dyn.any = 1; int *tg = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int n = comp_kind_first(c, NK_LambdaNode); n >= 0 && !g_dyn.any; n = comp_kind_next(c, n)) if (nt_kind(nt, n) == NK_LambdaNode && !dyn_cap_wrapper(c, n) && ((dyn_lit_bits(c, n) & 0xffffu) || dyn_lit_post_app(c, n))) g_dyn.any = 1; for (int n = comp_kind_first(c, NK_CallNode); n >= 0 && !g_dyn.any; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode) continue; const char *nm = nt_str(nt, n, "name"); if (!nm) continue; int b = nt_ref(nt, n, "block"); if (dyn_is_proc_literal(c, n)) { if ((dyn_lit_bits(c, b) & 0xffffu) || dyn_lit_post_app(c, b)) g_dyn.any = 1; continue; } if (b >= 0 && nt_kind(nt, b) == NK_BlockNode) { /* a block handed to a method that keeps it */ unsigned bits = dyn_lit_bits(c, b) & 0xffffu; /* `each_with_object({}) { |x, memo| memo[k] = v }`: the block's second parameter IS the memo the call names, so when that is written as a Hash or Array it is no String, whatever `k` and `v` turn out to be. Only the builtin: a user method of the same name hands its second parameter whatever it likes (a String it yields), and clearing the bit for it would drop a real appender, so a copy would lose the append silently. The Enumerable desugar's own specialization (`__enum_each_with_object__N`, builtins/enumerable.rb) is a scope too, but it is the builtin: its memo is the call's last argument. */ if (dyn_memo_is_container(c, n, nm) && (strncmp(nm, "__enum_each_with_object", 23) == 0 || an_any_scope_by_name(c, nm) < 0)) bits &= ~2u; if (!bits && !dyn_lit_post_app(c, b)) continue; int nk = dyn_block_targets(c, n, tg); for (int e = 0; e < nk && !g_dyn.any; e++) { Scope *m = &c->scopes[tg[e]]; if (m->blk_param && m->blk_param[0] && !m->yields) g_dyn.any = 1; } continue; } /* an UnboundMethod names its method too (`instance_method(:m).bind(o)`, then `to_proc.curry`, an open site) */ if (!sp_streq(nm, "method") && !sp_streq(nm, "public_method") && !sp_streq(nm, "instance_method") && !sp_streq(nm, "public_instance_method")) continue; for (int mi = dyn_scopes_named(c, method_sym_arg(c, n)); mi >= 0 && !g_dyn.any; mi = g_dyn.snext[mi]) if (dyn_meth_bits(c, mi) & 0xffffu) g_dyn.any = 1; } free(tg); return g_dyn.any; } /* Fold one target's memo entry, position k, into the reach. `open` says a later position of the target depends on the count. `ptype` is the parameter's slot type, when the target is known well enough to have one: a boxed or handle parameter reads the box, never the sp_int slot. */ static void dyn_fold(DynReach *r, unsigned bits, int k, int bound, int open, TyKind ptype) { if (!bound) { if (open) r->unknown = 1; return; } if (bits & (1u << k)) r->app = 1; int reads_box = ptype == TY_POLY || ptype == TY_STRBUF; if (!reads_box && (k >= 14 || (bits & (1u << (16 + k))))) r->keeps = 1; } /* Is method mi the wrapper a Method of one of Kernel's printers was retargeted at (`method(:p)`, `method(:puts)`: its def's bam_sym), with no method of that name in the program and no String#inspect or #to_s of its own? Such a Method neither changes nor keeps the String it is handed (#9207). */ static int dyn_method_prints(Compiler *c, int mi) { int dn = mi >= 0 && mi < c->nscopes ? c->scopes[mi].def_node : -1; const char *sym = dn >= 0 ? nt_str(c->nt, dn, "bam_sym") : NULL; static const char *const printers[] = { "p", "pp", "puts", "print", "warn", NULL }; int printer = 0; for (int i = 0; sym && printers[i]; i++) if (sp_streq(sym, printers[i])) printer = 1; if (!printer || comp_method_index(c, sym) >= 0) return 0; int si = comp_core_class(c, CORE_STRING); return si < 0 || (comp_method_in_chain(c, si, "inspect", NULL) < 0 && comp_method_in_chain(c, si, "to_s", NULL) < 0); } static void dyn_reach_method_node(Compiler *c, int mn, int k, DynReach *r) { const NodeTable *nt = c->nt; /* an UnboundMethod (`instance_method(:m)`, bound later with `bind` or called with `bind_call`) names its method as a Method does */ int mi = method_obj_target_mi(c, mn); if (mi < 0) { r->unknown = 1; return; } if (dyn_method_prints(c, mi)) return; /* a bound builtin's wrapper (`"ab".method(:center)`, `a.method(:push)`) may keep what it is handed (push stores it) */ if (method_call_param_shift(c, mn, mi)) { r->keeps = 1; return; } Scope *m = &c->scopes[mi]; int nreq = dyn_method_nreq(c, m); LocalVar *q = k < nreq && m->pnames[k] ? scope_local(m, m->pnames[k]) : NULL; dyn_fold(r, dyn_meth_bits(c, mi), k, k < nreq, m->nparams > nreq, q ? q->type : TY_UNKNOWN); if (k < nreq && !r->mname) { r->mname = m->name; r->pname = m->pnames[k]; } } const char *proc_rest_name(Compiler *c, int create); static int dyn_lit_rest_takes(Compiler *c, int lit, int k); /* Does a block one of method mi's call sites passes as its `&blk` take position k into a rest it appends to the elements of? */ static int dyn_blk_rest_takes(Compiler *c, int mi, int k) { const NodeTable *nt = c->nt; const char *cn = c->scopes[mi].name; if (!cn) return 0; dyn_blk_index(c); int h = anh_find(&g_dyn.bnames, cn); for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int b = nt_ref(nt, g_dyn.bnode[e], "block"); if (nt_kind(nt, b) == NK_BlockNode && dyn_lit_rest_takes(c, b, k)) return 1; } return 0; } /* A rest whose elements the body of a proc literal or a block appends to (`proc { |*q| q[0] << x }`) gathers the arguments past its leading parameters into an Array of their boxes: a String variable there has to be the handle, which the call boxes as itself, or the append lands in a copy (#6179). The positions it may take are those past the leading requireds, whatever the count. */ static int rest_elems_mutated_walk(Compiler *c, int node, const char *rn, int depth); static int dyn_lit_rest_takes(Compiler *c, int lit, int k) { const NodeTable *nt = c->nt; const char *rn = proc_rest_name(c, lit); if (!rn || !rn[0]) return 0; int pnode = dyn_lit_params(c, lit); int rn_req = 0; if (pnode >= 0) nt_arr(nt, pnode, "requireds", &rn_req); if (k < rn_req) return 0; int body = nt_kind(nt, lit) == NK_BlockNode ? nt_ref(nt, lit, "body") : a_proc_body(c, lit); return rest_elems_mutated_walk(c, body, rn, 0); } /* ...for what a call's receiver `v` can hold: a literal, or a method's `&blk`, whose call sites pass the blocks. */ static int dyn_rest_takes(Compiler *c, int v, int k) { const NodeTable *nt = c->nt; if (v < 0) return 0; NodeKind vk = nt_kind(nt, v); if (dyn_is_proc_literal(c, v) || vk == NK_BlockNode) return dyn_lit_rest_takes(c, vk == NK_CallNode ? nt_ref(nt, v, "block") : v, k); if (vk != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; if (!vs || !vs->name || !vs->blk_param || !sp_streq(vs->blk_param, vn)) return 0; return dyn_blk_rest_takes(c, (int)(vs - c->scopes), k); } static int dyn_name_in(const char *s, const char *const *list) { for (int i = 0; list[i]; i++) if (sp_streq(s, list[i])) return 1; return 0; } /* A value position of an if, an unless or parentheses: a missing arm is nil, which nothing calls, and a walk that gave up may be anything. An arm is a smaller subtree, not a step through a name: no depth. */ typedef struct { Compiler *c; int k, depth; DynReach *r; } DynArms; static int dyn_reach_arm(const VpLeaf *leaf, void *ud) { DynArms *a = ud; if (leaf->kind == VPK_GAVE_UP) a->r->unknown = 1; else if (leaf->kind != VPK_NIL) dyn_reach_value(a->c, leaf->node, a->k, a->depth, a->r); return 0; } static void dyn_reach_value(Compiler *c, int v, int k, int depth, DynReach *r) { const NodeTable *nt = c->nt; if (v < 0 || depth > 4) { r->unknown = 1; return; } NodeKind vk = nt_kind(nt, v); if (dyn_rest_takes(c, v, k)) r->app = 1; if (dyn_is_proc_literal(c, v) || vk == NK_BlockNode) { int lit = vk == NK_CallNode ? nt_ref(nt, v, "block") : v; const char *pn = dyn_lit_param_name(c, lit, k); /* a post the call's count binds there: it reads the boxed channel, so only whether the body appends to it matters */ const char *qn = !pn && r->argc1 > 0 ? dyn_lit_post_name(c, lit, k, r->argc1 - 1) : NULL; if (qn) { if (dyn_lit_name_appended(c, lit, qn)) r->app = 1; if (!r->pname) r->pname = qn; return; } int open = proc_opt_count(c, v) > 0 || proc_has_rest(c, v) || proc_post_count(c, v) > 0; Scope *bs = comp_scope_of(c, v); LocalVar *q = pn && bs ? scope_local(bs, pn) : NULL; dyn_fold(r, dyn_lit_bits(c, vk == NK_CallNode ? nt_ref(nt, v, "block") : v), k, pn != NULL, open, q ? q->type : TY_UNKNOWN); if (pn && !r->pname) r->pname = pn; return; } /* a value chosen at run time: each arm's */ if (vk == NK_IfNode || vk == NK_UnlessNode || vk == NK_ParenthesesNode) { DynArms a = { c, k, depth, r }; for_each_value_position(nt, v, VPS_IF | VPS_UNLESS, dyn_reach_arm, &a); return; } /* nil is never called; a Symbol is called as its to_proc, which sends the name to the argument: an append when the name is one of String's in-place methods, or a method of the program's own */ if (vk == NK_NilNode) return; if (vk == NK_SymbolNode) { const char *sv = nt_str(nt, v, "value"); static const char *const inplace[] = { "<<", "concat", "insert", "prepend", "replace", "clear", "[]=", "setbyte", "force_encoding", "encode!", NULL }; size_t sl = sv ? strlen(sv) : 0; if (!sv || (sl > 0 && sv[sl - 1] == '!') || dyn_name_in(sv, inplace) || comp_method_index(c, sv) >= 0) r->app = 1; return; } TyKind vt = comp_ntype(c, v); if (vt == TY_METHOD) { int *mns = NULL; int n = method_recv_nodes(c, v, &mns); if (n <= 0) r->unknown = 1; for (int i = 0; i < n; i++) dyn_reach_method_node(c, mns[i], k, r); free(mns); return; } if (vk == NK_CallNode) { const char *nm = nt_str(nt, v, "name"); if (nm && sp_streq(nm, "to_proc")) { int *mns = NULL; int n = proc_to_proc_method_nodes(c, v, &mns); if (n <= 0) dyn_reach_value(c, nt_ref(nt, v, "receiver"), k, depth + 1, r); for (int i = 0; i < n; i++) dyn_reach_method_node(c, mns[i], k, r); free(mns); return; } r->unknown = 1; return; } if (vk == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; if (!vs) { r->unknown = 1; return; } /* the method's own `&blk`: the blocks its call sites pass */ if (vs->name && vs->blk_param && sp_streq(vs->blk_param, vn)) { int mi = (int)(vs - c->scopes); unsigned bits = dyn_blk_bits(c, mi); if (bits & DYN_OPEN) r->unknown = 1; /* a block's parameter types are its own; the conservative answer for the slot is that it may keep what it reads */ dyn_fold(r, bits, k, 1, 0, TY_UNKNOWN); /* a post of one of the blocks may bind position k by the count */ if (g_dyn.blkpost[mi] == 2) r->app = 1; return; } /* every value written to it (a Method-valued local was taken above, by method_recv_nodes; a `m.to_proc` written here is the call's arm) */ int saw = 0; for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) { r->unknown = 1; continue; } saw = 1; dyn_reach_value(c, nt_ref(nt, w, "value"), k, depth + 1, r); } if (!saw) r->unknown = 1; return; } r->unknown = 1; } /* The same resolution for a keyword argument, asked by name: what the targets receiver `v` can hold do with keyword `key`. A keyword travels in the boxed Hash, never the sp_int slot, so whether a target keeps it does not matter here. */ static void dyn_reach_kw_method(Compiler *c, int mn, const char *key, DynReach *r) { /* an UnboundMethod names its method as a Method does (dyn_reach_method_node) */ int mi = method_obj_target_mi(c, mn); if (mi < 0 || method_call_param_shift(c, mn, mi)) { r->unknown = 1; return; } int j; unsigned bits = dyn_meth_kw_bits(c, mi, key, &j); if (j < 0) return; if (bits & 1u) r->app = 1; if (!r->mname) { r->mname = c->scopes[mi].name; r->pname = c->scopes[mi].pnames[j]; } } static void dyn_reach_kw(Compiler *c, int v, const char *key, int depth, DynReach *r) { const NodeTable *nt = c->nt; if (v < 0 || depth > 4) { r->unknown = 1; return; } NodeKind vk = nt_kind(nt, v); if (dyn_is_proc_literal(c, v) || vk == NK_BlockNode) { if (dyn_lit_kw_bits(c, vk == NK_CallNode ? nt_ref(nt, v, "block") : v, key) & 1u) { r->app = 1; if (!r->pname) r->pname = key; } return; } if (comp_ntype(c, v) == TY_METHOD) { int *mns = NULL; int n = method_recv_nodes(c, v, &mns); if (n <= 0) r->unknown = 1; for (int i = 0; i < n; i++) dyn_reach_kw_method(c, mns[i], key, r); free(mns); return; } if (vk == NK_CallNode) { const char *nm = nt_str(nt, v, "name"); if (nm && sp_streq(nm, "to_proc")) { int *mns = NULL; int n = proc_to_proc_method_nodes(c, v, &mns); if (n <= 0) dyn_reach_kw(c, nt_ref(nt, v, "receiver"), key, depth + 1, r); for (int i = 0; i < n; i++) dyn_reach_kw_method(c, mns[i], key, r); free(mns); return; } if (nm && sp_streq(nm, "curry")) { r->unlifted = "curry"; return; } r->unknown = 1; return; } if (vk == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; if (!vs) { r->unknown = 1; return; } /* the method's own `&blk`: the blocks its call sites pass */ if (vs->name && vs->blk_param && sp_streq(vs->blk_param, vn)) { dyn_blk_index(c); int h = anh_find(&g_dyn.bnames, vs->name), any = 0; for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int b = nt_ref(nt, g_dyn.bnode[e], "block"); any = 1; if (nt_kind(nt, b) != NK_BlockNode) { r->unknown = 1; continue; } dyn_reach_kw(c, b, key, depth + 1, r); } if (!any) r->unknown = 1; return; } int saw = 0; for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) { r->unknown = 1; continue; } saw = 1; dyn_reach_kw(c, nt_ref(nt, w, "value"), key, depth + 1, r); } if (!saw) r->unknown = 1; return; } r->unknown = 1; } /* The keyword arm of dyn_call_reach: what the targets of dynamic call `n` do with its keyword `key`. */ void dyn_call_kw_reach(Compiler *c, int n, const char *key, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); dyn_reach_kw(c, nt_ref(c->nt, n, "receiver"), key, 0, r); if (r->unknown && !r->app && dyn_any_kw_appender(c)) r->app = 1; } /* Does method `mi` append to its keyword `key`? *j_out: its index, or -1. */ int dyn_method_kw_appends(Compiler *c, int mi, const char *key, int *j_out) { if (!g_dyn.fresh) dyn_memo_reset(c); *j_out = -1; if (mi < 0 || mi >= c->nscopes || !key) return 0; return (int)(dyn_meth_kw_bits(c, mi, key, j_out) & 1u); } /* The keyword arm of dyn_value_reach: a value `v` (-1: unknown). */ void dyn_value_kw_reach(Compiler *c, int v, const char *key, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); if (v < 0) r->unknown = 1; else dyn_reach_kw(c, v, key, 0, r); if (r->unknown && !r->app && dyn_any_kw_appender(c)) r->app = 1; } /* The keyword a KeywordHashNode element binds by name: its Symbol key, when its value is a plain local read; NULL for anything else. */ const char *dyn_kw_elem_key(Compiler *c, int el, int *val) { const NodeTable *nt = c->nt; *val = -1; if (nt_kind(nt, el) != NK_AssocNode) return NULL; int key = nt_ref(nt, el, "key"); if (key < 0 || nt_kind(nt, key) != NK_SymbolNode) return NULL; *val = nt_ref(nt, el, "value"); return nt_str(nt, key, "value"); } /* A proc/Method call with the type-erased ABI: `.call`, `.()`, `[]`, `.yield`, `===` on a Proc or a Method value. A `&blk.call` in a method whose block is inlined at its call sites is a yield, and binds the block's parameter directly. In a method lowered to take its block as a proc (is_lowered_yield) it is a call on that proc like any other: `blk[x]` or `blk.yield(x)` beside a `yield` keeps the block out of line, and the String went over as a copy, neither pulled into the handle here nor refused, where a `yield` of it is (dyn_yield_site). */ int dyn_call_site(Compiler *c, int n) { const NodeTable *nt = c->nt; if (n < 0 || nt_kind(nt, n) != NK_CallNode) return 0; const char *nm = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"); if (!nm || r < 0) return 0; if (!is_proc_invoke(nm)) return 0; TyKind rt = comp_ntype(c, r); if (rt != TY_PROC && rt != TY_METHOD) return 0; if (dyn_cap_wrapper(c, r)) return 0; if (nt_kind(nt, r) == NK_LocalVariableReadNode) { Scope *rs = comp_scope_of(c, r); const char *rn = nt_str(nt, r, "name"); if (rs && rs->blk_param && rn && sp_streq(rs->blk_param, rn) && rs->yields && !rs->is_lowered_yield) return 0; } return 1; } /* A call through a callable the analysis cannot name, or an UnboundMethod's bind_call: `.call`, `.()`, `[]`, `.yield` or `===` on a boxed value (a Method or proc read out of a slot that holds other values too) or on a curried proc, which collects the boxes it is handed and passes them on; and `um.bind_call(o, s)`, whose first argument is the receiver (*shift). The String rides the box either way, so it is shared as at a dynamic call. */ /* May a boxed value `v` be a Proc or a Method? Followed back through its locals, a method's parameters (to the arguments its callers pass), what a method returns, and branches. Anything not followed may be one. A cycle counts as may-be, so an answer is never decided from a guess. */ static int dyn_may_callable(Compiler *c, int v, int depth); /* The indexes the walk reads, built once per pass. */ static void dyn_callable_index(Compiler *c) { const NodeTable *nt = c->nt; if (g_dyn.ibuilt) return; g_dyn.ibuilt = 1; /* sized by the table as it is now: codegen adds nodes after the memo was sized, and one past what is indexed here makes the walk give up */ g_dyn.icount = nt->count; g_dyn.cnext = (int *)malloc(sizeof(int) * ((size_t)g_dyn.icount + 1)); g_dyn.rnext = (int *)malloc(sizeof(int) * ((size_t)g_dyn.icount + 1)); g_dyn.rhead = (int *)malloc(sizeof(int) * ((size_t)g_dyn.nscope + 1)); if (!g_dyn.cnext || !g_dyn.rnext || !g_dyn.rhead) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i <= g_dyn.nscope; i++) g_dyn.rhead[i] = -1; for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { if (n >= g_dyn.icount) { g_dyn.open_names = 1; continue; } if (nt_kind(nt, n) != NK_CallNode) continue; const char *nm = nt_str(nt, n, "name"); if (!nm) continue; int h = anh_find(&g_dyn.cnames, nm); if (h < 0) { anh_add(&g_dyn.cnames, nm); h = g_dyn.cnames.n - 1; if (h >= g_dyn.ccap) { g_dyn.ccap = g_dyn.ccap ? g_dyn.ccap * 2 : 256; g_dyn.chead = (int *)realloc(g_dyn.chead, sizeof(int) * (size_t)g_dyn.ccap); if (!g_dyn.chead) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } g_dyn.chead[h] = -1; } g_dyn.cnext[n] = g_dyn.chead[h]; g_dyn.chead[h] = n; /* a method named by a Symbol or a String anywhere: send, method, define_method, alias_method, respond_to?... Every literal argument counts (`h["run"]` too): coarse, and safe */ int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); const char *lit = ak == NK_SymbolNode ? nt_str(nt, av[k], "value") : ak == NK_StringNode ? nt_str(nt, av[k], "content") : NULL; if (lit && anh_find(&g_dyn.lnames, lit) < 0) anh_add(&g_dyn.lnames, lit); } /* ... or by a name it computes */ static const char *const by_name[] = { "send", "__send__", "public_send", "method", "public_method", "instance_method", "public_instance_method", "define_method", "alias_method", "method_missing", NULL }; for (int k = 0; by_name[k]; k++) if (sp_streq(nm, by_name[k]) && ac > 0 && nt_kind(nt, av[0]) != NK_SymbolNode && nt_kind(nt, av[0]) != NK_StringNode) g_dyn.open_names = 1; } /* `super` hands its arguments to the method of the enclosing one's name, and an alias gives a method a second name: those names are open */ for (int pass = 0; pass < 2; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int n = comp_kind_first(c, sk); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != sk) continue; Scope *ss = comp_scope_of(c, n); if (!ss || !ss->name) { g_dyn.open_names = 1; continue; } if (anh_find(&g_dyn.lnames, ss->name) < 0) anh_add(&g_dyn.lnames, ss->name); } } for (int n = comp_kind_first(c, NK_AliasMethodNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_AliasMethodNode) continue; const char *names[2] = { NULL, NULL }; int nn = nt_ref(nt, n, "new_name"), on = nt_ref(nt, n, "old_name"); if (nn >= 0 && nt_kind(nt, nn) == NK_SymbolNode) names[0] = nt_str(nt, nn, "value"); if (on >= 0 && nt_kind(nt, on) == NK_SymbolNode) names[1] = nt_str(nt, on, "value"); if (!names[0] || !names[1]) { g_dyn.open_names = 1; continue; } for (int i = 0; i < 2; i++) if (anh_find(&g_dyn.lnames, names[i]) < 0) anh_add(&g_dyn.lnames, names[i]); } for (int r = comp_kind_first(c, NK_ReturnNode); r >= 0; r = comp_kind_next(c, r)) { if (r >= g_dyn.icount) { g_dyn.returns_open = 1; continue; } if (nt_kind(nt, r) != NK_ReturnNode) continue; Scope *rs = comp_scope_of(c, r); if (!rs) continue; int si = (int)(rs - c->scopes); g_dyn.rnext[r] = g_dyn.rhead[si]; g_dyn.rhead[si] = r; } } /* A value position of a method's tail: an absent arm or an empty body is nil, and a `return` is walked with the method's returns. A walk that gave up may be anything. */ typedef struct { Compiler *c; int depth; } DynTail; static int dyn_tail_leaf(const VpLeaf *leaf, void *ud) { const DynTail *t = ud; if (leaf->kind == VPK_NIL) return 0; if (leaf->kind == VPK_GAVE_UP) return 1; if (nt_kind(t->c->nt, leaf->node) == NK_ReturnNode) return 0; return dyn_may_callable(t->c, leaf->node, t->depth + 1); } static int dyn_tail_may_callable(Compiler *c, int n, int depth) { DynTail t = { c, depth }; return for_each_value_position(c->nt, n, VPS_IF | VPS_UNLESS | VPS_BEGIN, dyn_tail_leaf, &t); } /* What method mi returns: its body's last value and every `return`. */ static int dyn_returns_walk(Compiler *c, int mi, int depth) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (m->body < 0 || m->cs_synth || m->is_lowered_yield || g_dyn.returns_open) return 1; /* a define_method body, or one a method_missing could stand in for */ if (m->def_node < 0 || nt_kind(nt, m->def_node) != NK_DefNode) return 1; if (dyn_tail_may_callable(c, m->body, depth)) return 1; for (int r = g_dyn.rhead[mi]; r >= 0; r = g_dyn.rnext[r]) { int a = nt_ref(nt, r, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac == 1 && dyn_may_callable(c, av[0], depth + 1)) return 1; } return 0; } static int dyn_returns_may_callable(Compiler *c, int mi, int depth) { if (mi < 0 || mi >= g_dyn.nscope) return 1; /* a container seed's return is checked to be one when it returns */ if (mi < c->nscopes && c->scopes[mi].ret_rbs_cont) return 0; unsigned char *m = &g_dyn.rcall[mi]; if (*m == 3) return 1; if (*m) return *m == 2; *m = 3; int r = dyn_returns_walk(c, mi, depth); g_dyn.rcall[mi] = r ? 2 : 1; return r; } /* Can call `n` dispatch to method mi? Not when its receiver's type is one that does not have it: a builtin value, or an object whose class (or a subclass of it) has another. A method on Object, Kernel, a builtin class or a module can be anyone's. "Can" over-approximates on purpose: a subclass's method counts for a receiver typed as its parent. */ static int dyn_site_may_reach(Compiler *c, int n, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; if (m->is_cmethod || m->class_id < 0) return 1; ClassInfo *mc = &c->classes[m->class_id]; if (mc->def_node < 0 || nt_kind(nt, mc->def_node) != NK_ClassNode) return 1; if (is_builtin_class_name(mc->name) || sp_streq(mc->name, "Object") || sp_streq(mc->name, "BasicObject")) return 1; int recv = nt_ref(nt, n, "receiver"); int cls = -1; if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *encl = comp_scope_of(c, n); /* a class method's self is the class, whose methods are not scopes here */ if (!encl || encl->is_cmethod || encl->class_id < 0) return 1; cls = encl->class_id; } else { TyKind rt = comp_ntype(c, recv); if (rt == TY_POLY || rt == TY_UNKNOWN) return 1; if (!ty_is_object(rt)) return 0; cls = ty_object_class(rt); } /* mi's class is cls or one below it */ for (int k = m->class_id; k >= 0; k = c->classes[k].parent) if (k == cls) return 1; /* or cls inherits mi from a class above it */ return comp_method_in_chain(c, cls, m->name, NULL) == mi; } /* What parameter j of method mi can be handed: the argument each call that can reach it passes there. Only a method whose parameters are all plain required ones, in a program that names no method by a name it computes and nothing names this one by a literal. */ static int dyn_param_walk(Compiler *c, Scope *m, int j, int depth) { const NodeTable *nt = c->nt; int mi = (int)(m - c->scopes); if (!m->name || m->is_cmethod || sp_streq(m->name, "initialize")) return 1; if (m->nrequired != m->nparams || m->rest_idx >= 0 || m->kwrest_idx >= 0 || m->npost_rest) return 1; /* a method Ruby calls by itself has callers the source does not show: an operator (`case` calls ===, Comparable calls <=>), a setter (`||=`), and a few protocol names that take an argument */ static const char *const implicit[] = { "eql?", "coerce", "each", "method_missing", "respond_to_missing?", NULL }; size_t ml = strlen(m->name); if (!(isalpha((unsigned char)m->name[0]) || m->name[0] == '_') || name_is_plain_setter(m->name)) return 1; for (int i = 0; implicit[i]; i++) if (sp_streq(m->name, implicit[i])) return 1; if (g_dyn.open_names || anh_find(&g_dyn.lnames, m->name) >= 0) return 1; int h = anh_find(&g_dyn.cnames, m->name); for (int n = h >= 0 ? g_dyn.chead[h] : -1; n >= 0; n = g_dyn.cnext[n]) { if (!dyn_site_may_reach(c, n, mi)) continue; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); /* `...` has no NK_ kind of its own; its type name is the check */ if (ak == NK_SplatNode || ak == NK_KeywordHashNode || ak == NK_ForwardingArgumentsNode) return 1; } if (ac != m->nparams) continue; /* it raises ArgumentError instead */ if (dyn_may_callable(c, av[j], depth + 1)) return 1; } return 0; } static int dyn_param_may_callable(Compiler *c, Scope *m, int j, int depth) { int mi = (int)(m - c->scopes); if (mi < 0 || mi >= g_dyn.nscope || j < 0 || j >= DYN_ARGS) return 1; unsigned char *p = &g_dyn.pcall[(size_t)mi * DYN_ARGS + (size_t)j]; if (*p == 3) return 1; if (*p) return *p == 2; *p = 3; int r = dyn_param_walk(c, m, j, depth); g_dyn.pcall[(size_t)mi * DYN_ARGS + (size_t)j] = r ? 2 : 1; return r; } static int dyn_may_callable_walk(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; TyKind t = comp_ntype(c, v); if (t == TY_PROC || t == TY_METHOD || t == TY_CURRY) return 1; if (t != TY_POLY && t != TY_UNKNOWN) return 0; switch (nt_kind(nt, v)) { case NK_NilNode: case NK_HashNode: case NK_ArrayNode: case NK_StringNode: case NK_InterpolatedStringNode: case NK_IntegerNode: case NK_SymbolNode: case NK_TrueNode: case NK_FalseNode: return 0; case NK_ParenthesesNode: case NK_StatementsNode: case NK_IfNode: case NK_UnlessNode: case NK_BeginNode: return dyn_tail_may_callable(c, v, depth); case NK_AndNode: case NK_OrNode: return dyn_may_callable(c, nt_ref(nt, v, "left"), depth + 1) || dyn_may_callable(c, nt_ref(nt, v, "right"), depth + 1); case NK_LocalVariableReadNode: { const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->is_block_param || lv->is_cell) return 1; if (vs->blk_param && sp_streq(vs->blk_param, vn)) return 1; int saw = 0; if (lv->is_param) { int j = an_param_idx(vs, vn); if (j < 0 || dyn_param_may_callable(c, vs, j, depth)) return 1; saw = 1; } for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs) continue; const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) return 1; saw = 1; if (dyn_may_callable(c, nt_ref(nt, w, "value"), depth + 1)) return 1; } return !saw; } case NK_CallNode: { const char *nm = nt_str(nt, v, "name"); if (!nm || nt_ref(nt, v, "block") >= 0) return 1; /* on a boxed or builtin receiver it may be the builtin's answer, unless no builtin has the name and every method of it is a container return seed's, checked at its return (Scope.ret_rbs_cont) */ int recv = nt_ref(nt, v, "receiver"); if (recv >= 0 && nt_kind(nt, recv) != NK_SelfNode && !ty_is_object(comp_ntype(c, recv))) return !dyn_rbs_cont_name(c, nm); /* self in a class method is the class: its methods (a `class << self` accessor) are not walked */ if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *encl = comp_scope_of(c, v); if (!encl || encl->is_cmethod) return 1; } /* an attribute reader is a slot, which may hold anything */ for (int k = 0; k < c->nclasses; k++) if (comp_reader_in_chain(c, k, nm, NULL)) return 1; /* a user method only a subclass defines leaves the receiver's own class with the builtin's */ { int rcls = -1; if (recv >= 0 && nt_kind(nt, recv) != NK_SelfNode) rcls = ty_object_class(comp_ntype(c, recv)); else { Scope *encl = comp_scope_of(c, v); rcls = encl ? encl->class_id : -1; } if (rcls >= 0 && call_target(c, v, rcls, nm, 0, NULL) < 0) return 1; if (rcls < 0 && call_target(c, v, -1, nm, CT_TOP, NULL) < 0) return 1; } int any = 0; for (int mi = dyn_scopes_named(c, nm); mi >= 0; mi = g_dyn.snext[mi]) { if (!dyn_site_may_reach(c, v, mi)) continue; any = 1; if (dyn_returns_may_callable(c, mi, depth)) return 1; } return !any; } default: return 1; } } static int dyn_may_callable(Compiler *c, int v, int depth) { if (v < 0 || depth > 16) return 1; if (!g_dyn.fresh) dyn_memo_reset(c); dyn_callable_index(c); if (v >= g_dyn.nlit) return 1; /* a node made after the memo was sized */ unsigned char *m = &g_dyn.callable[v]; if (*m == 3) return 1; if (*m) return *m == 2; *m = 3; int r = dyn_may_callable_walk(c, v, depth); g_dyn.callable[v] = r ? 2 : 1; return r; } int dyn_open_site(Compiler *c, int n, int *shift) { const NodeTable *nt = c->nt; *shift = 0; if (n < 0 || nt_kind(nt, n) != NK_CallNode) return 0; const char *nm = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"); if (!nm || r < 0) return 0; TyKind rt = comp_ntype(c, r); if (sp_streq(nm, "bind_call")) { *shift = 1; return rt == TY_METHOD; } if (!is_proc_invoke(nm)) return 0; if (rt == TY_CURRY) return 1; return rt == TY_POLY && dyn_may_callable(c, r, 0); } void dyn_open_reach(Compiler *c, int n, int k, DynReach *r) { int shift; memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); if (dyn_open_site(c, n, &shift)) { /* the values the boxed receiver can hold, followed as at a call on a proc value: what is not followed is unknown */ if (!shift) r->argc1 = call_plain_argc(c, n) + 1; dyn_reach_value(c, nt_ref(c->nt, n, "receiver"), k - shift, 0, r); } else r->unknown = 1; if (r->unknown && !r->app && dyn_any_appender(c)) r->app = 1; } /* What the targets of dynamic call `n` do with its argument at position k (see DynReach). Asked by the pass below and by the emitters, which run after the last pass: the memo is refilled once for them. */ void dyn_call_reach(Compiler *c, int n, int k, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); r->argc1 = call_plain_argc(c, n) + 1; dyn_reach_value(c, nt_ref(c->nt, n, "receiver"), k, 0, r); if (r->unknown && !r->app && dyn_any_appender(c)) r->app = 1; } /* A `yield` in a method whose block is a real proc: one lowered to take it (it keeps its block, calls itself, or yields from a lifted body), where the yield calls the proc with the type-erased ABI as `blk.call` does. Answers the method, whose call sites pass the blocks it reaches, or -1 for a yield spliced into its call sites. */ int dyn_yield_site(Compiler *c, int y) { if (y < 0 || nt_kind(c->nt, y) != NK_YieldNode) return -1; Scope *s = comp_scope_of(c, y); return s && s->name && s->is_lowered_yield ? (int)(s - c->scopes) : -1; } /* What the blocks method mi's call sites pass do with position k: the yield of a method emitted in proc form, whose block is a parameter. */ void dyn_blk_reach(Compiler *c, int mi, int k, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); unsigned bits = dyn_blk_bits(c, mi); if (bits & DYN_OPEN) r->unknown = 1; dyn_fold(r, bits, k, 1, 0, TY_UNKNOWN); if (r->unknown && !r->app && dyn_any_appender(c)) r->app = 1; } /* The types position k's parameter has in the blocks method mi's call sites pass (mi a proc form: the method it copies), into out: the distinct ones, as many as DynParams holds; unknown when a call site that may be a call of mi passes no literal block or one without a plain parameter there, or when there are more types than that. app: a block appends to the parameter; keeps: one may keep what it reads. A boxed argument is handed to a pointer-typed parameter as that pointer (emit_proc_call_args). A call the plan does not resolve is of mi when its receiver is a builtin value of mi's class, and of another method of the name (a Hash's `pm` beside a String's) when it is of another class. */ void dyn_blk_params(Compiler *c, int mi, int k, DynParams *out) { const NodeTable *nt = c->nt; memset(out, 0, sizeof *out); if (mi >= 0 && mi < c->nscopes && c->scopes[mi].is_proc_form) mi = proc_form_source(c, mi); if (mi < 0 || mi >= c->nscopes || !c->scopes[mi].name || k < 0 || k >= DYN_ARGS) { out->unknown = 1; return; } if (!g_dyn.fresh) dyn_memo_reset(c); dyn_blk_index(c); int h = anh_find(&g_dyn.bnames, c->scopes[mi].name); for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int n = g_dyn.bnode[e], b = nt_ref(nt, n, "block"); const CallPlan *p = cplan_user_fresh(c, n); if (p->mi >= 0 && p->mi != mi) continue; int hit = p->mi == mi; if (p->mi < 0) { int rv = nt_ref(nt, n, "receiver"); const char *bn = rv >= 0 ? builtin_class_of_type(comp_ntype(c, rv)) : NULL; int ci = c->scopes[mi].class_id; if (bn && ci >= 0) { if (!sp_streq(bn, c->classes[ci].name)) continue; hit = 1; } } const char *pn = hit && nt_kind(nt, b) == NK_BlockNode ? dyn_lit_param_name(c, b, k) : NULL; Scope *bs = pn ? comp_scope_of(c, b) : NULL; LocalVar *q = bs ? scope_local(bs, pn) : NULL; if (!q) { out->unknown = 1; return; } if (dyn_lit_bits(c, b) & (1u << k)) out->app = 1; if (k >= 14 || (dyn_lit_bits(c, b) & (1u << (16 + k)))) out->keeps = 1; int seen = 0; for (int t = 0; t < out->n; t++) if (out->ty[t] == q->type) seen = 1; if (seen) continue; if (out->n == (int)(sizeof out->ty / sizeof out->ty[0])) { out->unknown = 1; return; } out->ty[out->n++] = q->type; } } /* What the blocks such a yield reaches do with its argument at position k. */ void dyn_yield_reach(Compiler *c, int y, int k, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); int mi = dyn_yield_site(c, y); if (mi >= 0 && dyn_blk_rest_takes(c, mi, k)) r->app = 1; unsigned bits = mi >= 0 ? dyn_blk_bits(c, mi) : DYN_OPEN; if (bits & DYN_OPEN) r->unknown = 1; dyn_fold(r, bits, k, 1, 0, TY_UNKNOWN); /* a post of one of the blocks may bind position k by the count */ if (mi >= 0 && g_dyn.blkpost[mi] == 2) r->app = 1; if (r->unknown && !r->app && dyn_any_appender(c)) r->app = 1; } /* May a lowered method's yield at position k hand the targets the shared handle's live bytes in the plain slot, as a call does when every target reads the box or only reads the parameter (emit_proc_call_args)? The blocks its call sites pass are the targets; a copy is taken otherwise, per yield, which an appending loop would pay for the whole String each time. */ int dyn_yield_live(Compiler *c, int mi, int k) { if (!g_dyn.fresh) dyn_memo_reset(c); if (mi < 0 || mi >= c->nscopes || !c->scopes[mi].is_lowered_yield || k < 0 || k >= 14) return 0; unsigned bits = dyn_blk_bits(c, mi); return !(bits & DYN_OPEN) && !(bits & (1u << (16 + k))); } /* Does lowered method `mi` yield its parameter j to a block that appends to it? The parameter is then the handle (dyn_pull_site_args), and a caller's String that cannot be pulled into it is refused. */ int dyn_yield_param_appends(Compiler *c, int mi, int j) { const NodeTable *nt = c->nt; if (mi < 0 || mi >= c->nscopes || !c->scopes[mi].is_lowered_yield) return 0; Scope *m = &c->scopes[mi]; if (j < 0 || j >= m->nparams || !m->pnames[j]) return 0; for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode || comp_scope_of(c, y) != m) continue; int a = nt_ref(nt, y, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac && k < DYN_ARGS; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) break; if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode || !nt_str(nt, av[k], "name") || !sp_streq(nt_str(nt, av[k], "name"), m->pnames[j])) continue; DynReach r; dyn_yield_reach(c, y, k, &r); if (r.app) return 1; } } return 0; } /* The emitters' memo is filled from the settled types; the analysis marks it stale whenever a pass may change them. */ static void dyn_memo_stale(void) { g_dyn.fresh = 0; } /* The same questions for the paths not shared yet, which the emitters refuse rather than compile with the append lost (refuse_string_copy): does method `mi` append to its positional parameter j, does literal block `blk` append to its parameter k, what can a block argument's value `v` do at position k, and does the program append through any dynamic target at all. */ int dyn_method_appends(Compiler *c, int mi, int j) { if (!g_dyn.fresh) dyn_memo_reset(c); if (mi < 0 || mi >= c->nscopes || j < 0 || j >= DYN_ARGS) return 0; if (j >= dyn_method_nreq(c, &c->scopes[mi])) return 0; return (dyn_meth_bits(c, mi) >> j) & 1u; } int dyn_block_appends(Compiler *c, int blk, int k) { if (!g_dyn.fresh) dyn_memo_reset(c); if (blk < 0 || k < 0 || k >= DYN_ARGS) return 0; return (dyn_lit_bits(c, blk) >> k) & 1u; } void dyn_value_reach(Compiler *c, int v, int k, DynReach *r) { memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); if (v < 0) r->unknown = 1; else dyn_reach_value(c, v, k, 0, r); if (r->unknown && !r->app && dyn_any_appender(c)) r->app = 1; } /* Pull a String variable at a dynamic call into the shared handle: its read is marked strbuf_box, so the argument is the handle and the call boxes it as one. Answers 1 when it changed anything. A variable this cannot pull -- a block parameter, a captured local, an ivar -- stays a plain String, and the emitter refuses the call (refuse_string_copy), never compiling the append away. An element of a local Array literal splatted into the call (`s = [v]; f.call(*s)`) leaves its read unmarked (`mark_read` 0): the container rule (promote_shared_stored_strings) widens the Array to hold the handle and marks the read itself, and a read marked here first kept the Array a StrArray the handle could not go into. */ static int dyn_pull_arg(Compiler *c, int a, int mark_read) { const NodeTable *nt = c->nt; if (nt_kind(nt, a) != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->is_cell) return 0; if (lv->type != TY_STRING && lv->type != TY_STRBUF) return 0; int changed = 0; /* --share-strings: a block's parameter the rule made the handle hands it over as a handle local does: only the read is left to mark */ if (lv->is_block_param && repr_of_slot(c, lv).share) { if (!mark_read || c->strbuf_box[a]) return 0; comp_set_strbuf_box(c, a, 1); comp_sn_retype(c, a, TY_STRBUF); return 1; } /* the method's own parameter, handed on (`def via(s, f) = f.call(s)`): it takes the handle, and convert_byref_handle_params pulls via's callers in on its next run */ if (lv->is_param) { if (lv->is_block_param || an_param_idx(vs, vn) < 0) return 0; if (lv->type != TY_STRBUF || !lv->str_shared) { lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } { lv->byref_out = 0; comp_touch_scope(lv->owner); } changed = 1; } if (mark_read && !c->strbuf_box[a]) { comp_set_strbuf_box(c, a, 1); comp_sn_retype(c, a, TY_STRBUF); changed = 1; } return changed; } if (lv->is_block_param) return 0; /* already the handle: only this read is left to mark (the shape walk below scans the scope's calls, and a pass asks at every call site) */ if (lv->type == TY_STRBUF && lv->str_shared) { if (!mark_read || c->strbuf_box[a]) return 0; comp_set_strbuf_box(c, a, 1); comp_sn_retype(c, a, TY_STRBUF); return 1; } if (!strbuf_slot_eligible_shape(c, vn, vs, lv)) { /* Also lent to a byref slot (`grow(s)` beside `f.call(s)`): a slot cannot carry the handle, so that parameter takes the handle instead, with its whole name group, as a POLY parameter's pull does for its own callers; convert_byref_handle_params then pulls its other callers. */ int vsi = (int)(vs - c->scopes); for (int u = comp_scall_first(c, vsi); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || comp_scope_of(c, u) != vs) continue; if (an_call_recv_builtin(c, u)) continue; /* lends to no method of the program's */ int mi = an_any_scope_by_name(c, nt_str(nt, u, "name")); if (mi < 0) continue; for (int j = 0; j < c->scopes[mi].nparams; j++) { if (!comp_byref_param(c, &c->scopes[mi], j)) continue; int a2 = arg_layout_param_node(c, &c->scopes[mi], u, j, NULL); const char *an2 = a2 >= 0 && nt_kind(nt, a2) == NK_LocalVariableReadNode ? nt_str(nt, a2, "name") : NULL; if (!an2 || !sp_streq(an2, vn)) continue; for (int m = dyn_scopes_named(c, c->scopes[mi].name); m >= 0; m = g_dyn.snext[m]) { Scope *mk = &c->scopes[m]; if (j >= mk->nparams) continue; LocalVar *q = mk->pnames[j] ? scope_local(mk, mk->pnames[j]) : NULL; if (!q || !q->byref_out) continue; { q->byref_out = 0; comp_touch_scope(q->owner); } { q->is_cell = 0; comp_touch_scope(q->owner); } lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } changed = 1; } } } if (!strbuf_slot_eligible_shape(c, vn, vs, lv)) return changed; } if (lv->type != TY_STRBUF || !lv->str_shared) { lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } changed = 1; } if (mark_read && !c->strbuf_box[a]) { comp_set_strbuf_box(c, a, 1); comp_sn_retype(c, a, TY_STRBUF); changed = 1; } return changed; } /* The callee side. A proc literal's (or a kept block's) String parameter that its body appends to reads the boxed channel as the handle (emit_proc_literal_here), and so does a method a `method(:m)` names: such a name keeps the value ABI (the DYN rule in compute_byref_out_params, since a Method's trampoline has no slot to lend), and a value parameter appends to a copy. The method takes the handle instead, which its trampoline, its thunk and a devirtualized `m.call(s)` all hand over, and its direct callers are pulled in as a handle method's are. */ static int dyn_convert_params(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; int *tg = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int pass = 0; pass < 2; pass++) { NodeKind want = pass ? NK_CallNode : NK_LambdaNode; for (int n = comp_kind_first(c, want); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != want || dyn_cap_wrapper(c, n)) continue; int create = -1, lit = -1; if (dyn_is_proc_literal(c, n)) { create = n; lit = pass ? nt_ref(nt, n, "block") : n; } else if (pass) { /* a block a method keeps as `&blk` is emitted as a proc too */ int b = nt_ref(nt, n, "block"); if (b < 0 || nt_kind(nt, b) != NK_BlockNode) continue; /* the targets of a class value's `new` are every class it can build, looked up again on each pass: ask first whether the block appends at all, since one that does not converts nothing */ const char *cn = nt_str(nt, n, "name"); if (cn && sp_streq(cn, "new") && !(dyn_lit_bits(c, b) & 0xffffu)) continue; int nk = dyn_block_targets(c, n, tg); for (int e = 0; e < nk && lit < 0; e++) { Scope *m = &c->scopes[tg[e]]; if (m->blk_param && m->blk_param[0] && !m->yields) create = lit = b; } } if (lit < 0) continue; unsigned app = dyn_lit_bits(c, lit) & 0xffffu; Scope *bs = app ? comp_scope_of(c, create) : NULL; for (int k = 0; bs && k < DYN_ARGS; k++) { if (!(app & (1u << k))) continue; const char *pn = dyn_lit_param_name(c, lit, k); LocalVar *lv = pn ? scope_local(bs, pn) : NULL; if (!lv || lv->type != TY_STRING || lv->is_cell) continue; lv_set_type(lv, TY_STRBUF); { lv->str_shared = 1; comp_touch_scope(lv->owner); } changed = 1; } } } for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode) continue; const char *nm = nt_str(nt, n, "name"); if (!nm || !(sp_streq(nm, "method") || sp_streq(nm, "public_method") || sp_streq(nm, "instance_method") || sp_streq(nm, "public_instance_method"))) continue; for (int mi = dyn_scopes_named(c, method_sym_arg(c, n)); mi >= 0; mi = g_dyn.snext[mi]) { Scope *m = &c->scopes[mi]; unsigned app = dyn_meth_bits(c, mi) & 0xffffu; for (int j = 0; app && j < m->nparams && j < DYN_ARGS; j++) { if (!(app & (1u << j))) continue; LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!q || !q->is_param || q->is_block_param || q->type != TY_STRING || q->byref_out || q->is_cell) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } q->dyn_handle = 1; changed = 1; } /* its keywords the same way (`method(:m).call(k: s)`): bound by name, and appended to as a copy while they kept the value ABI */ for (int j = 0; j < m->nparams; j++) { if (!m->pnames[j] || !callee_param_is_declared_kwarg(c, m, m->pnames[j])) continue; if (!(dyn_meth_kw_bits(c, mi, m->pnames[j], NULL) & 1u)) continue; LocalVar *q = scope_local(m, m->pnames[j]); if (!q || !q->is_param || q->is_block_param || q->type != TY_STRING || q->byref_out || q->is_cell) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } q->dyn_handle = 1; changed = 1; } } } /* A method `define_method` defines keeps the value ABI too (its name is a DYN literal), and a direct call reaches it as well as a Method: its appended String parameter takes the handle the same way. */ for (int mi = 1; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; int dn = m->def_node; if (!m->name || dn < 0 || nt_kind(nt, dn) != NK_CallNode || !nt_str(nt, dn, "name") || !(sp_streq(nt_str(nt, dn, "name"), "define_method") || sp_streq(nt_str(nt, dn, "name"), "define_singleton_method"))) continue; unsigned app = dyn_meth_bits(c, mi) & 0xffffu; for (int j = 0; app && j < m->nparams && j < DYN_ARGS; j++) { if (!(app & (1u << j))) continue; LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!q || !q->is_param || q->is_block_param || q->type != TY_STRING || q->byref_out || q->is_cell) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } q->dyn_handle = 1; changed = 1; } } free(tg); return changed; } /* ---- A String an `initialize` appends to: `new`, `raise C, s` (#6179) -- A constructor is reached through sp_C_new, which about two dozen emitters call: a constant's `new`, a class value's, `self.new` in a class method, `raise`, a Struct's and a Data's, the poly `new` dispatch. So its parameters kept the value ABI (compute_byref_out_params skips `initialize`: a slot lent at the `.new` site alone left the others passing values), and an initialize that appended to its String appended to a copy. A String parameter the initialize appends to, or hands on to a method or a `super` that does, takes the shared handle instead: one C type every binder can hand over (emit_arg_or_default's handle arm, and the class-value arms through the box). The caller's String variable at every `new` and `raise` that can reach it is pulled into the handle. Only such an initialize changes. One that reads its String, stores it or ignores it keeps `const char *`, and so does every caller of it, so a program without one emits the C it did. */ /* Which of initialize `mi`'s parameters are appended to: by the body, by a method the body hands one to, or by the parent initialize a `super` hands one to (whose parameter this pass made the handle already). Every parameter, keywords and optionals too: a constructor binds them all through the static layout. */ static unsigned ctor_append_scan(Compiler *c, int mi, unsigned *kept_out) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; const char *pn[DYN_ARGS]; int np = m->nparams < DYN_ARGS ? m->nparams : DYN_ARGS; unsigned app = 0, kept = 0; for (int k = 0; k < np; k++) { pn[k] = m->pnames[k]; if (an_param_mutated_in_place(c, mi, k)) app |= 1u << k; } dyn_body_scan(c, m->body, pn, np, &app, &kept); if (kept_out) *kept_out = kept; /* a parameter the body yields, where a `new(..., &pr)` hands the initialize a proc that appends at that position: the constructor runs its clone, whose yield boxes what the parameter holds (#6179) */ if (m->yields) { dyn_blk_index(c); int h = anh_find(&g_dyn.bnames, "new"); for (int y = comp_kind_first(c, NK_YieldNode); h >= 0 && y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode || comp_scope_of(c, y) != m) continue; int ya = nt_ref(nt, y, "arguments"), yc = 0; const int *yv = ya >= 0 ? nt_arr(nt, ya, "arguments", &yc) : NULL; for (int k = 0; k < yc && k < DYN_ARGS; k++) { NodeKind yk = nt_kind(nt, yv[k]); if (yk == NK_SplatNode || yk == NK_KeywordHashNode) break; int j = yk == NK_LocalVariableReadNode ? an_param_idx(m, nt_str(nt, yv[k], "name")) : -1; if (j < 0 || j >= DYN_ARGS || (app & (1u << j))) continue; for (int e = g_dyn.bhead[h]; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], ba = nt_ref(nt, u, "block"); if (nt_kind(nt, ba) != NK_BlockArgumentNode || dyn_call_target_mi(c, u) != mi) continue; DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, nt_ref(nt, ba, "expression"), k, 0, &r); if (!r.unlifted && (r.app || (r.unknown && dyn_any_appender(c)))) { app |= 1u << j; break; } } } } } int pi = m->class_id >= 0 ? a_super_target(c, m) : -1; if (pi < 0 || !c->scopes[pi].name || !sp_streq(c->scopes[pi].name, "initialize")) return app; Scope *pm = &c->scopes[pi]; int ph = 0; for (int pj = 0; pj < pm->nparams && !ph; pj++) { LocalVar *d = pm->pnames[pj] ? scope_local(pm, pm->pnames[pj]) : NULL; ph = d && d->dyn_handle; } for (int pass = 0; ph && pass < 2; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int q = comp_kind_first(c, sk); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != sk || comp_scope_of(c, q) != m) continue; for (int pj = 0; pj < pm->nparams; pj++) { LocalVar *d = pm->pnames[pj] ? scope_local(pm, pm->pnames[pj]) : NULL; if (!d || !d->dyn_handle) continue; int src = -1; if (pass) src = zsuper_param_source(c, m, pm, pj); else { int an = arg_layout_param_node(c, pm, q, pj, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode) src = an_param_idx(m, nt_str(nt, an, "name")); } if (src >= 0 && src < DYN_ARGS) app |= 1u << src; } } } return app; } /* ...memoized for the emitters' refusal, which asks after the last pass. The pass itself scans: converting a parent moves a child's answer within it. */ static unsigned ctor_append_bits(Compiler *c, int mi) { if (mi < 0 || mi >= g_dyn.nscope) return 0; if (!(g_dyn.ctor[mi] & DYN_DONE)) { unsigned kept = 0, app = ctor_append_scan(c, mi, &kept); g_dyn.ctor[mi] = DYN_DONE | (app & 0xffffu) | ((kept & 0x3fffu) << 16); } return g_dyn.ctor[mi]; } /* For the emitters' refusal: does initialize `mi` append to parameter j? */ int ctor_param_appends(Compiler *c, int mi, int j) { if (!g_dyn.fresh) dyn_memo_reset(c); if (mi < 0 || mi >= c->nscopes || j < 0 || j >= DYN_ARGS) return 0; return (ctor_append_bits(c, mi) >> j) & 1u; } /* For the binders: does initialize `mi` only read its parameter j's bytes (the receiver of a String method that answers a new value, printed, interpolated), in a program where some initialize appends to one? A String that is the shared handle can then go over as its live bytes: the copy a handle's plain read takes is for a callee that may keep it. A program with no appending initialize keeps its C. */ int ctor_param_reads_only(Compiler *c, int mi, int j) { if (!g_dyn.fresh) dyn_memo_reset(c); if (mi < 0 || mi >= c->nscopes || j < 0 || j >= 14) return 0; if (g_dyn.ctor_any < 0) { g_dyn.ctor_any = 0; for (int k = dyn_scopes_named(c, "initialize"); k >= 0 && !g_dyn.ctor_any; k = g_dyn.snext[k]) for (int q = 0; q < c->scopes[k].nparams && !g_dyn.ctor_any; q++) { LocalVar *v = c->scopes[k].pnames[q] ? scope_local(&c->scopes[k], c->scopes[k].pnames[q]) : NULL; g_dyn.ctor_any = v && v->dyn_handle; } } if (!g_dyn.ctor_any) return 0; unsigned bits = ctor_append_bits(c, mi); return !((bits >> j) & 1u) && !((bits >> (16 + j)) & 1u); } /* Does initialize `mi` pass its local `vn` to a yield? */ static int ctor_yields_name(Compiler *c, int mi, const char *vn) { const NodeTable *nt = c->nt; for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode || comp_scope_of(c, y) != &c->scopes[mi]) continue; int a = nt_ref(nt, y, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) if (nt_kind(nt, av[k]) == NK_LocalVariableReadNode && nt_str(nt, av[k], "name") && sp_streq(nt_str(nt, av[k], "name"), vn)) return 1; } return 0; } static int ctor_convert_params(Compiler *c) { int changed = 0; for (int mi = dyn_scopes_named(c, "initialize"); mi >= 0; mi = g_dyn.snext[mi]) { Scope *m = &c->scopes[mi]; if (m->is_cmethod || m->class_id < 0 || m->def_node < 0) continue; unsigned app = ctor_append_scan(c, mi, NULL); for (int j = 0; app && j < m->nparams && j < DYN_ARGS; j++) { if (!(app & (1u << j))) continue; LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!q || !q->is_param || q->is_block_param || q->byref_out || q->is_cell || q->dyn_handle) continue; /* TY_STRBUF: retained in a shared ivar (#4363), or typed from a handle argument */ if (q->type != TY_STRING && q->type != TY_STRBUF) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } q->dyn_handle = 1; changed = 1; } } return changed; } /* The initialize methods `new` or `raise` call `u` can reach, into out[] (at most cap; a cap of c->nscopes holds any set, each target being a scope of its own, and the callers pass that: a set cut short would leave an arm the pull and the refusal never see), with *first the index of the call's first argument the initialize binds (1 for `raise C, s`) and *boxed set when the call dispatches on a class value and so hands its arguments over boxed. `C.new` reaches C's; `new` or `self.new` in a class method, the class's and its subclasses'; a class value, the classes its switch has an arm for. A class with a `new` of its own is not constructed by the call. */ int dynamic_new_may_reach(Compiler *c, int call_id, int cid); int ctor_call_targets(Compiler *c, int u, int *first, int *boxed, int *out, int cap) { const NodeTable *nt = c->nt; *first = 0; *boxed = 0; if (!g_dyn.fresh) dyn_memo_reset(c); const char *un = nt_str(nt, u, "name"); if (!un) return 0; int recv = nt_ref(nt, u, "receiver"), cls = -1, sub = 0, any = 0; if (sp_streq(un, "new")) { NodeKind rk = recv >= 0 ? nt_kind(nt, recv) : NK_SelfNode; Scope *es = comp_scope_of(c, u); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { const char *cn = nt_str(nt, recv, "name"); cls = cn ? comp_class_index(c, cn) : -1; if (cls < 0) return 0; } else if (rk == NK_SelfNode) { if (!es || !es->is_cmethod || es->class_id < 0) return 0; cls = es->class_id; sub = 1; *boxed = recv >= 0; } else { TyKind rt = comp_ntype(c, recv); if (ty_is_object(rt)) return 0; /* an object's own `new` */ any = 2; *boxed = 1; } } else if (sp_streq(un, "raise") && recv < 0) { int a = nt_ref(nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (ac < 2 || nt_kind(nt, av[1]) == NK_SplatNode || nt_kind(nt, av[0]) == NK_SplatNode) return 0; *first = 1; NodeKind rk = nt_kind(nt, av[0]); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { const char *cn = nt_str(nt, av[0], "name"); cls = cn ? comp_class_index(c, cn) : -1; if (cls < 0) return 0; } else { any = 1; *boxed = 1; } } else return 0; int n = 0; /* a class value: the classes its `new` switch has an arm for -- the ones the call can reach (dynamic_new_may_reach), and for `raise k, s` every exception class (emit_raise_class_value) */ if (any) { for (int k = 0; k < c->nclasses && n < cap; k++) { if (any == 2 ? !dynamic_new_may_reach(c, u, k) : !class_is_exc_subclass(c, k)) continue; int mi = comp_method_in_chain(c, k, "initialize", NULL); if (mi < 0 || c->scopes[mi].is_cmethod) continue; int dup = 0; for (int e = 0; e < n && !dup; e++) dup = out[e] == mi; if (!dup) out[n++] = mi; } return n; } int nd = 0; const int *ds = sub ? comp_descendants(c, cls, &nd) : NULL; for (int d = -1; d < nd && n < cap; d++) { int k = d < 0 ? cls : ds[d]; if (comp_cmethod_in_chain(c, k, "new", NULL) >= 0) continue; int mi = comp_method_in_chain(c, k, "initialize", NULL); if (mi < 0 || c->scopes[mi].is_cmethod) continue; int dup = 0; for (int e = 0; e < n && !dup; e++) dup = out[e] == mi; if (!dup) out[n++] = mi; } return n; } /* The argument node call `u` binds parameter j of initialize `m` to, or -1: by the call's layout, or for `raise C, s` the one message argument. */ int ctor_param_arg(Compiler *c, Scope *m, int u, int first, int j) { if (!first) return arg_layout_param_node(c, m, u, j, NULL); int a = nt_ref(c->nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(c->nt, a, "arguments", &ac) : NULL; if (ac != 2) return -1; ArgLayout L; arg_layout(c, m, av + 1, 1, -1, 0, &L); int r = j < m->nparams && L.from[j] == ARG_NODE ? av[1 + L.arg[j]] : -1; arg_layout_free(&L); return r; } /* The splatted Array literal call `u` holds argument node `a` in, or -1. */ int ctor_arg_in_splat(Compiler *c, int u, int a) { const NodeTable *nt = c->nt; int args = nt_ref(nt, u, "arguments"), ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { if (nt_kind(nt, av[k]) != NK_SplatNode) continue; int x = nt_ref(nt, av[k], "expression"); int en = 0; const int *el = x >= 0 && nt_kind(nt, x) == NK_ArrayNode ? nt_arr(nt, x, "elements", &en) : NULL; for (int e = 0; e < en; e++) if (el[e] == a) return x; } return -1; } /* Can initialize parameter q take the handle? A String one can, unless a block captures it or the initialize yields it (see ctor_convert_params). */ static int ctor_param_can_share(Compiler *c, int mi, LocalVar *q) { Scope *m = &c->scopes[mi]; if (!q || !q->is_param || q->is_block_param || q->byref_out || q->is_cell) return 0; if (q->type != TY_STRING && q->type != TY_STRBUF) return 0; return !((m->yields || m->is_lowered_yield) && ctor_yields_name(c, mi, q->name)); } /* The caller side: the String variable each such call binds to a handle parameter becomes the handle. A static call binds it through the handle arm, which takes the local's handle. A call on a class value boxes its arguments once for every arm of its switch, so its read is marked to box the handle, and every initialize the switch can reach takes the handle at that argument, not only the ones that append: a class it reaches that keeps the String (`@s = s`) then keeps the caller's own, as in CRuby, where an arm reading a plain String would have taken the handle's live bytes. When one of them cannot take it, the read stays a copy, and the emitter refuses the call. What cannot be pulled -- a block's parameter, a captured local, a global -- is refused the same way (refuse_string_copies). */ static int ctor_pull_args(Compiler *c) { const NodeTable *nt = c->nt; int any = 0; for (int mi = dyn_scopes_named(c, "initialize"); mi >= 0 && !any; mi = g_dyn.snext[mi]) for (int j = 0; j < c->scopes[mi].nparams && !any; j++) { LocalVar *q = c->scopes[mi].pnames[j] ? scope_local(&c->scopes[mi], c->scopes[mi].pnames[j]) : NULL; any = q && q->dyn_handle; } if (!any) return 0; int changed = 0; int *tg = (int *)malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } /* `super(t)` in an initialize, into the parent's handle parameter: a parameter of its own the super hands on is converted with the parent's (convert_byref_handle_params), a local is pulled here */ for (int q = comp_kind_first(c, NK_SuperNode); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != NK_SuperNode) continue; Scope *s = comp_scope_of(c, q); int pi = s && s->name && sp_streq(s->name, "initialize") && s->class_id >= 0 ? a_super_target(c, s) : -1; if (pi < 0) continue; Scope *pm = &c->scopes[pi]; for (int j = 0; j < pm->nparams && j < DYN_ARGS; j++) { LocalVar *d = pm->pnames[j] ? scope_local(pm, pm->pnames[j]) : NULL; if (!d || !d->dyn_handle) continue; int a = arg_layout_param_node(c, pm, q, j, NULL); if (a >= 0 && nt_kind(nt, a) == NK_LocalVariableReadNode) changed |= dyn_pull_arg(c, a, 0); } } /* `super` in a class's own `def self.new` is Class#new: it constructs through the class's initialize (and a subclass's, when the method is inherited), so the String it hands on is pulled as a `new` call's is, and `self.new`'s callers follow its parameter (convert_byref_handle_params) */ for (int pass = 0; pass < 2; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int q = comp_kind_first(c, sk); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != sk) continue; Scope *s = comp_scope_of(c, q); if (!s || !s->is_cmethod || s->class_id < 0 || !s->name || !sp_streq(s->name, "new")) continue; int nd = 0; const int *ds = comp_descendants(c, s->class_id, &nd); for (int d = -1; d < nd; d++) { int k = d < 0 ? s->class_id : ds[d]; int mi = comp_method_in_chain(c, k, "initialize", NULL); if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int j = 0; j < m->nparams && j < DYN_ARGS; j++) { LocalVar *d2 = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!d2 || !d2->dyn_handle) continue; if (!pass) { int a = arg_layout_param_node(c, m, q, j, NULL); if (a >= 0 && nt_kind(nt, a) == NK_LocalVariableReadNode) changed |= dyn_pull_arg(c, a, 0); continue; } int pi = zsuper_param_source(c, s, m, j); LocalVar *src = pi >= 0 && s->pnames[pi] ? scope_local(s, s->pnames[pi]) : NULL; if (!src || !src->is_param || src->is_block_param || src->is_cell) continue; if (src->type != TY_STRING && src->type != TY_STRBUF) continue; if (src->type != TY_STRBUF || !src->str_shared) { lv_set_type(src, TY_STRBUF); { src->str_shared = 1; comp_touch_scope(src->owner); } { src->byref_out = 0; comp_touch_scope(src->owner); } changed = 1; } } } } } 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; int first, boxed; int n = ctor_call_targets(c, u, &first, &boxed, tg, c->nscopes); for (int t = 0; t < n; t++) { Scope *m = &c->scopes[tg[t]]; for (int j = 0; j < m->nparams && j < DYN_ARGS; j++) { LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; if (!q || !q->dyn_handle) continue; int a = ctor_param_arg(c, m, u, first, j); if (a < 0) continue; NodeKind ak = nt_kind(nt, a); if (!boxed) { /* An element of a splatted Array literal (`C.new(*[s, 1])`) rides the Array the splat builds: a boxed one holds the handle once the read is marked, as a dynamic call's splat does (#5957's container rule). A String-only Array holds bytes until promote_spread_string_args marks the element a parameter that appends is placed on; the emitter refuses what is left. */ int sx = ak == NK_LocalVariableReadNode ? ctor_arg_in_splat(c, u, a) : -1; if (sx >= 0) { if (comp_ntype(c, sx) == TY_POLY_ARRAY) changed |= dyn_pull_arg(c, a, 1); continue; } if (ak == NK_LocalVariableReadNode) changed |= dyn_pull_arg(c, a, 0); else if (ak == NK_InstanceVariableReadNode) { const char *vn = nt_str(nt, a, "name"); int cid = vn ? comp_ivar_owner(c, a) : -1; if (cid >= 0 && strbuf_ivar_mut_kind(c, cid, vn) >= 0 && strbuf_promote_ivar(c, cid, vn)) changed = 1; } continue; } if (ak != NK_LocalVariableReadNode || c->strbuf_box[a]) continue; /* every arm's parameter this argument binds takes the handle, or none does */ int ok = 1; for (int t2 = 0; t2 < n && ok; t2++) { Scope *m2 = &c->scopes[tg[t2]]; for (int j2 = 0; j2 < m2->nparams && ok; j2++) { LocalVar *q2 = m2->pnames[j2] ? scope_local(m2, m2->pnames[j2]) : NULL; if (!q2 || q2->dyn_handle || ctor_param_arg(c, m2, u, first, j2) != a) continue; if ((q2->type == TY_STRING || q2->type == TY_STRBUF) && !ctor_param_can_share(c, tg[t2], q2)) ok = 0; } } if (!ok) continue; for (int t2 = 0; t2 < n; t2++) { Scope *m2 = &c->scopes[tg[t2]]; for (int j2 = 0; j2 < m2->nparams; j2++) { LocalVar *q2 = m2->pnames[j2] ? scope_local(m2, m2->pnames[j2]) : NULL; if (!q2 || q2->dyn_handle || (q2->type != TY_STRING && q2->type != TY_STRBUF)) continue; if (ctor_param_arg(c, m2, u, first, j2) != a) continue; lv_set_type(q2, TY_STRBUF); { q2->str_shared = 1; comp_touch_scope(q2->owner); } q2->dyn_handle = 1; changed = 1; } } changed |= dyn_pull_arg(c, a, 1); } } } free(tg); return changed; } /* A POLY variable handed as argument k of dynamic call n: when a target the call can reach appends to the parameter it binds, its read is lifted (poly_strbuf_lift), and a method's own POLY parameter handed on this way pulls its callers in (poly_lift), as at a static call. */ static int dyn_lift_poly_arg(Compiler *c, int n, int k, int a) { const NodeTable *nt = c->nt; if (nt_kind(nt, a) == NK_InstanceVariableReadNode) { if (c->poly_strbuf_lift[a]) return 0; DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, nt_ref(nt, n, "receiver"), k, 0, &r); if (r.unlifted || (!r.app && !(r.unknown && dyn_any_appender(c)))) return 0; return lift_poly_read(c, NULL, NULL, a); } const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->type != TY_POLY || c->poly_strbuf_lift[a]) return 0; if (!poly_var_may_hold_string(c, NULL, vn, vs, 0,NULL)) return 0; DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, nt_ref(nt, n, "receiver"), k, 0, &r); if (r.unlifted) return 0; if (!r.app && !(r.unknown && dyn_any_appender(c))) return 0; c->poly_strbuf_lift[a] = 1; if (lv->is_param && !lv->is_block_param && an_param_idx(vs, vn) >= 0) lv->poly_lift |= POLY_LIFT_APPENDED; return 1; } /* Run in the fixpoint beside promote_shared_stored_strings (so a container holding the String widens with it) and again after it beside convert_byref_handle_params (so the byref slots have settled). */ /* What the target of dynamic site `n` -- a call, or a yield into a real proc -- does with its argument at position k. */ static void dyn_site_reach(Compiler *c, int n, int k, DynReach *r) { if (nt_kind(c->nt, n) == NK_YieldNode) { dyn_yield_reach(c, n, k, r); return; } memset(r, 0, sizeof *r); dyn_reach_value(c, nt_ref(c->nt, n, "receiver"), k, 0, r); } /* Pull the String variables dynamic site `n` hands a target that appends to them into the shared handle. */ static int spread_demand_changed_local(Compiler *c, int splat); /* The keyword arm of a site's reach (dyn_site_reach): a dynamic call's receiver, a lowered method's yield into the blocks its call sites pass (literal blocks, and a proc or Method passed with `&`), or an open site (a boxed callable or a curried proc, whose targets are unknown, and bind_call, whose UnboundMethod names its method). */ static void dyn_site_kw_reach(Compiler *c, int n, const char *key, DynReach *r) { const NodeTable *nt = c->nt; memset(r, 0, sizeof *r); if (!g_dyn.fresh) dyn_memo_reset(c); int shift = 0; if (nt_kind(nt, n) == NK_YieldNode) { int mi = dyn_yield_site(c, n); const char *mn = mi >= 0 ? c->scopes[mi].name : NULL; dyn_blk_index(c); int h = mn ? anh_find(&g_dyn.bnames, sp_streq(mn, "initialize") ? "new" : mn) : -1, any = 0; for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int b = nt_ref(nt, g_dyn.bnode[e], "block"); any = 1; if (nt_kind(nt, b) == NK_BlockNode) dyn_reach_kw(c, b, key, 0, r); else if (nt_kind(nt, b) == NK_BlockArgumentNode) dyn_reach_kw(c, nt_ref(nt, b, "expression"), key, 0, r); else r->unknown = 1; } if (!any) r->unknown = 1; } else if (dyn_open_site(c, n, &shift)) { /* a curried proc does not pass keywords on as keywords (CRuby counts them as a position and raises), so it is no keyword site: refused */ if (!shift && comp_ntype(c, nt_ref(nt, n, "receiver")) == TY_CURRY) { r->unlifted = "curry"; return; } if (shift) dyn_reach_kw(c, nt_ref(nt, n, "receiver"), key, 0, r); else r->unknown = 1; } else dyn_reach_kw(c, nt_ref(nt, n, "receiver"), key, 0, r); if (r->unknown && !r->app && dyn_any_kw_appender(c)) r->app = 1; } /* The keywords of site `n` (`f.call(k: s)`, `yield(k: s)` into a proc, an open site's): each value is bound by name to the target's keyword parameter of that name, through the boxed Hash the call builds, and a target that appends to it appended to a copy. The same pull as a position's: the Hash then holds the handle. */ static int dyn_pull_site_kw_args(Compiler *c, int n) { const NodeTable *nt = c->nt; int changed = 0; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { if (nt_kind(nt, av[k]) != NK_KeywordHashNode) continue; int en = 0; const int *el = nt_arr(nt, av[k], "elements", &en); for (int e = 0; e < en; e++) { int v; const char *key = dyn_kw_elem_key(c, el[e], &v); if (!key || v < 0 || nt_kind(nt, v) != NK_LocalVariableReadNode) continue; TyKind vt = comp_ntype(c, v); if (vt != TY_STRING && vt != TY_STRBUF) continue; if (!local_is_handle(c, v)) { DynReach r; dyn_site_kw_reach(c, n, key, &r); if (r.unlifted || !r.app) continue; } changed |= dyn_pull_arg(c, v, 1); } } return changed; } static int dyn_pull_site_args(Compiler *c, int n) { const NodeTable *nt = c->nt; int changed = 0; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac && k < DYN_ARGS; k++) { NodeKind ak = nt_kind(nt, av[k]); /* A splat of an Array literal, or of a local every write of which is one: its String elements are the arguments (#5957's container rule, at a dynamic call). The parser folds `f.call(s, *e)` into one splat of `[s, *e]`, so this is also how a plain argument ahead of a splat arrives. */ if (ak == NK_SplatNode) { int x = nt_ref(nt, av[k], "expression"); int any = 0; for (int j = k; j < DYN_ARGS && !any; j++) { DynReach r; dyn_site_reach(c, n, j, &r); any = !r.unlifted && (r.app || (r.unknown && dyn_any_appender(c))); } if (!any || x < 0) break; changed |= spread_demand_changed_local(c, av[k]); int lits[64], nl = 0; if (nt_kind(nt, x) == NK_ArrayNode) lits[nl++] = x; else if (nt_kind(nt, x) == NK_LocalVariableReadNode) { const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && nl < 64; w = comp_lvw_next_sc(c, w)) { if (nt_kind(nt, w) != NK_LocalVariableWriteNode || comp_scope_of(c, w) != xs) continue; int wv = nt_ref(nt, w, "value"); if (wv >= 0 && nt_kind(nt, wv) == NK_ArrayNode) lits[nl++] = wv; } } for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); for (int e = 0; e < en; e++) { TyKind et = comp_ntype(c, ev[e]); if (nt_kind(nt, ev[e]) == NK_LocalVariableReadNode && (et == TY_STRING || et == TY_STRBUF)) changed |= dyn_pull_arg(c, ev[e], nt_kind(nt, x) == NK_ArrayNode); } } break; /* the positions after a splat depend on its length */ } if (ak == NK_InstanceVariableReadNode && comp_ntype(c, av[k]) == TY_POLY && nt_kind(nt, n) == NK_CallNode) { changed |= dyn_lift_poly_arg(c, n, k, av[k]); continue; } if (ak != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, av[k]); /* A POLY variable boxes whatever it holds, and a plain String boxed is a copy: a target that appends to it gets its read lifted into the handle (convert_byref_handle_params' rule, at a dynamic call) */ if (at == TY_POLY) { if (nt_kind(nt, n) == NK_CallNode) changed |= dyn_lift_poly_arg(c, n, k, av[k]); continue; } if (at != TY_STRING && at != TY_STRBUF) continue; /* A local that is the handle already goes over as the handle at every dynamic call, appending target or not: demoted, a read-only call would copy the whole String per call. */ int already = local_is_handle(c, av[k]); if (!already) { DynReach r; dyn_site_reach(c, n, k, &r); if (r.unlifted) continue; /* refused by the emitter */ if (!r.app && !(r.unknown && dyn_any_appender(c))) continue; } changed |= dyn_pull_arg(c, av[k], 1); } /* the keywords, by name, at a call and at a lowered yield alike */ changed |= dyn_pull_site_kw_args(c, n); return changed; } /* ---- A String yielded into a spliced block (#6179) --------------------- A yielding method is spliced into its call sites, and so is the literal block: a parameter the block appends to binds as an alias of the variable yielded to it (block_param_wants_alias), and a parameter the method lends (appends to, hands to a lent parameter, yields to a block that lends it) as an alias of the caller's variable (inline_alias_params). An alias is a `const char **` slot, and a String that is the shared handle has none: the handle is carried instead. Such a parameter takes the handle, and every variable bound to it is pulled in (convert_byref_handle_params pulls a method's callers, dyn_pull_arg a yield's variables). A program that yields no handle into an appending block keeps its C. */ /* A plain String local that another local names too (`z = x; yield z`): the lent alias (a `const char **` slot) would grow only the yielded name, so such a local counts as a handle here, as an aliased local handed to a lent parameter does (convert_byref_handle_params). The parameter or block parameter takes the handle, the local is pulled in, and the pure-alias rule (promote_local_alias_pairs) then takes its other names along. */ static int an_local_is_handle_or_aliased(Compiler *c, const ALocalAliases *t, int a) { if (local_is_handle(c, a)) return 1; if (a < 0 || nt_kind(c->nt, a) != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(c->nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; return lv && !lv->is_param && !lv->is_cell && lv->type == TY_STRING && an_local_has_alias(c, t, vn, vs); } /* The arguments yield_splice_handles found going into a boxed parameter that the call's block appends to through a yield: a local is pulled into the handle there, and the emitter refuses any other String variable (refuse_yield_handle_args), which would go into the box as a copy. */ static unsigned char *g_ypoly_need; static int g_ypoly_cap; static void yield_poly_need_mark(Compiler *c, int a) { if (a < 0) return; if (a >= g_ypoly_cap) { int n = c->nt->count > a ? c->nt->count : a + 1; unsigned char *p = (unsigned char *)realloc(g_ypoly_need, (size_t)n); if (!p) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memset(p + g_ypoly_cap, 0, (size_t)(n - g_ypoly_cap)); g_ypoly_need = p; g_ypoly_cap = n; } g_ypoly_need[a] = 1; } int yield_poly_arg_wants_handle(Compiler *c, int a) { (void)c; return a >= 0 && a < g_ypoly_cap && g_ypoly_need[a]; } /* The keywords of a spliced yield (`yield(k: x)`): a block keyword a handle is yielded to, and that the block lends, takes the handle, and then every variable yielded to it is pulled in; a proc a call site passes with `&` that appends to the keyword pulls the variable, whose handle the yield's Hash then carries. */ static int yield_splice_kw_handles(Compiler *c, int mi, int h, int kwh, int pass) { const NodeTable *nt = c->nt; int changed = 0; int en = 0; const int *el = nt_arr(nt, kwh, "elements", &en); for (int e = 0; e < en; e++) { int v; const char *key = dyn_kw_elem_key(c, el[e], &v); if (!key || v < 0 || nt_kind(nt, v) != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, v); if (at != TY_STRING && at != TY_STRBUF) continue; int is_h = local_is_handle(c, v), into_h = 0; for (int ei = h >= 0 ? g_dyn.bhead[h] : -1; ei >= 0; ei = g_dyn.bnext[ei]) { int u = g_dyn.bnode[ei], blk = nt_ref(nt, u, "block"); if (nt_kind(nt, blk) != NK_BlockNode || dyn_call_target_mi(c, u) != mi) continue; const char *kp = an_block_kw_param(c, blk, key); Scope *bs = kp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, kp) : NULL; if (!t || t->is_cell) continue; if (t->type == TY_STRBUF && t->str_shared) { into_h = 1; continue; } if (pass || !is_h || t->type != TY_STRING || !an_block_kw_lent(c, blk, key)) continue; lv_set_type(t, TY_STRBUF); { t->str_shared = 1; comp_touch_scope(t->owner); } changed = 1; } if (pass && into_h && !is_h) changed |= dyn_pull_arg(c, v, 0); for (int ei = !pass && h >= 0 ? g_dyn.bhead[h] : -1; ei >= 0; ei = g_dyn.bnext[ei]) { int u = g_dyn.bnode[ei], ba = nt_ref(nt, u, "block"); if (nt_kind(nt, ba) != NK_BlockArgumentNode || dyn_call_target_mi(c, u) != mi) continue; int x = nt_ref(nt, ba, "expression"); if (x < 0) continue; DynReach r; memset(&r, 0, sizeof r); dyn_reach_kw(c, x, key, 0, &r); if (r.unlifted || !(r.app || (r.unknown && dyn_any_kw_appender(c)))) continue; changed |= dyn_pull_arg(c, v, 1); break; } } return changed; } /* One yield of a spliced method (or a `blk.call` the splice expands as one): a spliced block's parameter a handle is yielded to, and then every variable yielded to it. */ static int yield_splice_site(Compiler *c, int y, int pass, ALocalAliases *aliases) { const NodeTable *nt = c->nt; int changed = 0; Scope *ms = comp_scope_of(c, y); if (!ms || !ms->name || !ms->yields) return 0; int mi = (int)(ms - c->scopes); int aa = nt_ref(nt, y, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; /* an initialize is called by `new` */ int h = anh_find(&g_dyn.bnames, sp_streq(ms->name, "initialize") ? "new" : ms->name); for (int k = 0; k < ac && k < DYN_ARGS; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode) break; /* by keyword, to the blocks' keyword of that name */ if (ak == NK_KeywordHashNode) { changed |= yield_splice_kw_handles(c, mi, h, av[k], pass); break; } /* a block parameter a String value is yielded to, appended to through a local that names it: it takes the handle the alias shares, wrapped around the value (a yielded variable is pulled in below) */ TyKind at = comp_ntype(c, av[k]); if (!pass && (at == TY_STRING || at == TY_STRBUF)) for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], blk = nt_ref(nt, u, "block"); if (nt_kind(nt, blk) != NK_BlockNode || dyn_call_target_mi(c, u) != mi) continue; const char *bp = block_param_at(c, blk, k, call_plain_argc(c, y)); Scope *bs = bp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, bp) : NULL; if (!t || t->is_cell || t->type != TY_STRING || !an_block_param_alias_mutated(c, aliases, bs, bp)) continue; lv_set_type(t, TY_STRBUF); { t->str_shared = 1; comp_touch_scope(t->owner); } changed = 1; } if (ak != NK_LocalVariableReadNode) continue; /* A parameter a nil or other argument widened to POLY boxes what it is bound to: yielded to a proc a call site passes that appends to it, that call site's String variable is pulled in, as for a POLY parameter the method appends to itself (convert_byref_handle_params). Left as a plain String, the box the proc appended to was a copy. */ if (at == TY_POLY) { /* the parameter yielded, or a local written from it (`z = x; yield z`, through a chain too): that local may hold the parameter's String, and the block appends to whatever it holds */ const char *yn = nt_str(nt, av[k], "name"); int pj = an_param_idx(ms, yn); for (int j = 0; pj < 0 && yn && j < ms->nparams; j++) if (ms->pnames[j] && an_local_aliases_reach(aliases, mi, ms->pnames[j], yn)) pj = j; for (int e = !pass && pj >= 0 && h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], ba = nt_ref(nt, u, "block"); if (dyn_call_target_mi(c, u) != mi) continue; /* a literal block the method is spliced with appends to the parameter its position binds, directly or through an alias (dyn_lit_bits): the boxed value it reaches is the caller's, so the caller's String goes over as the handle */ if (nt_kind(nt, ba) == NK_BlockNode) { if (k >= 16 || !(dyn_lit_bits(c, ba) & (1u << k))) continue; } else { if (nt_kind(nt, ba) != NK_BlockArgumentNode) continue; int v = nt_ref(nt, ba, "expression"); if (v < 0) continue; DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, v, k, 0, &r); if (r.unlifted || !(r.app || (r.unknown && dyn_any_appender(c)))) continue; } int ua = arg_layout_param_node(c, ms, u, pj, NULL); if (ua < 0) continue; /* the emitter refuses what this cannot pull (yield_poly_arg_wants_handle) */ yield_poly_need_mark(c, ua); /* a POLY variable there is lifted into the handle, as at a call appending to it (lift_poly_alias_reads), and a POLY ivar too */ if (nt_kind(nt, ua) == NK_LocalVariableReadNode) changed |= dyn_pull_arg(c, ua, 1) | lift_poly_read(c, NULL, NULL, ua); else if (lift_poly_read(c, NULL, NULL, ua)) changed = 1; /* a String value written in the call (`w(+"s") { ... }`) is boxed as a fresh handle, so the block's append grows the value the method goes on to read */ else if (!c->strbuf_box[ua] && infer_type(c, ua) == TY_STRING) { comp_set_strbuf_box(c, ua, 1); changed = 1; } } continue; } if (at != TY_STRING && at != TY_STRBUF) continue; int is_h = an_local_is_handle_or_aliased(c, aliases, av[k]), into_h = 0; for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], blk = nt_ref(nt, u, "block"); if (nt_kind(nt, blk) != NK_BlockNode || dyn_call_target_mi(c, u) != mi) continue; const char *bp = block_param_at(c, blk, k, call_plain_argc(c, y)); Scope *bs = bp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, bp) : NULL; if (!t || t->is_cell) continue; if (t->type == TY_STRBUF && t->str_shared) { into_h = 1; continue; } /* a boxed parameter the block appends to in place appends through a box that holds the handle (#5957): the yielded variable has to be the handle, which the binding boxes as itself. Not for a method that is lowered to call its named block as a proc (it keeps it, or calls itself: the lowering's own test), where the yield boxes what it hands; one that only calls it (`b.call(1)`) is spliced like any yielder. */ if (t->type == TY_POLY && !yield_lowers_block(c, mi) && an_subtree_lends_local(c, nt_ref(nt, blk, "body"), bp, 0)) { into_h = 1; continue; } if (pass || !is_h || t->type != TY_STRING || !an_block_param_lent(c, blk, k, call_plain_argc(c, y))) continue; lv_set_type(t, TY_STRBUF); { t->str_shared = 1; comp_touch_scope(t->owner); } changed = 1; } if (pass && into_h && !local_is_handle(c, av[k])) changed |= dyn_pull_arg(c, av[k], 0); /* a call site handing the method a proc (`run(s, &pr)`, `&method(:m)`): the spliced yield calls it, boxing what it yields, which carries the handle into a proc that appends to it */ for (int e = !pass && h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], ba = nt_ref(nt, u, "block"); if (nt_kind(nt, ba) != NK_BlockArgumentNode || dyn_call_target_mi(c, u) != mi) continue; int v = nt_ref(nt, ba, "expression"); if (v < 0) continue; DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, v, k, 0, &r); if (r.unlifted || !(r.app || (r.unknown && dyn_any_appender(c)))) continue; /* the read stays unmarked: the literal blocks the same yield binds take the plain String, and the proc call boxes the handle (emit_proc_yield) */ changed |= dyn_pull_arg(c, av[k], 0); break; } } return changed; } static int yield_splice_handles(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; dyn_blk_index(c); ALocalAliases aliases; an_local_aliases_build(c, &aliases); /* a spliced method's parameter bound from a handle */ 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; int mi = dyn_call_target_mi(c, u); if (mi < 0 || !c->scopes[mi].yields) continue; Scope *m = &c->scopes[mi]; int blk = nt_ref(nt, u, "block"); if (blk >= 0 && nt_kind(nt, blk) != NK_BlockNode) blk = -1; for (int j = 0; j < m->nparams && j < 32; j++) { int an = arg_layout_param_node(c, m, u, j, NULL); LocalVar *q = m->pnames[j] ? scope_local(m, m->pnames[j]) : NULL; /* a parameter a proc captures is a heap cell, no slot to alias: one the method lends takes the handle whatever its callers pass */ int captured = q && q->is_param && !q->is_block_param && q->type == TY_STRING && q->is_cell && !q->inline_alias && an_subtree_lends_local(c, m->body, m->pnames[j], 0); if (!captured && (!an_local_is_handle_or_aliased(c, &aliases, an) || !an_inline_param_lent(c, mi, j, blk))) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } { q->byref_out = 0; comp_touch_scope(q->owner); } changed = 1; } } /* instance_exec binds its arguments to its block's parameters as a yield does: a parameter the block lends takes a handle handed to it, and one that is the handle takes the caller's variable pulled in */ for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || !nt_str(nt, u, "name") || !sp_streq(nt_str(nt, u, "name"), "instance_exec")) continue; int blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; int aa = nt_ref(nt, u, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; for (int k = 0; k < ac && k < DYN_ARGS; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode) break; if (ak != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, av[k]); if (at != TY_STRING && at != TY_STRBUF) continue; const char *bp = block_param_at(c, blk, k, call_plain_argc(c, u)); Scope *bs = bp ? comp_scope_of(c, blk) : NULL; LocalVar *t = bs ? scope_local(bs, bp) : NULL; if (!t || t->is_cell) continue; if (local_is_handle(c, av[k])) { if (t->type == TY_STRING && an_block_param_lent(c, blk, k, call_plain_argc(c, u))) { lv_set_type(t, TY_STRBUF); { t->str_shared = 1; comp_touch_scope(t->owner); } changed = 1; } } else if (t->type == TY_STRBUF && t->str_shared) changed |= dyn_pull_arg(c, av[k], 0); } } /* a spliced block's parameter a handle is yielded to, and then every variable yielded to it: through a `yield`, and through a `blk.call` on the method's own &block, which the splice expands as one */ for (int pass = 0; pass < 2; pass++) { for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) if (nt_kind(nt, y) == NK_YieldNode) changed |= yield_splice_site(c, y, pass, &aliases); for (int y = comp_kind_first(c, NK_CallNode); y >= 0; y = comp_kind_next(c, y)) { Scope *ys = nt_kind(nt, y) == NK_CallNode ? comp_scope_of(c, y) : NULL; if (ys && ys->yields && an_yield_like(c, y, ys)) changed |= yield_splice_site(c, y, pass, &aliases); } } an_local_aliases_free(&aliases); return changed; } /* ---- A String a yield gathers into a block's rest (#6179) --------------- A spliced yield, and instance_exec, gather the arguments past a block's leading and optional parameters into a fresh Array for its rest (emit_block_binds). The Array held a copy of a plain String, so a block appending to the rest's element (`{ |*q| q[0] << x }`) grew the copy, where CRuby grows the caller's String, as a method's gather did before promote_spread_string_args. Such a String variable becomes the shared handle, and the Array holds the handle. */ /* Pull the String variables among the n arguments av[] that literal block `blk` gathers into its rest, when it appends to the rest's elements. A parameter of the yielding method `ms` a nil or another kind widened to a box pulls the String variable call `u` binds to it instead: the box then holds the handle (as yield_splice_handles does for a boxed parameter). */ static int block_rest_pull_args(Compiler *c, int blk, const int *av, int n, Scope *ms, int u) { const NodeTable *nt = c->nt; int changed = 0; for (int k = 0; k < n; k++) { if (!block_rest_position(c, blk, av, n, k)) continue; if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, av[k]); if (at == TY_STRING || at == TY_STRBUF) changed |= dyn_pull_arg(c, av[k], 1); else if (at == TY_POLY && ms) { int pj = an_param_idx(ms, nt_str(nt, av[k], "name")); int ua = pj >= 0 ? arg_layout_param_node(c, ms, u, pj, NULL) : -1; if (ua >= 0) changed |= dyn_pull_arg(c, ua, 1); } } return changed; } static int yield_rest_handles(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; dyn_blk_index(c); for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode) continue; Scope *ms = comp_scope_of(c, y); if (!ms || !ms->name || !ms->yields) continue; int mi = (int)(ms - c->scopes); int aa = nt_ref(nt, y, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; if (ac == 0) continue; int h = anh_find(&g_dyn.bnames, sp_streq(ms->name, "initialize") ? "new" : ms->name); for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], blk = nt_ref(nt, u, "block"); if (nt_kind(nt, blk) != NK_BlockNode || dyn_call_target_mi(c, u) != mi) continue; changed |= block_rest_pull_args(c, blk, av, ac, ms, u); } } for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode || !nt_str(nt, u, "name") || !sp_streq(nt_str(nt, u, "name"), "instance_exec")) continue; int blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; int aa = nt_ref(nt, u, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; changed |= block_rest_pull_args(c, blk, av, ac, NULL, u); } return changed; } /* ---- A String a yield hands on through a splat (#6179) ----------------- A spliced yield, and instance_exec, with a splat gather every positional value into one Array and bind the block's parameters from it by a distribution only the run time knows (emit_block_binds). A parameter the block appends to was bound to a copy of the String it got, so the splice refused the yield (refuse_yield_splat), or appended to the copy when the String landed past the parameters that refusal asks. Such a parameter takes the handle, and every String variable the Array holds is pulled in: the Array then holds the handle, which the parameter unboxes as itself. */ static int an_local_array_changed(Compiler *c, const char *xn, Scope *xs); /* Pull the String variables among the n positional items iv[] a gathered Array holds: one written there, and the elements of each splatted Array literal, or of a local every write of which is one (a desugared yield nests them: `yield(*s, 2)` is `yield(*[*s, 2])`). An element of a local leaves its read to the container rule (inl 0), as dyn_pull_arg says. */ static int splat_pull_items(Compiler *c, const int *iv, int n, int inl, int depth) { const NodeTable *nt = c->nt; int changed = 0; if (depth > 4) return 0; for (int k = 0; k < n; k++) { NodeKind ak = nt_kind(nt, iv[k]); if (ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) break; if (ak != NK_SplatNode) { TyKind at = ak == NK_LocalVariableReadNode ? comp_ntype(c, iv[k]) : TY_UNKNOWN; if (at == TY_STRING || at == TY_STRBUF) changed |= dyn_pull_arg(c, iv[k], inl); /* a POLY one is lifted, and a parameter pulls its callers in */ else if (at == TY_POLY) changed |= lift_poly_read(c, NULL, NULL, iv[k]); continue; } int x = nt_ref(nt, iv[k], "expression"); if (x >= 0 && nt_kind(nt, x) == NK_ArrayNode) { int en = 0; const int *ev = nt_arr(nt, x, "elements", &en); changed |= splat_pull_items(c, ev, en, inl, depth + 1); /* a literal that holds a handle now is an Array of boxes, also when the handle came after its type settled */ for (int e = 0; inl && e < en; e++) if (c->strbuf_box[ev[e]] && comp_ntype(c, x) != TY_POLY_ARRAY) { comp_sn_retype(c, x, TY_POLY_ARRAY); changed = 1; } continue; } if (x < 0 || nt_kind(nt, x) != NK_LocalVariableReadNode) continue; const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; int lits[16], nl = 0; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && nl < 16; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != xs || nt_kind(nt, w) != NK_LocalVariableWriteNode || !sp_streq(nt_str(nt, w, "name"), xn)) continue; int wv = nt_ref(nt, w, "value"); if (wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) { nl = 0; break; } lits[nl++] = wv; } int param = 0; for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); changed |= splat_pull_items(c, ev, en, 0, depth + 1); for (int e = 0; e < en && !param; e++) { const char *en2 = nt_kind(nt, ev[e]) == NK_LocalVariableReadNode ? nt_str(nt, ev[e], "name") : NULL; LocalVar *ev2 = en2 ? scope_local(xs, en2) : NULL; param = ev2 && ev2->is_param && !ev2->is_block_param && ev2->type == TY_STRBUF && ev2->str_shared; } } /* an Array the program changes holds what was stored, and so does one that holds a parameter, which the container rule leaves alone */ if (xs && (param || an_local_array_changed(c, xn, xs))) changed |= strbuf_demand_container_stores(c, xn, xs); } return changed; } /* The literal blocks a splat reaches whose appended parameters all take the handle from the gathered Array (block_splat_pull_args): the refusals ask it. */ static unsigned char *g_bsplat; static int g_bsplat_cap; static void block_splat_mark(Compiler *c, int blk, int on) { if (blk >= g_bsplat_cap) { if (!on) return; int n = c->nt->count > blk ? c->nt->count : blk + 1; unsigned char *p = (unsigned char *)realloc(g_bsplat, (size_t)n); if (!p) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memset(p + g_bsplat_cap, 0, (size_t)(n - g_bsplat_cap)); g_bsplat = p; g_bsplat_cap = n; } g_bsplat[blk] = (unsigned char)on; } int block_splat_shares(Compiler *c, int blk) { (void)c; return blk >= 0 && blk < g_bsplat_cap && g_bsplat[blk]; } /* Does a call under `node` hand local `vn` as a plain argument to a method parameter the method appends to? */ static int subtree_hands_to_appender(Compiler *c, int node, const char *vn, int depth) { const NodeTable *nt = c->nt; if (node < 0 || node >= nt->count || depth > 300) return 0; if (nt_kind(nt, node) == NK_CallNode) { int a = nt_ref(nt, node, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int mi = ac > 0 ? dyn_call_target_mi(c, node) : -1; for (int k = 0; mi >= 0 && k < ac && k < c->scopes[mi].nparams; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) break; if (ak == NK_LocalVariableReadNode && sp_streq(nt_str(nt, av[k], "name"), vn) && an_param_mutated_in_place(c, mi, k)) return 1; } } const SpNode *nd = &nt->nodes[node]; for (int i = 0; i < nd->nr; i++) if (subtree_hands_to_appender(c, nd->r[i].ref, vn, depth + 1)) return 1; for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) if (subtree_hands_to_appender(c, nd->a[i].ids[j], vn, depth + 1)) return 1; return 0; } /* Does literal block `blk` append to its boxed parameter `bp`: in place, by lending it, or by handing it to a parameter that appends to it? */ static int block_poly_param_appended(Compiler *c, int blk, const char *bp) { int body = nt_ref(c->nt, blk, "body"); Scope *bs = comp_scope_of(c, blk); return an_subtree_lends_local(c, body, bp, 0) || (bs && strbuf_any_str_mut(c, bp, bs)) || subtree_hands_to_appender(c, body, bp, 0); } /* Is one of the n positional arguments av[] a splat? */ static int args_have_splat(const NodeTable *nt, const int *av, int n) { for (int k = 0; k < n; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) return 0; if (ak == NK_SplatNode) return 1; } return 0; } static int block_splat_pull_args(Compiler *c, int blk, const int *av, int n) { const NodeTable *nt = c->nt; int changed = 0; if (!args_have_splat(nt, av, n)) return 0; Scope *bs = comp_scope_of(c, blk); int lent = 0, copy = 0; for (int kind = 0; kind < 3 && bs; kind++) for (int i = 0; i < 64; i++) { const char *bp = kind == 0 ? block_param_name(c, blk, i) : kind == 1 ? block_opt_name(c, blk, i) : block_post_name(c, blk, i); if (!bp) break; LocalVar *t = scope_local(bs, bp); if (!t) continue; if (t->type == TY_STRING && !t->is_cell && an_block_name_lent(c, blk, bp)) { lv_set_type(t, TY_STRBUF); { t->str_shared = 1; comp_touch_scope(t->owner); } changed = 1; } /* a boxed one appends through the handle its box holds; a captured String one is a cell bound from a copy */ if ((t->type == TY_STRBUF && t->str_shared) || (t->type == TY_POLY && block_poly_param_appended(c, blk, bp))) lent = 1; else if (t->type == TY_STRING && t->is_cell && an_subtree_lends_local(c, nt_ref(nt, blk, "body"), bp, 0)) copy = 1; } /* the rest gathers boxes, which hold the handle (block_rest_position) */ const char *rn = block_rest_name(c, blk); if (rn && rn[0] && rest_elems_mutated_walk(c, nt_ref(nt, blk, "body"), rn, 0)) lent = 1; block_splat_mark(c, blk, lent && !copy); /* a copy stays refused (refuse_yield_splat) */ if (!lent || copy) return changed; return changed | splat_pull_items(c, av, n, 1, 0); } static int yield_splat_handles(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; dyn_blk_index(c); for (int y = comp_kind_first(c, NK_YieldNode); y >= 0; y = comp_kind_next(c, y)) { if (nt_kind(nt, y) != NK_YieldNode) continue; Scope *ms = comp_scope_of(c, y); if (!ms || !ms->name || !ms->yields) continue; int mi = (int)(ms - c->scopes); int aa = nt_ref(nt, y, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; if (ac == 0) continue; int h = anh_find(&g_dyn.bnames, sp_streq(ms->name, "initialize") ? "new" : ms->name); for (int e = h >= 0 ? g_dyn.bhead[h] : -1; e >= 0; e = g_dyn.bnext[e]) { int u = g_dyn.bnode[e], blk = nt_ref(nt, u, "block"); if (dyn_call_target_mi(c, u) != mi) continue; if (nt_kind(nt, blk) == NK_BlockNode) { changed |= block_splat_pull_args(c, blk, av, ac); continue; } /* a proc a call site passes with `&` (`y(s, &pr)`): the yield boxes the gathered values, so a String one there goes over as the handle when the proc appends at a position the splat can reach */ int x = blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode && args_have_splat(nt, av, ac) ? nt_ref(nt, blk, "expression") : -1; int app = 0; for (int k = 0; x >= 0 && k < DYN_ARGS && !app; k++) { DynReach r; memset(&r, 0, sizeof r); dyn_reach_value(c, x, k, 0, &r); app = !r.unlifted && (r.app || (r.unknown && dyn_any_appender(c))); } if (app) changed |= splat_pull_items(c, av, ac, 1, 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 || !nt_str(nt, u, "name") || !sp_streq(nt_str(nt, u, "name"), "instance_exec")) continue; int blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; int aa = nt_ref(nt, u, "arguments"), ac = 0; const int *av = aa >= 0 ? nt_arr(nt, aa, "arguments", &ac) : NULL; changed |= block_splat_pull_args(c, blk, av, ac); } return changed; } /* The literal block call `n` hands its arguments to as its parameters: `Thread.new(a) { |x| }`, and a `resume` of a Fiber made with one, `Fiber.new { |x| }.resume(a)` or through a local only ever written so; -1 for another call. */ int an_fiber_new_block(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0 || nt_kind(nt, v) != NK_CallNode || !sp_streq(nt_str(nt, v, "name"), "new")) return -1; int r = nt_ref(nt, v, "receiver"), b = nt_ref(nt, v, "block"); if (r < 0 || nt_kind(nt, r) != NK_ConstantReadNode || !sp_streq(nt_str(nt, r, "name"), "Fiber")) return -1; return b >= 0 && nt_kind(nt, b) == NK_BlockNode ? b : -1; } int an_thread_arg_block(Compiler *c, int n) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"); if (!nm || r < 0) return -1; if (sp_streq(nm, "new")) { int b = nt_ref(nt, n, "block"); return nt_kind(nt, r) == NK_ConstantReadNode && sp_streq(nt_str(nt, r, "name"), "Thread") && b >= 0 && nt_kind(nt, b) == NK_BlockNode ? b : -1; } if (!sp_streq(nm, "resume")) return -1; if (nt_kind(nt, r) != NK_LocalVariableReadNode) return an_fiber_new_block(c, r); const char *vn = nt_str(nt, r, "name"); Scope *vs = vn ? comp_scope_of(c, r) : NULL; int si = vs ? (int)(vs - c->scopes) : -1, blk = -1; for (int w = si >= 0 ? comp_lvw_first_sc(c, si, vn) : -1; w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs || !sp_streq(nt_str(nt, w, "name"), vn)) continue; int b = nt_kind(nt, w) == NK_LocalVariableWriteNode ? an_fiber_new_block(c, nt_ref(nt, w, "value")) : -1; if (b < 0 || (blk >= 0 && blk != b)) return -1; blk = b; } return blk; } static int promote_dyncall_string_args(Compiler *c) { const NodeTable *nt = c->nt; dyn_memo_reset(c); int yh = yield_splice_handles(c); yh |= yield_rest_handles(c); yh |= yield_splat_handles(c); int changed = dyn_convert_params(c); changed |= ctor_convert_params(c); changed |= ctor_pull_args(c); for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) if (dyn_call_site(c, n)) changed |= dyn_pull_site_args(c, n); /* --share-strings: a capture wrapper's call (`->(__cap_a) { .. }.call(a)`, desugar_block_capture_wrap) hands its parameter a variable holding the handle as the handle */ for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { int r = nt_ref(nt, n, "receiver"); if (nt_kind(nt, n) != NK_CallNode || r < 0 || nt_kind(nt, r) != NK_LambdaNode || !nt_int(nt, r, "cap_iife", 0)) continue; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) if (local_is_handle(c, av[k])) changed |= dyn_pull_arg(c, av[k], 1); } /* --share-strings: a Thread's or a Fiber's argument that reads an ivar, a global, a constant or a class variable holding the handle hands it to a block parameter that is one, or boxes it (an_thread_arg_block) */ for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { int blk = nt_kind(nt, n) == NK_CallNode ? an_thread_arg_block(c, n) : -1; if (blk < 0) continue; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { const char *bp = block_param_name(c, blk, k); LocalVar *pv = bp ? scope_local(comp_scope_of(c, blk), bp) : NULL; if (!pv || (!repr_of_slot(c, pv).handle && pv->type != TY_POLY) || c->strbuf_box[av[k]]) continue; int handle = repr_static_share(c, av[k]); if (!handle && nt_kind(nt, av[k]) == NK_InstanceVariableReadNode) { const char *ivn = nt_str(nt, av[k], "name"); int cid = ivn ? comp_ivar_owner(c, av[k]) : -1; int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], ivn) : -1; handle = iv >= 0 && repr_of_ivar(c, cid, iv).share; } if (!handle) continue; comp_set_strbuf_box(c, av[k], 1); comp_sn_retype(c, av[k], TY_STRBUF); changed = 1; } } /* a yield into a real proc is the same call (dyn_yield_site) */ for (int n = comp_kind_first(c, NK_YieldNode); n >= 0; n = comp_kind_next(c, n)) if (nt_kind(nt, n) == NK_YieldNode && dyn_yield_site(c, n) >= 0) changed |= dyn_pull_site_args(c, n); /* the open sites: a boxed callable, a curried proc, bind_call */ for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { int shift; if (!dyn_open_site(c, n, &shift)) continue; changed |= dyn_pull_site_kw_args(c, n); int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = shift; k < ac && k < DYN_ARGS; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) break; if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, av[k]); if (at != TY_STRING && at != TY_STRBUF) continue; if (!local_is_handle(c, av[k])) { DynReach r; memset(&r, 0, sizeof r); if (shift) dyn_reach_value(c, nt_ref(nt, n, "receiver"), k - shift, 0, &r); else r.unknown = 1; if (!r.app && !(r.unknown && dyn_any_appender(c))) continue; } changed |= dyn_pull_arg(c, av[k], 1); } } dyn_memo_stale(); return changed | yh; } /* A default that is an earlier parameter (`def m(x, y = x)`) binds the one String the caller passed for x: CRuby's y IS x, so an append through either is the caller's, and the other sees it. Filled at the call site, the default read x's bytes into a second slot -- a lent temp, or a fresh handle -- and the append landed in a copy the caller never saw, in every call form. When either parameter is appended to (or lent, or already the handle), both take the shared handle: the omitted default then binds x's handle itself (emit_arg_or_default_fill), and the callers are pulled in as a handle method's are (convert_byref_handle_params). A default that only starts from x (`y = x.dup`, `y = x + ""`) is a String of its own and is left alone. Answers 1 when it changed anything. */ static int default_param_mutated(Compiler *c, int mi, int j) { LocalVar *q = c->scopes[mi].pnames[j] ? scope_local(&c->scopes[mi], c->scopes[mi].pnames[j]) : NULL; return q && (q->byref_out || (q->type == TY_STRBUF && q->str_shared) || an_param_mutated_in_place(c, mi, j)); } static int promote_default_alias_params(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; for (int mi = 1; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; if (!m->name || !m->pdefault) continue; for (int j = 1; j < m->nparams; j++) { int dv = m->pdefault[j]; if (dv < 0 || nt_kind(nt, dv) != NK_LocalVariableReadNode) continue; int i = an_param_idx(m, nt_str(nt, dv, "name")); if (i < 0 || i >= j) continue; LocalVar *px = scope_local(m, m->pnames[i]), *py = scope_local(m, m->pnames[j]); if (!px || !py || px->is_block_param || py->is_block_param) continue; if ((px->type != TY_STRING && px->type != TY_STRBUF) || (py->type != TY_STRING && py->type != TY_STRBUF)) continue; if (!default_param_mutated(c, mi, i) && !default_param_mutated(c, mi, j)) continue; LocalVar *both[2] = { px, py }; for (int b = 0; b < 2; b++) { LocalVar *q = both[b]; if (q->type == TY_STRBUF && q->str_shared && !q->byref_out) continue; lv_set_type(q, TY_STRBUF); { q->str_shared = 1; comp_touch_scope(q->owner); } { q->byref_out = 0; comp_touch_scope(q->owner); } { q->is_cell = 0; comp_touch_scope(q->owner); } changed = 1; } } } return changed; } /* ---- A String bound through a splat or a gather (#6179, PR 3) ---------- A parameter a call fills from a splat's elements (`m(*e, v)`, `m(*s)` over `s = [v]`) or gathers into a rest (`def g(*r)` called `g(v)`) has no argument of its own to lend: the call builds an Array of the values and the method reads its parameter back out of it. The Array held a copy of a plain String, and a method appending to the parameter (or to the rest's element) grew the copy, where CRuby grows the caller's String. Such a String variable becomes the shared handle, so the Array holds the handle and the method appends through it in place; a parameter lent a slot (byref) takes the handle instead, since a slot cannot ride in an Array (convert_byref_handle_params). Only a call with a splat or a gather into such a parameter is touched. */ /* Does method mi append to the elements of its rest parameter: `r[i] << x`, `r.first.concat(x)`, or a mutator on the element an iterator over `r` binds (`r.each { |x| x << y }`)? */ static int rest_elems_mutated_walk(Compiler *c, int node, const char *rn, int depth) { const NodeTable *nt = c->nt; if (node < 0 || depth > 200) return 0; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return 0; if (k == NK_CallNode) { const char *un = nt_str(nt, node, "name"); int ur = nt_ref(nt, node, "receiver"); if (un && an_str_mutator_name(un) && ur >= 0 && nt_kind(nt, ur) == NK_CallNode) { const char *en = nt_str(nt, ur, "name"); int er = nt_ref(nt, ur, "receiver"); if (en && (sp_streq(en, "[]") || sp_streq(en, "first") || sp_streq(en, "last") || sp_streq(en, "fetch")) && er >= 0 && nt_kind(nt, er) == NK_LocalVariableReadNode && nt_str(nt, er, "name") && sp_streq(nt_str(nt, er, "name"), rn)) return 1; } int blk = nt_ref(nt, node, "block"); if (un && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && strbuf_elem_first_iterator(un) && ur >= 0 && nt_kind(nt, ur) == NK_LocalVariableReadNode && nt_str(nt, ur, "name") && sp_streq(nt_str(nt, ur, "name"), rn)) { const char *bp = block_param_name(c, blk, 0); if (bp && strbuf_mut_kind(c, bp, comp_scope_of(c, blk)) == 1) return 1; } } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (rest_elems_mutated_walk(c, nt_ref_at(nt, node, i), rn, depth + 1)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) if (rest_elems_mutated_walk(c, ids[j], rn, depth + 1)) return 1; } return 0; } static int rest_elems_mutated(Compiler *c, int mi) { Scope *m = &c->scopes[mi]; if (m->rest_idx < 0 || !m->pnames[m->rest_idx] || m->body < 0) return 0; return rest_elems_mutated_walk(c, m->body, m->pnames[m->rest_idx], 0); } /* The String variable reads call `call` can bind to parameter pj of m where its layout places none: pj is filled from a splat's elements, or pj is the rest a gather fills. They are the positional arguments from the first splat on (or the ones the rest gathers), and the elements of each splatted Array literal or local every write of which is one. direct[i] is 1 for an argument written in the call, which the call boxes itself, and 0 for an element of a local Array, which the container rule turns into the handle (promote_shared_stored_strings). Answers the count. */ int gather_lead_placed(Compiler *c, Scope *m, const int *argv, int argc, int i); int spread_string_reads(Compiler *c, Scope *m, int call, int pj, int *out, int *direct, int cap) { const NodeTable *nt = c->nt; int a = nt_ref(nt, call, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int pos = ac, fs = -1; if (nt_argv_kwh(nt, av, pos) >= 0) pos--; for (int k = 0; k < pos; k++) { if (nt_kind(nt, av[k]) == NK_ForwardingArgumentsNode) return 0; if (nt_kind(nt, av[k]) == NK_SplatNode && fs < 0) fs = k; } int lo, hi; if (fs >= 0) { int an = arg_layout_param_node(c, m, call, pj, NULL); /* A required parameter ahead of the first splat is filled by the argument at its own position, but the gather funds it when a later argument rebinds that variable (gather_lead_arg): the layout then names no node, and the String the argument read first is the one in the gathered Array (`gath(s, *xs, (s = +"v"; 9))`). When that String is the shared handle it is the read to follow: a lent parameter takes the handle, or the append lands in the slot's copy and the String's other name misses it. Any other String there stays lent as before, so a call that hands over no shared String keeps its slots. An optional parameter there is filled the same way when the arguments written outside the splats leave no splat length that moves the lead argument onto a later parameter (gather_lead_placed: `def g4(a = nil, *r, z)` called `g4(s, *e, 1)`, but not `g4(s, *e)`). */ if (an < 0 && pj < fs && pj < m->nparams && pj != m->rest_idx) { if (!gather_lead_placed(c, m, av, ac, pj)) return 0; if (cap < 1 || !an_arg_is_shared_handle(c, av[pj])) return 0; out[0] = av[pj]; direct[0] = 1; return 1; } if (an >= 0) { /* A splat of Array literals alone has a layout, and it places the parameter on an element: the literal the call builds holds that element, so it has to hold the handle (`new(*[t, t])`). An argument written in the call is the static binders' own. */ for (int k = fs; k < pos; k++) { if (nt_kind(nt, av[k]) != NK_SplatNode) continue; int op = nt_ref(nt, av[k], "expression"), en = 0; const int *ev = op >= 0 && nt_kind(nt, op) == NK_ArrayNode ? nt_arr(nt, op, "elements", &en) : NULL; for (int e = 0; e < en; e++) { if (ev[e] != an || nt_kind(nt, an) != NK_LocalVariableReadNode || cap < 1) continue; TyKind et = comp_ntype(c, an); if (et != TY_STRING && et != TY_STRBUF) return 0; out[0] = an; direct[0] = 1; return 1; } } return 0; } lo = fs; hi = pos; } else { if (pj != m->rest_idx) return 0; lo = m->rest_idx; hi = pos - m->npost_rest; } int n = 0; for (int k = lo; k < hi && n < cap; k++) { int x = av[k]; if (nt_kind(nt, x) == NK_SplatNode) { int op = nt_ref(nt, x, "expression"); int lits[16], nl = 0, inline_lit = 0; if (op >= 0 && nt_kind(nt, op) == NK_ArrayNode) { lits[nl++] = op; inline_lit = 1; } else if (op >= 0 && nt_kind(nt, op) == NK_LocalVariableReadNode) { const char *on = nt_str(nt, op, "name"); Scope *os = on ? comp_scope_of(c, op) : NULL; for (int w = os ? comp_lvw_first_sc(c, (int)(os - c->scopes), on) : -1; w >= 0 && nl < 16; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != os || !nt_str(nt, w, "name") || !sp_streq(nt_str(nt, w, "name"), on)) continue; int wv = nt_kind(nt, w) == NK_LocalVariableWriteNode ? nt_ref(nt, w, "value") : -1; if (wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) { nl = 0; break; } lits[nl++] = wv; } } for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); for (int e = 0; e < en && n < cap; e++) { if (nt_kind(nt, ev[e]) != NK_LocalVariableReadNode) continue; TyKind et = comp_ntype(c, ev[e]); if (et != TY_STRING && et != TY_STRBUF && et != TY_UNKNOWN) continue; out[n] = ev[e]; direct[n] = inline_lit; n++; } } continue; } if (nt_kind(nt, x) != NK_LocalVariableReadNode) continue; TyKind xt = comp_ntype(c, x); if (xt != TY_STRING && xt != TY_STRBUF) continue; out[n] = x; direct[n] = 1; n++; } return n; } /* Does method mi append to parameter pj, or (for its rest) to the rest's elements? The dynamic-call analysis's answer for a required positional (it follows a hand-on), the syntactic one otherwise. */ static int spread_param_appended(Compiler *c, int mi, int pj) { Scope *m = &c->scopes[mi]; if (pj == m->rest_idx) return rest_elems_mutated(c, mi); LocalVar *q = m->pnames[pj] ? scope_local(m, m->pnames[pj]) : NULL; if (q && (q->byref_out || (q->type == TY_STRBUF && q->str_shared))) return 1; if (pj < dyn_method_nreq(c, m) && pj < DYN_ARGS) return (dyn_meth_bits(c, mi) >> pj) & 1u; return an_param_mutated_in_place(c, mi, pj); } /* Run in the fixpoint beside promote_dyncall_string_args, so the container rule widens a splatted local Array with the handle it now holds, and after it beside convert_byref_handle_params. The targets are every method of the call's name (a splat's positions are the run time's, and a group member that only reads pulls nothing), and a class's initialize for its `new`: bound by value, that one is refused still (refuse_string_copies), since `new` does not share a String yet. A `super` with a splat binds the one method it calls from an Array of boxes, an argument written after the splat among them: each String variable there is pulled, and the parent's parameter, POLY, appends through the handle its box holds. */ /* Is local Array `xn` changed after the literal it is written from: a call on it that mutates an Array (`s << w`, `s.clear`, `s.replace(t)`), or handed to a call that may? Its layout is then the run time's, not the literal's, and what it holds is whatever was stored into it. */ /* an_local_array_changed's answers by (scope, name), kept while the node table and the scopes hold still: the answer reads only the calls' names and arguments. The spread pass asked it per splat site, every round, and each ask walked every call of the scope; on a Rails program late in its fixpoint that walk was 17% of a sampled window. */ typedef struct { int sidx; const char *name; signed char ans; } LacEntry; static LacEntry *lac_tab; static int lac_cap, lac_n; static const NodeTable *lac_nt; static unsigned lac_ver, lac_gen; static int lac_count, lac_nscopes = -1; static LacEntry *lac_slot(int sidx, const char *name) { unsigned h = (sp_strhash(name) ^ ((unsigned)sidx * 2654435761u)) & (unsigned)(lac_cap - 1); while (lac_tab[h].name && !(lac_tab[h].sidx == sidx && sp_streq(lac_tab[h].name, name))) h = (h + 1) & (unsigned)(lac_cap - 1); return &lac_tab[h]; } static LacEntry *lac_find(Compiler *c, int sidx, const char *name) { const NodeTable *nt = c->nt; if (lac_nt != nt || lac_ver != nt->version || lac_count != nt->count || lac_gen != comp_scope_index_gen() || lac_nscopes != c->nscopes) { free(lac_tab); lac_cap = 1024; lac_n = 0; lac_tab = calloc((size_t)lac_cap, sizeof *lac_tab); if (!lac_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } lac_nt = nt; lac_ver = nt->version; lac_count = nt->count; lac_gen = comp_scope_index_gen(); lac_nscopes = c->nscopes; } if (lac_n * 2 >= lac_cap) { LacEntry *old = lac_tab; int ocap = lac_cap; lac_cap *= 2; lac_tab = calloc((size_t)lac_cap, sizeof *lac_tab); if (!lac_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < ocap; i++) if (old[i].name) *lac_slot(old[i].sidx, old[i].name) = old[i]; free(old); } return lac_slot(sidx, name); } static int an_local_array_changed_scan(Compiler *c, const char *xn, Scope *xs); static int an_local_array_changed(Compiler *c, const char *xn, Scope *xs) { if (!xn || !xs) return 1; LacEntry *e = lac_find(c, (int)(xs - c->scopes), xn); if (e->name) return e->ans; int ans = an_local_array_changed_scan(c, xn, xs); e->sidx = (int)(xs - c->scopes); e->name = xn; e->ans = (signed char)ans; lac_n++; return ans; } static int an_local_array_changed_scan(Compiler *c, const char *xn, Scope *xs) { static const char *const MUT[] = { "<<", "push", "append", "concat", "insert", "unshift", "prepend", "replace", "clear", "[]=", "pop", "shift", "delete", "delete_at", "delete_if", "keep_if", "fill", "slice!", NULL }; const NodeTable *nt = c->nt; if (!xn || !xs) return 1; for (int u = comp_scall_first(c, (int)(xs - c->scopes)); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode) continue; const char *un = nt_str(nt, u, "name"); int r = nt_ref(nt, u, "receiver"); if (r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), xn) && comp_scope_of(c, r) == xs && un) { if (un[0] && un[strlen(un) - 1] == '!') return 1; for (int i = 0; MUT[i]; i++) if (sp_streq(un, MUT[i])) return 1; } int a = nt_ref(nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) if (nt_kind(nt, av[k]) == NK_LocalVariableReadNode && nt_str(nt, av[k], "name") && sp_streq(nt_str(nt, av[k], "name"), xn)) return 1; } return 0; } int an_local_array_changed_x(Compiler *c, const char *xn, Scope *xs) { return an_local_array_changed(c, xn, xs); } /* ...and does it store a String the container rule cannot make the handle: another Array's contents (`replace`, `concat`), or a global, a class variable, an instance variable, a block's parameter pushed into it? */ int an_local_array_stores_unshared(Compiler *c, const char *xn, Scope *xs) { const NodeTable *nt = c->nt; if (!xn || !xs) return 1; for (int u = comp_scall_first(c, (int)(xs - c->scopes)); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode) continue; const char *un = nt_str(nt, u, "name"); int r = nt_ref(nt, u, "receiver"); if (!un || r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode || !nt_str(nt, r, "name") || !sp_streq(nt_str(nt, r, "name"), xn) || comp_scope_of(c, r) != xs) continue; if (sp_streq(un, "replace") || sp_streq(un, "concat")) return 1; if (!(sp_streq(un, "<<") || sp_streq(un, "push") || sp_streq(un, "append") || sp_streq(un, "unshift") || sp_streq(un, "prepend") || sp_streq(un, "insert") || sp_streq(un, "[]="))) continue; int a = nt_ref(nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); TyKind at = comp_ntype(c, av[k]); if (at != TY_STRING && at != TY_STRBUF && at != TY_POLY) continue; if (ak == NK_GlobalVariableReadNode || ak == NK_ClassVariableReadNode || ak == NK_InstanceVariableReadNode) return 1; if (ak == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, av[k], "name"); LocalVar *lv = vn ? scope_local(xs, vn) : NULL; if (lv && lv->is_block_param) return 1; } } } return 0; } /* A splat of a local Array the program changes after its literal, into a parameter that appends: the container rule makes the Array hold the handle of every String stored into it (promote_shared_stored_strings), so the splat hands over the caller's Strings whatever they are by then. Answers 1 when it changed anything. */ static int spread_demand_changed_local(Compiler *c, int splat) { const NodeTable *nt = c->nt; int x = nt_ref(nt, splat, "expression"); if (x < 0 || nt_kind(nt, x) != NK_LocalVariableReadNode) return 0; const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; if (!xs || !an_local_array_changed(c, xn, xs)) return 0; return strbuf_demand_container_stores(c, xn, xs); } /* The first scope from mi on along its name's chain that has a rest, or -1, memoized in rf (-2: not asked): each chain is walked once a pass. */ static int spread_rest_from(Compiler *c, int *rf, int mi) { if (mi < 0) return -1; if (rf[mi] != -2) return rf[mi]; int r = c->scopes[mi].rest_idx >= 0 ? mi : -1; if (r < 0) { /* walk to the first answered or rest scope, then fill the stretch */ int k = g_dyn.snext[mi]; while (k >= 0 && rf[k] == -2 && c->scopes[k].rest_idx < 0) k = g_dyn.snext[k]; r = k < 0 ? -1 : rf[k] != -2 ? rf[k] : k; for (int j = mi; j >= 0 && j != k; j = g_dyn.snext[j]) rf[j] = r; } rf[mi] = r; return r; } /* An ivar among the elements a splat hands on (of an Array literal, or of any Array literal written to the local it splats), or written after the splat: its slot takes the handle, as a container store's does (strbuf_demand_store_leaf), so the gathered Array holds it. Another write of the local does not clear a literal one, which the local may still hold at the call. */ static int spread_demand_ivar(Compiler *c, int r) { if (nt_kind(c->nt, r) != NK_InstanceVariableReadNode || c->strbuf_box[r]) return 0; TyKind it = infer_type(c, r); return it == TY_STRING || it == TY_STRBUF ? strbuf_demand_store_leaf(c, r, 0) : 0; } static int spread_demand_ivar_elems(Compiler *c, const int *av, int ac) { const NodeTable *nt = c->nt; int changed = 0, fs = 0; for (int i = 0; i < ac; i++) { NodeKind ak = nt_kind(nt, av[i]); if (ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) break; /* one written after the splat is boxed into the same Array */ if (ak != NK_SplatNode) { if (fs) changed |= spread_demand_ivar(c, av[i]); continue; } fs = 1; int x = nt_ref(nt, av[i], "expression"), lits[16], nl = 0; if (x >= 0 && nt_kind(nt, x) == NK_ArrayNode) lits[nl++] = x; else if (x >= 0 && nt_kind(nt, x) == NK_LocalVariableReadNode) { const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && nl < 16; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != xs || nt_kind(nt, w) != NK_LocalVariableWriteNode || !sp_streq(nt_str(nt, w, "name"), xn)) continue; int wv = nt_ref(nt, w, "value"); if (wv >= 0 && nt_kind(nt, wv) == NK_ArrayNode) lits[nl++] = wv; } } for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); for (int e = 0; e < en; e++) changed |= spread_demand_ivar(c, ev[e]); } } return changed; } /* A parameter that is an element of a splatted local Array (`r = [x]; m(*r)`): the container rule (promote_shared_stored_strings) takes locals only, so the Array a parameter is stored into is demanded to hold the handle here, or it stayed a StrArray of copies and the append missed the caller's String. */ static int spread_demand_param_elem(Compiler *c, const int *av, int ac, int el) { const NodeTable *nt = c->nt; const char *en = nt_str(nt, el, "name"); Scope *es = en ? comp_scope_of(c, el) : NULL; LocalVar *elv = es ? scope_local(es, en) : NULL; if (!elv || !elv->is_param || elv->is_block_param) return 0; int changed = 0; for (int i = 0; i < ac; i++) { if (nt_kind(nt, av[i]) != NK_SplatNode) continue; int x = nt_ref(nt, av[i], "expression"); if (x < 0 || nt_kind(nt, x) != NK_LocalVariableReadNode) continue; const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; int holds = 0; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && !holds; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != xs || nt_kind(nt, w) != NK_LocalVariableWriteNode || !sp_streq(nt_str(nt, w, "name"), xn)) continue; int wv = nt_ref(nt, w, "value"), wn = 0; const int *ev = wv >= 0 && nt_kind(nt, wv) == NK_ArrayNode ? nt_arr(nt, wv, "elements", &wn) : NULL; for (int e = 0; e < wn && !holds; e++) holds = ev[e] == el; } if (holds) changed |= strbuf_demand_container_stores(c, xn, xs); } return changed; } /* A block parameter read `a` handed to a parameter that appends. */ static int spread_demand_block_param(Compiler *c, int a) { const NodeTable *nt = c->nt; if (nt_kind(nt, a) != NK_LocalVariableReadNode || c->strbuf_box[a]) return 0; const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || !lv->is_block_param || (lv->type != TY_STRING && lv->type != TY_STRBUF)) return 0; return strbuf_demand_store_leaf(c, a, 0); } /* Mark POLY parameter `pn` of scope `vs` appended to (poly_lift). */ static int spread_lift_poly_param(Compiler *c, Scope *vs, const char *pn) { LocalVar *pv = pn ? scope_local(vs, pn) : NULL; if (!pv || !pv->is_param || pv->is_block_param || pv->type != TY_POLY || an_param_idx(vs, pn) < 0 || (pv->poly_lift & POLY_LIFT_APPENDED)) return 0; pv->poly_lift |= POLY_LIFT_APPENDED; return 1; } /* A POLY variable among the elements a splat hands on (of an Array literal, or of a local every write of which is one), or written past the splat when `past` is set: its read is lifted, so a plain String it holds goes over as the handle, and a method's own POLY parameter pulls its callers in (poly_lift), as at a static call. */ static int spread_lift_poly_elems(Compiler *c, const int *av, int ac, int past) { const NodeTable *nt = c->nt; int changed = 0, fs = 0; for (int i = 0; i < ac; i++) { int reads[64], nr = 0; if (nt_kind(nt, av[i]) != NK_SplatNode) { if (fs && past) reads[nr++] = av[i]; } else { fs = 1; int x = nt_ref(nt, av[i], "expression"), lits[16], nl = 0; if (x >= 0 && nt_kind(nt, x) == NK_ArrayNode) lits[nl++] = x; else if (x >= 0 && nt_kind(nt, x) == NK_LocalVariableReadNode) { const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && nl < 16; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != xs || nt_kind(nt, w) != NK_LocalVariableWriteNode || !sp_streq(nt_str(nt, w, "name"), xn)) continue; int wv = nt_ref(nt, w, "value"); if (wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) { nl = 0; break; } lits[nl++] = wv; } } for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); for (int e = 0; e < en && nr < 64; e++) reads[nr++] = ev[e]; } } for (int r = 0; r < nr; r++) { int a = reads[r]; if (nt_kind(nt, a) != NK_LocalVariableReadNode) continue; const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; LocalVar *lv = vs ? scope_local(vs, vn) : NULL; if (!lv || lv->type != TY_POLY || lv->is_block_param || !poly_var_may_hold_string(c, NULL, vn, vs, 0,NULL)) continue; if (!c->poly_strbuf_lift[a]) { c->poly_strbuf_lift[a] = 1; changed = 1; } /* the method's own POLY parameter, or one a local is written from (`v = x`): its box is the caller's, so it pulls its callers in */ if (lv->is_param) changed |= spread_lift_poly_param(c, vs, vn); else { for (int w = comp_lvw_first_sc(c, (int)(vs - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != vs || nt_kind(nt, w) != NK_LocalVariableWriteNode || !sp_streq(nt_str(nt, w, "name"), vn)) continue; int wv = nt_ref(nt, w, "value"); if (wv >= 0 && nt_kind(nt, wv) == NK_LocalVariableReadNode) changed |= spread_lift_poly_param(c, vs, nt_str(nt, wv, "name")); } } } } return changed; } /* Is `an` one of the n arguments av[] written at or past the first splat? */ static int an_arg_after_splat(const NodeTable *nt, const int *av, int n, int an) { int fs = 0; for (int k = 0; k < n; k++) { if (nt_kind(nt, av[k]) == NK_SplatNode) fs = 1; else if (fs && av[k] == an) return 1; } return 0; } static int promote_spread_string_args(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; dyn_memo_reset(c); /* per scope, the parameters it appends to (bit 31: answered), asked once a pass: every call of a name visits every method of it */ unsigned *app = (unsigned *)calloc((size_t)c->nscopes + 1, sizeof(unsigned)); /* per scope, the first scope of its name, itself or later, that has a rest (-1 none): a call without a splat follows only those */ int *rest_from = (int *)malloc(sizeof(int) * ((size_t)c->nscopes + 1)); if (!app || !rest_from) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i <= c->nscopes; i++) rest_from[i] = -2; for (int n = comp_kind_first(c, NK_CallNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_CallNode) continue; const char *nm = nt_str(nt, n, "name"); int a = nt_ref(nt, n, "arguments"); if (!nm || a < 0) continue; /* without a splat, only a rest gathers the call's arguments */ int ac = 0; const int *av = nt_arr(nt, a, "arguments", &ac); int splat = nt_argv_has_splat(nt, av, ac); /* `C.new(*s)` binds C's initialize */ int ctor = -1, r = nt_ref(nt, n, "receiver"); if (sp_streq(nm, "new") && r >= 0 && (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode)) { int ci = nt_str(nt, r, "name") ? const_node_class_index(c, r) : -1; ctor = ci >= 0 ? comp_method_in_chain(c, ci, "initialize", NULL) : -1; } int first = ctor >= 0 ? ctor : dyn_scopes_named(c, nm); if (!splat && ctor < 0) first = spread_rest_from(c, rest_from, first); for (int mi = first; mi >= 0; mi = ctor >= 0 ? -1 : splat ? g_dyn.snext[mi] : spread_rest_from(c, rest_from, g_dyn.snext[mi])) { Scope *m = &c->scopes[mi]; if (!splat && m->rest_idx < 0) continue; if (!(app[mi] & 0x80000000u)) { app[mi] = 0x80000000u; for (int pj = 0; pj < m->nparams && pj < 31; pj++) if (spread_param_appended(c, mi, pj)) app[mi] |= 1u << pj; } /* without a splat, only the rest */ for (int pj = splat ? 0 : m->rest_idx; pj < (splat ? m->nparams : m->rest_idx + 1) && pj < 31; pj++) { if (!(app[mi] & (1u << pj))) continue; int out[32], direct[32]; int k = spread_string_reads(c, m, n, pj, out, direct, 32); for (int i = 0; i < k; i++) { changed |= dyn_pull_arg(c, out[i], direct[i]); if (!direct[i]) changed |= spread_demand_param_elem(c, av, ac, out[i]); } /* an element read the rest gathers (`m(h[:a])` into `def m(*r)`), written ahead of any splat: its container's Strings */ if (pj == m->rest_idx) for (int i = pj; i < ac && i - pj < 31; i++) { NodeKind ik = nt_kind(nt, av[i]); if (ik == NK_SplatNode || ik == NK_KeywordHashNode || ik == NK_BlockArgumentNode) break; int d = strbuf_demand_elem_arg(c, av[i]); if (d > 0) changed = 1; } if (splat) changed |= spread_lift_poly_elems(c, av, ac, 0); if (splat) changed |= spread_demand_ivar_elems(c, av, ac); /* a splatted local Array the program changes holds what was stored */ for (int i = 0; splat && i < ac; i++) if (nt_kind(nt, av[i]) == NK_SplatNode) changed |= spread_demand_changed_local(c, av[i]); } } } /* `super(*s)`, `super(*e, v)`: the one method the super calls */ for (int n = comp_kind_first(c, NK_SuperNode); n >= 0; n = comp_kind_next(c, n)) { if (nt_kind(nt, n) != NK_SuperNode) continue; int a = nt_ref(nt, n, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; int splat = nt_argv_has_splat(nt, av, ac); Scope *ss = splat ? comp_scope_of(c, n) : NULL; int mi = ss && ss->class_id >= 0 && ss->name ? a_super_target(c, ss) : -1; if (mi < 0) continue; Scope *m = &c->scopes[mi]; for (int pj = 0; pj < m->nparams && pj < 31; pj++) { if (!spread_param_appended(c, mi, pj)) continue; int out[32], direct[32]; int k = spread_string_reads(c, m, n, pj, out, direct, 32); /* an argument written past the splat, which a call's binder lends: super boxes it into the Array it binds from */ int an = k == 0 ? arg_layout_param_node(c, m, n, pj, NULL) : -1; if (an >= 0 && an_arg_after_splat(nt, av, ac, an)) { out[0] = an; direct[0] = 1; k = 1; } for (int i = 0; i < k; i++) { changed |= dyn_pull_arg(c, out[i], direct[i]); if (!direct[i]) changed |= spread_demand_param_elem(c, av, ac, out[i]); /* a block's parameter holds what its iterator hands it: those Strings are the ones demanded (strbuf_block_param_source_walk) */ else changed |= spread_demand_block_param(c, out[i]); } changed |= spread_lift_poly_elems(c, av, ac, 1); for (int i = 0; i < ac; i++) if (nt_kind(nt, av[i]) == NK_SplatNode) changed |= spread_demand_changed_local(c, av[i]); } } free(app); free(rest_from); dyn_memo_stale(); return changed; } /* The method a `super` in scope `m` calls, or -1. */ int a_super_target(Compiler *c, Scope *m) { const char *shadow = comp_super_shadow(c, m); if (shadow) return m->is_cmethod ? comp_cmethod_in_class(c, m->class_id, shadow) : comp_method_in_class(c, m->class_id, shadow); if (m->class_id < 0) return -1; /* a top-level def: no class, so no superclass method */ int p = c->classes[m->class_id].parent; if (p < 0) return -1; const char *u = comp_scope_user_name(m); return m->is_cmethod ? comp_cmethod_in_chain(c, p, u, NULL) : comp_method_in_chain(c, p, u, NULL); } /* Whether a `super` in scope `m` passes m's own block on: a bare `super`, one with arguments but no block (implicit), or `super(&blk)` naming m's block. */ static int a_scope_super_passes_block(Compiler *c, Scope *m) { const NodeTable *nt = c->nt; for (int q = 0; q < nt->count; q++) { NodeKind k = nt_kind(nt, q); if (k != NK_SuperNode && k != NK_ForwardingSuperNode) continue; if (comp_scope_of(c, q) != m) continue; int b = nt_ref(nt, q, "block"); if (b < 0) return 1; if (nt_kind(nt, b) != NK_BlockArgumentNode) continue; int e = nt_ref(nt, b, "expression"); if (e >= 0 && nt_kind(nt, e) == NK_LocalVariableReadNode && sp_streq(nt_str(nt, e, "name"), m->blk_param)) return 1; } return 0; } /* ---- A String forwarded by `super`, `*`, `...` (#6179) ------------------- A parameter a method only hands on appends to the caller's String when the parameter it lands on does: `def m(p, k:) = super` into a parent that appends to its `p`, and `def w(*a) = m(*a)`, `def w(*) = m(*)`, `def w(...) = m(...)` or `def m(*) = super` into a method that appends to the parameter an element of the rest lands on. A call with a `**h` types such a parameter POLY, and a rest is an Array of boxes: either way the caller boxed a copy of its String, which the target appended to. A POLY parameter handed on this way pulls its callers' Strings into the handle as one the method appends to itself does (convert_byref_handle_params), and a rest whose elements are forwarded pulls the Strings its callers gather into it (promote_forwarded_rest_args), so the box carries the handle, which the target appends through. */ /* The method a call names when it names one: self or none (a unique name), a constant's `new` (its initialize), an object's class. */ static int fwd_call_target(Compiler *c, int u) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, u, "name"); if (!nm) return -1; int recv = nt_ref(nt, u, "receiver"); NodeKind rk = recv >= 0 ? nt_kind(nt, recv) : NK_SelfNode; if (sp_streq(nm, "new") && (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode)) { TyKind ct = comp_ntype(c, u); return ty_is_object(ct) ? comp_method_in_chain(c, ty_object_class(ct), "initialize", NULL) : -1; } if (rk == NK_SelfNode) { Scope *es = comp_scope_of(c, u); /* `new` in a class method builds the class */ if (sp_streq(nm, "new") && es && es->is_cmethod && es->class_id >= 0) return comp_method_in_chain(c, es->class_id, "initialize", NULL); int mi = es && es->class_id >= 0 && !es->is_cmethod ? call_target(c, u, es->class_id, nm, 0, NULL) : -1; return mi >= 0 ? mi : an_any_scope_by_name(c, nm); } TyKind rt = comp_ntype(c, recv); if (ty_is_object(rt)) return call_target(c, u, ty_object_class(rt), nm, 0, NULL); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { int ci = nt_str(nt, recv, "name") ? const_node_class_index(c, recv) : -1; return ci >= 0 ? call_target(c, u, ci, nm, CT_CMETHOD, NULL) : -1; } return -1; } static unsigned fwd_rest_bits(Compiler *c, int mi); /* Set when an answer below was cut short, so it is not kept as final: 1 a method still being asked (a cycle of forwarders), 2 the depth bound. */ static int g_fwd_taint; /* How deep the POLY hand-ons one query follows, in steps (two a hand-on): a chain longer than that is refused rather than followed on down the C stack. Inside a rest forwarder being asked, where nothing is remembered, the walk stops after two hand-ons, as before. */ #define FWD_POLY_DEPTH 512 #define FWD_POLY_DEPTH_REST 4 /* fwd_rest_bits beside the elements' bits 0-15: an element at offset 16 or more, past what the bits and the dynamic masks hold, reaches a parameter that appends; and an answer below was cut at the bound. Either way the caller's String cannot be pulled in, and the refusal takes it. */ #define FWD_REST_PAST 0x10000u #define FWD_REST_OPEN 0x20000u static int g_fwd_rest_depth; /* forwarders being asked, nested */ /* The POLY hand-ons one query asks, remembered for that query: a parameter handed to many methods, each handing it on to many, reaches the same (method, parameter) by many paths, and asking each afresh made the walk the number of paths. Each is asked once a query: it is entered as no append while it is being asked, so a chain that comes back to it (a cycle of hand-ons) adds nothing, and the parameters that chain passes on are asked by the outer ask all the same. The query's own answer is then the whole reach of its parameter, however deep, where a bound of two hand-ons refused a String RDoc hands four methods down (`cross_reference(name)` into `link`, `resolve`, `resolve_local_symbol`) though none appends. An entry holds the taint its ask left, so a remembered answer leaves exactly what asking again would. Only outside a rest forwarder being asked: there an answer leans on that forwarder's partial bits. A query is one ask from outside (depth 0); the passes between queries change the types the answers read, so each query starts empty. */ typedef struct { unsigned gen; int mi, j; signed char r, taint; } FwdMemo; static FwdMemo *g_fwd_memo; static unsigned g_fwd_memo_cap, g_fwd_memo_n, g_fwd_memo_gen; static void fwd_memo_query(void) { if (++g_fwd_memo_gen == 0) { /* wrapped: forget every stamp */ if (g_fwd_memo) memset(g_fwd_memo, 0, sizeof(FwdMemo) * g_fwd_memo_cap); g_fwd_memo_gen = 1; } g_fwd_memo_n = 0; } static unsigned fwd_memo_slot(int mi, int j) { unsigned h = (unsigned)mi * 2654435761u ^ (unsigned)j * 40503u; unsigned k = h & (g_fwd_memo_cap - 1); while (g_fwd_memo[k].gen == g_fwd_memo_gen && (g_fwd_memo[k].mi != mi || g_fwd_memo[k].j != j)) k = (k + 1) & (g_fwd_memo_cap - 1); return k; } static FwdMemo *fwd_memo_find(int mi, int j) { if (!g_fwd_memo) return NULL; FwdMemo *e = &g_fwd_memo[fwd_memo_slot(mi, j)]; return e->gen == g_fwd_memo_gen ? e : NULL; } static void fwd_memo_put(int mi, int j, int r, int taint) { FwdMemo *e = g_fwd_memo ? fwd_memo_find(mi, j) : NULL; if (e) { e->r = (signed char)r; e->taint = (signed char)taint; return; } if ((g_fwd_memo_n + 1) * 2 > g_fwd_memo_cap) { FwdMemo *old = g_fwd_memo; unsigned ocap = g_fwd_memo_cap; g_fwd_memo_cap = ocap ? ocap * 2 : 1024; g_fwd_memo = (FwdMemo *)calloc(g_fwd_memo_cap, sizeof(FwdMemo)); if (!g_fwd_memo) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (unsigned k = 0; k < ocap; k++) if (old[k].gen == g_fwd_memo_gen) g_fwd_memo[fwd_memo_slot(old[k].mi, old[k].j)] = old[k]; free(old); } e = &g_fwd_memo[fwd_memo_slot(mi, j)]; e->gen = g_fwd_memo_gen; e->mi = mi; e->j = j; e->r = (signed char)r; e->taint = (signed char)taint; g_fwd_memo_n++; } /* Does method mi append to what its parameter j is bound to: in place, lent, the handle, or a POLY parameter or a rest element it hands on? */ static int fwd_param_appends(Compiler *c, int mi, int j, int depth) { Scope *m = &c->scopes[mi]; if (j < 0) return 0; if (depth == 0 && g_fwd_rest_depth == 0) fwd_memo_query(); /* a rest takes the arguments from its position on; a chain of them is memoized per method (fwd_rest_bits), so it does not count toward the depth, which bounds the POLY hand-ons below */ if (m->rest_idx >= 0 && j >= m->rest_idx) { unsigned rb = fwd_rest_bits(c, mi); if (rb & FWD_REST_OPEN) g_fwd_taint |= 2; return j - m->rest_idx < 16 ? (int)((rb >> (j - m->rest_idx)) & 1u) : (rb & FWD_REST_PAST) != 0; } if (j >= m->nparams || !m->pnames[j]) return 0; LocalVar *q = scope_local(m, m->pnames[j]); if (!q || !q->is_param || q->is_block_param) return 0; if (q->byref_out || (q->type == TY_STRBUF && q->str_shared)) return 1; if (q->type != TY_POLY) return 0; int memo = g_fwd_rest_depth == 0; FwdMemo *e = memo ? fwd_memo_find(mi, j) : NULL; if (e) { g_fwd_taint |= e->taint; return e->r; } int outer = g_fwd_taint, r; g_fwd_taint = 0; /* being asked: a hand-on that comes back to it answers no append */ if (memo) fwd_memo_put(mi, j, 0, 1); if (an_param_mutated_in_place(c, mi, j)) r = 1; else if (depth > (memo ? FWD_POLY_DEPTH : FWD_POLY_DEPTH_REST)) { g_fwd_taint |= 2; r = 0; } else r = fwd_poly_param_handed_on(c, mi, j, depth + 1); if (memo) fwd_memo_put(mi, j, r, g_fwd_taint); g_fwd_taint |= outer; return r; } /* The position a call or `super`'s splat of local `rn` starts at among the target's parameters (the plain arguments ahead of it), or -1. */ static int fwd_splat_start(Compiler *c, int u, const char *rn) { const NodeTable *nt = c->nt; int a = nt_ref(nt, u, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; for (int k = 0; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode) { int x = nt_ref(nt, av[k], "expression"); return x >= 0 && nt_kind(nt, x) == NK_LocalVariableReadNode && nt_str(nt, x, "name") && sp_streq(nt_str(nt, x, "name"), rn) ? k : -1; } if (ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) return -1; } return -1; } /* Per pass: which elements of each method's rest (bits 0-15, and the two flags above) it forwards to a parameter that appends; bit 30 answered, bit 29 being asked (with the bits found so far). An answer that leaned on a cut-short one is not kept: the outermost query of a cycle of forwarders asks again with what it found until that stops growing, and a forwarder a deeper query reached is asked afresh by its own. */ static unsigned *g_fwd_rest; static int g_fwd_n; static unsigned fwd_rest_bits_once(Compiler *c, int mi, const char *rn); static unsigned fwd_rest_bits(Compiler *c, int mi) { if (mi < 0 || mi >= g_fwd_n) return 0; if (g_fwd_rest[mi] & 0x40000000u) return g_fwd_rest[mi] & 0x3ffffu; if (g_fwd_rest[mi] & 0x20000000u) { g_fwd_taint |= 1; return g_fwd_rest[mi] & 0x3ffffu; } if (g_fwd_rest_depth > 64) { g_fwd_taint |= 2; return FWD_REST_OPEN; } Scope *m = &c->scopes[mi]; const char *rn = m->rest_idx >= 0 ? m->pnames[m->rest_idx] : NULL; if (!rn) { g_fwd_rest[mi] = 0x40000000u; return 0; } int outer = g_fwd_taint, top = g_fwd_rest_depth == 0; unsigned bits = 0; int tainted; g_fwd_rest_depth++; for (int round = 0; ; round++) { g_fwd_rest[mi] = 0x20000000u | bits; g_fwd_taint = 0; unsigned got = bits | fwd_rest_bits_once(c, mi, rn); tainted = g_fwd_taint; if (got == bits || !top || round >= 16) { bits = got; break; } bits = got; } g_fwd_rest_depth--; /* the top of a cycle has its answer; one cut at the bound says so */ if (tainted & 2) bits |= FWD_REST_OPEN; g_fwd_taint = outer | (top ? 0 : tainted); g_fwd_rest[mi] = !top && tainted ? 0 : 0x40000000u | bits; return bits; } /* Does target t, its parameters laid from position p on, append to one at offset 16 or more: one of its own, or (a rest) one it forwards that far? */ static unsigned fwd_rest_past(Compiler *c, int t, int p) { Scope *tm = &c->scopes[t]; int end = tm->rest_idx >= 0 ? tm->rest_idx + 17 : tm->nparams; for (int j = p + 16; j < end && j < p + 64; j++) if (fwd_param_appends(c, t, j, 0)) return FWD_REST_PAST; return 0; } static unsigned fwd_rest_bits_once(Compiler *c, int mi, const char *rn) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; unsigned bits = 0; for (int u = comp_scall_first(c, mi); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode) continue; int p = fwd_splat_start(c, u, rn); int t = p >= 0 ? fwd_call_target(c, u) : -1; for (int i = 0; t >= 0 && i < 16; i++) if (fwd_param_appends(c, t, p + i, 0)) bits |= 1u << i; if (t >= 0) bits |= fwd_rest_past(c, t, p); } /* `super(*a)`, and a zsuper handing on the rest at its own position */ for (int pass = 0; pass < 2 && m->class_id >= 0; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int q = comp_kind_first(c, sk); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != sk || comp_scope_of(c, q) != m) continue; int p = pass ? m->rest_idx : fwd_splat_start(c, q, rn); int t = p >= 0 ? a_super_target(c, m) : -1; for (int i = 0; t >= 0 && i < 16; i++) if (fwd_param_appends(c, t, p + i, 0)) bits |= 1u << i; if (t >= 0) bits |= fwd_rest_past(c, t, p); } } return bits; } /* Does POLY parameter pj of method mi reach a parameter that appends, by a `super` or a call it is handed to? */ static int fwd_poly_param_handed_on(Compiler *c, int mi, int pj, int depth) { const NodeTable *nt = c->nt; if (depth > (g_fwd_rest_depth == 0 ? FWD_POLY_DEPTH : FWD_POLY_DEPTH_REST)) { g_fwd_taint |= 2; return 0; } if (depth == 0 && g_fwd_rest_depth == 0) fwd_memo_query(); if (mi < 0 || mi >= c->nscopes) return 0; Scope *m = &c->scopes[mi]; if (pj < 0 || pj >= m->nparams || !m->pnames[pj]) return 0; const char *pn = m->pnames[pj]; int t = m->class_id >= 0 ? a_super_target(c, m) : -1; if (t >= 0) { Scope *tm = &c->scopes[t]; for (int q = comp_kind_first(c, NK_ForwardingSuperNode); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != NK_ForwardingSuperNode || comp_scope_of(c, q) != m) continue; for (int j = 0; j < tm->nparams; j++) if (zsuper_param_source(c, m, tm, j) == pj && fwd_param_appends(c, t, j, depth + 1)) return 1; } for (int q = comp_kind_first(c, NK_SuperNode); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != NK_SuperNode || comp_scope_of(c, q) != m) continue; for (int j = 0; j < tm->nparams; j++) { int an = arg_layout_param_node(c, tm, q, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode && nt_str(nt, an, "name") && sp_streq(nt_str(nt, an, "name"), pn) && fwd_param_appends(c, t, j, depth + 1)) return 1; } } } for (int u = comp_scall_first(c, mi); u >= 0; u = comp_scall_next(c, u)) { if (nt_kind(nt, u) != NK_CallNode) continue; int ct = fwd_call_target(c, u); if (ct < 0) continue; Scope *cm = &c->scopes[ct]; for (int j = 0; j < cm->nparams; j++) { int an = arg_layout_param_node(c, cm, u, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode && nt_str(nt, an, "name") && sp_streq(nt_str(nt, an, "name"), pn) && fwd_param_appends(c, ct, j, depth + 1)) return 1; } /* laid into the callee's rest (`def relay(*r) = pair(*r)` called `relay(other, value)`): the parameter is element k - rest_idx of that rest, and the rest hands it on to one that appends. The positions before the rest are the loop above's. */ if (cm->rest_idx >= 0) { int args = nt_ref(nt, u, "arguments"), ac = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &ac) : NULL; for (int k = cm->rest_idx; k < ac; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) break; if (ak != NK_LocalVariableReadNode || !nt_str(nt, av[k], "name") || !sp_streq(nt_str(nt, av[k], "name"), pn)) continue; unsigned rb = fwd_rest_bits(c, ct); int i = k - cm->rest_idx; if (i < 16 ? (rb >> i) & 1u : (rb & FWD_REST_PAST) != 0) return 1; if (rb & FWD_REST_OPEN) g_fwd_taint |= 2; } } } return 0; } /* For the emitters' refusal, after the last pass: does method mi forward element i of its rest to a parameter that appends, and does it hand its POLY parameter j on to one? */ static int g_fwd_codegen; static void fwd_memo_fresh(Compiler *c) { if (g_fwd_codegen && g_fwd_n == c->nscopes) return; free(g_fwd_rest); g_fwd_n = c->nscopes; g_fwd_rest = (unsigned *)calloc((size_t)g_fwd_n + 1, sizeof(unsigned)); if (!g_fwd_rest) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_fwd_codegen = 1; } /* Each answers 1 when it appends, 0 when it does not, and -1 when it cannot tell -- a POLY hand-on past the depth bound -- which the refusal takes as appending, so an answer cut short is refused rather than copied. */ int fwd_rest_elem_appends(Compiler *c, int mi, int i) { if (mi < 0 || mi >= c->nscopes || i < 0 || c->scopes[mi].rest_idx < 0) return 0; fwd_memo_fresh(c); unsigned rb = fwd_rest_bits(c, mi); if (i < 16 ? (rb >> i) & 1u : (rb & FWD_REST_PAST) != 0) return 1; return rb & FWD_REST_OPEN ? -1 : 0; } int fwd_param_appends_at(Compiler *c, int mi, int j) { if (mi < 0 || mi >= c->nscopes || j < 0) return 0; fwd_memo_fresh(c); int outer = g_fwd_taint; g_fwd_taint = 0; int r = fwd_param_appends(c, mi, j, 0); if (!r && (g_fwd_taint & 2)) r = -1; g_fwd_taint = outer; return r; } int fwd_poly_param_appends(Compiler *c, int mi, int j) { if (mi < 0 || mi >= c->nscopes || j < 0 || j >= c->scopes[mi].nparams) return 0; LocalVar *q = c->scopes[mi].pnames[j] ? scope_local(&c->scopes[mi], c->scopes[mi].pnames[j]) : NULL; if (!q || q->type != TY_POLY || an_param_mutated_in_place(c, mi, j)) return 0; fwd_memo_fresh(c); int outer = g_fwd_taint; g_fwd_taint = 0; int r = fwd_poly_param_handed_on(c, mi, j, 0); if (!r && (g_fwd_taint & 2)) r = -1; g_fwd_taint = outer; return r; } /* A String parameter a `super` hands on to a parameter that appends: one an included module's copy takes boxed, which no override family widens the child's to (the zsuper gathers it, a child's `**o` beside it). Such a parameter takes the handle, which the gather boxes, and its callers are pulled in as a handle method's are. Answers 1 when it changed anything. */ static int fwd_super_string_params(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; for (int pass = 0; pass < 2; pass++) { NodeKind sk = pass ? NK_ForwardingSuperNode : NK_SuperNode; for (int q = comp_kind_first(c, sk); q >= 0; q = comp_kind_next(c, q)) { if (nt_kind(nt, q) != sk) continue; Scope *m = comp_scope_of(c, q); int t = m && m->name && m->class_id >= 0 ? a_super_target(c, m) : -1; if (t < 0) continue; Scope *tm = &c->scopes[t]; for (int j = 0; j < tm->nparams && j < 32; j++) { LocalVar *d = tm->pnames[j] ? scope_local(tm, tm->pnames[j]) : NULL; if (!d || !d->is_param || d->is_block_param || !fwd_param_appends(c, t, j, 0)) continue; int src = -1; if (pass) src = zsuper_param_source(c, m, tm, j); else { int an = arg_layout_param_node(c, tm, q, j, NULL); if (an >= 0 && nt_kind(nt, an) == NK_LocalVariableReadNode) src = an_param_idx(m, nt_str(nt, an, "name")); } LocalVar *p = src >= 0 && src < m->nparams && m->pnames[src] ? scope_local(m, m->pnames[src]) : NULL; if (!p || !p->is_param || p->is_block_param) continue; /* a String into a boxed parameter: the child's takes the handle */ if (d->type == TY_POLY && p->type == TY_STRING && !p->byref_out && !p->is_cell) { lv_set_type(p, TY_STRBUF); { p->str_shared = 1; comp_touch_scope(p->owner); } changed = 1; } /* a box, which carries the caller's handle (fwd_poly_param_handed_on), into a lent slot: the parent's takes the handle, as a handle argument at a call converts one (convert_byref_handle_params) */ else if (p->type == TY_POLY && d->byref_out) { { d->byref_out = 0; comp_touch_scope(d->owner); } { d->is_cell = 0; comp_touch_scope(d->owner); } lv_set_type(d, TY_STRBUF); { d->str_shared = 1; comp_touch_scope(d->owner); } changed = 1; } } } } return changed; } /* The String variables a call's splat of an Array literal, or of a local every write of which is one, lays at positions from `p` on: out[i] the read, at[i] its position, direct[i] 1 for an element of a literal written in the call (the call boxes it) and 0 for one of a local's (the container rule makes the local Array hold the handle). Answers the count. */ static int fwd_splat_lit_reads(Compiler *c, int splat, int p, int *out, int *at, int *direct, int cap) { const NodeTable *nt = c->nt; int x = nt_ref(nt, splat, "expression"), lits[16], nl = 0, inl = 0; if (x >= 0 && nt_kind(nt, x) == NK_ArrayNode) { lits[nl++] = x; inl = 1; } else if (x >= 0 && nt_kind(nt, x) == NK_LocalVariableReadNode) { const char *xn = nt_str(nt, x, "name"); Scope *xs = xn ? comp_scope_of(c, x) : NULL; for (int w = xs ? comp_lvw_first_sc(c, (int)(xs - c->scopes), xn) : -1; w >= 0 && nl < 16; w = comp_lvw_next_sc(c, w)) { if (comp_scope_of(c, w) != xs || !nt_str(nt, w, "name") || !sp_streq(nt_str(nt, w, "name"), xn)) continue; int wv = nt_kind(nt, w) == NK_LocalVariableWriteNode ? nt_ref(nt, w, "value") : -1; if (wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) return 0; lits[nl++] = wv; } } int n = 0; for (int l = 0; l < nl; l++) { int en = 0; const int *ev = nt_arr(nt, lits[l], "elements", &en); for (int e = 0; e < en && n < cap; e++) { if (nt_kind(nt, ev[e]) == NK_SplatNode) break; if (nt_kind(nt, ev[e]) != NK_LocalVariableReadNode) continue; TyKind et = comp_ntype(c, ev[e]); if (et != TY_STRING && et != TY_STRBUF) continue; out[n] = ev[e]; at[n] = p + e; direct[n] = inl; n++; } } return n; } /* The member `a` of class `ci` a generated Struct or Data constructor call `u` stores: its positional argument, or the keyword naming it. -1 when the call hands it no value of its own (a splat, a `**`, a keyword_init: false Struct's keywords, which are one Hash). */ static int struct_new_member_arg(Compiler *c, int u, int ci, int a) { const NodeTable *nt = c->nt; const ClassInfo *k = &c->classes[ci]; int al = nt_ref(nt, u, "arguments"), argc = 0; const int *argv = al >= 0 ? nt_arr(nt, al, "arguments", &argc) : NULL; if (nt_argv_has_splat(nt, argv, argc)) return -1; if (argc == 1 && nt_kind(nt, argv[0]) == NK_KeywordHashNode) { if (k->kw_init == -1) return -1; int n = 0; const int *el = nt_arr(nt, argv[0], "elements", &n); for (int i = 0; i < n; i++) { if (nt_kind(nt, el[i]) != NK_AssocNode) return -1; int key = nt_ref(nt, el[i], "key"); const char *kn = key >= 0 && nt_kind(nt, key) == NK_SymbolNode ? nt_str(nt, key, "value") : NULL; if (kn && sp_streq(kn, k->ivars[a] + 1)) return nt_ref(nt, el[i], "value"); } return -1; } return a < argc && nt_kind(nt, argv[a]) != NK_KeywordHashNode ? argv[a] : -1; } /* The values stored into a String ivar that is the shared handle without an `@iv = v` of the program's own: a generated Struct or Data constructor's member arguments (`S.new(s)`), and an attribute writer's (`o.name = s`). Each String variable handed over takes the handle (dyn_pull_arg), as a handle parameter's callers do; left a plain String, the slot held a copy, and the constructor's sp_String * parameter did not build. Answers 1 when it changed anything. */ static int promote_ivar_handle_stores(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; NT_FOREACH_KIND(nt, NK_CallNode, u) { const char *nm = nt_str(nt, u, "name"); int recv = nt_ref(nt, u, "receiver"); if (!nm || recv < 0 || nt_ref(nt, u, "block") >= 0) continue; TyKind rt = infer_type(c, recv); if (ty_is_object(rt) && name_is_plain_setter(nm)) { int al = nt_ref(nt, u, "arguments"), argc = 0; const int *argv = al >= 0 ? nt_arr(nt, al, "arguments", &argc) : NULL; char base[256], ivn[258]; if (argc != 1 || !setter_base_name(nm, base, sizeof base)) continue; int defc = -1; if (!comp_writer_in_chain(c, ty_object_class(rt), base, &defc) || defc < 0) continue; snprintf(ivn, sizeof ivn, "@%s", comp_resolve_alias(c, defc, base)); int iv = comp_ivar_index(&c->classes[defc], ivn); if (iv >= 0 && c->classes[defc].ivar_types[iv] == TY_STRBUF && c->classes[defc].ivar_str_shared[iv]) changed |= dyn_pull_arg(c, argv[0], 1); continue; } if (!is_struct_constructor(nm)) continue; if (rt != TY_CLASS) continue; TyKind ut = infer_type(c, u); int ci = ty_is_object(ut) ? ty_object_class(ut) : -1; if (ci < 0 || !(c->classes[ci].is_struct || c->classes[ci].is_data) || comp_method_in_chain(c, ci, "initialize", NULL) >= 0) continue; for (int a = 0; a < c->classes[ci].nmembers; a++) { if (c->classes[ci].ivar_types[a] != TY_STRBUF || !c->classes[ci].ivar_str_shared[a]) continue; int v = struct_new_member_arg(c, u, ci, a); if (v >= 0) changed |= dyn_pull_arg(c, v, 1); } } return changed; } /* Pull the String variables a call gathers into a rest whose elements the method forwards to a parameter that appends. Answers 1 when it changed anything. */ static int promote_forwarded_rest_args(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; g_fwd_codegen = 0; fwd_memo_fresh(c); g_fwd_codegen = 0; changed |= fwd_super_string_params(c); 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; int a = nt_ref(nt, u, "arguments"); if (a < 0) continue; int t = fwd_call_target(c, u); if (t < 0 || c->scopes[t].rest_idx < 0) continue; unsigned bits = fwd_rest_bits(c, t); if (!bits) continue; Scope *m = &c->scopes[t]; int ac = 0; const int *av = nt_arr(nt, a, "arguments", &ac); int pos = ac; if (pos > 0 && (nt_kind(nt, av[pos - 1]) == NK_KeywordHashNode || nt_kind(nt, av[pos - 1]) == NK_BlockArgumentNode)) pos--; if (nt_argv_kwh(nt, av, pos) >= 0) pos--; for (int k = m->rest_idx; k < pos - m->npost_rest && k - m->rest_idx < 16; k++) { /* a splat of an Array literal, or of a local every write of which is one, lays its elements from here on (`w(*s)` with `s = [v]`) */ if (nt_kind(nt, av[k]) == NK_SplatNode) { int out[16], at[16], direct[16]; int ne = fwd_splat_lit_reads(c, av[k], k - m->rest_idx, out, at, direct, 16); for (int e = 0; e < ne; e++) if (at[e] < 16 && ((bits >> at[e]) & 1u)) changed |= dyn_pull_arg(c, out[e], direct[e]); break; } if (!((bits >> (k - m->rest_idx)) & 1u)) continue; /* an element read gathered into the rest: its container's Strings */ { int d = strbuf_demand_elem_arg(c, av[k]); if (d >= 0) { changed |= d; continue; } } if (nt_kind(nt, av[k]) != NK_LocalVariableReadNode) continue; TyKind at = comp_ntype(c, av[k]); if (at != TY_STRING && at != TY_STRBUF) continue; changed |= dyn_pull_arg(c, av[k], 1); } } dyn_memo_stale(); return changed; } /* The nodes of each scope, ascending (comp_scope_of's answer), for the yield-inlining pass below: it looked at one method's nodes by walking every node of the program, three times per method that takes a block. */ static int *bi_first, *bi_next, bi_ns = -1, bi_cnt = -1; static void bi_scope_nodes(Compiler *c) { int n = c->nt->count; if (bi_ns == c->nscopes && bi_cnt == n) return; free(bi_first); free(bi_next); bi_first = malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); bi_next = malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); if (!bi_first || !bi_next) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int s = 0; s < c->nscopes; s++) bi_first[s] = -1; for (int id = n - 1; id >= 0; id--) { int s = (int)(comp_scope_of(c, id) - c->scopes); bi_next[id] = bi_first[s]; bi_first[s] = id; } bi_ns = c->nscopes; bi_cnt = n; } /* The block-taking method named `fn` that KEEPS its block -- reads its &blk parameter as anything but the receiver of `.call` or a `&blk` forward (`blk_call_recv` / `blk_arg_expr` mark those reads) -- or -1. `*first` gets the first block-taking method of that name, keeper or not. For a callee whose receiver has no type yet: every same-named candidate is a possible target, and one that keeps the block decides. */ static int a_name_keeps_block(Compiler *c, const char *fn, const char *keeps, int *first) { if (first) *first = -1; if (!fn) return -1; for (int si = 1; si < c->nscopes; si++) { Scope *cs3 = &c->scopes[si]; if (cs3->is_cmethod || !cs3->name || !sp_streq(cs3->name, fn)) continue; if (!cs3->blk_param || !cs3->blk_param[0]) continue; if (first && *first < 0) *first = si; if (!keeps || keeps[si]) return si; } return -1; } /* Not asked per call: scanning every node for each same-named method at each call site was quadratic in the program. */ static char *a_scopes_keeping_block(Compiler *c, const char *blk_call_recv, const char *blk_arg_expr) { char *keeps = (char *)calloc((size_t)(c->nscopes > 0 ? c->nscopes : 1), 1); if (!keeps) return NULL; for (int q = 0; q < c->nt->count; q++) { int si = c->nscope[q]; if (si < 1 || si >= c->nscopes || keeps[si]) continue; Scope *cs3 = &c->scopes[si]; if (cs3->is_cmethod || !cs3->name || !cs3->blk_param || !cs3->blk_param[0]) continue; if (nt_kind(c->nt, q) != NK_LocalVariableReadNode) continue; const char *qn = nt_str(c->nt, q, "name"); if (!qn || !sp_streq(qn, cs3->blk_param)) continue; if (!(blk_call_recv && blk_call_recv[q]) && !(blk_arg_expr && blk_arg_expr[q])) keeps[si] = 1; } return keeps; } /* A literal block on a call whose receiver has no type yet, to a method name some class defines with a block it keeps (a_name_keeps_block). Such a block becomes a real proc once the receiver settles, so a pass that runs before then has to count it lifted already. A constant receiver names a class and is left to a_block_is_lifted, which resolves it without a type. */ static int a_block_lifted_by_callee_name(Compiler *c, int id, const char *keeps) { const NodeTable *nt = c->nt; if (nt_kind(nt, id) != NK_CallNode) return 0; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) return 0; NodeKind rk = nt_kind(nt, recv); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) return 0; if (infer_type(c, recv) != TY_UNKNOWN) return 0; return a_name_keeps_block(c, nt_str(nt, id, "name"), keeps, NULL) >= 0; } /* True if `blk`'s subtree contains a YieldNode. */ static int subtree_has_yield_node(Compiler *c, int node, int depth) { const NodeTable *nt = c->nt; if (node < 0 || depth > 64) return 0; if (nt_kind(nt, node) == NK_YieldNode) return 1; int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (subtree_has_yield_node(c, nt_ref_at(nt, node, i), depth + 1)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) if (subtree_has_yield_node(c, ids[j], depth + 1)) return 1; } return 0; } /* A Thread.new / Fiber.new block in this method whose body yields. Such a body is emitted as its own function, so the yield-splice has no block to inline. */ static int scope_yields_inside_lifted_body(Compiler *c, int mi) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (c->nscope[id] != mi || nt_kind(nt, id) != NK_CallNode) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || !subtree_has_yield_node(c, blk, 0)) continue; const char *nm = nt_str(nt, id, "name"); int r = nt_ref(nt, id, "receiver"); const char *rn = (r >= 0 && nt_kind(nt, r) == NK_ConstantReadNode) ? nt_str(nt, r, "name") : NULL; if (nm && sp_streq(nm, "new") && rn && (sp_streq(rn, "Thread") || sp_streq(rn, "Fiber"))) return 1; /* The same for a block that becomes a first-class proc because its callee keeps a real &blk: the proc's body is its own C function, where the enclosing method's block is as out of reach as it is from a Thread's. `Net::HTTP#request(req, &blk)` with `blk.call(res) unless blk.nil?` is that callee, and campfire's `yield response` inside the block it is given raised LocalJumpError (#4438). */ if (a_block_is_lifted(c, id)) return 1; } return 0; } /* Does any call have a poly receiver and this method's name? With a block, or without one: a poly receiver dispatches by class to a callable symbol, and an inlined method has none, so a blockless `slots["h"].request(req)` to a `request(req, &blk)` that inlines at its (all blockless) call sites found no arm and raised NoMethodError (#4492, the edge the #4477 approval of `blk.nil?` walked over). The proc form is that arm: its block is the real proc, NULL here, which is what `blk.nil?` and a bare `yield` (a LocalJumpError) already answer for. */ /* Is there a `new` whose class is only known at run time (`k.new`, a Class read out of a container)? Its switch arm calls sp_X_new, which cannot splice a yielding initialize the way a constant `X.new` site does, so the class needs the clone for its constructor to run the body at all. */ static int super_new_init_yields(Compiler *c, Scope *s) { if (!s->is_cmethod || !sp_streq(comp_scope_user_name(s), "new") || super_reach(c, s) >= 0) return 0; for (int k = 0; k < c->nclasses; k++) { int mi = k == s->class_id || is_descendant(c, k, s->class_id) ? comp_method_in_chain(c, k, "initialize", NULL) : -1; if (mi >= 0 && c->scopes[mi].yields) return 1; } return 0; } static int pf_dynamic_new(Compiler *c) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "new")) continue; /* `new(..., &pr)`: a proc known only at run time reaches the body through the clone as well */ int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode) return 1; int recv = nt_ref(nt, id, "receiver"); if (recv < 0) continue; int rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) return 1; } for (int s = 0; s < c->nscopes; s++) if (c->scopes[s].class_id >= 0 && super_new_init_yields(c, &c->scopes[s])) return 1; return 0; } static void mark_ctor_cycles(Compiler *c); static int ctor_graph_init(Compiler *c, int s); /* Does a `method(:name)` (or `public_method`, `instance_method`) name it? A Method object calls the method as a function, and a yielding one has none but its clone. */ static int pf_method_obj(Compiler *c, const char *name) { static const char *const getters[] = { "method", "public_method", "instance_method" }; for (int g = 0; g < 3; g++) for (int id = an_calls_named_first(c, getters[g]); id >= 0; id = an_calls_named_next(id)) { const char *sym = method_sym_arg(c, id); if (sym && sp_streq(sym, name)) return 1; } return 0; } static int pf_wanted(Compiler *c, const char *name) { const NodeTable *nt = c->nt; if (pf_method_obj(c, name)) return 1; if (sp_streq(name, "initialize")) return pf_dynamic_new(c); /* the calls of this name, not every node: asked per yielding method (rubys in #5035) */ for (int id = an_calls_named_first(c, name); id >= 0; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, name)) continue; int recv = nt_ref(nt, id, "receiver"); if (recv >= 0 && infer_type(c, recv) == TY_POLY) return 1; /* a parameter typed one class so far may turn out boxed (handed a module method's self), and a dispatch with a block reaches an inherited yielding method only through its clone (#9261) */ if (recv >= 0 && nt_ref(nt, id, "block") >= 0 && nt_kind(nt, recv) == NK_LocalVariableReadNode && ty_is_object(infer_type(c, recv))) { Scope *rs = comp_scope_of(c, recv); LocalVar *lv = rs ? scope_local(rs, nt_str(nt, recv, "name")) : NULL; if (lv && lv->is_param) return 1; } /* a Class value known only at run time dispatches the same way */ if (recv >= 0 && infer_type(c, recv) == TY_CLASS && class_recv_is_dynamic(c, recv)) return 1; /* an Array, Hash or Numeric reopening's method on a receiver of that kind is called, not spliced: the reopen fallback reaches it through the clone. String reopenings need the same callable form. */ if (recv >= 0) { TyKind rt = infer_type(c, recv); const char *rcn = ty_is_array(rt) ? "Array" : ty_is_hash(rt) ? "Hash" : (rt == TY_STRING || rt == TY_STRBUF) ? "String" : rt == TY_RANDOM ? "Random" : rt == TY_FIBER ? "Fiber" : rt == TY_THREAD ? "Thread" : (rt == TY_INT || rt == TY_FLOAT || rt == TY_BIGINT) ? "Numeric" : NULL; int rci = rcn ? comp_class_index(c, rcn) : -1; if (rci >= 0 && comp_method_in_class(c, rci, name) >= 0) return 1; /* a Float's or Symbol's own reopening is reached the same way */ int oci = rt == TY_FLOAT ? comp_core_class(c, CORE_FLOAT) : rt == TY_SYMBOL ? comp_core_class(c, CORE_SYMBOL) : -1; if (oci >= 0 && comp_method_in_class(c, oci, name) >= 0) return 1; /* and an Object reopening's, on a builtin receiver, with a block or without one: the Object fallback reaches it the same way */ int obj = comp_core_class(c, CORE_OBJECT); if (obj >= 0 && rt != TY_UNKNOWN && !ty_is_object(rt) && builtin_class_of_type(rt) && comp_method_in_class(c, obj, name) >= 0) return 1; } /* a method the program adds to Object, called with a block on an object whose class chain stops short of Object (#5779): the call reaches it through the clone, since it cannot be spliced in on that class */ if (recv >= 0 && nt_ref(nt, id, "block") >= 0) { TyKind rt = infer_type(c, recv); int obj = comp_core_class(c, CORE_OBJECT); if (obj >= 0 && ty_is_object(rt) && ty_object_class(rt) != obj && comp_method_in_chain(c, ty_object_class(rt), name, NULL) < 0 && comp_method_in_class(c, obj, name) >= 0) return 1; } } return 0; } /* Does a class above or below `src`'s define the same name? Then a call on the ancestor's type dispatches on cls_id, and an inline-only `src` can only take its arm through the clone: with none, the arm was dropped and the ancestor's method ran in its place. */ static int pf_in_class_dispatch(Compiler *c, const Scope *src) { /* a constructor is not reached through that switch */ if (src->is_cmethod || sp_streq(src->name, "initialize")) return 0; /* a Struct's generated each/each_pair is copied into every struct class, so a struct subclass always "overrides" it; those copies are one method, and their bodies read desugared accumulators a clone does not carry */ if (c->classes[src->class_id].is_struct) return 0; for (int k = 0; k < c->nclasses; k++) { if (k == src->class_id) continue; if (!is_descendant(c, k, src->class_id) && !is_descendant(c, src->class_id, k)) continue; if (comp_method_in_class(c, k, src->name) >= 0) return 1; } return 0; } /* The method named `name` beside src: in its class, or a top-level one */ static int pf_scope_named(Compiler *c, const Scope *src, const char *name) { if (src->class_id < 0) return comp_method_index(c, name); return (src->is_cmethod ? comp_cmethod_in_class : comp_method_in_class)(c, src->class_id, name); } int make_yield_proc_forms(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int made = 0; mark_ctor_cycles(c); for (int next = 1; next < c->nscopes; next++) { int s = next; Scope *src = &c->scopes[s]; /* Visit the appended clones too: a clone's super needs a callable yielding shadow even when that shadow is outside the constructor cycle. Following it here also handles a chain of module shadows, in either definition order, without rescanning the scope table. */ int shadow_clone = src->is_proc_form; if (shadow_clone) { const char *shadow = comp_super_shadow(c, src); if (!shadow || !cr_scope_has_super(c, s)) continue; s = pf_scope_named(c, src, shadow); if (s < 0) continue; src = &c->scopes[s]; } /* reachability is decided after this pass, so do not consult it: an unused clone is a static function the C compiler drops. */ if (!src->yields) continue; /* a top-level method is cloned only for a Method object of it */ if (src->class_id < 0) { if (src->is_cmethod || !src->name || !pf_method_obj(c, src->name)) continue; } /* a by-value or Struct constructor is built by its own emitter, which does not run the clone (ctor_init_proc_form) */ else if (sp_streq(src->name ? src->name : "", "initialize") && (c->classes[src->class_id].is_struct || c->classes[src->class_id].is_value_type)) continue; if (src->is_transplanted_source || !src->name) continue; if (src->body < 0) continue; /* an exception class is built through sp_X_new by raise, rescue's re-raise and its own `new` sites alike, none of which splice the body */ int exc_init = src->class_id >= 0 && sp_streq(src->name, "initialize") && !src->is_cmethod && class_is_exc_subclass(c, src->class_id); if (src->class_id >= 0 && !shadow_clone && !exc_init && !pf_wanted(c, comp_scope_user_name(src)) && !pf_in_class_dispatch(c, src) && !(src->ctor_cycle && ctor_graph_init(c, s))) continue; /* A method the program reopens has two definitions in the scope table and the last one wins (comp_method_in_class): only that one gets the clone. Cloning the first left the poly dispatch arm running the package's transport where every other call site ran the program's stub in front of it (#4502). */ if (pf_scope_named(c, src, src->name) != s) continue; /* `#` cannot appear in a Ruby method name, so the clone is invisible to the by-name lookups while still mangling to a valid C identifier. */ char pfname[192]; snprintf(pfname, sizeof pfname, "%s#pf", src->name); if (pf_scope_named(c, src, pfname) >= 0) continue; int nb = nt_clone_subtree(nt, src->body); if (nb < 0) continue; comp_scope_new(c, pfname, src->def_node); int di = c->nscopes - 1; comp_scope_copy_shadow(c, di, s, "#pf"); comp_grow_node_arrays(c); src = &c->scopes[s]; Scope *dst = &c->scopes[di]; { dst->class_id = src->class_id; comp_touch_scope(dst->idx); } { dst->is_cmethod = src->is_cmethod; comp_touch_scope(dst->idx); } { dst->body = nb; comp_touch_scope(dst->idx); } { dst->reachable = 1; comp_touch_scope(dst->idx); } dst->yields = 0; { dst->nparams = src->nparams; comp_touch_scope(dst->idx); } dst->nrequired = src->nrequired; dst->rest_idx = src->rest_idx; /* the posts too: without them the clone's layout took a rest's post for a keyword, bound by name */ dst->npost_rest = src->npost_rest; dst->kwrest_idx = src->kwrest_idx; if (src->nparams > 0) { dst->pnames = (char **)calloc((size_t)src->nparams, sizeof(char *)); dst->pdefault = (int *)malloc(sizeof(int) * (size_t)src->nparams); if (!dst->pnames || !dst->pdefault) { { dst->nparams = 0; comp_touch_scope(dst->idx); } } else for (int p = 0; p < src->nparams; p++) { dst->pnames[p] = src->pnames[p] ? strdup(src->pnames[p]) : NULL; dst->pdefault[p] = src->pdefault[p]; } } /* A method that already declares `&blk` binds its block under THAT name, and the cloned body reads it under that name. Renaming the parameter to the synthetic one left the body reading a `blk` that nothing assigns -- a nil proc call, silently answering the slot default. Only a method with no declared block param needs a name invented for it. */ dst->blk_param = (src->blk_param && src->blk_param[0]) ? strdup(src->blk_param) : strdup("__pf_blk"); dst->is_proc_form = 1; walk_scope(c, nb, di, dst->class_id); /* The defaults are the clone's own too: one reading an earlier parameter is typed against the clone's, which arrive boxed, where sharing the original's nodes read them as the original's sp_int. */ scope_own_defaults(c, di); made = 1; } if (made) { register_locals(c); /* register_locals interns the body's names as plain locals; the clone's parameters must be marked as such or the prologue declares them a second time on top of the C parameters. The block parameter is the proc. */ for (int s = 1; s < c->nscopes; s++) { Scope *d = &c->scopes[s]; if (!d->is_proc_form) continue; for (int p = 0; p < d->nparams; p++) { if (!d->pnames[p]) continue; LocalVar *lv = scope_local_intern(d, d->pnames[p]); if (!lv) continue; lv->is_param = 1; /* No call site binds the clone's parameters (the dispatch arm is the only caller, and it boxes what it passes), so a parameter no inference touches stays unknown, and an unknown local in a value position is emitted as nil: `perform(req)` inside the clone ran `perform(nil)` (#4502). The signature spells it sp_RbVal; the local is that from the start. It arrives boxed from the dispatch arm, so it also has to survive the re-narrow reset, which clears every poly parameter each round: with no call site to widen it again, a clone reached only by a zero-argument call ended the fixpoint unknown and read its boxed argument as an sp_int. */ /* A *rest or **kwrest is the array or hash the arm packs, as in the method it clones; typed poly, the arm's sp_PolyArray * did not fit the clone's sp_RbVal parameter */ if (lv->type == TY_UNKNOWN && p == d->rest_idx) lv_set_type(lv, TY_POLY_ARRAY); else if (lv->type == TY_UNKNOWN && p == d->kwrest_idx) lv_set_type(lv, TY_SYM_POLY_HASH); if (lv->type == TY_UNKNOWN) { lv_set_type(lv, TY_POLY); lv->poly_dispatch_widened = 1; } } if (d->blk_param) { LocalVar *bl = scope_local_intern(d, d->blk_param); if (bl) { bl->is_param = 1; lv_set_type(bl, TY_PROC); } } } } return made; } /* Representation family of a settled type: 1 for a flat scalar in a machine register, 2 for a heap pointer. 0 for anything the check must not judge -- poly and unknown (no static claim), nil (a literal nil is legal in every slot), bigint (int converts at the boundary), a by-value object or a shared-string handle (neither is a plain pointer). */ static int seed_repr_family(Compiler *c, TyKind t) { if (t == TY_STRBUF) t = TY_STRING; /* (one-String mode: a String slot is the handle) */ switch (t) { case TY_INT: case TY_FLOAT: case TY_BOOL: case TY_SYMBOL: return 1; case TY_STRING: case TY_PROC: return 2; default: break; } if (ty_is_array(t) || ty_is_hash(t)) return 2; if (ty_is_object(t) && !comp_ty_value_obj(c, t)) return 2; return 0; } /* Within the pointer family the representations still differ, with no conversion between them: an Array, a Hash and a String are three different layouts. Anything else that is a pointer (a Proc, an object -- which may carry to_s / to_ary conversions the emitter knows) is left unjudged. */ static int seed_ptr_kind(TyKind t) { if (t == TY_STRBUF) t = TY_STRING; if (ty_is_array(t)) return 1; if (ty_is_hash(t)) return 2; if (t == TY_STRING) return 3; return 0; } /* The value a call's keyword hash gives the keyword `name`, when exactly one element names it by a literal symbol; -1 otherwise. */ static int seed_kwarg_value(const NodeTable *nt, int kwh, const char *name) { int en = 0; const int *elems = nt_arr(nt, kwh, "elements", &en); int found = -1, hits = 0; for (int e = 0; elems && e < en; e++) { int key = nt_ref(nt, elems[e], "key"); int val = nt_ref(nt, elems[e], "value"); if (key < 0 || val < 0 || nt_kind(nt, key) != NK_SymbolNode) continue; const char *kn = nt_str(nt, key, "value"); if (kn && sp_streq(kn, name)) { found = val; hits++; } } return hits == 1 ? found : -1; } /* 1 iff a value of type `val` placed in a slot the seed pinned to `slot` would be REINTERPRETED rather than converted -- the whole point of the rule. `ret` selects the wider judgement a return slot allows (see seed_ret_family). */ /* The values a returned expression `v` answers with: a conditional's branches (`f ? "s" : 1`, an if, unless, case, case/in, begin, rescue modifier, or the operands of `||` and `&&`) each in turn, since each returns on its own, converted to the method's type. The whole conditional is the boxed union of its branches, which makes no claim. A conditional with no else lists the branches it has; a branch that is missing answers nil, which every slot holds. An `||` or `&&` is a site of its own as well, the type it has as a whole; reach[] says which truth values a site answers with: the left of `||` only a truthy one, the left of `&&` only a falsy one (VPR_*). */ typedef struct { int *sites, *n, cap; unsigned *reach; } SeedSites; static int seed_ret_site(const VpLeaf *leaf, void *ud) { SeedSites *s = ud; if (leaf->kind == VPK_NIL) return 0; s->reach[*s->n] = leaf->reach; s->sites[(*s->n)++] = leaf->node; return *s->n >= s->cap; } static void seed_ret_branch_sites(const NodeTable *nt, int v, int *sites, unsigned *reach, int *n, int cap) { SeedSites s = { sites, n, cap, reach }; if (v < 0 || *n >= cap) return; for_each_value_position(nt, v, VP_AND_LEFT | VP_LOGIC_NODES | VPS_ALL, seed_ret_site, &s); } static int seed_ret_family(Compiler *c, TyKind t); static int seed_ret_kind(TyKind t); static int seed_contradicts(Compiler *c, TyKind slot, TyKind val, int ret) { int fs = ret ? seed_ret_family(c, slot) : seed_repr_family(c, slot); int fv = ret ? seed_ret_family(c, val) : seed_repr_family(c, val); if (!fs || !fv) return 0; if (fs != fv) return 1; /* Two objects at a RETURN: a subclass in an ancestor's slot is legitimate, but an ancestor (or an unrelated class) in a subclass's slot is a pointer the declared C return type cannot hold, and cc refused it (#7278). */ if (ret && ty_is_object(slot) && ty_is_object(val) && slot != val) { int cs = ty_object_class(slot), cv = ty_object_class(val); if (cs < 0 || cs >= c->nclasses || cv < 0 || cv >= c->nclasses) return 0; if (is_descendant(c, cv, cs)) return 0; /* A module's `self` is the object of a class that includes the module. A slot of such a class holds it when every class that includes the module is that class or one under it: `def attach = self` in Widget, declared `-> Widget::Gadget?` where only Gadget includes Widget (#9161). Another includer's object would not fit the slot. */ if (comp_class_is_module(c, &c->classes[cv])) { int any = 0; for (int k = 0; k < c->nclasses; k++) { if (k == cv || comp_class_is_module(c, &c->classes[k]) || !comp_module_includes(c, k, cv, 0)) continue; if (!is_descendant(c, k, cs)) return 1; any = 1; } if (any) return 0; } return 1; } int ks = ret ? seed_ret_kind(slot) : seed_ptr_kind(slot); int kv = ret ? seed_ret_kind(val) : seed_ptr_kind(val); /* Two HASHES differ convertibly in their VALUE kind -- the emitter rebuilds one variant from another -- but never in their KEY kind. A Symbol-keyed hash can never satisfy a `Hash[String, ...]` slot, and passing that pointer unconverted had a Symbol dereferenced as a char * (#3975). A poly key holds either, so it is not a clash. */ if (fs == 2 && ks == 2 && kv == 2) { TyKind kslot = ty_hash_key(slot), kval = ty_hash_key(val); return kslot != kval && kslot != TY_POLY && kval != TY_POLY && kslot != TY_UNKNOWN && kval != TY_UNKNOWN; } /* Two ARRAYS whose ELEMENT kind differs are different C structs, and unlike a hash's VALUE kind the emitter does not rebuild one from the other at a return: rebuilding would hand back a copy, so writes through the getter would stop reaching the receiver's own array. The clash therefore reached cc, which named sp_IntArray / sp_PolyArray -- types the author has no way to connect back to the `Array[untyped]` they wrote (#4151). */ if (fs == 2 && ks == 1 && kv == 1) { /* A POLY array is the one array kind the emitter DOES materialize into a typed one at a boundary (sp_StrArray_from_poly_array, #1827), so it can satisfy any array slot. A RETURN converts in the other direction too: a typed array entering a declared Array[untyped] return is rebuilt with its elements boxed, the same copy the unseeded compiler emits when a caller's use widens such a value (sp_PolyArray_from_int_array at the call site) -- so the wider declaration is satisfied, not contradicted (#4191). An IVAR store converts nothing (the slot keeps the object), so there only an identical kind will do. */ if (val == TY_POLY_ARRAY || (ret && slot == TY_POLY_ARRAY)) return 0; return slot != val; } if (fs != 2 || !ks || !kv || ks == kv) return 0; return 1; } /* The same judgement for a seeded RETURN, which is decidable in two more cases than an ivar assignment. A seeded return converts NOTHING: an object defining #to_h or #to_str placed in a Hash or String slot is the same C error as one defining neither (verified), so a user object crossing into a container or String slot is as decidable as an Array is, and a by-value object -- which the shared family function leaves alone because it is neither a scalar nor a pointer -- is decidable too. Two OBJECTS still stay unjudged: a subclass in an ancestor's slot is legitimate. */ static int seed_ret_family(Compiler *c, TyKind t) { int f = seed_repr_family(c, t); if (f) return f; if (ty_is_object(t)) return 3; /* A Time or a Range is a struct the function returns by value, and no conversion makes it a pointer, a number or another of these: each is a family of its own. activesupport's Time.current, declared `-> TimeWithZone`, answers Time.now when no zone is set, and the sp_Time reached the C compiler in an sp_TimeWithZone * function. (A Rational or a Complex is left out: a number converts into them.) */ switch (t) { case TY_TIME: return 4; case TY_RANGE: return 5; case TY_FLOAT_RANGE: return 6; case TY_STR_RANGE: return 7; default: return 0; } } /* ty_name has no spelling for a user class (it answers "?"), which is exactly the half of the message that has to be readable here. */ /* The type as the AUTHOR would have written it in the RBS. The internal lattice names (int_array, poly_array) named nothing the signature contains, so a contradiction on a container read as being about types the author had no way to connect back to the `Array[untyped]` they wrote (#4151). The buffer is the caller's: one message prints two of these. */ static const char *seed_ty_name_into(Compiler *c, TyKind t, char *buf, size_t n) { if (ty_is_object(t)) { int cid = ty_object_class(t); if (cid >= 0 && cid < c->nclasses && c->classes[cid].name) return c->classes[cid].name; } if (ty_is_array(t)) { char eb[128]; TyKind e = ty_array_elem(t); snprintf(buf, n, "Array[%s]", seed_ty_name_into(c, e, eb, sizeof eb)); return buf; } if (ty_is_hash(t)) { char kb[128], vb[128]; snprintf(buf, n, "Hash[%s, %s]", seed_ty_name_into(c, ty_hash_key(t), kb, sizeof kb), seed_ty_name_into(c, ty_hash_val(t), vb, sizeof vb)); return buf; } switch (t) { case TY_INT: return "Integer"; case TY_FLOAT: return "Float"; case TY_STRING: return "String"; case TY_SYMBOL: return "Symbol"; case TY_BOOL: return "bool"; case TY_NIL: return "nil"; case TY_PROC: return "Proc"; case TY_RANGE: case TY_FLOAT_RANGE: case TY_STR_RANGE: return "Range"; case TY_TIME: return "Time"; case TY_POLY: case TY_UNKNOWN: return "untyped"; default: break; } { const char *nm = ty_name(t); return nm ? nm : "?"; } } static int seed_ret_kind(TyKind t) { int k = seed_ptr_kind(t); return k ? k : (ty_is_object(t) ? 4 : 0); } /* An --rbs seed the program statically contradicts. A seed is trusted, so codegen narrows whatever arrives into the pinned slot and a wrong one reinterprets the value rather than converting it. Where both the seed and the value are concretely typed and one is a flat scalar while the other is a heap pointer, no conversion exists and none is emitted -- the result is a pointer read as an integer. That is decidable here, so refuse rather than emit it. Deliberately narrow. Two types in the SAME family may still disagree (a String slot fed a Symbol) but the emitter has coercions for many such pairs, and a false error breaks a build that works; the dynamic half of this is -DSP_RBS_CHECK, which needs no such caution because it sees the real tag. */ /* Defined in codegen_util.c: 1 under SP_COLLECT_ERRORS. Reached by name rather than through codegen_internal.h, which analyze does not include. */ int collect_mode(void); /* One contradiction used to end the run. On a seeded tree that is the check hit first, and fixing one signature only buys the next one a whole compile later -- 115 seconds a round on a 347-signature tree (#4140). Under SP_COLLECT_ERRORS, report each one and keep going, then fail once at the end: the run still refuses to emit (a seed is trusted, so acting on a contradicted one is the bug this check exists to prevent), but it hands back the whole list. Same bargain the codegen gaps already make. */ static int g_seed_bad = 0; typedef struct { int fid, ln; const char *nm; } SeedIvarReport; /* Can the value of `v` only be false or nil (falsy), or only a value that is neither (not falsy)? Read off the literals, the operators and the methods a call reaches (each of their values); 0 when it cannot tell. The left of a `||` answers only when truthy and the left of a `&&` only when falsy (VPR_*): an operand that cannot be that never is the value. */ static int seed_value_only(Compiler *c, int v, int falsy, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 8) return 0; switch (nt_kind(nt, v)) { case NK_NilNode: case NK_FalseNode: return falsy; case NK_TrueNode: return !falsy; /* `a || b` is a when truthy and b when not; `a && b` is a when falsy and b when not */ case NK_OrNode: return falsy ? seed_value_only(c, nt_ref(nt, v, "left"), 1, depth + 1) && seed_value_only(c, nt_ref(nt, v, "right"), 1, depth + 1) : seed_value_only(c, nt_ref(nt, v, "left"), 0, depth + 1) || seed_value_only(c, nt_ref(nt, v, "right"), 0, depth + 1); case NK_AndNode: return falsy ? seed_value_only(c, nt_ref(nt, v, "right"), 1, depth + 1) : seed_value_only(c, nt_ref(nt, v, "left"), 0, depth + 1) && seed_value_only(c, nt_ref(nt, v, "right"), 0, depth + 1); case NK_CallNode: { int ms[CPT_MAX], nm = cplan_targets(c, v, ms, CPT_MAX); if (nm <= 0) return 0; for (int i = 0; i < nm; i++) { int vals[64]; /* a path that answers nil adds no value to the falsy walk, and ends the other: it is nil */ int n = falsy ? method_value_leaves_or_nil(c, ms[i], vals, 64) : method_value_leaves(c, ms[i], vals, 64); if (n < 0 || (!falsy && n == 0)) return 0; for (int j = 0; j < n; j++) if (!seed_value_only(c, vals[j], falsy, depth + 1)) return 0; } return 1; } default: return 0; } } /* The method the parameter check takes call id to reach: a receiverless call as its own scope reaches it (a sibling class method inside a class method), `Mod.m` through the class's singleton methods first, a call on an object through its class; -1 when the target is not known here. */ static int seed_call_target(Compiler *c, int id, const char *name) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, id, "receiver"); int mi = -1; if (recv < 0) mi = comp_self_call_mi(c, id, name); else if (nt_kind(nt, recv) == NK_ConstantReadNode) { const char *cn = nt_str(nt, recv, "name"); int rc = cn ? comp_class_index(c, cn) : -1; /* `Mod.m` is a class (singleton) method, which is not in the instance chain; fall back to it before giving up. */ if (rc >= 0) mi = call_target(c, id, rc, name, CT_CMETHOD, NULL); if (rc >= 0 && mi < 0) mi = call_target(c, id, rc, name, 0, NULL); } else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) mi = call_target(c, id, ty_object_class(rt), name, 0, NULL); } return mi; } /* seed_chase_contradicts' walk: the (method, parameter) pairs it has entered, the call sites it has read, and a type of the values it found, for the message */ typedef struct { int *seen; int nseen, cseen, sites; TyKind shown; } SeedChase; #define SEED_CHASE_DEPTH 4 #define SEED_CHASE_SITES 64 /* May method mi be called in a way no call site names: a Symbol spelling its name (send, public_send, method, alias, define_method), or a super from a method of that name */ static int seed_chase_dynamic(Compiler *c, const char *name) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_SymbolNode, s) { const char *v = nt_str(nt, s, "value"); if (v && sp_streq(v, name)) return 1; } static const NodeKind sk[] = { NK_SuperNode, NK_ForwardingSuperNode }; for (int q = 0; q < 2; q++) NT_FOREACH_KIND(nt, sk[q], s) { Scope *ss = comp_scope_of(c, s); if (ss && ss->name && sp_streq(ss->name, name)) return 1; } return 0; } /* Is every value that can reach node a of a type the seed `slot` contradicts? A concretely typed value is judged as the parameter check judges an argument. A boxed one is followed only while it is the read of a method parameter its body never writes: the values are then the arguments every call site of the method hands that parameter (and its default), each followed in turn. Anything else -- a call that cannot be resolved, a name a Symbol spells, a splat or keyword shape, too many sites or too deep a chain, a method no call reaches -- answers 0, and the run-time check stays the only one. */ static int seed_chase_contradicts(Compiler *c, TyKind slot, int a, int depth, SeedChase *ch) { const NodeTable *nt = c->nt; if (a < 0) return 0; TyKind t = infer_type(c, a); if (t != TY_POLY) { if (t == TY_UNKNOWN) return 0; if (slot == TY_POLY_ARRAY && (t == TY_INT_ARRAY || t == TY_STR_ARRAY || t == TY_FLOAT_ARRAY)) return 0; if (!seed_contradicts(c, slot, t, 0)) return 0; if (ch->shown == TY_UNKNOWN) ch->shown = t; return 1; } if (depth >= SEED_CHASE_DEPTH || nt_kind(nt, a) != NK_LocalVariableReadNode) return 0; const char *pn = nt_str(nt, a, "name"); Scope *m = comp_scope_of(c, a); if (!pn || !m || !m->name || m->def_node < 0 || m->rest_idx >= 0 || m->kwrest_idx >= 0) return 0; int pi = -1; for (int i = 0; i < m->nparams; i++) if (m->pnames[i] && sp_streq(m->pnames[i], pn)) { pi = i; break; } LocalVar *lv = pi >= 0 ? scope_local(m, pn) : NULL; if (!lv || lv->is_block_param) return 0; int mi = (int)(m - c->scopes); /* the parameter holds what it was handed: no write of it anywhere */ if (comp_vsite_first(c, VS_WRITE, NK_LocalVariableReadNode, pn, lv->owner >= 0 ? lv->owner : mi) >= 0) return 0; for (int i = 0; i < ch->nseen; i += 2) if (ch->seen[i] == mi && ch->seen[i + 1] == pi) return 1; /* (a cycle hands on nothing new) */ if (ch->nseen + 2 > ch->cseen) { ch->cseen = ch->cseen ? ch->cseen * 2 : 16; ch->seen = realloc(ch->seen, sizeof(int) * (size_t)ch->cseen); if (!ch->seen) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } ch->seen[ch->nseen++] = mi; ch->seen[ch->nseen++] = pi; if (seed_chase_dynamic(c, m->name)) return 0; int any = 0; int dflt = m->pdefault ? m->pdefault[pi] : -1; for (int id = an_calls_named_first(c, m->name); id >= 0; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; if (++ch->sites > SEED_CHASE_SITES) return 0; int tmi = seed_call_target(c, id, m->name); if (tmi < 0) return 0; /* a call that may reach the method at run time */ if (tmi != mi) continue; if (nt_ref(nt, id, "block") >= 0 && nt_kind(nt, nt_ref(nt, id, "block")) == NK_BlockArgumentNode) return 0; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (argc > m->nparams) return 0; for (int k = 0; k < argc; k++) { NodeKind ak = nt_kind(nt, argv[k]); if (ak == NK_SplatNode || ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) return 0; } int v = pi < argc ? argv[pi] : dflt; if (v < 0) return 0; if (!seed_chase_contradicts(c, slot, v, depth + 1, ch)) return 0; any = 1; } if (dflt >= 0 && !seed_chase_contradicts(c, slot, dflt, depth + 1, ch)) return 0; return any; } static void check_seed_contradictions(Compiler *c) { const NodeTable *nt = c->nt; char _sn1[192], _sn2[192]; SeedIvarReport *reported = NULL; int nreported = 0; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_InstanceVariableWriteNode) continue; const char *nm = nt_str(nt, id, "name"); int v = nt_ref(nt, id, "value"); if (!nm || v < 0) continue; Scope *sc = comp_scope_of(c, id); int cid = sc ? sc->class_id : -1; if (cid < 0) continue; ClassInfo *ci = &c->classes[cid]; if (!class_ivar_pinned(ci, nm)) continue; int iv = comp_ivar_index(ci, nm); if (iv < 0) continue; TyKind slot = ci->ivar_types[iv]; /* `@a = @b = nil` assigns @a the nil at the bottom of the chain, not @b's slot type: the write's value is what was written */ for (int d = 0; d < 32; d++) { NodeKind vk = nt_kind(nt, v); if (vk != NK_InstanceVariableWriteNode && vk != NK_LocalVariableWriteNode && vk != NK_GlobalVariableWriteNode && vk != NK_ClassVariableWriteNode) break; int inner = nt_ref(nt, v, "value"); if (inner < 0) break; v = inner; } TyKind val = infer_type(c, v); /* A String Array the seed pins whose Strings are shared settles in its poly form, the boxes holding the handles (share_default_apply), and a String Array written into it is wrapped element by element: the same Ruby type in its shared C form, as a local's is (reassert_rbs_param_seeds), not a contradiction (#9626). So is a String Hash the seed pins, in its String-keyed poly form. */ if (slot == TY_POLY_ARRAY && ci->ivar_elems_shared && (ci->ivar_elems_shared[iv] & ELEMS_POLY) && (val == TY_STR_ARRAY)) continue; if (slot == TY_STR_POLY_HASH && val == TY_STR_STR_HASH) continue; if (!seed_contradicts(c, slot, val, 0)) continue; int ln = (int)nt_int(nt, id, "node_line", 0); int fid = (int)nt_int(nt, id, "node_file", 0); const char *file = nt_file_path(nt, fid); if (!file || !*file) file = nt->source_file; if (!file || !*file) file = "source.rb"; const char *by = inline_pin_at_ivar(cid, nm); /* A write in a mixed-in module's method is cloned into each includer's copy, and the clone carries the annotation, so one assignment in the source can contradict the pin twice: it is reported once. */ if (by) { int seen = 0; for (int k = 0; k < nreported && !seen; k++) seen = reported[k].fid == fid && reported[k].ln == ln && sp_streq(reported[k].nm, nm); if (seen) continue; reported = realloc(reported, sizeof *reported * (size_t)(nreported + 1)); reported[nreported++] = (SeedIvarReport){ fid, ln, nm }; } if (by) fprintf(stderr, "spinel: %s:%d: inline RBS annotation contradicted: %s is declared %s at %s " "but this assigns %s\n" " An annotation is trusted, so the emitted code would reinterpret the value " "rather than convert it.\n" " Fix the annotation or the assignment.\n", file, ln, nm, seed_ty_name_into(c, slot, _sn1, sizeof _sn1), by, seed_ty_name_into(c, val, _sn2, sizeof _sn2)); else fprintf(stderr, "spinel: %s:%d: --rbs seed contradicted: %s is declared %s but this " "assigns %s\n" " A seed is trusted, so the emitted code would reinterpret the value " "rather than convert it.\n" " Fix the signature or the assignment.\n", file, ln, nm, seed_ty_name_into(c, slot, _sn1, sizeof _sn1), seed_ty_name_into(c, val, _sn2, sizeof _sn2)); if (!collect_mode()) exit(1); g_seed_bad = 1; } free(reported); /* The same contradiction on a RETURN. A seeded return is trusted, so the emitted function carries the pinned C type and the body's value is placed in it as-is: a String body under a `-> Hash[...]` seed returns a char* from a function typed sp_SymPolyHash*, which only the C compiler used to report -- against generated code, and in its voice rather than spinel's (#4005). Same judgement as the ivar rule above: decidable only where both sides are concretely typed and their representations cannot convert. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->ret_rbs_seeded || sc->body < 0 || !sc->reachable) continue; /* A definition a later `def` has REPLACED cannot be called: dispatch resolves to the last one, and did before this rule existed. Judging the replaced body refused a program spinel compiles and runs correctly (#4024). Only the definition the chain actually resolves to is judged. */ if (sc->class_id >= 0 && sc->name) { int eff = sc->is_cmethod ? comp_cmethod_in_chain(c, sc->class_id, sc->name, NULL) : comp_method_in_chain(c, sc->class_id, sc->name, NULL); if (eff >= 0 && eff != s) continue; } TyKind slot = sc->ret; /* every value the method can answer with: the body's tail, and each explicit return inside this scope -- a conditional one branch at a time */ int sites[64]; unsigned reach[64]; int nsites = 0; seed_ret_branch_sites(nt, sc->body, sites, reach, &nsites, 64); NT_FOREACH_KIND(nt, NK_ReturnNode, id) { if (nsites >= 64) break; if (c->nscope[id] != s) continue; int rv = nt_ref(nt, id, "arguments"); if (rv < 0) continue; int an = 0; const int *av = nt_arr(nt, rv, "arguments", &an); if (av && an == 1) seed_ret_branch_sites(nt, av[0], sites, reach, &nsites, 64); } for (int k = 0; k < nsites; k++) { /* The left of `||` answers only when truthy and the left of `&&` only when falsy; a left of both is never the value. A truthy answer of a site that is only false or nil, and a falsy one of a site that is only truthy, never happens (`falsev || s`, `true && s`). Of the falsy answers nil fits every slot, so only a false, a Boolean, can contradict. */ if (reach[k] == (VPR_TRUTHY | VPR_FALSY)) continue; if (reach[k] == VPR_FALSY && (infer_type(c, sites[k]) != TY_BOOL || seed_value_only(c, sites[k], 0, 0))) continue; if (reach[k] == VPR_TRUTHY && seed_value_only(c, sites[k], 1, 0)) continue; /* An EMPTY container literal carries a default kind, not evidence: a bare `{}` reads as the String-keyed variant and so contradicted every Symbol-keyed seed, though it has no keys to disagree about and the seed is the only thing in the program that says which kind it is (#4025). The seed wins; there is nothing here to judge. */ { int lit = sites[k]; if (nt_kind(nt, lit) == NK_StatementsNode) { int bn2 = 0; const int *bb2 = nt_arr(nt, lit, "body", &bn2); lit = (bn2 > 0 && bb2) ? bb2[bn2 - 1] : -1; } if (lit >= 0) { NodeKind sk = nt_kind(nt, lit); if (sk == NK_HashNode || sk == NK_KeywordHashNode || sk == NK_ArrayNode) { int en = 0; nt_arr(nt, lit, "elements", &en); /* A RETURNED container literal is built at the declared kind -- the return boundary carries the seed's type and the literal has no identity to preserve, so there is nothing here to judge either (#3279). An ivar WRITE of a literal is a different matter: the ivar keeps the object, so its kind has to be the declared one. */ if (en > 0 && sk == NK_ArrayNode && ty_is_array(slot)) continue; if (en == 0) { /* An EMPTY container literal carries a DEFAULT kind, not evidence: a bare `{}` reads as the String-keyed variant and contradicted every Symbol-keyed seed, though it has no keys to disagree about. The seed is the only thing in the program that says which kind it is, so the seed wins -- and the literal is built at it, or the emitted return would still be the default kind (#4025). */ if (lit < c->node_cap && ((ty_is_hash(slot) && (sk == NK_HashNode || sk == NK_KeywordHashNode)) || (ty_is_array(slot) && sk == NK_ArrayNode))) { comp_set_ntype(c, lit, slot); if (c->hash_want && ty_is_hash(slot)) comp_set_hash_want(c, lit, slot); } continue; } } } } TyKind val = infer_type(c, sites[k]); /* the seeded String Array return settled in its poly form, the boxes holding the shared handles (ret_elems_shared): a String Array the body answers is wrapped into it, the same Ruby type (#9626) */ if (sc->ret_elems_shared && slot == TY_POLY_ARRAY && (val == TY_STR_ARRAY || val == TY_POLY_ARRAY)) continue; if (!seed_contradicts(c, slot, val, 1)) continue; int ln = (int)nt_int(nt, sites[k], "node_line", 0); int fid = (int)nt_int(nt, sites[k], "node_file", 0); const char *file = nt_file_path(nt, fid); if (!file || !*file) file = nt->source_file; if (!file || !*file) file = "source.rb"; const char *cn = (sc->class_id >= 0 && sc->class_id < c->nclasses) ? c->classes[sc->class_id].name : NULL; char pos[1200]; if (ln > 0) snprintf(pos, sizeof pos, "%s:%d: ", file, ln); else snprintf(pos, sizeof pos, "%s: ", file); const char *by = inline_pin_at_method(sc, -1); if (by) fprintf(stderr, "spinel: %sinline RBS annotation contradicted: %s%s%s is declared to return " "%s at %s but this returns %s\n" " An annotation is trusted, so the emitted function carries the declared type " "and the value is placed in it rather than converted.\n" " Fix the annotation or the body.\n", pos, cn ? cn : "", cn ? "#" : "", sc->name ? sc->name : "?", seed_ty_name_into(c, slot, _sn1, sizeof _sn1), by, seed_ty_name_into(c, val, _sn2, sizeof _sn2)); else fprintf(stderr, "spinel: %s--rbs seed contradicted: %s%s%s is declared to return " "%s but this returns %s\n" " A seed is trusted, so the emitted function carries the declared type " "and the value is placed in it rather than converted.\n" " Fix the signature or the body.\n", pos, cn ? cn : "", cn ? "#" : "", sc->name ? sc->name : "?", seed_ty_name_into(c, slot, _sn1, sizeof _sn1), seed_ty_name_into(c, val, _sn2, sizeof _sn2)); if (!collect_mode()) exit(1); g_seed_bad = 1; } } /* The same contradiction one step earlier: an argument whose type is concretely known, handed to a parameter the seed pinned to a type in the other representation family. Codegen trusts the pin, so the emitted C reads a hash pointer as a C string rather than converting it. Only the plain positional shape is judged -- a rest/keyword param, a splat argument, or a call whose target cannot be resolved statically leaves the mapping in doubt, and a false error breaks a working build. */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *name = nt_str(nt, id, "name"); if (!name) continue; int mi = seed_call_target(c, id, name); if (mi < 0 || mi >= c->nscopes) continue; Scope *m = &c->scopes[mi]; if (m->rest_idx >= 0 || m->kwrest_idx >= 0) continue; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (!argv || argc > m->nparams) continue; /* each parameter against the argument the call's layout hands it */ ArgLayout L; call_layout(c, m, argv, argc, &L); for (int i = 0; i < m->nparams; i++) { int a = layout_plain_arg(c, m, argv, &L, i); /* a keyword the call names by a literal symbol: the value its hash holds */ if (a < 0 && L.from[i] == ARG_BY_NAME && L.kw.role == KWH_KEYWORDS && L.kw.kwh >= 0 && !L.kw.spread && m->pnames[i] && callee_param_is_declared_kwarg(c, m, m->pnames[i])) a = seed_kwarg_value(nt, L.kw.kwh, m->pnames[i]); if (a < 0 || nt_kind(nt, a) == NK_BlockArgumentNode) continue; LocalVar *lv = m->pnames[i] ? scope_local(m, m->pnames[i]) : NULL; if (!lv || !lv->rbs_seeded) continue; /* The DECLARED type is what a seed promises. Inference may narrow the slot afterwards -- an `untyped` (poly) parameter whose body indexes it with a symbol key reads as a symbol-keyed hash -- and that narrowing is spinel's own guess, not a declaration to hold a call site to (#3977). */ TyKind slot = (lv->rbs_type != TY_UNKNOWN) ? lv->rbs_type : lv->type; TyKind val = infer_type(c, a); if (slot == TY_POLY) continue; /* `untyped` accepts anything */ /* a boxed argument read from a parameter the seed leaves untyped: the values its call sites hand it, followed back while they are parameters too. When every one is of a type the seed contradicts, the call is the direct call's contradiction one step removed (#10681); anything less certain is left to the run-time check (sp_rbs_need_ary). */ int via_mi = -1, via_pi = -1; if (val == TY_POLY) { SeedChase ch = { NULL, 0, 0, 0, TY_UNKNOWN }; int all = seed_chase_contradicts(c, slot, a, 0, &ch); if (all && ch.nseen >= 2) { via_mi = ch.seen[0]; via_pi = ch.seen[1]; } free(ch.seen); if (!all || ch.shown == TY_UNKNOWN) continue; val = ch.shown; } /* emit_arg_or_default_fill boxes these array kinds into a general Array parameter (#3137). This is a conversion, not a contradiction (#6514). Its mutation guard still refuses a copy that would lose stores into an array the caller holds (#4480). Do not apply this exception to ivar storage or to kinds without a converter. */ if (slot == TY_POLY_ARRAY && (val == TY_INT_ARRAY || val == TY_STR_ARRAY || val == TY_FLOAT_ARRAY)) continue; if (!seed_contradicts(c, slot, val, 0)) continue; /* An arm of a runtime-name send (`send(name, h)`, `method(name).call(h)`) runs only when the name selects this method, and the arm set holds every method the program names anywhere. Judging the arm as a call site refused a program whose send never picks the contradicted method (#6672). The arm keeps its name in the dispatch but raises TypeError when chosen; its call is never emitted, so the seeded parameter is neither reinterpreted nor widened. */ if (nt_int(nt, id, "dyn_arm", 0) && nt_str(nt, id, "dyn_name")) { const char *vc = builtin_class_of_type(val); if (!vc && ty_is_object(val)) vc = seed_ty_name_into(c, val, _sn2, sizeof _sn2); if (!vc) vc = seed_ty_name_into(c, val, _sn2, sizeof _sn2); char msg[512]; snprintf(msg, sizeof msg, "%s: parameter %s is declared %s by --rbs but got %s", name, m->pnames[i], seed_ty_name_into(c, slot, _sn1, sizeof _sn1), vc); nt_node_set_str((NodeTable *)nt, id, "seed_type_error", msg); break; } int ln = (int)nt_int(nt, a, "node_line", 0); int fid = (int)nt_int(nt, a, "node_file", 0); const char *file = nt_file_path(nt, fid); if (!file || !*file) file = nt->source_file; if (!file || !*file) file = "source.rb"; const char *by = inline_pin_at_method(m, i); if (by) fprintf(stderr, "spinel: %s:%d: inline RBS annotation contradicted: parameter %s of %s is " "declared %s at %s but this call passes %s\n" " An annotation is trusted, so the emitted code would reinterpret the value " "rather than convert it.\n" " Fix the annotation or the call.\n", file, ln, m->pnames[i], name, seed_ty_name_into(c, slot, _sn1, sizeof _sn1), by, seed_ty_name_into(c, val, _sn2, sizeof _sn2)); else fprintf(stderr, "spinel: %s:%d: --rbs seed contradicted: parameter %s of %s is " "declared %s but this call passes %s\n" " A seed is trusted, so the emitted code would reinterpret the value " "rather than convert it.\n" " Fix the signature or the call.\n", file, ln, m->pnames[i], name, seed_ty_name_into(c, slot, _sn1, sizeof _sn1), seed_ty_name_into(c, val, _sn2, sizeof _sn2)); if (via_mi >= 0) fprintf(stderr, " The value is parameter %s of %s, which every call of %s hands a %s.\n", c->scopes[via_mi].pnames[via_pi], c->scopes[via_mi].name, c->scopes[via_mi].name, seed_ty_name_into(c, val, _sn2, sizeof _sn2)); if (!collect_mode()) exit(1); g_seed_bad = 1; } arg_layout_free(&L); } /* Collect mode gathered them; the run still fails, and stops before codegen so nothing is emitted from a seed we just refused. */ if (g_seed_bad) exit(1); } /* Re-assert the type an --rbs seed declared for a parameter. The seed is the promise the emitted code is allowed to trust, and inference narrows a slot from evidence that is only about ONE call site -- an `untyped` (poly) parameter whose body reads `value[:key]` looks like a symbol-keyed hash, and every other caller then had its argument reinterpreted through that layout (#3977). A concrete seed is already left alone by the passes that check rbs_seeded; this catches the ones that do not. */ static int reassert_rbs_param_seeds(Compiler *c) { int changed = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if (!lv->is_param || !lv->rbs_seeded) continue; if (lv->rbs_type == TY_UNKNOWN || lv->type == lv->rbs_type) continue; /* a String the share rule made the handle (share_default_apply): the same Ruby type in its shared C form */ if (lv->rbs_type == TY_STRING && lv->type == TY_STRBUF && lv->str_shared) continue; /* and a String Array whose Strings it shares, boxed as their handles */ if (lv->rbs_type == TY_STR_ARRAY && lv->type == TY_POLY_ARRAY && lv->elems_shared) continue; /* and a String Hash whose Strings it shares, boxed as their handles */ if (lv->rbs_type == TY_STR_STR_HASH && lv->type == TY_STR_POLY_HASH) continue; lv_set_type(lv, lv->rbs_type); changed = 1; } } return changed; } /* Names whose miss answers nil while the type stays TY_INT (a search index, a pop off an empty array): the slot they leave holds nil. */ /* A File::Stat field on an IO (or a boxed value the call reaches one through) and `File.size?` answer nil where the stat fails or the file is empty: the runtime's sp_stat_field / sp_file_size_q answer an sp_oint. */ int file_stat_nil_call(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0 || nt_kind(nt, v) != NK_CallNode || nt_ref(nt, v, "block") >= 0) return 0; const char *nm = nt_str(nt, v, "name"); int rc = nt_ref(nt, v, "receiver"); if (!nm || rc < 0) return 0; int args = nt_ref(nt, v, "arguments"), argc = 0; if (args >= 0) nt_arr(nt, args, "arguments", &argc); if (sp_streq(nm, "size?") && argc == 1 && nt_kind(nt, rc) == NK_ConstantReadNode) { const char *cn = nt_str(nt, rc, "name"); return cn && (sp_streq(cn, "File") || sp_streq(cn, "FileTest")); } /* `size` on a boxed value: a Range of non-numbers answers nil (sp_poly_size) */ if (sp_streq(nm, "size") && argc == 0 && infer_type(c, rc) == TY_POLY) return 1; /* a stat's numeric fields and size?, each of which can read nil */ if (argc != 0 || (stat_field_slot(nm) < 0 && !sp_streq(nm, "size?"))) return 0; TyKind rt = infer_type(c, rc); if (rt == TY_IO) return 1; if (rt != TY_POLY) return 0; /* a boxed receiver: only where no class of the program answers the name (its dispatch is the IO's) */ for (int k = 0; k < c->nclasses; k++) if (call_target(c, v, k, nm, 0, NULL) >= 0 || comp_reader_in_chain(c, k, nm, NULL)) return 0; return 1; } /* Is a `new` on recv a run-time switch over every class of the program: a class value no analysis names (`Object.const_get(x).new`, `o.class.new`), not a constant, self, an inline `Struct.new(...)`, a `self.class` naming one class or a local holding one? (#9093) */ int new_recv_every_class(Compiler *c, int recv) { if (recv < 0) return 0; NodeKind rk = nt_kind(c->nt, recv); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode || rk == NK_SelfNode) return 0; if (rk == NK_CallNode && is_struct_call(c, recv)) return 0; if (self_class_static_ci(c, recv) >= 0 || class_var_static_ci(c, recv) >= 0) return 0; TyKind rt = infer_type(c, recv); return rt == TY_CLASS || rt == TY_POLY; } /* The classes a `new` receiver can be: a local holding one class, a constant, or a class value chosen at run time -- each branch of a `?:`, an `if`, an `unless`, a `case`, a `case`/`in`, a `begin`, a rescue modifier, either side of a `||` or the right side of a `&&`, a parenthesized one, or an element of a literal Array of them (`[A, B][i]`). Up to cap of them, in *out; answers the count, or 0 when some value the receiver can be names no class here (a call, a local no class is known to hold, a branch that is missing) or the classes do not all fit: the caller then takes the receiver for a class value no analysis names, and builds every class. A partial set built only the classes it named, and a class outside it, chosen at run time, was read as another. */ typedef struct { Compiler *c; int *out; int cap, n, ok; } NewRecvArms; static int new_recv_arm(const VpLeaf *leaf, void *ud) { NewRecvArms *a = ud; int k = leaf->kind == VPK_EXPR && a->cap - a->n > 0 ? new_recv_classes(a->c, leaf->node, a->out + a->n, a->cap - a->n) : 0; if (k <= 0) { a->ok = 0; return 1; } a->n += k; return 0; } /* The iterators whose block's first parameter is each element in turn */ static int elem_block_iter(const char *nm) { static const char *const IT[] = { "each", "map", "collect", "flat_map", "collect_concat", "select", "filter", "reject", "find", "detect", "each_with_index", "filter_map", "any?", "all?", "none?", "one?", "sum", "count", "min_by", "max_by", "sort_by", "group_by", "partition", "find_index", "each_entry", "take_while", "drop_while", NULL }; return nm && str_in(nm, IT); } /* A read of a block's first parameter where the block walks a literal Array (`[A, B].map { |k| k.new(v) }`) and nothing in it writes the parameter: the classes the literal's elements name, as new_recv_classes answers for `[A, B][i]`. 0 otherwise. */ static int new_recv_block_param_classes(Compiler *c, int recv, int *out, int cap) { const NodeTable *nt = c->nt; const char *vn = nt_str(nt, recv, "name"); if (!vn) return 0; const int *par = comp_parent_map(c); int blk = -1; for (int p = par[recv]; p >= 0 && blk < 0; p = par[p]) { NodeKind pk = nt_kind(nt, p); if (pk == NK_DefNode || pk == NK_ClassNode || pk == NK_ModuleNode || pk == NK_LambdaNode) return 0; if (pk != NK_BlockNode) continue; int bp = nt_ref(nt, p, "parameters"); int ps = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int rn = 0; const int *rq = ps >= 0 ? nt_arr(nt, ps, "requireds", &rn) : NULL; for (int i = 0; i < rn; i++) { const char *pn = nt_str(nt, rq[i], "name"); if (pn && sp_streq(pn, vn)) { if (i != 0 || nt_kind(nt, rq[i]) != NK_RequiredParameterNode) return 0; blk = p; } } /* a block that declares the name among its locals otherwise shadows it */ if (blk < 0 && bp >= 0) { int ln = 0; const int *ls = nt_arr(nt, bp, "locals", &ln); for (int i = 0; i < ln; i++) if (sp_streq(nt_str(nt, ls[i], "name"), vn)) return 0; } } if (blk < 0) return 0; int call = par[blk]; if (call < 0 || nt_kind(nt, call) != NK_CallNode || nt_ref(nt, call, "block") != blk || !elem_block_iter(nt_str(nt, call, "name"))) return 0; int ar = nt_ref(nt, call, "receiver"); if (ar < 0 || nt_kind(nt, ar) != NK_ArrayNode) return 0; /* the parameter keeps the element: no write of the name inside the block */ Scope *sc = comp_scope_of(c, recv); if (!sc) return 0; for (int w = comp_lvw_first_sc(c, (int)(sc - c->scopes), vn); w >= 0; w = comp_lvw_next_sc(c, w)) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, vn)) continue; for (int p = par[w]; p >= 0; p = par[p]) if (p == blk) return 0; } int en = 0; const int *ev = nt_arr(nt, ar, "elements", &en); int n = 0; for (int e = 0; e < en; e++) { int k = n < cap ? new_recv_classes(c, ev[e], out + n, cap - n) : 0; if (k <= 0) return 0; n += k; } return n; } int new_recv_classes(Compiler *c, int recv, int *out, int cap) { const NodeTable *nt = c->nt; if (recv < 0 || cap <= 0) return 0; NodeKind rk = nt_kind(nt, recv); if (rk == NK_LocalVariableReadNode) { int k = class_var_static_ci(c, recv); if (k < 0) return new_recv_block_param_classes(c, recv, out, cap); out[0] = k; return 1; } if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { int k = const_node_class_index(c, recv); if (k < 0) return 0; out[0] = k; return 1; } if (rk == NK_ParenthesesNode || rk == NK_IfNode || rk == NK_UnlessNode || rk == NK_CaseNode || rk == NK_CaseMatchNode || rk == NK_OrNode || rk == NK_AndNode || rk == NK_BeginNode || rk == NK_RescueModifierNode || rk == NK_ElseNode || rk == NK_StatementsNode) { NewRecvArms a = { c, out, cap, 0, 1 }; for_each_value_position(nt, recv, VPS_ALL, new_recv_arm, &a); return a.ok ? a.n : 0; } /* `[A, B][i]`: every element of the literal */ if (rk == NK_CallNode && nt_str(nt, recv, "name") && sp_streq(nt_str(nt, recv, "name"), "[]")) { int ar = nt_ref(nt, recv, "receiver"); if (ar < 0 || nt_kind(nt, ar) != NK_ArrayNode) return 0; int en = 0; const int *ev = nt_arr(nt, ar, "elements", &en); int n = 0; for (int e = 0; e < en; e++) { int k = n < cap ? new_recv_classes(c, ev[e], out + n, cap - n) : 0; if (k <= 0) return 0; n += k; } return n; } return 0; } static int nullable_int_call_name(const char *nm) { if (!nm) return 0; static const char *const N[] = { "index", "rindex", "find_index", "byteindex", "byterindex", "delete_at", "slice!", "pop", "shift", "delete", "nonzero?", "infinite?", "getbyte", "bsearch", "bsearch_index", "unpack1", /* Process::Status: nil when the process did not exit / was not signaled */ "exitstatus", "termsig", "stopsig", /* `a <=> b` answers nil when the two are not comparable, and the poly helper spells that with the nil flag like every other nullable int */ "<=>", NULL }; return str_in(nm, N); } /* Calls whose Integer or Float answer is a boxed value unboxed into the slot, and which can answer nil: the unbox sets the slot's nil flag, so the value is a nullable one. `r&.m` is nil when r is; `p x` hands back x (and a bare `p`, nil); a `catch` answers what a `throw` carried, nil by default; a Proc's call answers its body's value, which arrives boxed. */ static int nn_call_unboxes_nil(Compiler *c, int v) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, v, "name"); if (!nm) return 0; if (nt_safe_nav(nt, v)) return 1; int rcv = nt_ref(nt, v, "receiver"); if (rcv < 0) { if (comp_self_call_mi(c, v, nm) >= 0) return 0; /* the program's own p or catch */ if (sp_streq(nm, "catch")) return 1; if (sp_streq(nm, "p") && nt_ref(nt, v, "block") < 0) { int ca = nt_ref(nt, v, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; return an == 0 || (an == 1 && nullable_int_value(c, av[0])); } return 0; } if (is_proc_invoke(nm) && infer_type(c, rcv) == TY_PROC) return 1; /* Reflection reads the same slot, and its nil byte, as an ivar reader. A boxed receiver can also lack the slot altogether. Keep that nil through boxing, scalar operations and assignments to other locals. remove_instance_variable answers the field it removes: the same read. */ if (sp_streq(nm, "instance_variable_get") || sp_streq(nm, "remove_instance_variable")) { int ca = nt_ref(nt, v, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (an != 1) return 0; NodeKind ak = nt_kind(nt, av[0]); const char *sym = ak == NK_SymbolNode ? nt_str(nt, av[0], "value") : ak == NK_StringNode ? nt_str(nt, av[0], "content") : NULL; if (!sym || sym[0] != '@') return 0; TyKind rt = infer_type(c, rcv); if (rt == TY_POLY) return 1; if (ty_is_object(rt)) { int cid = ty_object_class(rt); if (call_target(c, v, cid, nm, 0, NULL) >= 0) return 0; ClassInfo *ci = &c->classes[cid]; int iv = comp_ivar_index(ci, sym); return iv >= 0 && (!ci->is_struct || iv >= ci->nmembers) && (ci->ivar_types[iv] == TY_INT || ci->ivar_types[iv] == TY_FLOAT); } } return 0; } /* A method's own answer can be nil in an Integer or Float slot: its value is nil (`def m = nil`), or its scalar return can hold nil. */ static int method_answers_nil(const Scope *m) { return m->ret == TY_NIL || m->ret_nullable_int; } /* A scalar slot on a class the fixpoint could not pin to a receiver still dispatches at runtime: codegen emits a cls_id switch over every class that defines the name. Ask whether ANY of those targets can answer nil. Over-marking here only costs the boxing branch; missing one is the silent-wrong hash key of #3505, so the conservative direction is `yes`. */ /* The scopes named `cn` and the classes with a reader `cn` in their chain, per name: both are fixed once the program's methods and classes are, and a poly call asked for its nil scanned every scope and class per call -- quadratic in the program, in codegen as well as in the rounds (scale-test). */ typedef struct { char *name; int *sc; int nsc; int *cls; int ncl; } PdnEnt; static PdnEnt *pdn_tab = NULL; static int pdn_cap = 0, pdn_used = 0, pdn_nscopes = -1, pdn_nclasses = -1; static const Compiler *pdn_owner = NULL; static unsigned pdn_hash(const char *s) { unsigned h = 2166136261u; while (*s) { h ^= (unsigned char)*s++; h *= 16777619u; } return h; } static void pdn_reset(void) { for (int i = 0; i < pdn_cap; i++) if (pdn_tab[i].name) { free(pdn_tab[i].name); free(pdn_tab[i].sc); free(pdn_tab[i].cls); } free(pdn_tab); pdn_tab = NULL; pdn_cap = pdn_used = 0; } static PdnEnt *pdn_slot(PdnEnt *tab, int cap, const char *cn) { unsigned i = pdn_hash(cn) & (unsigned)(cap - 1); while (tab[i].name && strcmp(tab[i].name, cn) != 0) i = (i + 1) & (unsigned)(cap - 1); return &tab[i]; } static const PdnEnt *pdn_lookup(Compiler *c, const char *cn) { if (pdn_owner != c || pdn_nscopes != c->nscopes || pdn_nclasses != c->nclasses) { pdn_reset(); pdn_owner = c; pdn_nscopes = c->nscopes; pdn_nclasses = c->nclasses; } if (pdn_used * 2 >= pdn_cap) { int ncap = pdn_cap ? pdn_cap * 2 : 256; PdnEnt *nt2 = calloc((size_t)ncap, sizeof *nt2); if (!nt2) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < pdn_cap; i++) if (pdn_tab[i].name) *pdn_slot(nt2, ncap, pdn_tab[i].name) = pdn_tab[i]; free(pdn_tab); pdn_tab = nt2; pdn_cap = ncap; } PdnEnt *e = pdn_slot(pdn_tab, pdn_cap, cn); if (e->name) return e; e->name = strdup(cn); if (!e->name) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } pdn_used++; for (int si = 1; si < c->nscopes; si++) if (c->scopes[si].name && sp_streq(c->scopes[si].name, cn)) { int *g = realloc(e->sc, sizeof(int) * (size_t)(e->nsc + 1)); if (!g) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } e->sc = g; e->sc[e->nsc++] = si; } for (int ci = 0; ci < c->nclasses; ci++) if (comp_reader_in_chain(c, ci, cn, NULL)) { int *g = realloc(e->cls, sizeof(int) * (size_t)(e->ncl + 1)); if (!g) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } e->cls = g; e->cls[e->ncl++] = ci; } return e; } static int poly_dispatch_nullable(Compiler *c, int v, const char *cn) { if (!cn) return 0; /* a target whose value is nil rides in the call's scalar slot as its nil too, when the call is typed Integer or Float (or nil) */ TyKind vt = infer_type(c, v); int nil_rides = vt == TY_INT || vt == TY_FLOAT || vt == TY_NIL; const PdnEnt *pe = pdn_lookup(c, cn); /* only the methods the dispatch has arms for: a class the receiver's class fact rules out is no target (share_target_armed), as inference leaves it out of the call's type (comp_poly_candidates_at) */ for (int k = 0; k < pe->nsc; k++) { int si = pe->sc[k]; if ((nil_rides ? method_answers_nil(&c->scopes[si]) : c->scopes[si].ret_nullable_int) && share_target_armed(c, v, si)) return 1; } /* an attr_reader over a scalar ivar: those slots start nil, their nil byte set (emit_ivar_nil_inits_from), so the read can answer nil like a `return nil` would -- the resolved-receiver twin of this lives in codegen's call_returns_nullable_int */ int rcv = nt_ref(c->nt, v, "receiver"); for (int k = 0; k < pe->ncl; k++) { int ci = pe->cls[k]; if (cls_fact_excludes(c, rcv, ci)) continue; char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, ci, cn)); int iv = comp_ivar_index(&c->classes[ci], ivb); if (iv >= 0 && (c->classes[ci].ivar_types[iv] == TY_INT || c->classes[ci].ivar_types[iv] == TY_FLOAT)) return 1; /* a boxed one (every Integer slot under --int-overflow=promote) starts nil too, and its nil rides in a scalar call's slot the same way */ if (iv >= 0 && nil_rides && c->classes[ci].ivar_types[iv] == TY_POLY) return 1; } return 0; } /* Call `call` on class `cid` dispatches over the method `name` resolves to there and every descendant's override, which is how inference typed it (dispatch_ret_over): a nil-answering target among them rides in the call's scalar slot as its nil. Only the method the name resolved to was asked, and only for a scalar return, so a nil method beside an Integer one (`def nop = nil` in C, `def nop = 7` in D, or `c ? z : 1` over `def z = nil`) left the value unmarked, and nil's own methods (`to_a`, `to_h`, `=~`) raised Integer's NoMethodError. */ static int dispatch_answers_nil(Compiler *c, int call, int cid, const char *name, int cmeth) { if (cid < 0 || !name) return 0; int mi = call_target(c, call, cid, name, cmeth ? CT_CMETHOD : 0, NULL); if (mi >= 0 && method_answers_nil(&c->scopes[mi])) return 1; int nd = 0; const int *ds = comp_descendants(c, cid, &nd); for (int i = 0; i < nd; i++) { int kmi = call_target(c, call, ds[i], name, cmeth ? CT_CMETHOD : 0, NULL); if (kmi >= 0 && kmi != mi && method_answers_nil(&c->scopes[kmi])) return 1; } return 0; } /* The int/float-array local `nm` is read or written at `at`, and is a candidate for the element marking: NULL when it is not one, or is already marked. */ static LocalVar *nullable_elem_local(Compiler *c, int at, const char *nm) { Scope *sc = nm ? comp_scope_of(c, at) : NULL; LocalVar *lv = sc ? scope_local(sc, nm) : NULL; if (!lv || (lv->nullable_int_elem & NIE_MARKED)) return NULL; return (lv->type == TY_INT_ARRAY || lv->type == TY_FLOAT_ARRAY) ? lv : NULL; } /* Ivar `nm` of class `cid`'s instances, for the element marking: the slot of the topmost ancestor that carries it. A subclass copies its parent's slot and a method the parent defines reads the parent's, so a mark made through the subclass (`k2.arr << v`, a child method's `@arr << v`) sits where the whole family reads it. */ int nullable_elem_ivar_in(Compiler *c, int cid, const char *nm, ClassInfo **out) { int iv = -1; for (; nm && cid >= 0 && cid < c->nclasses; cid = c->classes[cid].parent) { int k = comp_ivar_index(&c->classes[cid], nm); if (k < 0) break; *out = &c->classes[cid]; iv = k; } return iv; } /* The ivar an ivar read/write node names, for the element marking. */ static int nullable_elem_ivar(Compiler *c, int at, ClassInfo **out) { Scope *s = comp_scope_of(c, at); int cid = s ? s->class_id : -1; if (cid < 0) cid = c->toplevel_class; return nullable_elem_ivar_in(c, cid, nt_str(c->nt, at, "name"), out); } /* An array method whose result elements are the receiver's own, so element nilability passes straight through it. `compact` is deliberately absent: it is what REMOVES the nils. */ int elem_preserving_call(const char *nm) { static const char *const N[] = { "select", "filter", "reject", "sort", "sort_by", "uniq", "reverse", "rotate", "take", "drop", "take_while", "drop_while", "shuffle", "to_a", "dup", "clone", "freeze", "values_at", "slice", "flatten", "first", "last", "sample", "slice!", "*", "-", "&", "difference", "intersection", NULL }; return str_in(nm, N); } /* A method that hands back one ELEMENT of its receiver. */ static int elem_returning_call(const char *nm) { static const char *const N[] = { "[]", "at", "first", "last", "min", "max", "sample", "pop", "shift", "fetch", "dig", NULL }; return str_in(nm, N); } /* `a[i, n]` / `a[r]`: an index read answering a sub-array. */ int slice_read_call(Compiler *c, int call) { const NodeTable *nt = c->nt; int ca = nt_ref(nt, call, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (!av) return 0; return an == 2 || (an == 1 && (nt_kind(nt, av[0]) == NK_RangeNode || infer_type(c, av[0]) == TY_RANGE)); } /* The tail expression of a literal block attached to `call`, or -1. */ static int call_block_tail(Compiler *c, int call) { const NodeTable *nt = c->nt; int blk = nt_ref(nt, call, "block"); if (blk < 0) return -1; int body = nt_ref(nt, blk, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) return -1; int n = 0; const int *st = nt_arr(nt, body, "body", &n); return st && n > 0 ? st[n - 1] : -1; } /* Can an ELEMENT of this array-valued expression be nil? A slot answers from its own marking; an expression that builds or forwards an array answers from what it was built out of, which is what lets a chain with no local in it (`[r].map { |x| x.p_ }[0]`) be seen at all (#3505). */ static int nullable_int_elem_expr(Compiler *c, int v, int depth); static int struct_member_slot(Compiler *c, int cid, const char *name, ClassInfo **out); /* What an object call (`k.arr`) hands out: *mi is the method the receiver's class chain resolves, whose tail decides, or else the answer is the ivar an attr_reader reads, found by its alias-resolved name in the class that declares it. A method whose tail reads an ivar names that one too, so a mutation through `k.grab` marks the slot `k.grab` returns. -1 when no ivar is named. */ static int object_call_ivar(Compiler *c, int call, ClassInfo **out, int *mi) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); int rc = nt_ref(nt, call, "receiver"); *mi = -1; int cid = -1, defc = -1; if (!nm) return -1; if (rc >= 0 && ty_is_object(infer_type(c, rc))) cid = ty_object_class(infer_type(c, rc)); else if (rc < 0) { /* a receiverless reader in an instance method: self's class (`bottom[x] = v` inside the Struct's own method marks the member) */ Scope *cs = comp_scope_of(c, call); if (cs && cs->class_id >= 0 && !cs->is_cmethod) cid = cs->class_id; } if (cid < 0) return -1; *mi = call_target(c, call, cid, nm, 0, NULL); if (*mi > 0) { int tail = scope_body_last(c, *mi); return tail >= 0 && nt_kind(nt, tail) == NK_InstanceVariableReadNode ? nullable_elem_ivar(c, tail, out) : -1; } if (!comp_reader_in_chain(c, cid, nm, &defc) || defc < 0) { /* a Struct / Data member's reader */ ClassInfo *sci = NULL; int siv = struct_member_slot(c, cid, nm, &sci); if (siv >= 0 && sci) { *out = sci; return siv; } return -1; } char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, cid, nm)); return nullable_elem_ivar_in(c, defc, ivb, out); } /* An array mutator that answers its receiver, so `a.push(v)` is `a`. */ static int self_mutator_call(const char *nm) { static const char *const N[] = { "<<", "push", "append", "unshift", "prepend", "insert", "fill", "concat", NULL }; return str_in(nm, N); } /* An index write that can land past the end, where CRuby fills the gap with nil and the typed array sets nil bits: `a[a.size + k] = v` for a literal k >= 1, or a literal index beyond the shortest array literal the local receiver is ever assigned (`a = [1, 2]; a[5] = v`). Only what can be shown: an index computed some other way is taken as in range, or every `a[i] = v` in a typed-array loop would box through the nil check. The gap such a write can still leave is not lost: the runtime sets the array's may_nil where it fills one (sp_IntArray_set_slow), and the whole-array reads ask that flag, so this mark only types the elements read out. */ static int index_write_gaps(Compiler *c, int call, int ix) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, call, "receiver"); if (recv < 0) return 0; if (nt_kind(nt, ix) == NK_CallNode && sp_streq(nt_str(nt, ix, "name"), "+")) { int l = nt_ref(nt, ix, "receiver"), ca = nt_ref(nt, ix, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; const char *ln = l >= 0 && nt_kind(nt, l) == NK_CallNode ? nt_str(nt, l, "name") : NULL; int lr = ln ? nt_ref(nt, l, "receiver") : -1; if (!ln || !is_len_alias(ln) || lr < 0 || !av || an != 1 || nt_kind(nt, av[0]) != NK_IntegerNode || nt_int(nt, av[0], "value", 0) < 1) return 0; /* the same local or ivar on both sides */ if (nt_kind(nt, lr) != nt_kind(nt, recv) || (nt_kind(nt, recv) != NK_LocalVariableReadNode && nt_kind(nt, recv) != NK_InstanceVariableReadNode)) return 0; const char *a = nt_str(nt, lr, "name"), *b = nt_str(nt, recv, "name"); return a && b && sp_streq(a, b); } if (nt_kind(nt, ix) != NK_IntegerNode || nt_kind(nt, recv) != NK_LocalVariableReadNode) return 0; long long k = nt_int(nt, ix, "value", 0); Scope *sc = comp_scope_of(c, recv); const char *nm = nt_str(nt, recv, "name"); if (k < 1 || !sc || !nm) return 0; for (int r = lw_shared_first(c, nm, (int)(sc - c->scopes)); r >= 0; r = lw_shared_next(r)) { int id = lw_shared_node(r); if (nt_kind(nt, id) != NK_LocalVariableWriteNode || comp_scope_of(c, id) != sc) continue; const char *wn = nt_str(nt, id, "name"); int wv = nt_ref(nt, id, "value"); if (!wn || !sp_streq(wn, nm) || wv < 0 || nt_kind(nt, wv) != NK_ArrayNode) continue; int en = 0; nt_arr(nt, wv, "elements", &en); if (k > en) return 1; } return 0; } /* `n` reads the size of the array `recv` writes into: `a.size` / `a.length` on the same local or ivar. */ static int reads_own_size(Compiler *c, int recv, int n) { const NodeTable *nt = c->nt; const char *ln = n >= 0 && nt_kind(nt, n) == NK_CallNode ? nt_str(nt, n, "name") : NULL; int lr = ln ? nt_ref(nt, n, "receiver") : -1; if (!ln || !is_len_alias(ln) || lr < 0 || nt_ref(nt, n, "arguments") >= 0 || nt_kind(nt, lr) != nt_kind(nt, recv) || (nt_kind(nt, recv) != NK_LocalVariableReadNode && nt_kind(nt, recv) != NK_InstanceVariableReadNode)) return 0; const char *a = nt_str(nt, lr, "name"), *b = nt_str(nt, recv, "name"); return a && b && sp_streq(a, b); } /* A write start that provably opens no gap: none at all, a literal at or below 0 (a negative start counts from the end and raises past the front), or the receiver's own size (`a[a.size, 0] = v` appends) or less (`a.size - k`). Anything else may land past the end, where the runtime pads with nil, so the array has to be marked (#6208). */ static int index_write_in_range(Compiler *c, int call, int ix) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, call, "receiver"); if (ix < 0) return 1; ix = nt_paren_single(nt, ix); if (nt_kind(nt, ix) == NK_IntegerNode) return nt_int(nt, ix, "value", 1) <= 0; if (recv < 0 || nt_kind(nt, ix) != NK_CallNode) return 0; if (reads_own_size(c, recv, ix)) return 1; const char *op = nt_str(nt, ix, "name"); int ca = nt_ref(nt, ix, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; return op && sp_streq(op, "-") && av && an == 1 && nt_kind(nt, av[0]) == NK_IntegerNode && nt_int(nt, av[0], "value", -1) >= 0 && reads_own_size(c, recv, nt_ref(nt, ix, "receiver")); } /* The start a range argument writes from: its first, or -1 (0) for a beginless one. A range held in a variable has no start to read, so it answers the range node itself, which no proof accepts. */ static int range_write_start(Compiler *c, int r) { const NodeTable *nt = c->nt; int u = nt_paren_single(nt, r); return u >= 0 && nt_kind(nt, u) == NK_RangeNode ? nt_ref(nt, u, "left") : r; } /* How many leading parameters of call `call`'s block are bound to elements of the array it iterates, which goes in *recv (past the enumerators a blockless call hands on): 0 for none. A lone parameter is one whatever the iterator, a numbered one and `it` too; `reduce`/`inject` and a comparing block (`sort { |a, b| }`, `min`, `max`, `minmax`) take an element in both. An iterator whose block takes the element first binds only that one -- the second is `each_with_index`'s index, `each_with_object`'s memo -- through an enumerator chain as well (`map.with_index`, `each.with_object`). Anything else with two (`|k, v|`) has a non-element slot. */ int elem_block_params(Compiler *c, int call, int *recv_out) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, call, "receiver"), blk = nt_ref(nt, call, "block"); const char *rnm2 = nt_str(nt, call, "name"); *recv_out = -1; if (recv < 0 || blk < 0 || !rnm2) return 0; int both = is_reduce_alias(rnm2) || is_sort_family(rnm2) || is_extrema_family(rnm2) || sp_streq(rnm2, "slice_when") || sp_streq(rnm2, "chunk_while"); int first = strbuf_elem_first_iterator(rnm2) || is_enumerator_with(rnm2); /* the enumerator a blockless call hands on yields the same elements */ for (int guard = 0; first && guard < 4 && nt_kind(nt, recv) == NK_CallNode && nt_ref(nt, recv, "block") < 0; guard++) { const char *cn = nt_str(nt, recv, "name"); int ca = nt_ref(nt, recv, "arguments"); if (!cn || !(strbuf_elem_first_iterator(cn) || is_enumerator_with(cn) || is_lazy_name(cn))) break; if (ca >= 0 && !is_enumerator_with(cn) && !is_with_object_alias(cn)) break; recv = nt_ref(nt, recv, "receiver"); if (recv < 0) break; } if (recv < 0) return 0; int rn = 0; while (rn < 9 && block_param_name(c, blk, rn)) rn++; if (rn < 1 || (rn > 2 && both)) return 0; if (rn > 1 && !both && !first) return 0; *recv_out = recv; return rn == 1 || !both ? 1 : 2; } /* What array mutation `call` (`<<`, push, append, unshift, prepend, insert, `[]=`, fill, concat) stores: -1 when it is none of them, 1 when it can leave a nil gap, else 0 with the operands it stores in [*from, *to) -- each an Array whose elements land when *elems (concat, a slice's `[]=` given an Array), each one value otherwise. A splice, a range write, insert and fill pad a gap before a start past the end with nil, so they count unless the start is provably in range: a computed start (`a[i, 0] = v`) is taken as able to miss. These forms are rare in a hot loop, unlike a one-index `a[i] = v`, which counts only where a gap can be shown (index_write_gaps) unless `strict` asks for the same proof of it (index_write_in_range): an Integer or Float Array's runtime may_nil flag catches the gap the mark misses, a pointer Array has none. */ int array_mutation_stores(Compiler *c, int call, int strict, int *from, int *to, int *elems) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); int ca = nt_ref(nt, call, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; *from = 0; *to = an; *elems = 0; if (!nm || !av || (!self_mutator_call(nm) && !is_index_assign(nm))) return -1; if (sp_streq(nm, "insert")) { if (!index_write_in_range(c, call, av[0])) return 1; *from = 1; } else if (is_index_assign(nm)) { /* where the write starts: the index, a slice's start (`a[s, n] = v`) or a range's first (`a[s..e] = v`). Past the end, CRuby nil-fills up to it whatever the value is, and so does the typed splice. */ int rng = an == 2 && (nt_kind(nt, nt_paren_single(nt, av[0])) == NK_RangeNode || infer_type(c, av[0]) == TY_RANGE); if (an == 3 || rng) { if (!index_write_in_range(c, call, rng ? range_write_start(c, av[0]) : av[0])) return 1; } else if (an == 2 && (strict ? !index_write_in_range(c, call, av[0]) : index_write_gaps(c, call, av[0]))) return 1; TyKind vt = infer_type(c, av[an - 1]); *elems = ty_is_array(vt) || ty_is_obj_array(vt); *from = an - 1; } else if (sp_streq(nm, "fill")) { /* fill(v, start, n) / fill(v, s..e), or fill(start, n) / fill(s..e) with a block: a start past the end leaves a nil gap before the filled run. With no length (`fill(v, start)`) nothing is written past the end. The block's values are the caller's to read. */ int blk = nt_ref(nt, call, "block") >= 0; int fa = 1 - blk, fx = an > fa ? av[fa] : -1; int frng = fx >= 0 && (nt_kind(nt, nt_paren_single(nt, fx)) == NK_RangeNode || infer_type(c, fx) == TY_RANGE); if ((frng || an > fa + 1) && !index_write_in_range(c, call, frng ? range_write_start(c, fx) : fx)) return 1; *to = blk ? 0 : 1; } else if (is_concat_name(nm)) *elems = 1; return 0; } /* An array mutation that can leave a nil in its receiver: `<<`, push, append, unshift, prepend, insert (past its index), `[]=` and a blockless fill given a value that can be nil, or concat and a slice's `[]=` given an array whose elements can be. */ static int nullable_elem_mutation(Compiler *c, int call, int depth) { const NodeTable *nt = c->nt; int from, to, elems; /* strict: a one-index write the in-range proof does not cover can leave a nil gap, which a plain slot reading it would see as a silent 0 (nil out of band) */ int g = array_mutation_stores(c, call, 1, &from, &to, &elems); if (g) return g > 0; int ca = nt_ref(nt, call, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; for (int k = from; k < to; k++) if (elems ? nullable_int_elem_expr(c, av[k], depth + 1) : nullable_int_value(c, av[k])) return 1; return 0; } /* The array a mutation lands in, past the calls chained before it that answer their receiver: the mutators (`(a << 1) << v`, `a.push(1).push(v)`) and every builtin whose row says so (bop_answers_recv: `a.each { }`, `a.tap { }`, `a.sort!`, `a.map! { }`), whose value is that array under another name. */ int mutated_array(Compiler *c, int recv) { const NodeTable *nt = c->nt; for (int d = 0; d < 8; d++) { /* (the array is the one the receiver's value comes from: `(a; b)`'s is b) */ int one; if (vp_one(nt, recv, VPS_NONE, &one) != VP1_NODE) return -1; recv = one; if (nt_kind(nt, recv) != NK_CallNode || nt_ref(nt, recv, "receiver") < 0) break; const char *rn = nt_str(nt, recv, "name"); int rr = nt_ref(nt, recv, "receiver"); int ra = nt_ref(nt, recv, "arguments"), ran = 0; if (ra >= 0) nt_arr(nt, ra, "arguments", &ran); if (!self_mutator_call(rn) && !bop_answers_recv(infer_type(c, rr), rn, ran, nt_ref(nt, recv, "block") >= 0)) break; recv = rr; } return recv; } /* The node whose array the k-th parameter of call's block is, by its iterator row: the receiver (YS_RECV: `a.tap { |x| }`, `then`) or the one argument (YS_MEMO given one: `each_with_object(b) { |e, m| }`, `inject(b)`), so a nil stored through the parameter is in that array; -1 for an element, an index, or no row. */ static int block_param_alias(Compiler *c, int call, int k) { const NodeTable *nt = c->nt; int recv = nt_ref(nt, call, "receiver"), blk = nt_ref(nt, call, "block"); if (recv < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return -1; int a = nt_ref(nt, call, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; const IterRow *r = iter_row_of(infer_type(c, recv), nt_str(nt, call, "name"), an); if (!r || k >= r->nyield) return -1; if (r->yield[k] == YS_RECV) return recv; if (r->yield[k] == YS_MEMO && an == 1) return av[0]; return -1; } /* The values a `yield` answers: the tails of the blocks its method is called with (a `next v` too), every one of them -- a full buffer may have left some out, so it grows until they fit. A malloc'd array (the caller frees it), its length into *np. */ static int *yield_value_tails(Compiler *c, int y, int *np) { Scope *ys = comp_scope_of(c, y); int ymi = ys ? (int)(ys - c->scopes) : -1; int *out = NULL, n = 0; if (ymi >= 0) { int sv_ybn = yield_block_tails_next; yield_block_tails_next = 1; for (int max = 64;; max *= 2) { int *grown = realloc(out, (size_t)max * sizeof *out); if (!grown) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } out = grown; n = yield_block_tails(c, ymi, out, max); if (n < max) break; } yield_block_tails_next = sv_ybn; } *np = n; return out; } /* The nil mark is the array's, not a name's: an int/float array expression `v` denotes the array of a local, an ivar, or the ivar an object call hands out (`k.arr`), and a user method's answer or a `yield`'s value is the array its tail reads (`b = same(a)` reaches what `same` returns: its parameter, and through it a). fn sees each such slot's NIE_ bits (`bits`, with `node` the slot's read) and each expression on the way (`bits` NULL, `node` the expression); `tail` is set past a method's or block's tail. The OR of what fn answers. */ typedef int (*ElemSlotFn)(Compiler *c, int *bits, int node, int tail, void *ud); /* What fn is handed for a value the walk cannot follow to its end (a chain past the value-position budget or the depth limit). Asked, it answers yes; marked, it sets nie_gave_up, after which every Integer or Float array of the program counts as one a nil can reach: the proof gives up rather than read an element it lost track of untested. */ #define ELEM_SLOT_UNKNOWN (-2) static int nie_gave_up; static int elem_slots_d(Compiler *c, int v, int tail, int depth, ElemSlotFn fn, void *ud); /* Each value an expression of several arms can be (an if's arms, a begin's body and rescue) is a name of the array; for a method's answer (call >= 0), `self` is the call's receiver. */ typedef struct { Compiler *c; int call, tail, depth; ElemSlotFn fn; void *ud; int r; } SlotArms; static int slot_arm(const VpLeaf *leaf, void *ud) { SlotArms *a = (SlotArms *)ud; if (leaf->kind == VPK_GAVE_UP) a->r |= a->fn(a->c, NULL, ELEM_SLOT_UNKNOWN, a->tail, a->ud); if (leaf->kind != VPK_EXPR) return 0; int v = leaf->node; if (a->call >= 0 && nt_kind(a->c->nt, v) == NK_SelfNode) v = nt_ref(a->c->nt, a->call, "receiver"); if (v >= 0) a->r |= elem_slots_d(a->c, v, a->tail, a->depth + 1, a->fn, a->ud); return 0; } static int elem_slots_arms(Compiler *c, int v, int call, int tail, int depth, ElemSlotFn fn, void *ud) { SlotArms a = { c, call, tail, depth, fn, ud, 0 }; if (v >= 0) for_each_value_position(c->nt, v, VPS_ALL, slot_arm, &a); return a.r; } /* The `next` or `break` nodes (kind k) that leave the block whose body is `id`: not those of a block, lambda, def or loop inside it. Into a grown *out; how many, or -1 when the body is too deep to walk. */ static int block_jumps_d(const NodeTable *nt, int id, NodeKind k, int **out, int *n, int *cap, int depth) { if (id < 0) return 0; if (depth > 200) return -1; NodeKind nk = nt_kind(nt, id); if (nk == k) { if (*n == *cap) { *cap = *cap ? *cap * 2 : 16; int *g = realloc(*out, sizeof(int) * (size_t)*cap); if (!g) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } *out = g; } (*out)[(*n)++] = id; return 0; } if (nk == NK_DefNode || nk == NK_ClassNode || nk == NK_ModuleNode || nk == NK_WhileNode || nk == NK_UntilNode || nk == NK_LambdaNode || nk == NK_ForNode || nk == NK_BlockNode) return 0; int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) if (block_jumps_d(nt, nt_ref_at(nt, id, i), k, out, n, cap, depth + 1) < 0) return -1; int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int an = 0; const int *ids = nt_arr_at(nt, id, i, &an); for (int j = 0; j < an; j++) if (block_jumps_d(nt, ids[j], k, out, n, cap, depth + 1) < 0) return -1; } return 0; } /* Each value a jump of kind k out of block blk hands on is the array's */ static int elem_slots_jumps(Compiler *c, int blk, NodeKind k, int depth, ElemSlotFn fn, void *ud) { const NodeTable *nt = c->nt; int *jv = NULL, n = 0, cap = 0, r = 0; int bb = nt_ref(nt, blk, "body"); if (block_jumps_d(nt, bb, k, &jv, &n, &cap, 0) < 0) r |= fn(c, NULL, ELEM_SLOT_UNKNOWN, 1, ud); for (int i = 0; i < n; i++) r |= elem_slots_d(c, jv[i], 1, depth + 1, fn, ud); free(jv); return r; } /* What one outermost walk has already walked, by value and tail. The walk reaches the same value along many paths -- each arm of a value of several arms, each target of a call, each tail of a yield, a method's tail through every call of it -- and walked it again in full from each, within its depth budget: on a Rails-sized program the boxed-array marking of one round did not end (an hour inside one walk, where the program's analysis took two minutes before #9960's review fixes let the walk follow calls, jumps and yields). A value walked again, no shallower than before, answers what it answered: its marks are made, and the asking walk's answer is the same; one still being walked is a cycle, adding nothing. The entries are dropped when the outermost walk returns. */ typedef struct { int v, tail, depth, res, done; unsigned gen; } EsSeen; static EsSeen *es_seen; static int es_seen_cap, es_seen_n, es_nest; static unsigned es_gen = 1; static EsSeen *es_slot(int v, int tail) { if (es_seen_n * 2 >= es_seen_cap) { int oc = es_seen_cap; EsSeen *old = es_seen; es_seen_cap = oc ? oc * 2 : 1024; es_seen = calloc((size_t)es_seen_cap, sizeof *es_seen); if (!es_seen) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < oc; i++) if (old[i].gen == es_gen) *es_slot(old[i].v, old[i].tail) = old[i]; free(old); } unsigned h = ((unsigned)v * 2654435761u + (unsigned)tail * 40503u) & (unsigned)(es_seen_cap - 1); while (es_seen[h].gen == es_gen && !(es_seen[h].v == v && es_seen[h].tail == tail)) h = (h + 1) & (unsigned)(es_seen_cap - 1); return &es_seen[h]; } /* What the marking walks (mark_slot_fn) have marked since the last reset: by value, tail and the bits asked, the depth the value was reached at. The slots a value denotes are the node table's and the types' to decide, which no mark changes, and a slot's bits only grow: a value marked with the bits once, from no deeper, is marked, and a walk from it again would set no bit, so it is not made. The table above keeps that rule within one outermost walk; this one keeps it across them. mark_nullable_int_locals asks it per round: a round marks the array every boxed argument of every call denotes, and the method tails those reach are the same from each call of the method -- a Rails application's round walked each in full from every call, most of its reconcile phase. Reset as a round starts (em_reset), and by itself when a type or a node has changed since. */ typedef struct { int v, tail, bits, depth; unsigned gen; } EmSeen; static EmSeen *em_seen; static int em_seen_cap, em_seen_n; static unsigned em_gen = 1; static unsigned long em_ty_edits, em_nt_edits; static const NodeTable *em_nt; static void em_reset(void) { em_seen_n = 0; if (++em_gen == 0) { if (em_seen) memset(em_seen, 0, sizeof *em_seen * (size_t)em_seen_cap); em_gen = 1; } } static EmSeen *em_slot(int v, int tail, int bits) { if (em_seen_n * 2 >= em_seen_cap) { int oc = em_seen_cap; EmSeen *old = em_seen; em_seen_cap = oc ? oc * 2 : 4096; em_seen = calloc((size_t)em_seen_cap, sizeof *em_seen); if (!em_seen) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < oc; i++) if (old[i].gen == em_gen) *em_slot(old[i].v, old[i].tail, old[i].bits) = old[i]; free(old); } unsigned h = ((unsigned)v * 2654435761u + (unsigned)tail * 40503u + (unsigned)bits * 97u) & (unsigned)(em_seen_cap - 1); while (em_seen[h].gen == em_gen && !(em_seen[h].v == v && em_seen[h].tail == tail && em_seen[h].bits == bits)) h = (h + 1) & (unsigned)(em_seen_cap - 1); return &em_seen[h]; } static int mark_slot_fn(Compiler *c, int *bits, int node, int tail, void *ud); static int elem_slots_walk(Compiler *c, int v, int tail, int depth, ElemSlotFn fn, void *ud); static int elem_slots_d(Compiler *c, int v, int tail, int depth, ElemSlotFn fn, void *ud) { int r = fn(c, NULL, v, tail, ud); if (v < 0) return r; if (depth >= 16) return r | fn(c, NULL, ELEM_SLOT_UNKNOWN, tail, ud); int mbits = fn == mark_slot_fn ? *(const int *)ud : -1; if (mbits >= 0) { if (em_nt != c->nt || em_nt_edits != c->nt->edits || em_ty_edits != comp_ty_edits) { em_reset(); em_nt = c->nt; em_nt_edits = c->nt->edits; em_ty_edits = comp_ty_edits; } EmSeen *m = em_slot(v, tail, mbits); if (m->gen == em_gen && m->depth <= depth) return r; } EsSeen *s = es_slot(v, tail); if (s->gen == es_gen) { if (s->depth <= depth) return r | (s->done ? s->res : 0); s->depth = depth; s->done = 0; } else { s->gen = es_gen; s->v = v; s->tail = tail; s->depth = depth; s->res = 0; s->done = 0; es_seen_n++; } es_nest++; int w = elem_slots_walk(c, v, tail, depth, fn, ud); es_nest--; if (es_nest == 0) { es_seen_n = 0; if (++es_gen == 0) { memset(es_seen, 0, sizeof *es_seen * (size_t)es_seen_cap); es_gen = 1; } } else { s = es_slot(v, tail); s->res = w; s->done = 1; } /* found again: the table may have grown */ if (mbits >= 0) { EmSeen *m = em_slot(v, tail, mbits); /* (found again: the table may have grown) */ if (m->gen != em_gen) { m->gen = em_gen; m->v = v; m->tail = tail; m->bits = mbits; m->depth = depth; em_seen_n++; } else if (depth < m->depth) m->depth = depth; } return r | w; } static int elem_slots_walk(Compiler *c, int v, int tail, int depth, ElemSlotFn fn, void *ud) { const NodeTable *nt = c->nt; int r = 0; /* `next v` / `break v` hand on v */ if (nt_kind(nt, v) == NK_NextNode || nt_kind(nt, v) == NK_BreakNode) { int ja = nt_ref(nt, v, "arguments"), jn = 0; const int *jav = ja >= 0 ? nt_arr(nt, ja, "arguments", &jn) : NULL; return jn == 1 && nt_kind(nt, jav[0]) != NK_SplatNode ? r | elem_slots_d(c, jav[0], tail, depth + 1, fn, ud) : r; } /* a call with a block also answers what a `break` in the block hands on (`b = xs.each { break a }`) */ { int one, cv = vp_one(nt, v, VPS_NONE, &one) == VP1_NODE ? one : -1; int cb = cv >= 0 && nt_kind(nt, cv) == NK_CallNode ? nt_ref(nt, cv, "block") : -1; if (cb >= 0 && nt_kind(nt, cb) == NK_BlockNode) r |= elem_slots_jumps(c, cb, NK_BreakNode, depth, fn, ud); } /* a value of several arms is each arm's array (`x = c ? a : b`) */ int one; if (vp_one(nt, v, VPS_NONE, &one) == VP1_NODE && vp_one(nt, one, VPS_ALL, &one) == VP1_MANY) return r | elem_slots_arms(c, v, -1, tail, depth, fn, ud); int recv = mutated_array(c, v); if (recv < 0) return r; if (nt_kind(nt, recv) == NK_YieldNode) { int n, *tails = yield_value_tails(c, recv, &n); for (int k = 0; k < n; k++) r |= elem_slots_d(c, tails[k], 1, depth + 1, fn, ud); free(tails); return r; } if (nt_kind(nt, recv) == NK_LocalVariableReadNode) { const char *nm = nt_str(nt, recv, "name"); Scope *sc = nm ? comp_scope_of(c, recv) : NULL; LocalVar *lv = sc ? scope_local(sc, nm) : NULL; if (lv && (lv->type == TY_INT_ARRAY || lv->type == TY_FLOAT_ARRAY)) r |= fn(c, &lv->nullable_int_elem, recv, tail, ud); return r; } /* a builtin iterator that answers its block's value (`a.then { |x| x }`) or its memo (`each_with_object(b) { }`, `inject(b) { }`): the array is the block's tail's, or the argument's */ int vblk = nt_kind(nt, recv) == NK_CallNode ? nt_ref(nt, recv, "block") : -1; if (vblk >= 0 && nt_kind(nt, vblk) == NK_BlockNode && nt_ref(nt, recv, "receiver") >= 0) { int va = nt_ref(nt, recv, "arguments"), van = 0; const int *vav = va >= 0 ? nt_arr(nt, va, "arguments", &van) : NULL; const IterRow *ir = iter_row_of(infer_type(c, nt_ref(nt, recv, "receiver")), nt_str(nt, recv, "name"), van); if (ir && (ir->answer == IA_BLOCKVAL || ir->answer == IA_MEMO)) { if (ir->answer == IA_MEMO && van == 1) r |= elem_slots_d(c, vav[0], 1, depth + 1, fn, ud); /* the block's value: its tail's arms, and each `next v` */ r |= elem_slots_arms(c, nt_ref(nt, vblk, "body"), -1, 1, depth, fn, ud); r |= elem_slots_jumps(c, vblk, NK_NextNode, depth, fn, ud); return r; } } ClassInfo *ci = NULL; int mi = -1, iv = -1; if (nt_kind(nt, recv) == NK_InstanceVariableReadNode) iv = nullable_elem_ivar(c, recv, &ci); else if (nt_kind(nt, recv) == NK_CallNode) iv = object_call_ivar(c, recv, &ci, &mi); if (iv < 0 && nt_kind(nt, recv) == NK_CallNode) { int tg[CPT_MAX], ntg = cplan_targets(c, recv, tg, CPT_MAX); for (int k = 0; k < ntg; k++) r |= elem_slots_arms(c, scope_body_last(c, tg[k]), recv, 1, depth, fn, ud); return r; } /* (an ivar's bits are a byte: fn sees them as an int, written back) */ if (iv >= 0 && (ci->ivar_types[iv] == TY_INT_ARRAY || ci->ivar_types[iv] == TY_FLOAT_ARRAY)) { int bits = ci->ivar_nullable_int_elem[iv]; r |= fn(c, &bits, recv, tail, ud); ci->ivar_nullable_int_elem[iv] = (unsigned char)bits; } return r; } /* Mark the array `v` denotes, through every slot of it: a store marks its own name (NIE_MARKED, which types its reads) and sets NIE_STORED | NIE_REACH, which the slots past a tail get without the typing mark. The other names of the array -- the local a mutation's receiver was assigned from, the argument a parameter was bound to -- are marked through this too (mark_nullable_params_of_call, the alias arms of the fixpoint), or the proved loop read through the unmarked name took the nil for an Integer. 1 when a bit was added. */ static int mark_slot_fn(Compiler *c, int *bits, int node, int tail, void *ud) { (void)c; if (node == ELEM_SLOT_UNKNOWN && !nie_gave_up) { nie_gave_up = 1; return 1; } int add = *(const int *)ud & (tail ? ~NIE_MARKED : ~0); if (!bits || (*bits & add) == add) return 0; *bits |= add; return 1; } static int mark_elem_slot(Compiler *c, int v, int bits) { return elem_slots_d(c, v, 0, 0, mark_slot_fn, &bits); } /* Can a nil be in the array `v` denotes at run time, through any name of it (NIE_REACH, or the typing mark itself)? */ static int reach_slot_fn(Compiler *c, int *bits, int node, int tail, void *ud) { (void)tail; (void)ud; if (node == ELEM_SLOT_UNKNOWN || nie_gave_up) return 1; return bits ? (*bits & (NIE_MARKED | NIE_REACH)) != 0 : nullable_int_elem_expr(c, node, 0); } int elem_nil_reach_expr(Compiler *c, int v) { return elem_slots_d(c, v, 0, 0, reach_slot_fn, NULL); } /* A typed Array whose value lands in a boxed slot -- an element of a mixed literal, a mixed local, ivar or parameter, a push onto a mixed Array -- is the same array behind the box, so a nil a mutation through a boxed receiver stores (`[a, "x"].each { |x| x << h[k] }`) can be in it: each such array, wherever its value comes from (value positions), is marked as one a nil reaches. Only a program with such a mutation marks any (boxed_nil_store), and the mark asks only the proved loop read. */ typedef struct { Compiler *c; int changed, depth; } BoxMark; static int mark_boxed_value_d(Compiler *c, int v, int depth); static int box_mark_leaf(const VpLeaf *leaf, void *ud) { BoxMark *m = (BoxMark *)ud; if (leaf->kind != VPK_EXPR) return 0; /* a `yield` boxes what its blocks answer (`def each = (@got = yield)`); past the depth limit, every array counts as one a nil can reach */ if (nt_kind(m->c->nt, leaf->node) == NK_YieldNode && m->depth >= 8) { if (!nie_gave_up) { nie_gave_up = 1; m->changed = 1; } return 0; } if (nt_kind(m->c->nt, leaf->node) == NK_YieldNode) { int n, *tails = yield_value_tails(m->c, leaf->node, &n); for (int k = 0; k < n; k++) m->changed |= mark_boxed_value_d(m->c, tails[k], m->depth + 1); free(tails); return 0; } /* the value is an array, or another name of one (`xs.each { break a }`, `a.tap { }`): every array it can be */ m->changed |= mark_elem_slot(m->c, leaf->node, NIE_STORED | NIE_REACH); return 0; } static int mark_boxed_value_d(Compiler *c, int v, int depth) { BoxMark m = { c, 0, depth }; if (v >= 0) for_each_value_position(c->nt, v, VPS_ALL, box_mark_leaf, &m); return m.changed; } static int mark_boxed_value(Compiler *c, int v) { return mark_boxed_value_d(c, v, 0); } /* Does the program mutate a boxed receiver in a way that can store a nil element (a typed Array behind the box takes it)? Asked once a round of mark_nullable_int_locals, into nie_boxed_store. */ static int nie_boxed_store; static int boxed_nil_store(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) { int r = nt_ref(nt, id, "receiver"); if (r >= 0 && infer_type(c, r) == TY_POLY && nullable_elem_mutation(c, id, 0)) return 1; } return 0; } static int is_poly_container(TyKind t) { return t == TY_POLY_ARRAY || t == TY_SYM_POLY_HASH || t == TY_STR_POLY_HASH || t == TY_POLY_POLY_HASH; } /* The boxing places a typed Array's value can reach, for mark_boxed_value. */ static int mark_boxed_arrays(Compiler *c) { const NodeTable *nt = c->nt; if (!nie_boxed_store) return 0; int changed = 0; NT_FOREACH_KIND(nt, NK_ArrayNode, id) { if (infer_type(c, id) != TY_POLY_ARRAY) continue; int en = 0; const int *els = nt_arr(nt, id, "elements", &en); for (int k = 0; els && k < en; k++) changed |= mark_boxed_value(c, els[k]); } NT_FOREACH_KIND(nt, NK_HashNode, id) { if (!is_poly_container(infer_type(c, id))) continue; int en = 0; const int *els = nt_arr(nt, id, "elements", &en); for (int k = 0; els && k < en; k++) changed |= mark_boxed_value(c, nt_ref(nt, els[k], "value")); } NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, id) { Scope *ws = comp_scope_of(c, id); LocalVar *wl = ws && nt_str(nt, id, "name") ? scope_local(ws, nt_str(nt, id, "name")) : NULL; if (wl && wl->type == TY_POLY) changed |= mark_boxed_value(c, nt_ref(nt, id, "value")); } NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, id) if (infer_type(c, id) == TY_POLY) changed |= mark_boxed_value(c, nt_ref(nt, id, "value")); NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); int r = nt_ref(nt, id, "receiver"); if (!nm || r < 0 || !is_poly_container(infer_type(c, r)) || !(self_mutator_call(nm) || sp_streq(nm, "[]=") || sp_streq(nm, "store"))) continue; int ca = nt_ref(nt, id, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; for (int k = 0; av && k < an; k++) changed |= mark_boxed_value(c, av[k]); } return changed; } /* One level further in: can an element of a container HELD BY `v` be nil? `v` is the outer container -- an array of arrays, a hash whose values are arrays, or a slot holding one -- so `v[k][j]` reaches the scalar. */ static int nested_elem_nilable(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 8) return 0; if (nt_kind(nt, v) == NK_ArrayNode || nt_kind(nt, v) == NK_HashNode) { int en = 0; const int *els = nt_arr(nt, v, "elements", &en); int hash = nt_kind(nt, v) == NK_HashNode; for (int k = 0; els && k < en; k++) { int e = hash ? nt_ref(nt, els[k], "value") : els[k]; if (nullable_int_elem_expr(c, e, depth + 1)) return 1; } return 0; } if (nt_kind(nt, v) == NK_LocalVariableReadNode) { Scope *sc = comp_scope_of(c, v); const char *nm = nt_str(nt, v, "name"); if (!sc || !nm) return 0; for (int r = lw_shared_first(c, nm, (int)(sc - c->scopes)); r >= 0; r = lw_shared_next(r)) { int id = lw_shared_node(r); if (nt_kind(nt, id) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, nm) || comp_scope_of(c, id) != sc) continue; if (nested_elem_nilable(c, nt_ref(nt, id, "value"), depth + 1)) return 1; } } return 0; } /* One value position of a conditional or a sequence: the array there is asked at the next level. `depth` counts the calls and slots followed, as it did; the structure between two of them costs none. A walk that gave up is a position that may hold a nil. */ typedef struct { Compiler *c; int depth; } ElemArms; static int elem_arm(const VpLeaf *leaf, void *ud) { ElemArms *a = ud; if (leaf->kind == VPK_NIL) return 0; return leaf->kind == VPK_GAVE_UP || nullable_int_elem_expr(a->c, leaf->node, a->depth + 1); } static int nullable_int_elem_expr(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 8) return 0; if (nt_kind(nt, v) == NK_ArrayNode) { int en = 0; const int *els = nt_arr(nt, v, "elements", &en); for (int k = 0; els && k < en; k++) if (nullable_int_value(c, els[k])) return 1; return 0; } if (nt_kind(nt, v) == NK_LocalVariableReadNode) { Scope *s = comp_scope_of(c, v); const char *n = nt_str(nt, v, "name"); LocalVar *lv = n && s ? scope_local(s, n) : NULL; return lv && (lv->nullable_int_elem & NIE_MARKED); } if (nt_kind(nt, v) == NK_InstanceVariableReadNode) { ClassInfo *ci = NULL; int iv = nullable_elem_ivar(c, v, &ci); return iv >= 0 && (ci->ivar_nullable_int_elem[iv] & NIE_MARKED); } /* A conditional's value is one of its arms, as nullable_int_value walks them: the array is marked if any arm's array is. An absent arm is a nil, not an array holding one. */ switch (nt_kind(nt, v)) { case NK_ParenthesesNode: case NK_StatementsNode: case NK_ElseNode: case NK_IfNode: case NK_UnlessNode: case NK_CaseNode: case NK_CaseMatchNode: case NK_BeginNode: case NK_RescueModifierNode: case NK_OrNode: case NK_AndNode: { ElemArms a = { c, depth }; return for_each_value_position(nt, v, VP_AND_LEFT | VP_BEGIN_BOTH | VPS_ALL, elem_arm, &a); } default: break; } if (nt_kind(nt, v) == NK_CallNode) { const char *nm = nt_str(nt, v, "name"); int tail = call_block_tail(c, v); /* the mapped element IS the block's value */ if (nm && tail >= 0 && (sp_streq(nm, "map") || sp_streq(nm, "collect") || sp_streq(nm, "flat_map") || sp_streq(nm, "collect_concat"))) return nullable_int_value(c, tail); int rc = nt_ref(nt, v, "receiver"); /* the values of a typed Integer-valued Hash that may hold a nil (D3b-ii) */ if (nm && (sp_streq(nm, "values") || sp_streq(nm, "values_at") || sp_streq(nm, "fetch_values")) && hash_vals_nullable(c, rc)) return 1; /* `Array.new(n) { ... }` / `Array.new(n, v)` builds its elements here */ if (nm && sp_streq(nm, "new") && rc >= 0 && nt_kind(nt, rc) == NK_ConstantReadNode) { const char *rn = nt_str(nt, rc, "name"); if (rn && sp_streq(rn, "Array")) { if (tail >= 0) return nullable_int_value(c, tail); int ca = nt_ref(nt, v, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; return an >= 2 && nullable_int_value(c, av[1]); } } /* The array-method rules below go by name alone, so they hold only for a receiver that is not a user object: `Maker.new.fill` or `k.to_a` runs the class's own method, whose tail decides (the object-receiver case at the end). Read as Array#fill, the receiver was asked instead and the array the method builds went unmarked. */ int obj_rc = rc >= 0 && ty_is_object(infer_type(c, rc)); /* a slice (`a[1..]`, `a[i, n]`) is a run of the receiver's own elements, as `slice` is; a one-index read is one element, below */ if (!obj_rc && nm && sp_streq(nm, "[]") && slice_read_call(c, v)) return nullable_int_elem_expr(c, rc, depth + 1); if (!obj_rc && elem_preserving_call(nm)) return nullable_int_elem_expr(c, rc, depth + 1); /* `a + b`, `a | b`, `a.union(b)`: the elements of either side, and Kernel#Array(a) is `a` itself */ if (!obj_rc && nm && (sp_streq(nm, "+") || sp_streq(nm, "|") || sp_streq(nm, "union") || (sp_streq(nm, "Array") && rc < 0))) { int ca = nt_ref(nt, v, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (rc >= 0 && nullable_int_elem_expr(c, rc, depth + 1)) return 1; for (int k = 0; av && k < an; k++) if (nullable_int_elem_expr(c, av[k], depth + 1)) return 1; return 0; } /* a mutator answers its receiver, which may hold one already or now */ if (rc >= 0 && !obj_rc && self_mutator_call(nm)) return nullable_elem_mutation(c, v, depth + 1) || nullable_int_elem_expr(c, rc, depth + 1); /* the value is one ELEMENT of the receiver, and that element is itself the container being indexed into (`t[i][j]`, `h[:a][0]`) */ if (!obj_rc && elem_returning_call(nm)) return nested_elem_nilable(c, rc, depth + 1); /* a method returning such an array: its own tail decides */ if (nm && rc < 0) { int mi = call_target(c, v, -1, nm, CT_TOP, NULL); if (mi > 0) return nullable_int_elem_expr(c, scope_body_last(c, mi), depth + 1); } /* the same through an object receiver (`k.arr`): the method the class chain resolves, or the ivar an attr_reader hands out */ if (nm && rc >= 0) { ClassInfo *ci = NULL; int mi = -1; int iv = object_call_ivar(c, v, &ci, &mi); if (mi > 0) return nullable_int_elem_expr(c, scope_body_last(c, mi), depth + 1); return iv >= 0 && (ci->ivar_nullable_int_elem[iv] & NIE_MARKED); } return 0; } return 0; } /* The receiver-side question for codegen: did analyze see a nil stored into this Integer or Float array expression? A marked array's whole-array reads take the path that reads each slot's nil bit outright; an unmarked one asks its run-time may_nil (whether it has a nil bitmap at all) for the nils only the runtime sees (a gap, a conversion). */ int nullable_int_elem_array(Compiler *c, int node) { return nullable_int_elem_expr(c, node, 0); } /* ---- constant arrays of a known length ---- A constant bound to an Array literal with no splat (`LUT = [4, 8, 16]`, also `[..].freeze` and `[..].map { }`) that nothing can change has the literal's length: a read at an index proven below it is no miss, and a multiple assignment from it fills that many targets. Constants are interned by their last name (`DMC::LUT` and a `LUT` elsewhere share one), so every definition of the name is asked, and every read of the name must be the receiver of a call that leaves the array as it is, or a value position of the value a multiple assignment takes apart. */ typedef struct { const char *name; int len; int nlit; int lit[8]; int blk[8]; } CArr; static CArr *carr_tab; static int carr_n, carr_cap, carr_count = -1; static int carr_lit(const NodeTable *nt, int v, int *blk) { *blk = -1; if (v >= 0 && nt_kind(nt, v) == NK_CallNode && nt_str(nt, v, "name") && nt_ref(nt, v, "arguments") < 0) { const char *nm = nt_str(nt, v, "name"); int b = nt_ref(nt, v, "block"); if (sp_streq(nm, "freeze") && b < 0) v = nt_ref(nt, v, "receiver"); else if ((sp_streq(nm, "map") || sp_streq(nm, "collect")) && b >= 0 && nt_kind(nt, b) == NK_BlockNode) { *blk = b; v = nt_ref(nt, v, "receiver"); } } if (v < 0 || nt_kind(nt, v) != NK_ArrayNode) return -1; int en = 0; const int *ev = nt_arr(nt, v, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, ev[e]) == NK_SplatNode) return -1; return v; } static int carr_is_read(const NodeTable *nt, int n, const char *name) { NodeKind k = n >= 0 ? nt_kind(nt, n) : NK_NONE; return (k == NK_ConstantReadNode || k == NK_ConstantPathNode) && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), name); } /* the reads a multiple assignment's value takes apart: each value position of the value, by the iterator's `if` and `unless` shapes, that reads the constant. A walk that gave up leaves the marks made before it, and the reads it did not reach unmarked, which fail the constant's check. */ typedef struct { const NodeTable *nt; const char *name; unsigned char *ok; } CarrMasgn; static int carr_masgn_leaf(const VpLeaf *leaf, void *ud) { CarrMasgn *m = ud; if (leaf->kind == VPK_EXPR && carr_is_read(m->nt, leaf->node, m->name)) m->ok[leaf->node] = 1; return 0; } static void carr_mark_masgn_value(const NodeTable *nt, int v, const char *name, unsigned char *ok) { CarrMasgn m = { nt, name, ok }; for_each_value_position(nt, v, VPS_IF | VPS_UNLESS, carr_masgn_leaf, &m); } static int carr_keeps(const char *nm) { static const char *const K[] = { "[]", "at", "slice", "first", "last", "size", "length", "count", "empty?", "any?", "all?", "none?", "include?", "index", "find_index", "each", "each_with_index", "each_index", "map", "collect", "select", "filter", "reject", "find", "detect", "min", "max", "sum", "inject", "reduce", "sort", "sort_by", "reverse", "each_slice", "each_cons", "zip", "join", "inspect", "to_s", "hash", "==", "!=", "eql?", "frozen?", "dig", "fetch", "values_at", "take", "drop", "uniq", "flatten", "+", "-", "*", "&", "|", NULL }; return str_in(nm, K); } static const CArr *const_array(Compiler *c, const char *name) { const NodeTable *nt = c->nt; if (!name) return NULL; if (carr_count != nt->count) { carr_n = 0; carr_count = nt->count; } for (int i = 0; i < carr_n; i++) if (sp_streq(carr_tab[i].name, name)) return carr_tab[i].len >= 0 ? &carr_tab[i] : NULL; CArr r; memset(&r, 0, sizeof r); r.name = name; r.len = -1; int len = -1, bad = 0; for (int id = 0; id < nt->count && !bad; id++) { NodeKind k = nt_kind(nt, id); const char *wn = NULL; int v = -1; if (k == NK_ConstantWriteNode) { wn = nt_str(nt, id, "name"); v = nt_ref(nt, id, "value"); } else if (k == NK_ConstantPathWriteNode) { int tg = nt_ref(nt, id, "target"); wn = tg >= 0 ? nt_str(nt, tg, "name") : NULL; v = nt_ref(nt, id, "value"); } else if (k == NK_ConstantOrWriteNode || k == NK_ConstantAndWriteNode || k == NK_ConstantOperatorWriteNode || k == NK_ConstantTargetNode || k == NK_ConstantPathTargetNode) wn = nt_str(nt, id, "name"); else if (k == NK_ConstantPathOrWriteNode || k == NK_ConstantPathAndWriteNode || k == NK_ConstantPathOperatorWriteNode) { int tg = nt_ref(nt, id, "target"); wn = tg >= 0 ? nt_str(nt, tg, "name") : NULL; } else if (k == NK_CallNode && nt_str(nt, id, "name") && (sp_streq(nt_str(nt, id, "name"), "const_set") || sp_streq(nt_str(nt, id, "name"), "remove_const"))) bad = 1; if (!wn || !sp_streq(wn, name)) continue; int blk, lit = (k == NK_ConstantWriteNode || k == NK_ConstantPathWriteNode) ? carr_lit(nt, v, &blk) : -1; if (lit < 0 || r.nlit >= 8) { bad = 1; break; } int en = 0; nt_arr(nt, lit, "elements", &en); if (len < 0 || en < len) len = en; r.lit[r.nlit] = lit; r.blk[r.nlit] = blk; r.nlit++; } if (!bad && r.nlit > 0) { unsigned char *ok = calloc((size_t)nt->count + 1, 1); if (!ok) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } NT_FOREACH_KIND(nt, NK_CallNode, cl) { int rc = nt_ref(nt, cl, "receiver"); if (carr_is_read(nt, rc, name) && nt_str(nt, cl, "name") && carr_keeps(nt_str(nt, cl, "name")) && !nt_call_has_block_arg(nt, cl)) ok[rc] = 1; } NT_FOREACH_KIND(nt, NK_MultiWriteNode, mw) carr_mark_masgn_value(nt, nt_ref(nt, mw, "value"), name, ok); for (int id = 0; id < nt->count && !bad; id++) if (carr_is_read(nt, id, name) && !ok[id]) bad = 1; free(ok); } if (!bad && r.nlit > 0) r.len = len; if (carr_n == carr_cap) { carr_cap = carr_cap ? carr_cap * 2 : 16; carr_tab = realloc(carr_tab, sizeof(CArr) * (size_t)carr_cap); if (!carr_tab) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } carr_tab[carr_n++] = r; return r.len >= 0 ? &carr_tab[carr_n - 1] : NULL; } /* `CONST[i]` with i proven in 0...len (or -len...0): a literal, or `x & K` with 0 <= K < len */ static int const_array_index_in_range(Compiler *c, int recv, int idx) { const NodeTable *nt = c->nt; if (!carr_is_read(nt, recv, nt_str(nt, recv, "name"))) return 0; const CArr *ca = const_array(c, nt_str(nt, recv, "name")); if (!ca || idx < 0) return 0; if (nt_kind(nt, idx) == NK_IntegerNode && !nt_str(nt, idx, "bigval")) { long long k = (long long)nt_int(nt, idx, "value", 0); return k < ca->len && k >= -(long long)ca->len; } if (nt_kind(nt, idx) == NK_CallNode && nt_str(nt, idx, "name") && sp_streq(nt_str(nt, idx, "name"), "&") && infer_type(c, nt_ref(nt, idx, "receiver")) == TY_INT) { int a = nt_ref(nt, idx, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an == 1 && nt_kind(nt, av[0]) == NK_IntegerNode && !nt_str(nt, av[0], "bigval")) { long long k = (long long)nt_int(nt, av[0], "value", 0); return k >= 0 && k < ca->len; } } return 0; } /* Is position `pos` of a multiple assignment's value, read from the constant Array `value` of a proven length, within it and no nil? The element is then no miss and holds no nil (masgn_const_pos_may_nil: a literal's nil, a nil a `map` block can answer). A position from the end is not asked. */ static int masgn_const_pos_may_nil(Compiler *c, int v, int pos); int masgn_const_elem_in_range(Compiler *c, int value, long long pos) { const NodeTable *nt = c->nt; const char *nm = value >= 0 ? nt_str(nt, value, "name") : NULL; if (!nm || !carr_is_read(nt, value, nm) || pos < 0) return 0; const CArr *ca = const_array(c, nm); return ca && pos < ca->len && !masgn_const_pos_may_nil(c, value, (int)pos); } /* Can position `pos` of a multiple assignment's value be nil, where the value is a constant Array of a known length, or a conditional or a sequence whose value positions are such (1 when any is anything else)? An element past the length is nil; one within it is the literal's element, or the value the `map` block gives it. Every value position of the value, by the iterator's `if` and `unless` shapes, is asked; a missing one, or a walk that gave up, is nil. */ typedef struct { Compiler *c; int pos; } MasgnPos; static int masgn_leaf_may_nil(const VpLeaf *leaf, void *ud) { MasgnPos *m = ud; Compiler *c = m->c; const NodeTable *nt = c->nt; int v = leaf->node; if (leaf->kind != VPK_EXPR || !carr_is_read(nt, v, nt_str(nt, v, "name"))) return 1; const CArr *ca = const_array(c, nt_str(nt, v, "name")); if (!ca || m->pos >= ca->len) return 1; for (int i = 0; i < ca->nlit; i++) { if (ca->blk[i] >= 0) { int bd = nt_ref(nt, ca->blk[i], "body"), bn = 0; const int *bs = bd >= 0 ? nt_arr(nt, bd, "body", &bn) : NULL; if (!bs || bn == 0 || nt_kind(nt, bs[bn - 1]) == NK_NilNode || nullable_int_value(c, bs[bn - 1])) return 1; continue; } int en = 0; const int *ev = nt_arr(nt, ca->lit[i], "elements", &en); if (m->pos >= en || nt_kind(nt, ev[m->pos]) == NK_NilNode || nullable_int_value(c, ev[m->pos])) return 1; } return 0; } static int masgn_const_pos_may_nil(Compiler *c, int v, int pos) { MasgnPos m = { c, pos }; return for_each_value_position(c->nt, v, VPS_IF | VPS_UNLESS, masgn_leaf_may_nil, &m); } /* An Array literal with an element and no splat: it is never empty. */ int array_literal_filled(const NodeTable *nt, int v) { if (nt_kind(nt, v) != NK_ArrayNode) return 0; int en = 0; const int *el = nt_arr(nt, v, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, el[e]) == NK_SplatNode) return 0; return en > 0; } /* A call that answers one element of its receiver, or nil when there is none (`a[i]`, `h[k]`, `[].max`, `a.find { }`), plus `Integer(s, exception: false)`. */ static int elem_miss_call(Compiler *c, int v) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, v, "name"); if (!nm) return 0; int recv = nt_ref(nt, v, "receiver"); int args = nt_ref(nt, v, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; int blk = nt_ref(nt, v, "block"); if (recv < 0) { if ((is_numeric_class_name(nm)) && argc >= 2 && nt_argv_kwh(nt, argv, argc) >= 0) return 1; return 0; } /* the element a multiple assignment hands one of its targets, read back only as type evidence (desugar_masgn_store_evidence): it is no more a miss there than it is for the assignment's local and ivar targets, except past the end of a constant Array of a known length, which leaves nil in the slot the target stores through */ if (nt_int(nt, v, "masgn_elem", LLONG_MIN) != LLONG_MIN) { if (argc != 1) return 0; /* a receiver typed at run time dispatches to the writer of whichever class it holds, and the element may be missing from any Array: the nil is evidence for each such writer's parameter, which would otherwise be a plain Integer that takes 0 */ if (!carr_is_read(nt, recv, nt_str(nt, recv, "name"))) { long long tr = nt_int(nt, v, "masgn_recv", -1); return tr >= 0 && infer_type(c, (int)tr) == TY_POLY; } /* a target after the splat, counted from the end: the j-th of the rights takes the element at max(len - rights + j, lefts + j), nil when that is past the end, which is when lefts + j >= len */ const CArr *ca = const_array(c, nt_str(nt, recv, "name")); long long pos = nt_int(nt, v, "masgn_elem", 0); if (ca && pos < 0) return nt_int(nt, v, "masgn_left", 0) + pos + nt_int(nt, v, "masgn_right", 0) >= ca->len; return !const_array_index_in_range(c, recv, argv[0]); } /* Integer#[] reads a bit (0 or 1), never nil */ if (sp_streq(nm, "[]")) { TyKind brt = infer_type(c, recv); if (brt == TY_INT || brt == TY_BIGINT) return 0; } /* (a block given to them goes unused, as in CRuby: `a.at(i, &b)` misses as `a.at(i)` does) */ if (sp_streq(nm, "[]") || sp_streq(nm, "at") || sp_streq(nm, "slice")) return argc == 1 && (blk < 0 || call_unused_block(c, v) >= 0) && !const_array_index_in_range(c, recv, argv[0]); if (sp_streq(nm, "dig")) return argc >= 1; if (is_element_pick(nm)) return argc == 0 && blk < 0; /* a literal with an element and no splat always has an extreme. The nil of an element is a separate question (nullable_int_elem_expr). */ if (is_minmax_query(nm)) return argc == 0 && !(blk < 0 && array_literal_filled(nt, recv)); if (is_find_alias(nm)) return blk >= 0; /* a fold without an initial value answers nil on an empty receiver: `inject(:+)` and `inject { |s, x| ... }`, not `inject(0) { ... }` */ if (is_reduce_alias(nm)) return argc == 0 || (argc == 1 && blk < 0); return 0; } /* A call of one of the names nil answers and neither Integer nor Float has, on a receiver of type rt: to_a, to_h and =~ (with !~, which is =~ negated), and on a Float the boolean operators &, | and ^. On a slot that can hold nil it has a value -- CRuby's [], {}, nil and a boolean -- rather than only the NoMethodError the receiver's class gives, so it is typed boxed. Integer's own &, | and ^ are typed Integer, a slot nil's boolean cannot ride in, so they are not here. */ int scalar_nil_only_call(Compiler *c, int id, TyKind rt) { const NodeTable *nt = c->nt; if (id < 0 || nt_kind(nt, id) != NK_CallNode) return 0; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || recv < 0 || nt_ref(nt, id, "block") >= 0) return 0; if (nt_safe_nav(nt, id)) return 0; if (rt != TY_INT && rt != TY_FLOAT) return 0; int args = nt_ref(nt, id, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (nt_argv_has(nt, argv, argc, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK)) return 0; int hit = argc == 0 ? (sp_streq(nm, "to_a") || sp_streq(nm, "to_h")) : argc == 1 ? (is_match_operator(nm) || (rt == TY_FLOAT && is_bit_op(nm))) : 0; return hit; } /* ... on a receiver that can hold nil: the call answers nil's value */ int nullable_scalar_nil_only_call(Compiler *c, int id) { int recv = id >= 0 && nt_kind(c->nt, id) == NK_CallNode ? nt_ref(c->nt, id, "receiver") : -1; return recv >= 0 && scalar_nil_only_call(c, id, c->ntype[recv]) && nullable_int_value(c, recv); } /* Can this expression leave nil in an int slot? */ /* Whether an unconditional write of ivar `ivn` is among the top-level statements of class k's initialize, or of the initialize it inherits. */ /* Does the subtree at `n` read `ivn` (a nested def or class excluded)? */ static int ivs_subtree_reads(const NodeTable *nt, int n, const char *ivn, int depth) { if (n < 0 || depth > 400) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode) return 0; if ((k == NK_InstanceVariableReadNode || k == NK_InstanceVariableOrWriteNode || k == NK_InstanceVariableAndWriteNode || k == NK_InstanceVariableOperatorWriteNode) && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), ivn)) return 1; int nr = nt_num_refs(nt, n); for (int i = 0; i < nr; i++) if (ivs_subtree_reads(nt, nt_ref_at(nt, n, i), ivn, depth + 1)) return 1; int na = nt_num_arrs(nt, n); for (int i = 0; i < na; i++) { int an = 0; const int *ids = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (ivs_subtree_reads(nt, ids[j], ivn, depth + 1)) return 1; } return 0; } /* Statement `w` of a body the constructor runs on every path writes `ivn` (1), certainly does not let anything read it first (0), or may read it before any write (-1): a write, `super` into a parent initialize that writes it, or a call to one of the class's own methods (`reset`) whose own body writes it before reading it. */ static int ivs_body_writes(Compiler *c, int k, int defcls, int body, const char *ivn, int depth); static int ivs_stmt_writes(Compiler *c, int k, int defcls, int w, const char *ivn, int depth) { const NodeTable *nt = c->nt; /* `def initialize = (@v = 7; ...)`: the parenthesized sequence, in order */ if (nt_kind(nt, w) == NK_ParenthesesNode) return ivs_body_writes(c, k, defcls, nt_ref(nt, w, "body"), ivn, depth); /* `@a = @b = 0` assigns both */ for (int x = w; x >= 0 && nt_kind(nt, x) == NK_InstanceVariableWriteNode; x = nt_ref(nt, x, "value")) { const char *wn = nt_str(nt, x, "name"); if (wn && sp_streq(wn, ivn)) return 1; } /* a `super` statement runs the parent's initialize, every path of it */ if ((nt_kind(nt, w) == NK_SuperNode || nt_kind(nt, w) == NK_ForwardingSuperNode) && defcls >= 0 && c->classes[defcls].parent >= 0 && c->classes[defcls].parent != defcls && ivar_assigned_in_initialize(c, c->classes[defcls].parent, ivn)) return 1; if (nt_kind(nt, w) == NK_MultiWriteNode) { int ln = 0; const int *ls = nt_arr(nt, w, "lefts", &ln); for (int j = 0; j < ln; j++) if (nt_kind(nt, ls[j]) == NK_InstanceVariableTargetNode && nt_str(nt, ls[j], "name") && sp_streq(nt_str(nt, ls[j], "name"), ivn)) return 1; } /* an unconditional call of the object's own method (`reset`, `self.reset`) with no block: its body, as this class resolves the name */ if (nt_kind(nt, w) == NK_CallNode && nt_ref(nt, w, "block") < 0 && depth < 4) { int r = nt_ref(nt, w, "receiver"); const char *nm = nt_str(nt, w, "name"); if (nm && (r < 0 || nt_kind(nt, r) == NK_SelfNode) && !sp_streq(nm, "initialize")) { int ca = nt_ref(nt, w, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; for (int j = 0; av && j < an; j++) if (ivs_subtree_reads(nt, av[j], ivn, 0)) return -1; int mdef = -1; int mi = call_target(c, w, k, nm, 0, &mdef); if (mi >= 0 && !c->scopes[mi].is_cmethod && c->scopes[mi].body >= 0) return ivs_body_writes(c, k, mdef, c->scopes[mi].body, ivn, depth + 1); } } return 0; } static int ivs_body_writes(Compiler *c, int k, int defcls, int body, const char *ivn, int depth) { const NodeTable *nt = c->nt; if (body < 0) return 0; int n = 0; const int *st = nt_kind(nt, body) == NK_StatementsNode ? nt_arr(nt, body, "body", &n) : &body; if (nt_kind(nt, body) != NK_StatementsNode) n = 1; for (int i = 0; i < n; i++) { int r = ivs_stmt_writes(c, k, defcls, st[i], ivn, depth); if (r != 0) return r; /* inside a called method only: a statement that reads the ivar (or whose effect is unknown) ahead of its write sees the nil */ if (depth > 0 && ivs_subtree_reads(nt, st[i], ivn, 0)) return -1; } return 0; } /* Memo per (class, ivar name): the walk follows calls into methods. */ static struct { int k; char *ivn; int v; } *iai_memo = NULL; static int iai_cap = 0, iai_used = 0, iai_ncls = -1, iai_nsc = -1; static const Compiler *iai_owner = NULL; int ivar_assigned_in_initialize(Compiler *c, int k, const char *ivn) { if (!ivn) return 0; if (iai_owner != c || iai_ncls != c->nclasses || iai_nsc != c->nscopes) { for (int i = 0; i < iai_cap; i++) free(iai_memo[i].ivn); free(iai_memo); iai_memo = NULL; iai_cap = iai_used = 0; iai_owner = c; iai_ncls = c->nclasses; iai_nsc = c->nscopes; } unsigned h = (unsigned)k * 2654435761u; for (const char *p = ivn; *p; p++) h = (h ^ (unsigned char)*p) * 16777619u; if (iai_cap) { for (unsigned i = h & (unsigned)(iai_cap - 1); iai_memo[i].ivn; i = (i + 1) & (unsigned)(iai_cap - 1)) if (iai_memo[i].k == k && strcmp(iai_memo[i].ivn, ivn) == 0) return iai_memo[i].v; } int defcls = -1; int mi = comp_method_in_chain(c, k, "initialize", &defcls); int v = mi >= 0 && ivs_body_writes(c, k, defcls, c->scopes[mi].body, ivn, 0) == 1; if (iai_used * 2 >= iai_cap) { int ncap = iai_cap ? iai_cap * 2 : 256; void *nm2 = calloc((size_t)ncap, sizeof *iai_memo); if (!nm2) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } __typeof__(iai_memo) nt2 = nm2; for (int i = 0; i < iai_cap; i++) if (iai_memo[i].ivn) { unsigned hh = (unsigned)iai_memo[i].k * 2654435761u; for (const char *p = iai_memo[i].ivn; *p; p++) hh = (hh ^ (unsigned char)*p) * 16777619u; unsigned j = hh & (unsigned)(ncap - 1); while (nt2[j].ivn) j = (j + 1) & (unsigned)(ncap - 1); nt2[j] = iai_memo[i]; } free(iai_memo); iai_memo = nt2; iai_cap = ncap; } unsigned i = h & (unsigned)(iai_cap - 1); while (iai_memo[i].ivn) i = (i + 1) & (unsigned)(iai_cap - 1); iai_memo[i].k = k; iai_memo[i].ivn = strdup(ivn); iai_memo[i].v = v; if (!iai_memo[i].ivn) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } iai_used++; return v; } /* An argument whose boxing must allow nil though its typed reads need no nil check: an int ivar read nothing has to assign first, or a parameter already bound from one. */ int box_nullable_arg(Compiler *c, int v) { return repr_box_nullable_arg(c, v); /* repr.c */ } /* The class variable a read or write of `@@x` names, resolved as infer_type resolves it: the method's class, a class body's own class, else Toplevel. -1 when there is none. */ static int cvar_slot(Compiler *c, int id, ClassInfo **cio) { const char *nm = nt_str(c->nt, id, "name"); Scope *s = comp_scope_of(c, id); int cid = s ? s->class_id : -1; if (cid < 0 && c->node_cbody && id < c->node_cap) cid = c->node_cbody[id]; if (cid < 0) cid = c->toplevel_class; if (cid < 0 || cid >= c->nclasses || !nm) return -1; *cio = &c->classes[cid]; return comp_cvar_index(*cio, nm); } /* The member slot `name` names on class cid's Struct or Data, which its generated constructor and accessors read and write rather than an ivar node or an attr_reader: -1 when the chain has no such member. */ static int struct_member_slot(Compiler *c, int cid, const char *name, ClassInfo **out) { char ivb[300]; if (!name) return -1; snprintf(ivb, sizeof ivb, "@%s", name); for (; cid >= 0 && cid < c->nclasses; cid = c->classes[cid].parent) { if (!c->classes[cid].is_struct) continue; int iv = comp_ivar_index(&c->classes[cid], ivb); if (iv >= 0) { *out = &c->classes[cid]; return iv; } } return -1; } /* Nil narrowing: flow-sensitive non-nil facts for single reads. A local is typed once for its whole life, so one nil written to it anywhere makes LocalVar.nullable_int 1, and every read of it then pays the nil flag's test, its `_or_nil` boxing, its arm in a type test -- and hands the mark on to every local, parameter and block parameter it is copied into. This pass proves single READS non-nil, and nullable_int_value answers 0 for them, so every consumer of that question follows. The facts come from: - a guard on the variable itself: `if x`, `x.nil?` and `x == nil` in their false arm, `!`, `x && ...`, `x.nil? || ...`, `return/next/break/raise ... if x.nil?`; - a write: `x = v` with v non-nil, `x op= v` (the arithmetic raised on nil), `x ||= v` with v non-nil; - an operator that raised on nil: after `x < y` or `x + y` on an Integer or Float, x and y (nn_operands_nonnil); - a flag set together with the variable's non-nil writes (`found = true` beside `lo = x`, every nil written to lo beside `found = false`): under `if found`, lo is non-nil (nn_corr); - an index read `a[i]` inside `while i < a.size` with i >= 0 (or `while i < n` after `n = a.size`), of an Integer or Float array no nil can be in at run time (the static nil mark, elem_nil_reach_expr). A builtin that can only grow an array (nn_growth_call) keeps the bound. The walk follows statement lists in evaluation order (Spinel has no CFG): a condition yields the facts of its truthy and its falsy arm, and arms meet by intersection; `return`/`next`/`break`/`raise` make the rest of a list unreachable; a loop first drops what its body writes, so its entry facts hold throughout it; a block or lambda body starts with none. A call drops the facts a closure it may run can break (nn_exposed). Anything the walk does not follow keeps no facts inside and replays its kills outside. */ #define NN_MAXF 24 #define NN_MAXR 6 /* `i < a.size`: i a local, a an array slot (nn_slot_of) */ typedef struct { LocalVar *i; int slot; } NNRel; /* `n = a.size`: n is no more than the length of the array in slot, while neither is written and no call that can shrink an array has run (a growth-only builtin keeps it: nn_growth_call) */ typedef struct { LocalVar *n; int slot; } NNLen; typedef struct { int bot; /* unreachable: every fact holds */ int n; LocalVar *v[NN_MAXF]; /* locals known non-nil */ int nn; LocalVar *nonneg[NN_MAXF]; /* integer locals known >= 0 */ int nr; NNRel r[NN_MAXR]; int nl; NNLen len[NN_MAXR]; /* `n = a.size` locals */ int ni; int iv[NN_MAXF]; /* self's ivars (nn_ivar) known non-nil */ } NNF; static Compiler *nn_c; static unsigned char *nn_nonnil; /* per node: a local read proven non-nil */ static unsigned char *nn_inb; /* per node: an `a[i]` proven to read an element */ static int *nn_cand; /* per node: an in-bounds `a[i]`, slot + 1 */ static unsigned char *nn_wrok; /* per node: an `a[i] = v` whose index is in range */ static unsigned char *nn_ivwok; /* per node: an `@x = v` of a non-nil v where @x was non-nil */ static int nn_cap; static int nn_ready; /* the bitmaps describe the current state */ static int nn_busy; /* the facts are being recomputed */ static int nn_inferring; /* recomputed during type inference */ static unsigned nn_epoch = 1, nn_done_epoch; static void nn_compute_now(Compiler *c, int reuse); /* During inference the facts are recomputed lazily, once per round, on the first question asked of them; the nullable marking recomputes them at the start of each of its rounds (nn_compute). */ static int nn_fresh(void) { if (nn_busy) return 0; if (nn_inferring && nn_done_epoch != nn_epoch && nn_c) nn_compute_now(nn_c, 0); return nn_ready; } /* The two questions the rest of the compiler asks of the facts. Each fact used is a decision (src/decide.c), keyed by the read's position and, for a local, the name it was written with: a refused one leaves the read as possibly nil, which is what it was before the fact was proven. */ static int nn_read_nonnil(Compiler *c, int id, const char *name) { if (!(id >= 0 && nn_fresh() && id < nn_cap && nn_nonnil[id])) return 0; if (!g_decide_on) return 1; char written[300]; snprintf(written, sizeof written, "%.*s", (int)block_param_written_len(c, name), name); return decide_node(c->nt, id, "nn-read", written); } static int nn_index_inbounds(Compiler *c, int id) { return id >= 0 && nn_fresh() && id < nn_cap && nn_inb[id] && decide_node(c->nt, id, "nn-inb", NULL); } /* `a[i]` on an array of pointers that the facts show reads within it: an element, never the nil past the end (whether the element itself can be nil is the nil fact's NFW_ELEM_NIL, asked apart) */ int an_elem_read_in_bounds(Compiler *c, int id) { return id >= 0 && nn_fresh() && id < nn_cap && nn_cand[id] && ty_is_ptr_array(comp_ntype(c, nt_ref(c->nt, id, "receiver"))) && decide_node(c->nt, id, "nn-inb-ptr", NULL); } /* An ivar read after a use that raised on its nil, with no write of it and no call that could make one between (nn_visit): its nil was tested */ static int nn_ivar_read_nonnil(Compiler *c, int id) { return id >= 0 && nn_fresh() && id < nn_cap && nn_nonnil[id] && decide_node(c->nt, id, "nn-ivread", nt_str(c->nt, id, "name")); } /* `@x = v`: v is non-nil and so was @x, so its nil byte is already clear */ int nn_ivar_write_keeps_clear(Compiler *c, int id) { return id >= 0 && nn_fresh() && id < nn_cap && nn_ivwok[id] && decide_node(c->nt, id, "nn-ivwok", nt_str(c->nt, id, "name")); } /* nullable_int_value as the slot's own marks answer it, without the facts: what the variable can hold anywhere, rather than at this read. */ int nullable_int_value_raw(Compiler *c, int id) { int sv = nn_busy; nn_busy = 1; int r = nullable_int_value(c, id); nn_busy = sv; return r; } /* ---- per-variable summary ---- */ typedef struct { LocalVar *lv; int owner; /* outermost context it occurs in: a block node, or -1 */ int owner_set; int *w; int nw, cw; /* the writes that can leave it nil */ int *occ; int nocc, cocc; /* every node naming it */ int bad_flag; /* as a flag: a write other than `= true` / `= false` */ int has_true; /* as a flag: some `= true` */ int param; /* a method or block parameter */ int iter_index; /* a block parameter an index iterator binds (nn_iter_index) */ LocalVar **corr; int ncorr, ccorr; /* as a flag: the locals it vouches for */ int cache_ctx, cache_val; int allw_ctx; /* the one context every write is in, or -2 */ int allw_set; } NNVar; static NNVar *nn_vars; static int nn_nvars, nn_cvars; static int *nn_vhash; static int nn_vhcap; /* ivar slots, by name across every class: a block run by instance_exec, or a method a subclass inherits, reads the name on another class's object */ typedef struct { const char *name; int *occ; int nocc, cocc; } NNIvar; static NNIvar *nn_ivars; static int nn_nivars, nn_civars; static int *nn_ihash; static int nn_ihcap; /* per node, from the pre-walk */ static int *nn_par; /* parent node, or -1 */ static int *nn_ctx; /* innermost block/lambda node, -1 for the frame */ static int *nn_seq; /* visit order */ static int *nn_send; /* last seq inside the node */ static int *nn_ctxpar; /* (block nodes) the enclosing context */ static int *nn_ctxdep; /* (block nodes) nesting depth */ static int *nn_stlist; /* (statements) the StatementsNode holding it */ static int *nn_stidx; /* (statements) its index there */ static int *nn_loopout; /* outermost loop around the node in its own context */ static unsigned char *nn_retry; /* (a block, or a frame's root) a retry or redo in it */ static int *nn_frame; /* the root of the frame the node is in */ static int nn_cur_frame; static unsigned char *nn_jump; /* (blocks) a next or break of its own; (loops) a redo */ static unsigned char *nn_defbody; /* a method's body: 1, initialize's: 2 */ static const char **nn_attrs; static int nn_nattrs, nn_cattrs; /* names attr_* hands out */ static int nn_seqc; static int nn_no_ivar_slots; /* reflection or a reopened Array: no ivar is nil-free */ static int nn_no_ivar_facts; /* ... or code that can run between two statements: no ivar fact */ static int nn_no_local_slots; /* binding's local_variable_set, or a reopened Array */ static int nn_numeric_reopened; /* a class a number's or nil's operator may come from is reopened */ static unsigned nn_user_named; /* bits of nn_named[] the program defines a method for */ static LocalVar *nn_local_of(Compiler *c, int id) { const char *nm = nt_str(c->nt, id, "name"); Scope *s = nm ? comp_scope_of(c, id) : NULL; return s ? scope_local(s, nm) : NULL; } static int nn_var(LocalVar *lv) { if (!lv) return -1; if (nn_nvars * 2 >= nn_vhcap) { int ncap = nn_vhcap ? nn_vhcap * 2 : 256; int *nh = malloc(sizeof(int) * (size_t)ncap); for (int k = 0; k < ncap; k++) nh[k] = -1; for (int k = 0; k < nn_nvars; k++) { unsigned h = (unsigned)(((uintptr_t)nn_vars[k].lv >> 4) * 2654435761u) & (unsigned)(ncap - 1); while (nh[h] >= 0) h = (h + 1) & (unsigned)(ncap - 1); nh[h] = k; } free(nn_vhash); nn_vhash = nh; nn_vhcap = ncap; } unsigned h = (unsigned)(((uintptr_t)lv >> 4) * 2654435761u) & (unsigned)(nn_vhcap - 1); while (nn_vhash[h] >= 0) { if (nn_vars[nn_vhash[h]].lv == lv) return nn_vhash[h]; h = (h + 1) & (unsigned)(nn_vhcap - 1); } if (nn_nvars == nn_cvars) { nn_cvars = nn_cvars ? nn_cvars * 2 : 128; nn_vars = realloc(nn_vars, sizeof(NNVar) * (size_t)nn_cvars); } NNVar *v = &nn_vars[nn_nvars]; memset(v, 0, sizeof *v); v->lv = lv; v->owner = -1; v->cache_ctx = -3; v->allw_ctx = -2; nn_vhash[h] = nn_nvars; return nn_nvars++; } static unsigned nn_strhash(const char *p) { unsigned h = 2166136261u; for (; *p; p++) { h ^= (unsigned char)*p; h *= 16777619u; } return h; } static int nn_ivar(const char *name) { if (!name) return -1; if (nn_nivars * 2 >= nn_ihcap) { int ncap = nn_ihcap ? nn_ihcap * 2 : 64; int *nh = malloc(sizeof(int) * (size_t)ncap); for (int k = 0; k < ncap; k++) nh[k] = -1; for (int k = 0; k < nn_nivars; k++) { unsigned h = nn_strhash(nn_ivars[k].name) & (unsigned)(ncap - 1); while (nh[h] >= 0) h = (h + 1) & (unsigned)(ncap - 1); nh[h] = k; } free(nn_ihash); nn_ihash = nh; nn_ihcap = ncap; } unsigned h = nn_strhash(name) & (unsigned)(nn_ihcap - 1); while (nn_ihash[h] >= 0) { if (sp_streq(nn_ivars[nn_ihash[h]].name, name)) return nn_ihash[h]; h = (h + 1) & (unsigned)(nn_ihcap - 1); } nn_ihash[h] = nn_nivars; if (nn_nivars == nn_civars) { nn_civars = nn_civars ? nn_civars * 2 : 32; nn_ivars = realloc(nn_ivars, sizeof(NNIvar) * (size_t)nn_civars); } memset(&nn_ivars[nn_nivars], 0, sizeof(NNIvar)); nn_ivars[nn_nivars].name = name; return nn_nivars++; } static void nn_push(int **a, int *n, int *cap, int x) { if (*n == *cap) { *cap = *cap ? *cap * 2 : 4; *a = realloc(*a, sizeof(int) * (size_t)*cap); } (*a)[(*n)++] = x; } /* An array slot an index read can name: a local (2k) or an ivar (2k + 1). */ static int nn_slot_of(Compiler *c, int id) { if (id < 0) return -1; NodeKind k = nt_kind(c->nt, id); if (k == NK_LocalVariableReadNode) { int v = nn_var(nn_local_of(c, id)); return v < 0 ? -1 : 2 * v; } if (k == NK_InstanceVariableReadNode) { int v = nn_ivar(nt_str(c->nt, id, "name")); return v < 0 ? -1 : 2 * v + 1; } return -1; } static int nn_lca(int a, int b) { while (a != b) { int da = a >= 0 ? nn_ctxdep[a] : 0, db = b >= 0 ? nn_ctxdep[b] : 0; if (da >= db) a = a >= 0 ? nn_ctxpar[a] : -1; else b = b >= 0 ? nn_ctxpar[b] : -1; } return a; } static void nn_occur(Compiler *c, int id, int ctx) { int k = nn_var(nn_local_of(c, id)); if (k < 0) return; NNVar *v = &nn_vars[k]; if (!v->owner_set) { v->owner = ctx; v->owner_set = 1; } else v->owner = nn_lca(v->owner, ctx); nn_push(&v->occ, &v->nocc, &v->cocc, id); } static int nn_is_loop(NodeKind k) { return k == NK_WhileNode || k == NK_UntilNode || k == NK_ForNode; } static int nn_is_local_node(NodeKind k) { return k == NK_LocalVariableReadNode || k == NK_LocalVariableWriteNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode || k == NK_LocalVariableOperatorWriteNode || k == NK_LocalVariableTargetNode; } static int nn_is_ivar_node(NodeKind k) { return k == NK_InstanceVariableReadNode || k == NK_InstanceVariableWriteNode || k == NK_InstanceVariableOrWriteNode || k == NK_InstanceVariableAndWriteNode || k == NK_InstanceVariableOperatorWriteNode || k == NK_InstanceVariableTargetNode; } static int nn_is_param_node(NodeKind k) { return k == NK_RequiredParameterNode || k == NK_OptionalParameterNode || k == NK_RestParameterNode || k == NK_OptionalKeywordParameterNode || k == NK_KeywordRestParameterNode || k == NK_BlockParameterNode; } /* Pre-walk: parents, contexts, order, statement positions, loops, and every occurrence of each local and ivar. */ static void nn_pre(Compiler *c, int id, int par, int ctx, int loopout, int dep) { const NodeTable *nt = c->nt; if (id < 0 || id >= nn_cap) return; NodeKind k = nt_kind(nt, id); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) return; nn_par[id] = par; nn_frame[id] = nn_cur_frame; nn_seq[id] = nn_seqc++; nn_ctx[id] = ctx; nn_loopout[id] = loopout; int cctx = ctx, cloop = loopout, cdep = dep; if (k == NK_BlockNode || k == NK_LambdaNode) { nn_ctxpar[id] = ctx; nn_ctxdep[id] = dep + 1; cctx = id; cloop = -1; cdep = dep + 1; } else if (nn_is_loop(k) && loopout < 0) cloop = id; if (k == NK_RetryNode || k == NK_RedoNode) nn_retry[ctx >= 0 ? ctx : nn_cur_frame] = 1; if ((k == NK_NextNode || k == NK_BreakNode) && ctx >= 0) nn_jump[ctx] = 1; /* a redo runs again the body of the loop or block it is in */ if (k == NK_RedoNode) for (int p = par; p >= 0; p = nn_par[p]) { NodeKind pk = nt_kind(nt, p); if (pk == NK_WhileNode || pk == NK_UntilNode) { nn_jump[p] = 1; break; } if (pk == NK_ForNode || pk == NK_BlockNode || pk == NK_LambdaNode) break; } if (nn_is_local_node(k)) nn_occur(c, id, ctx); if (nn_is_ivar_node(k)) { int iv = nn_ivar(nt_str(nt, id, "name")); if (iv >= 0) nn_push(&nn_ivars[iv].occ, &nn_ivars[iv].nocc, &nn_ivars[iv].cocc, id); } if (nn_is_param_node(k)) { nn_occur(c, id, ctx); int kv = nn_var(nn_local_of(c, id)); if (kv >= 0) nn_vars[kv].param = 1; } if (k == NK_StatementsNode) { int n = 0; const int *st = nt_arr(nt, id, "body", &n); for (int i = 0; i < n; i++) if (st[i] >= 0 && st[i] < nn_cap) { nn_stlist[st[i]] = id; nn_stidx[st[i]] = i; } } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) nn_pre(c, nt_ref_at(nt, id, i), id, cctx, cloop, cdep); int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, id, i, &n); for (int j = 0; j < n; j++) nn_pre(c, a[j], id, cctx, cloop, cdep); } nn_send[id] = nn_seqc - 1; } /* Is the write `w` of a variable owned by `owner` unable to run while context `ctx` is suspended in a call? */ static int nn_write_safe(int w, int owner, int ctx) { int wc = nn_ctx[w]; /* in ctx itself: another activation of ctx has its own copy, and this one is busy in the call */ if (wc == ctx) return owner == ctx; /* in an ancestor context that owns the variable: it must come before the block leading down to ctx is made, and no loop or retry may run it again while that block is alive (a stored proc, a Fiber) */ int b = ctx; while (b >= 0 && nn_ctxpar[b] != wc) b = nn_ctxpar[b]; if (b < 0 || owner != wc) return 0; if (nn_retry[wc >= 0 ? wc : nn_frame[w]]) return 0; if (nn_seq[w] >= nn_seq[b]) return 0; int lo = nn_loopout[w]; if (lo >= 0 && nn_seq[b] >= nn_seq[lo] && nn_seq[b] <= nn_send[lo]) return 0; return 1; } /* Can a call made in context `ctx` leave variable k nil? */ static int nn_exposed(int k, int ctx) { NNVar *v = &nn_vars[k]; if (v->cache_ctx == ctx) return v->cache_val; int ex = 0; for (int i = 0; i < v->nw && !ex; i++) if (!nn_write_safe(v->w[i], v->owner, ctx)) ex = 1; v->cache_ctx = ctx; v->cache_val = ex; return ex; } /* ---- facts ---- */ static int nn_has(const NNF *f, LocalVar *lv) { if (f->bot) return 1; for (int i = 0; i < f->n; i++) if (f->v[i] == lv) return 1; return 0; } static void nn_add(NNF *f, LocalVar *lv) { if (f->bot || !lv || nn_has(f, lv) || f->n == NN_MAXF) return; f->v[f->n++] = lv; } static int nn_hasnn(const NNF *f, LocalVar *lv) { if (f->bot) return 1; for (int i = 0; i < f->nn; i++) if (f->nonneg[i] == lv) return 1; return 0; } static void nn_addnn(NNF *f, LocalVar *lv) { if (f->bot || !lv || nn_hasnn(f, lv) || f->nn == NN_MAXF) return; f->nonneg[f->nn++] = lv; } static int nn_hasiv(const NNF *f, int iv) { if (f->bot) return 1; for (int i = 0; i < f->ni; i++) if (f->iv[i] == iv) return 1; return 0; } static void nn_addiv(NNF *f, int iv) { if (f->bot || iv < 0 || nn_no_ivar_facts || nn_hasiv(f, iv) || f->ni == NN_MAXF) return; f->iv[f->ni++] = iv; } static void nn_killiv(NNF *f, int iv) { if (f->bot) return; for (int i = 0; i < f->ni; i++) if (f->iv[i] == iv) { f->iv[i] = f->iv[--f->ni]; break; } } static void nn_kill_rel_slot(NNF *f, int slot) { for (int i = 0; i < f->nr; ) { if (f->r[i].slot == slot) f->r[i] = f->r[--f->nr]; else i++; } for (int i = 0; i < f->nl; ) { if (f->len[i].slot == slot) f->len[i] = f->len[--f->nl]; else i++; } } static void nn_kill(NNF *f, LocalVar *lv) { if (f->bot) return; for (int i = 0; i < f->n; i++) if (f->v[i] == lv) { f->v[i] = f->v[--f->n]; break; } for (int i = 0; i < f->nn; i++) if (f->nonneg[i] == lv) { f->nonneg[i] = f->nonneg[--f->nn]; break; } int k = nn_var(lv); for (int i = 0; i < f->nr; ) { if (f->r[i].i == lv || (k >= 0 && f->r[i].slot == 2 * k)) f->r[i] = f->r[--f->nr]; else i++; } for (int i = 0; i < f->nl; ) { if (f->len[i].n == lv || (k >= 0 && f->len[i].slot == 2 * k)) f->len[i] = f->len[--f->nl]; else i++; } } static void nn_meet(NNF *a, const NNF *b) { if (b->bot) return; if (a->bot) { *a = *b; return; } for (int i = 0; i < a->n; ) { if (!nn_has(b, a->v[i])) a->v[i] = a->v[--a->n]; else i++; } for (int i = 0; i < a->nn; ) { if (!nn_hasnn(b, a->nonneg[i])) a->nonneg[i] = a->nonneg[--a->nn]; else i++; } for (int i = 0; i < a->ni; ) { if (!nn_hasiv(b, a->iv[i])) a->iv[i] = a->iv[--a->ni]; else i++; } for (int i = 0; i < a->nr; ) { int keep = 0; for (int j = 0; j < b->nr; j++) if (b->r[j].i == a->r[i].i && b->r[j].slot == a->r[i].slot) keep = 1; if (!keep) a->r[i] = a->r[--a->nr]; else i++; } for (int i = 0; i < a->nl; ) { int keep = 0; for (int j = 0; j < b->nl; j++) if (b->len[j].n == a->len[i].n && b->len[j].slot == a->len[i].slot) keep = 1; if (!keep) a->len[i] = a->len[--a->nl]; else i++; } } /* The kill log the generic walk and the loop entry replay: a variable written (incr: by `+= = 0>`), an ivar slot written, or a call. */ enum { NN_LOG_VAR, NN_LOG_IVAR, NN_LOG_CALL }; typedef struct { int kind; LocalVar *lv; int slot; int incr; } NNLog; static NNLog *nn_log; static int nn_nlog, nn_clog; static int nn_dry; /* a loop's dry walk: collect the kills, mark nothing */ static void nn_logpush(int kind, LocalVar *lv, int slot, int incr) { if (nn_nlog == nn_clog) { nn_clog = nn_clog ? nn_clog * 2 : 256; nn_log = realloc(nn_log, sizeof(NNLog) * (size_t)nn_clog); } nn_log[nn_nlog].kind = kind; nn_log[nn_nlog].lv = lv; nn_log[nn_nlog].slot = slot; nn_log[nn_nlog].incr = incr; nn_nlog++; } static void nn_call_kill(NNF *f, int ctx, int pure) { if (f->bot) return; for (int i = 0; i < f->n; ) { int k = nn_var(f->v[i]); if (k >= 0 && nn_exposed(k, ctx)) f->v[i] = f->v[--f->n]; else i++; } if (pure) return; /* the call may shrink an array, write a local through a closure, or run a method that writes one of self's ivars */ f->nr = 0; f->nl = 0; f->ni = 0; for (int i = 0; i < f->nn; ) { int k = nn_var(f->nonneg[i]); if (k < 0 || nn_vars[k].allw_ctx != ctx || nn_vars[k].owner != ctx) f->nonneg[i] = f->nonneg[--f->nn]; else i++; } } static void nn_write_kill(NNF *f, LocalVar *lv, int incr) { nn_kill(f, lv); nn_logpush(NN_LOG_VAR, lv, -1, incr); } /* A numeric slot, or during inference one not typed yet. */ static int nn_numeric(TyKind t) { return t == TY_INT || t == TY_FLOAT || (nn_inferring && t == TY_UNKNOWN); } static int nn_int_array(TyKind t) { return t == TY_INT_ARRAY || t == TY_FLOAT_ARRAY || (nn_inferring && t == TY_UNKNOWN); } /* An array whose length the facts follow: a numeric one, or one of pointers (objects, rows), whose element read in range is an element rather than the nil past the end (an_elem_read_in_bounds) */ static int nn_len_array(TyKind t) { return nn_int_array(t) || ty_is_ptr_array(t); } static int nn_name_in(const char *nm, const char *const *set) { if (!nm) return 0; return str_in(nm, set); } static int nn_array_reopened; /* the program reopens Array: its builtins may run its code */ /* A builtin call on an Integer or Float array that can only grow it -- `<<` / push / append, a one-index `[]=` -- and runs none of the program's code: an `i < a.size` or `n = a.size` known before it still holds after it, whichever array it grows, and it writes no local. */ static int nn_growth_call(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || recv < 0 || nn_array_reopened || nt_ref(nt, id, "block") >= 0 || !nn_len_array(comp_ntype(c, recv))) return 0; int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (nt_argv_has(nt, av, an, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK)) return 0; if (is_push_alias(nm)) return an >= 1; return sp_streq(nm, "[]=") && an == 2 && comp_ntype(c, av[0]) == TY_INT; } /* A call that cannot run Ruby code or change an array's length: an operator on numbers, an element read, a size. */ static int nn_pure_call(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || recv < 0 || nt_ref(nt, id, "block") >= 0) return 0; TyKind rt = comp_ntype(c, recv); if (rt == TY_INT || rt == TY_FLOAT) { static const char *const ops[] = { "+", "-", "*", "/", "<", "<=", ">", ">=", "==", "!=", "<=>", "&", "|", "^", "%", "~", "<<", ">>", "nil?", "!", "-@", "abs", NULL }; return nn_name_in(nm, ops); } if (rt == TY_INT_ARRAY || rt == TY_FLOAT_ARRAY || ty_is_ptr_array(rt)) return !nn_array_reopened && (sp_streq(nm, "[]") || sp_streq(nm, "size") || sp_streq(nm, "length")); /* an attribute reader the class does not define a method for: it reads its ivar and runs nothing */ if (ty_is_object(rt) && nt_ref(nt, id, "arguments") < 0) { int k = ty_object_class(rt); return comp_reader_in_chain(c, k, nm, NULL) && call_target(c, id, k, nm, 0, NULL) < 0; } return 0; } /* The calls the walk takes by their name for Kernel's or a builtin iterator's: raise, exit and abort do not come back (the first three), and the iterators throw their block's value away. A method the program defines by the name may do neither (`def raise(msg) = nil` in a class, `def each = (@got = yield)`), so nn_structure notes the names it defines. */ static const char *const nn_named[] = { "raise", "exit", "abort", "each", "each_with_index", "each_index", "times", "upto", "downto", "step", "loop", "each_slice", "each_cons", "reverse_each", "tap", NULL }; #define NN_NAMED_ITER 3 static int nn_name_at(const char *nm, const char *const *set, int from) { for (int i = from; set[i]; i++) if (sp_streq(nm, set[i])) return i; return -1; } static void nn_visit(Compiler *c, int id, NNF *f, int ctx); /* A slot read directly at node r, whose nil a use has just raised on: an ivar of self, or a local or parameter */ static void nn_used_slot(Compiler *c, int r, NNF *f) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, r); if (k == NK_InstanceVariableReadNode) nn_addiv(f, nn_ivar(nt_str(nt, r, "name"))); else if (k == NK_LocalVariableReadNode) nn_add(f, nn_local_of(c, r)); } /* A call that raises on a nil it is handed: the receiver of an arithmetic or ordering operator on a number (nil answers & | ^, so not those), an operand of one, or an Integer or Float array's index. A slot read directly there is non-nil once the call has returned. `&.` answers nil for a nil receiver instead of raising, and a program that reopens a class nil's or a number's operator can come from may answer for a nil too (nn_numeric_reopened): neither proves anything. */ static void nn_used(Compiler *c, int call, NNF *f, int ctx, int amark) { const NodeTable *nt = c->nt; static const char *const recv_raises[] = { "+", "-", "*", "/", "%", "**", "<<", ">>", "<", "<=", ">", ">=", "-@", "~", NULL }; static const char *const opnd_raises[] = { "+", "-", "*", "/", "%", "**", "&", "|", "^", "<<", ">>", "<", "<=", ">", ">=", NULL }; /* a local operand of these is nn_operands_nonnil's */ static const char *const local_ops[] = { "<", "<=", ">", ">=", "+", "-", "*", "/", NULL }; const char *nm = nt_str(nt, call, "name"); int recv = nt_ref(nt, call, "receiver"); if (!nm || recv < 0 || nt_ref(nt, call, "block") >= 0 || nt_safe_nav(nt, call) || nn_numeric_reopened) return; TyKind rt = comp_ntype(c, recv); int num = rt == TY_INT || rt == TY_FLOAT; int lops = nn_name_in(nm, local_ops); if (num && nn_name_in(nm, recv_raises)) { /* the receiver was read before the arguments ran: a write of it there, or a call that may write it, means the slot no longer holds the value read (the kill log from amark on tells) */ NodeKind rk = nt_kind(nt, recv); if (rk == NK_LocalVariableReadNode && !lops) { LocalVar *rl = nn_local_of(c, recv); int k = nn_var(rl); for (int i = amark; rl && i < nn_nlog; i++) if ((nn_log[i].kind == NN_LOG_VAR && nn_log[i].lv == rl) || (nn_log[i].kind == NN_LOG_CALL && k >= 0 && nn_exposed(k, ctx))) rl = NULL; if (rl) nn_add(f, rl); } else if (rk == NK_InstanceVariableReadNode) { int iv = nn_ivar(nt_str(nt, recv, "name")), keep = iv >= 0; for (int i = amark; keep && i < nn_nlog; i++) if ((nn_log[i].kind == NN_LOG_IVAR && nn_log[i].slot == 2 * iv + 1) || nn_log[i].kind == NN_LOG_CALL) keep = 0; if (keep) nn_used_slot(c, recv, f); } } int idx = (rt == TY_INT_ARRAY || rt == TY_FLOAT_ARRAY) && sp_streq(nm, "[]"); if (!(num && nn_name_in(nm, opnd_raises)) && !idx) return; int ca = nt_ref(nt, call, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (an != 1) return; TyKind at = comp_ntype(c, av[0]); if (!(at == TY_INT || at == TY_FLOAT)) return; if (num && lops && nt_kind(nt, av[0]) == NK_LocalVariableReadNode) return; nn_used_slot(c, av[0], f); } /* The value is an Integer or a Float the marks call non-nil. A value of another static type gets no mark to ask: a parameter or a local that only nil is ever written to is typed nil, not Integer-or-nil, and written to an Integer local it is that local's nil. */ static int nn_nonnil_value(Compiler *c, int v) { return v >= 0 && nn_numeric(comp_ntype(c, v)) && !nullable_int_value(c, v); } /* `a.size` (or `.length`) on an Integer or Float array slot: the slot, or -1 */ static int nn_size_slot(Compiler *c, int sz) { const NodeTable *nt = c->nt; if (sz < 0 || nt_kind(nt, sz) != NK_CallNode) return -1; const char *sn = nt_str(nt, sz, "name"); int arr = nt_ref(nt, sz, "receiver"); if (!sn || arr < 0 || !is_len_alias(sn) || nt_ref(nt, sz, "arguments") >= 0 || nt_ref(nt, sz, "block") >= 0) return -1; if (!nn_len_array(comp_ntype(c, arr))) return -1; return nn_slot_of(c, arr); } /* `i < a.size` (or `a.size > i`, `.length`) on an Integer or Float array slot, or `i < n` where f knows `n = a.size`: the relation, or 0 */ static int nn_rel_of(Compiler *c, int cond, const NNF *f, NNRel *out) { const NodeTable *nt = c->nt; if (nt_kind(nt, cond) != NK_CallNode) return 0; const char *nm = nt_str(nt, cond, "name"); int recv = nt_ref(nt, cond, "receiver"); int ca = nt_ref(nt, cond, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (!nm || recv < 0 || an != 1) return 0; int lhs, sz; if (sp_streq(nm, "<")) { lhs = recv; sz = av[0]; } else if (sp_streq(nm, ">")) { lhs = av[0]; sz = recv; } else return 0; if (nt_kind(nt, lhs) != NK_LocalVariableReadNode) return 0; int slot = nn_size_slot(c, sz); if (slot < 0 && nt_kind(nt, sz) == NK_LocalVariableReadNode && !f->bot) { LocalVar *n = nn_local_of(c, sz); for (int i = 0; n && i < f->nl; i++) if (f->len[i].n == n) { slot = f->len[i].slot; break; } } LocalVar *iv = nn_local_of(c, lhs); if (slot < 0 || !iv) return 0; out->i = iv; out->slot = slot; return 1; } /* `a[i]` (`a[i] = v` with set) naming slot and index i the facts relate */ static int nn_index_in_range(Compiler *c, int id, const NNF *f, int set) { const NodeTable *nt = c->nt; if (f->bot) return 0; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (!nm || recv < 0 || !sp_streq(nm, set ? "[]=" : "[]") || an != (set ? 2 : 1) || nt_ref(nt, id, "block") >= 0 || nt_kind(nt, av[0]) != NK_LocalVariableReadNode) return 0; LocalVar *iv = nn_local_of(c, av[0]); int slot = nn_slot_of(c, recv); if (!iv || slot < 0) return 0; for (int i = 0; i < f->nr; i++) if (f->r[i].i == iv && f->r[i].slot == slot) return slot + 1; return 0; } /* `x < y`, `x + y` and the rest of `< <= > >= + - * /` on an Integer or Float: once the call returns, neither local operand was nil. On a nil receiver it is a NoMethodError, and given a nil the number's method raises (the comparison's ArgumentError, the arithmetic's TypeError), as `x op= v` raised on a nil. `==` answers false for a nil and `&.` answers nil, so neither proves anything, and a program that reopens a class the operator could come from may answer for a nil too. The receiver was read before the argument ran, so a local the argument wrote since (`x + (x = nil; 1)`), or a call in it may have, is not the value read: the kill log from amark on tells. */ static void nn_operands_nonnil(Compiler *c, int id, NNF *f, int ctx, int amark) { const NodeTable *nt = c->nt; static const char *const ops[] = { "<", "<=", ">", ">=", "+", "-", "*", "/", NULL }; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (f->bot || nn_numeric_reopened || !nn_name_in(nm, ops) || recv < 0 || an != 1 || nt_ref(nt, id, "block") >= 0 || nt_safe_nav(nt, id)) return; TyKind rt = comp_ntype(c, recv), at = comp_ntype(c, av[0]); if (rt != TY_INT && rt != TY_FLOAT) return; LocalVar *rl = nt_kind(nt, recv) == NK_LocalVariableReadNode ? nn_local_of(c, recv) : NULL; int k = nn_var(rl); for (int i = amark; rl && i < nn_nlog; i++) if ((nn_log[i].kind == NN_LOG_VAR && nn_log[i].lv == rl) || (nn_log[i].kind == NN_LOG_CALL && k >= 0 && nn_exposed(k, ctx))) rl = NULL; if (rl) nn_add(f, rl); if ((at == TY_INT || at == TY_FLOAT) && nt_kind(nt, av[0]) == NK_LocalVariableReadNode) nn_add(f, nn_local_of(c, av[0])); } /* Visit a condition: the facts that hold when it is truthy (t) and falsy (e). */ static void nn_cond(Compiler *c, int id, const NNF *in, NNF *t, NNF *e, int ctx) { const NodeTable *nt = c->nt; NodeKind k = id >= 0 ? nt_kind(nt, id) : NK_NilNode; if (k == NK_AndNode || k == NK_OrNode) { NNF lt, le, rt, re; nn_cond(c, nt_ref(nt, id, "left"), in, <, &le, ctx); if (k == NK_AndNode) { nn_cond(c, nt_ref(nt, id, "right"), <, &rt, &re, ctx); *t = rt; *e = le; nn_meet(e, &re); } else { nn_cond(c, nt_ref(nt, id, "right"), &le, &rt, &re, ctx); *t = lt; nn_meet(t, &rt); *e = re; } return; } if (k == NK_ParenthesesNode) { int b = nt_ref(nt, id, "body"); int n = 0; const int *st = b >= 0 && nt_kind(nt, b) == NK_StatementsNode ? nt_arr(nt, b, "body", &n) : NULL; if (st && n == 1) { nn_cond(c, st[0], in, t, e, ctx); return; } } if (k == NK_CallNode && nt_ref(nt, id, "block") < 0) { const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (nm && recv >= 0 && ca < 0 && sp_streq(nm, "!")) { nn_cond(c, recv, in, e, t, ctx); return; } /* `x.nil?`, `x == nil`, `x != nil` on a local */ int nilcmp = nm && recv >= 0 && nt_kind(nt, recv) == NK_LocalVariableReadNode && ((ca < 0 && sp_streq(nm, "nil?")) || (an == 1 && nt_kind(nt, av[0]) == NK_NilNode && (is_eq_or_ne(nm)))); if (nilcmp) { *t = *in; nn_visit(c, recv, t, ctx); *e = *t; nn_add(sp_streq(nm, "!=") ? t : e, nn_local_of(c, recv)); return; } NNRel r; if (nn_rel_of(c, id, in, &r)) { *t = *in; nn_visit(c, id, t, ctx); *e = *t; if (!t->bot && nn_hasnn(t, r.i) && t->nr < NN_MAXR) t->r[t->nr++] = r; return; } } if (k == NK_LocalVariableReadNode) { *t = *in; nn_visit(c, id, t, ctx); *e = *t; LocalVar *lv = nn_local_of(c, id); nn_add(t, lv); int kv = nn_var(lv); if (kv >= 0) for (int i = 0; i < nn_vars[kv].ncorr; i++) nn_add(t, nn_vars[kv].corr[i]); return; } *t = *in; nn_visit(c, id, t, ctx); *e = *t; } static void nn_replay(NNF *f, int mark, int ctx) { for (int i = mark; i < nn_nlog; i++) { if (nn_log[i].kind == NN_LOG_VAR) nn_kill(f, nn_log[i].lv); else if (nn_log[i].kind == NN_LOG_IVAR) { nn_kill_rel_slot(f, nn_log[i].slot); nn_killiv(f, (nn_log[i].slot - 1) / 2); } else nn_call_kill(f, ctx, 0); } } static void nn_visit_children_generic(Compiler *c, int id, NNF *f, int ctx) { const NodeTable *nt = c->nt; int mark = nn_nlog; int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) { NNF e; memset(&e, 0, sizeof e); nn_visit(c, nt_ref_at(nt, id, i), &e, ctx); } int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, id, i, &n); for (int j = 0; j < n; j++) { NNF e; memset(&e, 0, sizeof e); nn_visit(c, a[j], &e, ctx); } } nn_replay(f, mark, ctx); } static int nn_ptr_cmp(const void *a, const void *b) { uintptr_t x = (uintptr_t)*(LocalVar *const *)a, y = (uintptr_t)*(LocalVar *const *)b; return (x > y) - (x < y); } /* The kills of a loop, replayed on the facts at its entry: a dry walk collects every write and call in it. `i += 1` keeps i non-nil and non-negative when every write of i in the loop is one. */ static void nn_loop_entry(Compiler *c, int id, NNF *f, int ctx) { const NodeTable *nt = c->nt; int mark = nn_nlog; int sv = nn_dry; nn_dry = 1; int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) { NNF x; memset(&x, 0, sizeof x); nn_visit(c, nt_ref_at(nt, id, i), &x, ctx); } nn_dry = sv; /* the locals some write in the loop does not increment, gathered once: asked again for every write, a long loop body cost its writes squared */ LocalVar **plain = (LocalVar **)malloc(sizeof(LocalVar *) * (size_t)(nn_nlog - mark + 1)); int np = 0; for (int j = mark; j < nn_nlog; j++) if (nn_log[j].kind == NN_LOG_VAR && !nn_log[j].incr) plain[np++] = nn_log[j].lv; qsort(plain, (size_t)np, sizeof(LocalVar *), nn_ptr_cmp); for (int i = mark; i < nn_nlog; i++) { if (nn_log[i].kind == NN_LOG_CALL) { nn_call_kill(f, ctx, 0); continue; } if (nn_log[i].kind == NN_LOG_IVAR) { nn_kill_rel_slot(f, nn_log[i].slot); nn_killiv(f, (nn_log[i].slot - 1) / 2); continue; } LocalVar *lv = nn_log[i].lv; int incr = !bsearch(&lv, plain, (size_t)np, sizeof(LocalVar *), nn_ptr_cmp); int had = nn_has(f, lv), hadnn = nn_hasnn(f, lv); nn_kill(f, lv); if (incr && had) nn_add(f, lv); if (incr && hadnn) nn_addnn(f, lv); } free(plain); nn_nlog = mark; } static int nn_nonneg_lit(Compiler *c, int v) { return v >= 0 && nt_kind(c->nt, v) == NK_IntegerNode && nt_int(c->nt, v, "value", -1) >= 0; } static void nn_visit_args(Compiler *c, int ca, NNF *f, int ctx) { const NodeTable *nt = c->nt; int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; for (int i = 0; i < an; i++) { NodeKind ak = nt_kind(nt, av[i]); if (ak == NK_BlockArgumentNode || ak == NK_SplatNode || ak == NK_KeywordHashNode) nn_visit_children_generic(c, av[i], f, ctx); else nn_visit(c, av[i], f, ctx); } } static void nn_visit(Compiler *c, int id, NNF *f, int ctx) { const NodeTable *nt = c->nt; if (id < 0 || id >= nn_cap) return; NodeKind k = nt_kind(nt, id); switch (k) { case NK_DefNode: case NK_ClassNode: case NK_ModuleNode: case NK_SingletonClassNode: return; case NK_LocalVariableReadNode: { LocalVar *lv = nn_local_of(c, id); if (!nn_dry && lv && nn_has(f, lv)) nn_nonnil[id] = 1; return; } case NK_IntegerNode: case NK_FloatNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: case NK_StringNode: case NK_SymbolNode: case NK_SelfNode: return; case NK_InstanceVariableReadNode: { int iv = nn_ivar(nt_str(nt, id, "name")); if (!nn_dry && iv >= 0 && f->ni && nn_hasiv(f, iv)) nn_nonnil[id] = 1; return; } case NK_InstanceVariableWriteNode: { int v = nt_ref(nt, id, "value"); nn_visit(c, v, f, ctx); int iv = nn_ivar(nt_str(nt, id, "name")); if (iv >= 0) { int nonnil = !nn_no_ivar_facts && nn_nonnil_value(c, v); if (!nn_dry && nonnil && !f->bot && nn_hasiv(f, iv)) nn_ivwok[id] = 1; nn_kill_rel_slot(f, 2 * iv + 1); nn_killiv(f, iv); nn_logpush(NN_LOG_IVAR, NULL, 2 * iv + 1, 0); if (nonnil) nn_addiv(f, iv); } return; } case NK_LocalVariableWriteNode: { int v = nt_ref(nt, id, "value"); nn_visit(c, v, f, ctx); LocalVar *lv = nn_local_of(c, id); if (!lv) return; nn_write_kill(f, lv, 0); if (nn_nonnil_value(c, v)) nn_add(f, lv); if (nn_nonneg_lit(c, v)) nn_addnn(f, lv); /* `n = a.size` */ int ls = nn_size_slot(c, v); if (ls >= 0 && ls != 2 * nn_var(lv) && !f->bot && f->nl < NN_MAXR) { f->len[f->nl].n = lv; f->len[f->nl].slot = ls; f->nl++; } return; } case NK_LocalVariableOperatorWriteNode: { int v = nt_ref(nt, id, "value"); nn_visit(c, v, f, ctx); LocalVar *lv = nn_local_of(c, id); if (!lv) return; int wasnn = nn_hasnn(f, lv); const char *op = nt_str(nt, id, "binary_operator"); int incr = op && sp_streq(op, "+") && nn_nonneg_lit(c, v); nn_write_kill(f, lv, incr); /* the arithmetic raised on a nil */ if (lv->type == TY_INT || lv->type == TY_FLOAT) nn_add(f, lv); if (wasnn && incr) nn_addnn(f, lv); return; } case NK_LocalVariableOrWriteNode: case NK_LocalVariableAndWriteNode: { int v = nt_ref(nt, id, "value"); LocalVar *lv = nn_local_of(c, id); NNF fv = *f; if (lv && k == NK_LocalVariableAndWriteNode) nn_add(&fv, lv); nn_visit(c, v, &fv, ctx); nn_meet(f, &fv); if (!lv) return; int had = nn_has(f, lv); nn_write_kill(f, lv, 0); int nonnil = nn_nonnil_value(c, v); if (k == NK_LocalVariableOrWriteNode ? nonnil : (had && nonnil)) nn_add(f, lv); return; } case NK_LocalVariableTargetNode: { LocalVar *lv = nn_local_of(c, id); if (lv) nn_write_kill(f, lv, 0); return; } case NK_StatementsNode: { int n = 0; const int *st = nt_arr(nt, id, "body", &n); for (int i = 0; i < n; i++) { if (f->bot) { NNF e; memset(&e, 0, sizeof e); nn_visit(c, st[i], &e, ctx); continue; } nn_visit(c, st[i], f, ctx); } return; } case NK_ParenthesesNode: nn_visit(c, nt_ref(nt, id, "body"), f, ctx); return; case NK_IfNode: case NK_UnlessNode: { NNF t, e; nn_cond(c, nt_ref(nt, id, "predicate"), f, &t, &e, ctx); if (k == NK_UnlessNode) { NNF x = t; t = e; e = x; } nn_visit(c, nt_ref(nt, id, "statements"), &t, ctx); nn_visit(c, nt_ref(nt, id, k == NK_IfNode ? "subsequent" : "else_clause"), &e, ctx); *f = t; nn_meet(f, &e); return; } case NK_ElseNode: nn_visit(c, nt_ref(nt, id, "statements"), f, ctx); return; case NK_AndNode: case NK_OrNode: { NNF t, e; nn_cond(c, id, f, &t, &e, ctx); *f = t; nn_meet(f, &e); return; } case NK_WhileNode: case NK_UntilNode: { /* a nested loop in a dry walk is walked as any other node */ if (nn_dry) { nn_visit_children_generic(c, id, f, ctx); return; } int post = nt_loop_post(nt, id); NNF in = *f; nn_loop_entry(c, id, &in, ctx); NNF t, e; if (post) { NNF b = in; nn_visit(c, nt_ref(nt, id, "statements"), &b, ctx); if (b.bot) b = in; nn_cond(c, nt_ref(nt, id, "predicate"), &b, &t, &e, ctx); if (k == NK_UntilNode) { NNF x = t; t = e; e = x; } } else { nn_cond(c, nt_ref(nt, id, "predicate"), &in, &t, &e, ctx); if (k == NK_UntilNode) { NNF x = t; t = e; e = x; } /* a redo runs the body again without the condition, so the body starts with the loop's invariant facts only */ NNF b = nn_jump[id] ? in : t; nn_visit(c, nt_ref(nt, id, "statements"), &b, ctx); } /* a break leaves with the loop's invariant facts only */ *f = e; nn_meet(f, &in); return; } case NK_ReturnNode: case NK_NextNode: case NK_BreakNode: case NK_RedoNode: case NK_RetryNode: nn_visit_args(c, nt_ref(nt, id, "arguments"), f, ctx); memset(f, 0, sizeof *f); f->bot = 1; return; case NK_CallNode: { nn_visit(c, nt_ref(nt, id, "receiver"), f, ctx); int amark = nn_nlog; nn_visit_args(c, nt_ref(nt, id, "arguments"), f, ctx); if (!nn_dry) { int s = nn_index_in_range(c, id, f, 0); if (s) nn_cand[id] = s; if (nn_index_in_range(c, id, f, 1)) nn_wrok[id] = 1; } int blk = nt_ref(nt, id, "block"); if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode) { NNF e; memset(&e, 0, sizeof e); nn_visit(c, nt_ref(nt, blk, "parameters"), &e, blk); nn_visit(c, nt_ref(nt, blk, "body"), &e, blk); } else if (blk >= 0) nn_visit_children_generic(c, blk, f, ctx); nn_operands_nonnil(c, id, f, ctx, amark); if (!nn_pure_call(c, id) && !nn_growth_call(c, id)) { nn_call_kill(f, ctx, 0); nn_logpush(NN_LOG_CALL, NULL, -1, 0); } else nn_used(c, id, f, ctx, amark); /* Kernel's raise, exit and abort do not come back */ const char *nm = nt_str(nt, id, "name"); int ex = nm && nt_ref(nt, id, "receiver") < 0 ? nn_name_at(nm, nn_named, 0) : -1; if (ex >= 0 && ex < NN_NAMED_ITER && !(nn_user_named & (1u << ex))) { memset(f, 0, sizeof *f); f->bot = 1; } return; } case NK_YieldNode: case NK_SuperNode: { nn_visit_args(c, nt_ref(nt, id, "arguments"), f, ctx); int blk = nt_ref(nt, id, "block"); if (blk >= 0) nn_visit_children_generic(c, blk, f, ctx); nn_call_kill(f, ctx, 0); nn_logpush(NN_LOG_CALL, NULL, -1, 0); return; } case NK_ForwardingSuperNode: nn_visit_children_generic(c, id, f, ctx); nn_call_kill(f, ctx, 0); nn_logpush(NN_LOG_CALL, NULL, -1, 0); return; case NK_LambdaNode: { NNF e; memset(&e, 0, sizeof e); nn_visit(c, nt_ref(nt, id, "parameters"), &e, id); nn_visit(c, nt_ref(nt, id, "body"), &e, id); return; } case NK_ArgumentsNode: nn_visit_args(c, id, f, ctx); return; default: { nn_visit_children_generic(c, id, f, ctx); /* control flow the walk does not follow: nothing survives it */ if (k == NK_BeginNode || k == NK_CaseNode || k == NK_CaseMatchNode || k == NK_ForNode || k == NK_RescueModifierNode) { f->n = 0; f->nr = 0; f->nl = 0; f->nn = 0; f->ni = 0; } /* an ivar written other than by `@x = v` (`@x op= v`, `||=`, a multiple-assignment target): what was known of it is gone */ if (nn_is_ivar_node(k) && k != NK_InstanceVariableReadNode) { int iv = nn_ivar(nt_str(nt, id, "name")); if (iv >= 0) { nn_kill_rel_slot(f, 2 * iv + 1); nn_killiv(f, iv); nn_logpush(NN_LOG_IVAR, NULL, 2 * iv + 1, 0); } } return; } } } /* ---- the flag rule ---- `f = true` beside `x = `, and every nil written to x beside `f = false`, with only simple statements between: then f true means x non-nil everywhere outside those call-free windows, and at entry too (f is nil until written). */ static int nn_simple_stmt(Compiler *c, int s) { const NodeTable *nt = c->nt; if (nt_kind(nt, s) != NK_LocalVariableWriteNode) return 0; int v = nt_ref(nt, s, "value"); NodeKind vk = v >= 0 ? nt_kind(nt, v) : NK_NilNode; return vk == NK_NilNode || vk == NK_TrueNode || vk == NK_FalseNode || vk == NK_IntegerNode || vk == NK_FloatNode || vk == NK_LocalVariableReadNode; } /* `f = false` beside statement s, with only simple statements between. The nearest write of f on each side decides: a `f = true` between s and a `f = false` leaves f true while x holds the nil. */ static int nn_beside_false(Compiler *c, int s, LocalVar *flag) { const NodeTable *nt = c->nt; int l = nn_stlist[s]; if (l < 0) return 0; int n = 0; const int *st = nt_arr(nt, l, "body", &n); for (int d = -1; d <= 1; d += 2) for (int j = nn_stidx[s] + d; j >= 0 && j < n; j += d) { if (nt_kind(nt, st[j]) == NK_LocalVariableWriteNode && nn_local_of(c, st[j]) == flag) { int v = nt_ref(nt, st[j], "value"); if (v >= 0 && nt_kind(nt, v) == NK_FalseNode) return 1; break; } if (!nn_simple_stmt(c, st[j])) break; } return 0; } static void nn_corr(Compiler *c) { const NodeTable *nt = c->nt; for (int k = 0; k < nn_nvars; k++) { NNVar *fv = &nn_vars[k]; if (fv->param || fv->lv->is_param || fv->bad_flag || !fv->has_true) continue; /* candidates: the non-nil writes ahead of every `f = true` */ LocalVar *cand[NN_MAXF]; int nc = 0, first = 1, ok = 1; for (int o = 0; o < fv->nocc && ok; o++) { int w = fv->occ[o]; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) continue; int v = nt_ref(nt, w, "value"); if (v < 0 || nt_kind(nt, v) != NK_TrueNode) continue; int l = nn_stlist[w]; if (l < 0) { ok = 0; break; } int n = 0; const int *st = nt_arr(nt, l, "body", &n); /* only each local's last write before `f = true` counts: `x = v; x = nil; f = true` leaves x nil */ LocalVar *here[NN_MAXF]; int nh = 0; LocalVar *seen[NN_MAXF]; int ns = 0; for (int j = nn_stidx[w] - 1; j >= 0 && j < n && nn_simple_stmt(c, st[j]); j--) { LocalVar *xl = nn_local_of(c, st[j]); if (!xl || xl == fv->lv) continue; int dup = 0; for (int i = 0; i < ns; i++) if (seen[i] == xl) dup = 1; if (dup) continue; if (ns == NN_MAXF) break; seen[ns++] = xl; if (nn_nonnil_value(c, nt_ref(nt, st[j], "value")) && nh < NN_MAXF) here[nh++] = xl; } if (first) { for (int i = 0; i < nh; i++) cand[nc++] = here[i]; first = 0; } else for (int i = 0; i < nc; ) { int in = 0; for (int j = 0; j < nh; j++) if (here[j] == cand[i]) in = 1; if (!in) cand[i] = cand[--nc]; else i++; } if (nc == 0) ok = 0; } if (!ok || first) continue; for (int i = 0; i < nc; i++) { int xk = nn_var(cand[i]); if (xk < 0) continue; NNVar *xv = &nn_vars[xk]; if (xv->owner != nn_vars[k].owner) continue; int good = 1; for (int j = 0; j < xv->nw && good; j++) { int w = xv->w[j]; if (nt_kind(nt, w) != NK_LocalVariableWriteNode || nn_stlist[w] < 0 || !nn_beside_false(c, w, nn_vars[k].lv)) good = 0; } if (!good) continue; fv = &nn_vars[k]; if (fv->ncorr == fv->ccorr) { fv->ccorr = fv->ccorr ? fv->ccorr * 2 : 4; fv->corr = realloc(fv->corr, sizeof(LocalVar *) * (size_t)fv->ccorr); } fv->corr[fv->ncorr++] = cand[i]; } } } /* ---- nil-free arrays ---- An in-bounds read of an array can still meet a nil: one a write left in it, or the gap a write past the end fills with nil. The static mark sees the first only where the array is reached by name, and not through a callee (`def add(x) = x << nil`). So an in-bounds read is narrowed only for a slot -- a local, or an ivar by name across every class -- whose array never escapes: every write builds a fresh array of values that cannot be nil, and every read is a call that only reads, or a mutation that can leave neither a nil nor a hole. Anything else, and the slot keeps its reads as they were. */ /* A block parameter an index iterator binds: `n.times { |i| }`, `Array.new(n) { |i| }`, `(a..b).each { |i| }`, `upto`/`downto`, `each_index`, and `each_with_index`'s second. */ static int nn_iter_index(Compiler *c, int param) { const NodeTable *nt = c->nt; int pl = nn_par[param]; int ps = pl >= 0 ? nn_par[pl] : -1; /* BlockParametersNode */ int blk = ps >= 0 ? nn_par[ps] : -1; if (pl < 0 || ps < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; int call = nn_par[blk]; if (call < 0 || nt_kind(nt, call) != NK_CallNode || nt_ref(nt, call, "block") != blk) return 0; int rn = 0; const int *reqs = nt_arr(nt, pl, "requireds", &rn); int pos = -1; for (int i = 0; reqs && i < rn; i++) if (reqs[i] == param) pos = i; if (pos < 0) return 0; const char *nm = nt_str(nt, call, "name"); int recv = nt_ref(nt, call, "receiver"); if (!nm || recv < 0) return 0; NodeKind rk = nt_kind(nt, recv); if (rk == NK_ParenthesesNode) { int b = nt_ref(nt, recv, "body"); int n = 0; const int *st = b >= 0 && nt_kind(nt, b) == NK_StatementsNode ? nt_arr(nt, b, "body", &n) : NULL; if (st && n == 1) { recv = st[0]; rk = nt_kind(nt, recv); } } TyKind rt = comp_ntype(c, recv); if (pos == 0 && is_int_step(nm) && (rt == TY_INT || (nn_inferring && rt == TY_UNKNOWN))) return 1; if (pos == 0 && sp_streq(nm, "each_index")) return 1; if (pos == 1 && sp_streq(nm, "each_with_index")) return 1; if (pos == 0 && rk == NK_RangeNode && (sp_streq(nm, "each") || sp_streq(nm, "map") || sp_streq(nm, "collect"))) return 1; if (pos == 0 && sp_streq(nm, "new") && rk == NK_ConstantReadNode) { const char *cn = nt_str(nt, recv, "name"); return cn && sp_streq(cn, "Array"); } return 0; } static void nn_reset(void) { for (int k = 0; k < nn_nvars; k++) { free(nn_vars[k].w); free(nn_vars[k].corr); free(nn_vars[k].occ); } nn_nvars = 0; if (nn_vhash) for (int k = 0; k < nn_vhcap; k++) nn_vhash[k] = -1; for (int k = 0; k < nn_nivars; k++) free(nn_ivars[k].occ); nn_nivars = 0; if (nn_ihash) for (int k = 0; k < nn_ihcap; k++) nn_ihash[k] = -1; } static void nn_alloc(int cap) { free(nn_nonnil); free(nn_inb); free(nn_cand); free(nn_wrok); free(nn_ivwok); free(nn_par); free(nn_ctx); free(nn_seq); free(nn_send); free(nn_ctxpar); free(nn_ctxdep); free(nn_stlist); free(nn_stidx); free(nn_loopout); free(nn_retry); free(nn_jump); free(nn_defbody); free(nn_frame); size_t n = (size_t)cap + 1; nn_cap = cap; nn_nonnil = malloc(n); nn_inb = malloc(n); nn_wrok = malloc(n); nn_ivwok = malloc(n); nn_retry = malloc(n); nn_jump = malloc(n); nn_defbody = malloc(n); nn_frame = malloc(sizeof(int) * n); nn_cand = malloc(sizeof(int) * n); nn_par = malloc(sizeof(int) * n); nn_ctx = malloc(sizeof(int) * n); nn_seq = malloc(sizeof(int) * n); nn_send = malloc(sizeof(int) * n); nn_ctxpar = malloc(sizeof(int) * n); nn_ctxdep = malloc(sizeof(int) * n); nn_stlist = malloc(sizeof(int) * n); nn_stidx = malloc(sizeof(int) * n); nn_loopout = malloc(sizeof(int) * n); } /* The program's shape, which the facts' rounds share: parents, contexts, statement positions, loops, every occurrence of each local and ivar. */ static int *nn_roots; static int nn_nroots, nn_croots; static int nn_struct_count = -1; static void nn_structure(Compiler *c) { const NodeTable *nt = c->nt; if (nn_cap != nt->count) nn_alloc(nt->count); nn_struct_count = nt->count; size_t n = (size_t)nn_cap + 1; memset(nn_retry, 0, n); memset(nn_jump, 0, n); memset(nn_defbody, 0, n); memset(nn_ctxdep, 0, sizeof(int) * n); memset(nn_seq, 0, sizeof(int) * n); memset(nn_send, 0, sizeof(int) * n); for (size_t i = 0; i < n; i++) { nn_stlist[i] = -1; nn_ctxpar[i] = -1; nn_loopout[i] = -1; nn_ctx[i] = -1; nn_par[i] = -1; } nn_reset(); nn_seqc = 0; /* frames: every program body, class body and method body, once each */ unsigned char *done = calloc(n, 1); nn_nroots = 0; #define NN_ROOT(b) do { int _b = (b); if (_b >= 0 && _b < nn_cap && !done[_b]) { done[_b] = 1; \ nn_push(&nn_roots, &nn_nroots, &nn_croots, _b); } } while (0) NT_FOREACH_KIND(nt, NK_ProgramNode, p) NN_ROOT(nt_ref(nt, p, "statements")); NT_FOREACH_KIND(nt, NK_ClassNode, p) NN_ROOT(nt_ref(nt, p, "body")); NT_FOREACH_KIND(nt, NK_ModuleNode, p) NN_ROOT(nt_ref(nt, p, "body")); NT_FOREACH_KIND(nt, NK_SingletonClassNode, p) NN_ROOT(nt_ref(nt, p, "body")); NT_FOREACH_KIND(nt, NK_DefNode, p) { int pr = nt_ref(nt, p, "parameters"); if (pr >= 0 && pr < nn_cap && !done[pr]) { done[pr] = 1; nn_cur_frame = pr; nn_pre(c, pr, -1, -1, -1, 0); } int body = nt_ref(nt, p, "body"); const char *dn = nt_str(nt, p, "name"); if (body >= 0 && body < nn_cap) nn_defbody[body] = dn && sp_streq(dn, "initialize") ? 2 : 1; NN_ROOT(body); } #undef NN_ROOT free(done); for (int i = 0; i < nn_nroots; i++) { nn_cur_frame = nn_roots[i]; nn_pre(c, nn_roots[i], -1, -1, -1, 0); } /* the flags' shapes, the contexts the writes sit in, the index iterators */ for (int k = 0; k < nn_nvars; k++) { NNVar *v = &nn_vars[k]; for (int o = 0; o < v->nocc; o++) { int w = v->occ[o]; NodeKind wk = nt_kind(nt, w); if (wk == NK_LocalVariableReadNode || nn_is_param_node(wk)) { if (nn_is_param_node(wk) && nn_iter_index(c, w)) v->iter_index = 1; continue; } int wc = nn_ctx[w]; if (!v->allw_set) { v->allw_ctx = wc; v->allw_set = 1; } else if (v->allw_ctx != wc) v->allw_ctx = -2; int val = nt_ref(nt, w, "value"); NodeKind vk = val >= 0 ? nt_kind(nt, val) : NK_NilNode; if (wk != NK_LocalVariableWriteNode || (vk != NK_TrueNode && vk != NK_FalseNode)) v->bad_flag = 1; if (wk == NK_LocalVariableWriteNode && vk == NK_TrueNode) v->has_true = 1; } /* a parameter bound to an iterator's index is no other parameter */ if (v->iter_index) for (int o = 0; o < v->nocc; o++) if (nn_is_param_node(nt_kind(nt, v->occ[o])) && !nn_iter_index(c, v->occ[o])) v->iter_index = 0; } /* the names of nn_named the program defines methods for, asked of the methods themselves: the facts can be computed while the analysis derives a builtin-only answer, under which an_user_defines_method answers no. A class's singleton methods (`def self.raise`) count too: a receiverless call in a class method finds them first. */ nn_user_named = 0; for (int i = 0; nn_named[i]; i++) { int own = comp_method_index(c, nn_named[i]) >= 0; for (int k = 0; k < c->nclasses && !own; k++) own = comp_method_in_chain(c, k, nn_named[i], NULL) >= 0 || comp_cmethod_in_chain(c, k, nn_named[i], NULL) >= 0; if (own) nn_user_named |= 1u << i; } /* reflection that reads or writes an ivar by name, or an Array reopened by the program, leaves no ivar slot to reason about */ nn_no_ivar_slots = nn_no_local_slots = nn_array_reopened = comp_core_class(c, CORE_ARRAY) >= 0; /* ... and a reopened Integer, Float or class above them can give `<` or `+` a method that takes a nil (nn_operands_nonnil) */ static const int reopen[] = { CORE_NILCLASS, CORE_INTEGER, CORE_FLOAT, CORE_NUMERIC, CORE_COMPARABLE, CORE_OBJECT, CORE_KERNEL, CORE_BASIC_OBJECT, -1 }; nn_numeric_reopened = 0; for (int i = 0; reopen[i] >= 0; i++) if (comp_core_class(c, reopen[i]) >= 0) nn_numeric_reopened = 1; nn_nattrs = 0; NT_FOREACH_KIND(nt, NK_CallNode, a) { const char *an = nt_str(nt, a, "name"); if (an && strncmp(an, "attr", 4) == 0) { int ca = nt_ref(nt, a, "arguments"); int na = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &na) : NULL; for (int i = 0; i < na; i++) { if (nn_nattrs == nn_cattrs) { nn_cattrs = nn_cattrs ? nn_cattrs * 2 : 16; nn_attrs = realloc(nn_attrs, sizeof(const char *) * (size_t)nn_cattrs); } /* a name not spelled as a symbol could be any */ nn_attrs[nn_nattrs++] = nt_kind(nt, av[i]) == NK_SymbolNode ? nt_str(nt, av[i], "value") : NULL; } } if (an && (strncmp(an, "instance_variable", 17) == 0 || sp_streq(an, "remove_instance_variable"))) nn_no_ivar_slots = 1; if (an && strncmp(an, "local_variable_", 15) == 0) nn_no_local_slots = 1; } /* ... or names it to send or method */ NT_FOREACH_KIND(nt, NK_SymbolNode, y) { const char *sv = nt_str(nt, y, "value"); if (sv && (strncmp(sv, "instance_variable", 17) == 0 || sp_streq(sv, "remove_instance_variable"))) nn_no_ivar_slots = 1; if (sv && strncmp(sv, "local_variable_", 15) == 0) nn_no_local_slots = 1; } /* An ivar fact holds from a test to the next write or call; code that can run between two statements (another thread, a signal handler, a finalizer) could write the ivar in between */ nn_no_ivar_facts = nn_no_ivar_slots; static const char *const async[] = { "Thread", "Queue", "SizedQueue", "Mutex", "Monitor", "ConditionVariable", "Signal", "ObjectSpace", NULL }; NT_FOREACH_KIND(nt, NK_ConstantReadNode, k) if (nn_name_in(nt_str(nt, k, "name"), async)) nn_no_ivar_facts = 1; NT_FOREACH_KIND(nt, NK_ConstantPathNode, k) if (nn_name_in(nt_str(nt, k, "name"), async)) nn_no_ivar_facts = 1; NT_FOREACH_KIND(nt, NK_CallNode, a) { const char *an = nt_str(nt, a, "name"); if (an && (sp_streq(an, "trap") || sp_streq(an, "define_finalizer"))) nn_no_ivar_facts = 1; } } /* The facts themselves, which depend on the marks: the writes that can leave each local nil, the flags, the walk, the nil-free arrays. */ static void nn_facts(Compiler *c) { const NodeTable *nt = c->nt; size_t n = (size_t)nn_cap + 1; memset(nn_nonnil, 0, n); memset(nn_inb, 0, n); memset(nn_wrok, 0, n); memset(nn_ivwok, 0, n); memset(nn_cand, 0, sizeof(int) * n); for (int k = 0; k < nn_nvars; k++) { NNVar *v = &nn_vars[k]; v->nw = 0; v->ncorr = 0; v->cache_ctx = -3; for (int o = 0; o < v->nocc; o++) { int w = v->occ[o]; NodeKind wk = nt_kind(nt, w); if (wk == NK_LocalVariableReadNode || nn_is_param_node(wk)) continue; int nullable = wk == NK_LocalVariableTargetNode || (wk != NK_LocalVariableOperatorWriteNode && !nn_nonnil_value(c, nt_ref(nt, w, "value"))); if (nullable) nn_push(&v->w, &v->nw, &v->cw, w); } } nn_corr(c); for (int i = 0; i < nn_nroots; i++) { NNF f; memset(&f, 0, sizeof f); nn_nlog = 0; nn_visit(c, nn_roots[i], &f, -1); } /* an in-bounds read of a nil-free array reads an element that is one. A candidate's array is an Integer or Float one (nn_rel_of), whose nil-freeness the static nil mark answers for every name of it, a parameter's and an ivar's too: no nil, and no gap a write past the end left, can be in it at run time (elem_nil_reach_expr) */ for (int id = 0; id < nn_cap; id++) { if (!nn_cand[id] || !nn_int_array(comp_ntype(c, nt_ref(nt, id, "receiver")))) continue; if (!elem_nil_reach_expr(c, nt_ref(nt, id, "receiver"))) nn_inb[id] = 1; } } static void nn_compute_now(Compiler *c, int reuse) { nn_busy = 1; nn_done_epoch = nn_epoch; if (!reuse || nn_struct_count != c->nt->count) nn_structure(c); nn_facts(c); nn_busy = 0; nn_ready = 1; } /* The facts for a round of the nullable marking. The tree does not change across its rounds, so only the first one walks its shape. */ void nn_compute(Compiler *c, int round) { nn_c = c; nn_inferring = 0; nn_epoch++; nn_compute_now(c, round > 0); } /* A new round of type inference: the facts are recomputed when first asked. */ void nn_inference_round(Compiler *c) { nn_c = c; nn_inferring = 1; nn_epoch++; } /* a String operand that is never nil: a literal */ static int nn_str_literal(const NodeTable *nt, int n) { NodeKind k = n >= 0 ? nt_kind(nt, n) : NK_NONE; return k == NK_StringNode || k == NK_InterpolatedStringNode; } /* Is call v a native class's method (a package's native_method) declared to answer a plain Integer or Float? */ int native_call_ret_plain_num(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0 || nt_kind(nt, v) != NK_CallNode) return 0; int r = nt_ref(nt, v, "receiver"); const char *nm = nt_str(nt, v, "name"); if (r < 0 || !nm) return 0; TyKind rt = comp_ntype(c, r); if (!ty_is_object(rt)) return 0; int cid = ty_object_class(rt); if (cid < 0 || cid >= c->nclasses || !c->classes[cid].is_native_class) return 0; int a = nt_ref(nt, v, "arguments"), an = 0; if (a >= 0) nt_arr(nt, a, "arguments", &an); int nmi = comp_native_method_find(c, cid, nm, an, 0); if (nmi < 0 || !c->native_methods[nmi].ret) return 0; return sp_streq(c->native_methods[nmi].ret, "int") || sp_streq(c->native_methods[nmi].ret, "float"); } int g_nn_builtin_call = -1; int class_has_subclass(Compiler *c, int ocid); /* Is call v a class-level accessor's reader (`class << self; attr_reader :x; end`) reading a class-level @x that holds an Integer or a Float? Such a slot starts nil and may be set to nil by its writer, which no flow the analysis follows rules out: its read is nil where CRuby's is. The receivers sg_accessor_type answers: the class's constant, self in a class method or body, and self.class in an instance method of a class with no subclass. */ static int sg_civ_reader_call(Compiler *c, int v) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, v, "name"); int r = nt_ref(nt, v, "receiver"); if (!nm || r < 0 || nt_ref(nt, v, "arguments") >= 0) return 0; int ci = -1; NodeKind rk = nt_kind(nt, r); if (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) { const char *cn = nt_str(nt, r, "name"); ci = cn ? comp_class_index(c, cn) : -1; } else { Scope *s = comp_scope_of(c, v); if (!s || s->class_id < 0) return 0; if (rk == NK_SelfNode && s->is_cmethod) ci = s->class_id; else if (rk == NK_CallNode && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), "class") && nt_ref(nt, r, "receiver") >= 0 && nt_kind(nt, nt_ref(nt, r, "receiver")) == NK_SelfNode && !s->is_cmethod && !class_has_subclass(c, s->class_id)) ci = s->class_id; } if (ci < 0) return 0; ClassInfo *cls = &c->classes[ci]; const char *rn = comp_resolve_cm_alias_at(c, ci, nm, NULL); if (!rn) rn = nm; if (!comp_is_sg_reader(cls, rn) || !comp_is_sg_civ(cls, rn)) return 0; char ivn[256]; snprintf(ivn, sizeof ivn, "@%s", rn); int ivi = comp_ivar_index(cls, ivn); return ivi >= 0 && (cls->ivar_types[ivi] == TY_INT || cls->ivar_types[ivi] == TY_FLOAT); } /* a value position of nullable_int_value's conditional: an absent arm is nil, and a walk that gave up may be */ static int nullable_int_leaf(const VpLeaf *leaf, void *ud) { if (leaf->kind == VPK_NIL || leaf->kind == VPK_GAVE_UP) return 1; return nullable_int_value(ud, leaf->node); } int nullable_int_value(Compiler *c, int v) { const NodeTable *nt = c->nt; if (v < 0) return 0; /* (it follows a parameter to its callers and a local to its writes across the program: an inference that asks depends on the program) */ IMEMO_DEP_GLOBAL(); if (nt_kind(nt, v) == NK_NilNode) return 1; /* `next v` hands the block's caller v; a bare `next`, nil */ if (nt_kind(nt, v) == NK_NextNode) { int na = nt_ref(nt, v, "arguments"); int nn = 0; const int *nav = na >= 0 ? nt_arr(nt, na, "arguments", &nn) : NULL; if (nn == 0) return 1; return nav && nn == 1 ? nullable_int_value(c, nav[0]) : 0; } /* `return e` / `(e)` carry their inner value unchanged; a block tail can be either, and so can a method's own tail statement. A bare `return` answers nil, which an Integer or Float return carries as its nil. */ if (nt_kind(nt, v) == NK_ReturnNode) { int rv = nt_ref(nt, v, "arguments"); int rn = 0; const int *ra = rv >= 0 ? nt_arr(nt, rv, "arguments", &rn) : NULL; if (rn == 0) return 1; return ra && rn == 1 ? nullable_int_value(c, ra[0]) : 0; } /* A conditional's value is one of its arms, so it can be nil if any arm can. A ternary, an `if`/`unless` or `case` used as an expression, a begin/rescue and `a || b` all reach a Hash key this way (#3505). An arm that is absent yields nil, which an int slot holds as its nil. */ switch (nt_kind(nt, v)) { case NK_StatementsNode: case NK_ElseNode: case NK_IfNode: case NK_UnlessNode: case NK_CaseNode: case NK_CaseMatchNode: case NK_BeginNode: case NK_RescueModifierNode: return for_each_value_position(nt, v, VP_BEGIN_BOTH | VP_CASE_IN_RAISE_NIL | VPS_IF | VPS_UNLESS | VPS_CASE | VPS_CASE_IN | VPS_BEGIN | VPS_RESCUE_MOD, nullable_int_leaf, c); default: break; } /* `a && b` / `a || b` answers one of its operands: nil where one is nil (`nil && 2`, `false || nil`), not only where one is a number that can be */ if (nt_kind(nt, v) == NK_OrNode || nt_kind(nt, v) == NK_AndNode) { int l = nt_ref(nt, v, "left"), r = nt_ref(nt, v, "right"); /* `a || b` answers a only when a is no nil: nil only through b (`@ram[i] || 0` never is) */ if (nt_kind(nt, v) == NK_OrNode && r >= 0) { TyKind rt2 = infer_type(c, r); return nt_kind(nt, r) == NK_NilNode || rt2 == TY_NIL || rt2 == TY_VOID || nullable_int_value(c, r); } for (int s = 0; s < 2; s++) { int o = s ? r : l; if (o < 0) continue; TyKind ot = infer_type(c, o); if (nt_kind(nt, o) == NK_NilNode || ot == TY_NIL || ot == TY_VOID) return 1; } return nullable_int_value(c, l) || nullable_int_value(c, r); } /* Reading a slot some write left nil in: the reader method a caller resolves to is this read, so its callers box through it too. */ if (nt_kind(nt, v) == NK_InstanceVariableReadNode) { Scope *s = comp_scope_of(c, v); int cid = s ? s->class_id : -1; /* inside an instance_eval / exec splice the ivar is the receiver's; a receiver of no class of the program's has none: nil */ if (ie_nonobj_node(v)) return 1; if (ie_class_of(c, v) >= 0) cid = ie_class_of(c, v); if (cid < 0) cid = c->toplevel_class; if (cid < 0 || cid >= c->nclasses) return 0; ClassInfo *ci = &c->classes[cid]; int iv = comp_ivar_index(ci, nt_str(nt, v, "name")); /* a splice's receiver class that never writes the ivar reads it nil */ if (iv < 0 && ie_class_of(c, v) >= 0) { int has = 0; for (int k = cid, hop = 0; k >= 0 && !has && hop < 64; k = c->classes[k].parent, hop++) has = comp_ivar_index(&c->classes[k], nt_str(nt, v, "name")) >= 0; if (!has) return 1; } /* a boxed ivar (a promote-widened member) can hold nil too; a read after a use that raised on its nil cannot be (nn_ivar_read_nonnil) */ return iv >= 0 && (ci->ivar_nullable_int[iv] || ci->ivar_types[iv] == TY_POLY) && !nn_ivar_read_nonnil(c, v); } /* A class variable some write left nil in, as an ivar: `def self.b = (@@x = nil)` beside `@@x = 1` read back 0 */ if (nt_kind(nt, v) == NK_ClassVariableReadNode) { ClassInfo *ci = NULL; int cv = cvar_slot(c, v, &ci); /* a boxed one (a promote-widened member) can hold nil too, as an ivar */ return cv >= 0 && (ci->cvar_nullable_int[cv] || ci->cvar_types[cv] == TY_POLY); } /* A Float global is declared holding nil, which it keeps until its first assignment; and any scalar global some write left nil in holds it after (marked below, as a local is). */ if (nt_kind(nt, v) == NK_GlobalVariableReadNode) { const char *gn = nt_str(nt, v, "name"); const char *rn = gn ? comp_resolve_gvar(c, gn + 1) : NULL; LocalVar *g = rn ? comp_gvar(c, rn) : NULL; if (g && (g->nullable_int || g->type == TY_POLY)) return 1; return g && g->type == TY_FLOAT && !gvar_seeded_before_read(c, rn); } /* A constant holds nil where a write of it can leave one: it is boxed so only after inference (widen_nullable_keyed_hash_literals), and a parameter it is the default of, or a method answering it, would keep a plain Integer slot */ if (nt_kind(nt, v) == NK_ConstantReadNode) { const char *cn = nt_str(nt, v, "name"); LocalVar *k = cn ? comp_const(c, cn) : NULL; if (!k || (k->type != TY_INT && k->type != TY_FLOAT && k->type != TY_POLY)) return 0; NT_FOREACH_KIND(nt, NK_ConstantWriteNode, w) if (sp_streq(nt_str(nt, w, "name"), cn) && nullable_int_value(c, nt_ref(nt, w, "value"))) return 1; return 0; } /* `(e)` is e, and a parenthesized sequence `(y = a[i]; y)` is its last statement, as a block's body is: the arm above walks it. Only the one-statement form was read, so the sequence's nil left the local it was assigned to unmarked. `()` is nil. */ if (nt_kind(nt, v) == NK_ParenthesesNode) { int pb = nt_ref(nt, v, "body"); return pb < 0 || nullable_int_value(c, pb); } /* The value of `yield x` is the BLOCK's, decided per call site. The yield's own type says only TY_INT, which an `Integer?` and an `Integer` share, so ask the blocks themselves (#3505). */ if (nt_kind(nt, v) == NK_YieldNode) { Scope *ys = comp_scope_of(c, v); int ymi = ys ? (int)(ys - c->scopes) : -1; if (ymi < 0) return 0; int tails[64]; /* a block's `next v` is one of its values too */ int sv_ybn = yield_block_tails_next; yield_block_tails_next = 1; int n = yield_block_tails(c, ymi, tails, (int)(sizeof tails / sizeof tails[0])); yield_block_tails_next = sv_ybn; /* no literal block in sight (an escaping &blk called through the proc ABI): its value arrives boxed, so nothing unboxed carries a nil */ for (int i = 0; i < n; i++) if (nullable_int_value(c, tails[i])) return 1; return 0; } /* `super` answers the ancestor's method, and a yielding one answers its block's value: a block written at the super (`super { nil }`, `super(&proc { nil })`) with a nil-capable tail */ if (nt_kind(nt, v) == NK_SuperNode || nt_kind(nt, v) == NK_ForwardingSuperNode) { Scope *ss = comp_scope_of(c, v); int tmi = ss ? a_super_target(c, ss) : -1; if (tmi >= 0 && c->scopes[tmi].ret_nullable_int) return 1; int blk = nt_ref(nt, v, "block"), bb = -1; if (blk >= 0 && nt_kind(nt, blk) == NK_BlockNode) bb = nt_ref(nt, blk, "body"); else if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode) { int e = nt_ref(nt, blk, "expression"); if (e >= 0 && nt_kind(nt, e) == NK_LambdaNode) bb = nt_ref(nt, e, "body"); else if (e >= 0 && nt_kind(nt, e) == NK_CallNode && nt_ref(nt, e, "receiver") < 0 && nt_str(nt, e, "name") && (sp_streq(nt_str(nt, e, "name"), "proc") || sp_streq(nt_str(nt, e, "name"), "lambda"))) { int pb = nt_ref(nt, e, "block"); if (pb >= 0 && nt_kind(nt, pb) == NK_BlockNode) bb = nt_ref(nt, pb, "body"); } } if (bb >= 0 && tmi >= 0 && c->scopes[tmi].yields) { int n = 0; const int *st = nt_kind(nt, bb) == NK_StatementsNode ? nt_arr(nt, bb, "body", &n) : &bb; if (nt_kind(nt, bb) != NK_StatementsNode) n = 1; if (n <= 0) return 1; int tl = st[n - 1]; if (nt_kind(nt, tl) == NK_NilNode || nullable_int_value(c, tl)) return 1; } return 0; } if (nt_kind(nt, v) == NK_CallNode) { /* `a[i] = v` answers v: a nil, or a boxed value that can be one */ { const char *sn = nt_str(nt, v, "name"); int sa = nt_ref(nt, v, "arguments"), san = 0; const int *sav = sa >= 0 ? nt_arr(nt, sa, "arguments", &san) : NULL; if (sn && sp_streq(sn, "[]=") && san >= 2 && nt_ref(nt, v, "receiver") >= 0) { int sv2 = sav[san - 1]; TyKind st2 = infer_type(c, sv2); return nt_kind(nt, sv2) == NK_NilNode || st2 == TY_POLY || st2 == TY_NIL || nullable_int_value(c, sv2); } /* a present entry of a typed Integer-valued Hash that may hold a nil value is that nil (D3b-ii): `[]`, fetch / dig / delete in every form */ if (sn && (sp_streq(sn, "[]") || sp_streq(sn, "fetch") || sp_streq(sn, "dig") || sp_streq(sn, "delete")) && hash_vals_nullable(c, nt_ref(nt, v, "receiver"))) return 1; } if (nn_index_inbounds(c, v)) return 0; /* a call that answers nothing (`$stdout.puts(x)`) is nil */ { TyKind cvt = infer_type(c, v); if (cvt == TY_NIL || cvt == TY_VOID) return 1; } /* a native class's method declared to answer an Integer or a Float (IO::Buffer#<=>) answers one */ if (native_call_ret_plain_num(c, v)) return 0; /* `<=>` over two numbers (or two Strings, two Symbols) always answers: only a pairing of other kinds can be nil */ if (sp_streq(nt_str(nt, v, "name"), "<=>")) { int cr = nt_ref(nt, v, "receiver"), ca = nt_ref(nt, v, "arguments"), can = 0; const int *cav = ca >= 0 ? nt_arr(nt, ca, "arguments", &can) : NULL; if (cr >= 0 && cav && can == 1) { TyKind lt = infer_type(c, cr), at = infer_type(c, cav[0]); /* a Float side can be NaN, which compares to nothing; a String side other than a literal can be the nil a String slot holds */ int num_l = lt == TY_INT || lt == TY_FLOAT || lt == TY_BIGINT || lt == TY_RATIONAL; int num_r = at == TY_INT || at == TY_FLOAT || at == TY_BIGINT || at == TY_RATIONAL; int flo = lt == TY_FLOAT || at == TY_FLOAT; int str_lit = nn_str_literal(nt, cr) && nn_str_literal(nt, cav[0]); /* ... and a nil number on either side answers nil */ if (num_l && num_r && (nullable_int_value(c, cr) || nullable_int_value(c, cav[0]))) return 1; if ((num_l && num_r && !flo) || (lt == TY_STRING && at == TY_STRING && str_lit) || (lt == TY_SYMBOL && at == TY_SYMBOL) || (lt == TY_TIME && at == TY_TIME)) return 0; } return 1; } if (nullable_int_call_name(nt_str(nt, v, "name"))) return 1; /* `o.instance_variable_set(:@x, v)` answers v */ if (sp_streq(nt_str(nt, v, "name"), "instance_variable_set") && nt_ref(nt, v, "receiver") >= 0) { int sa = nt_ref(nt, v, "arguments"), sn = 0; const int *sv = sa >= 0 ? nt_arr(nt, sa, "arguments", &sn) : NULL; if (sn == 2 && (nt_kind(nt, sv[1]) == NK_NilNode || nullable_int_value(c, sv[1]))) return 1; } if (file_stat_nil_call(c, v)) return 1; /* a Range's omitted bound is nil: (1.0..).end, (..2).begin. A literal with both ends has both */ { const char *bn = nt_str(nt, v, "name"); int br = nt_ref(nt, v, "receiver"); if (br >= 0 && bn && nt_ref(nt, v, "arguments") < 0 && (sp_streq(bn, "begin") || sp_streq(bn, "end"))) { TyKind brt = infer_type(c, br); int lit = nt_paren_single(nt, br); int both = lit >= 0 && nt_kind(nt, lit) == NK_RangeNode && nt_ref(nt, lit, "left") >= 0 && nt_ref(nt, lit, "right") >= 0; if ((brt == TY_RANGE || brt == TY_FLOAT_RANGE) && !both) return 1; } } if (nn_call_unboxes_nil(c, v)) return 1; if (sg_civ_reader_call(c, v)) return 1; /* A setter assignment answers its RHS, not the writer's return. Its nullable scalar must survive when the assignment itself is boxed. The check above already accounts for a safe-navigation receiver. */ if (call_is_setter_assign(nt, v)) { int ca = nt_ref(nt, v, "arguments"), an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (an == 1) return nullable_int_value(c, av[0]); } /* a missed element read is nil in an int slot; only boxing is affected, typed reads keep their inline arms */ if (elem_miss_call(c, v)) return 1; /* a method whose --rbs signature pins `Integer?`, or whose own return expression can be nil: either way it answers its nil beside the value and a caller that boxes the value has to answer nil. Resolved the way emission resolves it. */ const char *cn = nt_str(nt, v, "name"); int rcv0 = nt_ref(nt, v, "receiver"); /* an element read out of an array some element of which is nil */ if (rcv0 >= 0 && elem_returning_call(cn) && nullable_int_elem_expr(c, rcv0, 0)) return 1; /* Array#fetch answers its default argument, or its block's value, for an index out of range: either can be nil */ if (rcv0 >= 0 && cn && sp_streq(cn, "fetch") && ty_is_array(infer_type(c, rcv0))) { int fa = nt_ref(nt, v, "arguments"), fn = 0; const int *fav = fa >= 0 ? nt_arr(nt, fa, "arguments", &fn) : NULL; if (fn == 2 && (nt_kind(nt, fav[1]) == NK_NilNode || nullable_int_value(c, fav[1]))) return 1; int ft = call_block_tail(c, v); if (fn == 1 && ft >= 0 && (nt_kind(nt, ft) == NK_NilNode || nullable_int_value(c, ft))) return 1; } /* a fold's value is its block's, and `find`/`detect` hand back an element */ if (cn && (is_reduce_alias(cn))) { int ft = call_block_tail(c, v); if (ft >= 0 && nullable_int_value(c, ft)) return 1; } if (rcv0 >= 0 && cn && (is_find_alias(cn)) && nullable_int_elem_expr(c, rcv0, 0)) return 1; int rcv = nt_ref(nt, v, "receiver"); /* a receiverless call resolves as emission does: the class-method chain in a class method, then the instance chain, a top-level def last */ int mi = cn && rcv < 0 ? comp_self_call_mi(c, v, cn) : (cn ? call_target(c, v, -1, cn, CT_TOP, NULL) : -1); /* the builtin's own answer: no method of the program is a target -- the dispatch's builtin arm re-entering its call (g_nn_builtin_call), and a call the desugaring made the builtin's alone (builtin_only: the length probe a splat's arms branch on, `a.length == 1`, as splat_dispatch_on_length writes it). Asked of the program's methods of the name, the probe took the nil a `def length = nil` answers and was emitted as an oint, while its emitter -- the builtin's, by the same mark -- answers the plain sp_int, and the C did not compile. */ if (v == g_nn_builtin_call || nt_int(nt, v, "builtin_only", 0)) return 0; if (mi < 0 && cn) { if (rcv >= 0) { TyKind rt = infer_type(c, rcv); /* an attr reader answers its ivar, nil included: an Integer or Float one some write left nil, or a Float one initialize need not assign. Asking only the Float once let `A.new(nil).x > 0` of an Integer ivar compare its nil as a number. */ int rdc = -1; if (ty_is_object(rt) && comp_reader_in_chain(c, ty_object_class(rt), cn, &rdc)) { int k = rdc >= 0 ? rdc : ty_object_class(rt); char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, ty_object_class(rt), cn)); int iv = comp_ivar_index(&c->classes[k], ivb); TyKind ivt = iv >= 0 ? c->classes[k].ivar_types[iv] : TY_UNKNOWN; if ((ivt == TY_FLOAT || ivt == TY_INT) && c->classes[k].ivar_nullable_int[iv]) return 1; if (ivt == TY_FLOAT && !ivar_assigned_in_initialize(c, k, ivb)) return 1; } /* ... and so does a Struct member's, which is no attr_reader */ else if (ty_is_object(rt) && call_target(c, v, ty_object_class(rt), cn, 0, NULL) < 0) { ClassInfo *sci = NULL; int sm = struct_member_slot(c, ty_object_class(rt), cn, &sci); if (sm >= 0 && sci->ivar_nullable_int[sm]) return 1; } if (ty_is_object(rt)) mi = call_target(c, v, ty_object_class(rt), cn, 0, NULL); else if (nt_kind(nt, rcv) == NK_ConstantReadNode || nt_kind(nt, rcv) == NK_ConstantPathNode) { int rci = const_node_class_index(c, rcv); if (rci >= 0) mi = call_target(c, v, rci, cn, CT_CMETHOD, NULL); } /* the receiver stayed poly, so no single callee resolves -- fall back to the runtime dispatch set. Not for a builtin_only call (a desugar's own count of a splat, `args.length` in the arity test a `recipient.public_send(m, *args)` or `h.slice(*keys)` lowers to): no method of the program is a target of it, and its emitter takes the builtin's arm alone (emit_call), whose Integer is plain. Marked nil-able by a program method of the name that answers nil (`def length = nil` in an unrelated class), the call's slot was an sp_oint the builtin's sp_int could not initialize. */ else if (rt == TY_POLY && !nt_int(nt, v, "builtin_only", 0)) return poly_dispatch_nullable(c, v, cn); /* a class or module value only the run time knows (`g.upd` with g a module): any class's method of the name the dispatch can reach, as emit's class-value switch reaches them (#8985) */ else if (rt == TY_CLASS) { for (int k = 0; k < c->nclasses; k++) { if (is_builtin_reopen(c->classes[k].name)) continue; int kmi = call_target(c, v, k, cn, CT_CMETHOD, NULL); if (kmi >= 0 && c->scopes[kmi].ret_nullable_int) return 1; } } } } if (mi >= 0 && c->scopes[mi].ret_nullable_int) return 1; /* A bare (receiverless) call to an attr_reader resolves `mi` through comp_self_call_mi straight to the synthesized `def x = @x` scope, but that scope's own ret_nullable_int is never marked by the tail- nullability fixpoint above when a sibling write (`self.x = ` in the same or another method) settles the ivar's inferred TYPE to a concrete Integer/Float only after this scope's own tail was already checked -- the fixpoint revisits a scope's return type, but nothing re-walks a trivial reader's already-resolved tail once the ivar backing it turns out nullable. The explicit-receiver arm above (`rcv >= 0`) already re-derives an attr reader's nullability straight from ivar_nullable_int instead of trusting ret_nullable_int; apply the same re-derivation for the receiverless form (found 2026-10-12, kilau K-017: `if id.nil? ... else ... self.id = ... end` kept answering id.nil? as a compile-time false, running the elsif branch unconditionally, while `if self.id.nil?` -- same method, explicit receiver -- answered correctly). */ if (rcv < 0 && cn) { Scope *cs = comp_scope_of(c, v); int scid = cs ? cs->class_id : -1; int rdc = -1; if (scid >= 0 && comp_reader_in_chain(c, scid, cn, &rdc)) { int k = rdc >= 0 ? rdc : scid; char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, scid, cn)); int iv = comp_ivar_index(&c->classes[k], ivb); TyKind ivt = iv >= 0 ? c->classes[k].ivar_types[iv] : TY_UNKNOWN; if ((ivt == TY_FLOAT || ivt == TY_INT) && c->classes[k].ivar_nullable_int[iv]) return 1; /* `ivar_assigned_in_initialize` means "a literal `def initialize` assigns it": a Struct.new/Data.define member is always populated through its auto-generated constructor instead, so this answers false for EVERY member regardless of whether it can really hold nil -- wrongly flagging `Struct.new(:amt) { def cost = amt }`'s bare `cost` call nullable (sp_ofloat) when codegen's own struct- member reader returns a plain sp_float, a C type mismatch (found 2026-10-12 testing this fix: kilau/test's issue_2872, struct_block_overrides_bracket). The explicit-receiver arm above never asks this question for a struct member at all -- its own `else if` branch answers the struct case purely from ivar_nullable_int, already checked above. Mirror that boundary here instead of trusting the initialize-based heuristic for a class ivar_nullable_int already said no about. */ if (ivt == TY_FLOAT && !c->classes[k].is_struct && !ivar_assigned_in_initialize(c, k, ivb)) return 1; } } /* the methods the call dispatches over, by the receiver's class: self's for a receiverless call, an object's, or a class constant's class methods */ TyKind vt = infer_type(c, v); if (vt != TY_INT && vt != TY_FLOAT && vt != TY_NIL) return 0; if (rcv < 0) { if (mi >= 0 && method_answers_nil(&c->scopes[mi])) return 1; Scope *ss = comp_scope_of(c, v); return ss && ss->class_id >= 0 && dispatch_answers_nil(c, v, ss->class_id, cn, ss->is_cmethod && mi >= 0 && c->scopes[mi].is_cmethod); } TyKind rt = infer_type(c, rcv); if (ty_is_object(rt)) return dispatch_answers_nil(c, v, ty_object_class(rt), cn, 0); if (nt_kind(nt, rcv) == NK_ConstantReadNode || nt_kind(nt, rcv) == NK_ConstantPathNode) return dispatch_answers_nil(c, v, const_node_class_index(c, rcv), cn, 1); /* `self.class.m` in an instance method: the class methods of self's class and of every class below it (codegen's dispatch switch) */ if (nt_kind(nt, rcv) == NK_CallNode && sp_streq(nt_str(nt, rcv, "name"), "class") && nt_ref(nt, rcv, "arguments") < 0) { int x = nt_ref(nt, rcv, "receiver"); Scope *ss = x < 0 || nt_kind(nt, x) == NK_SelfNode ? comp_scope_of(c, v) : NULL; if (ss && !ss->is_cmethod && ss->class_id >= 0) return dispatch_answers_nil(c, v, ss->class_id, cn, 1); } return 0; } if (nt_kind(nt, v) == NK_LocalVariableReadNode) { const char *rn = nt_str(nt, v, "name"); Scope *rs = rn ? comp_scope_of(c, v) : NULL; LocalVar *rv = rs ? scope_local(rs, rn) : NULL; /* a local that only ever holds nil is nil wherever it is read */ if (rv && rv->nil_only) return 1; return rv && rv->nullable_int && !nn_read_nonnil(c, v, rn); } /* `x &&= v` answers x's nil when x is nil, and `x ||= v` answers v when x is nil, so the value can be nil as the local or v can */ if (nt_kind(nt, v) == NK_LocalVariableAndWriteNode) { const char *rn = nt_str(nt, v, "name"); Scope *rs = rn ? comp_scope_of(c, v) : NULL; LocalVar *rv = rs ? scope_local(rs, rn) : NULL; return rv && (rv->type == TY_INT || rv->type == TY_FLOAT) && rv->nullable_int; } if (nt_kind(nt, v) == NK_LocalVariableOrWriteNode) return nullable_int_value(c, nt_ref(nt, v, "value")); /* `@x &&= v` answers @x's nil when it is nil (unset); `@x ||= v` answers v where it assigns */ if (nt_kind(nt, v) == NK_InstanceVariableAndWriteNode) return 1; if (nt_kind(nt, v) == NK_InstanceVariableOrWriteNode) return nullable_int_value(c, nt_ref(nt, v, "value")); /* `h[k] &&= v` answers the element when it is nil (a miss); `h[k] ||= v` answers v when it assigns */ if (nt_kind(nt, v) == NK_IndexAndWriteNode) return 1; if (nt_kind(nt, v) == NK_IndexOrWriteNode) return nullable_int_value(c, nt_ref(nt, v, "value")); /* ... and `o.x &&= v` / `o.x ||= v` the same of the attribute */ if (nt_kind(nt, v) == NK_CallAndWriteNode) return 1; if (nt_kind(nt, v) == NK_CallOrWriteNode) { int ov = nt_ref(nt, v, "value"); return ov < 0 || nt_kind(nt, ov) == NK_NilNode || nullable_int_value(c, ov); } /* `x = v` answers v: `s0 = s1 = nil` hands s0 the inner write's nil */ if (nt_kind(nt, v) == NK_LocalVariableWriteNode) return nullable_int_value(c, nt_ref(nt, v, "value")); /* ... and so do `@@x = v`, `@x = v`, `$x = v` (`def self.b = (@@x = nil)`) */ if (nt_kind(nt, v) == NK_ClassVariableWriteNode || nt_kind(nt, v) == NK_InstanceVariableWriteNode || nt_kind(nt, v) == NK_GlobalVariableWriteNode) { int wv = nt_ref(nt, v, "value"); return wv >= 0 && (nt_kind(nt, wv) == NK_NilNode || nullable_int_value(c, wv)); } return 0; } /* ---- array-or-nil proof (GC root elision) ---- A poly slot proven to hold only a poly array or nil has an index read that takes the runtime's inline array arm: no allocation, and no re-entry into Ruby code (the Struct arm calls a generated `to_h`, the String arm allocates a character). That is what lets codegen drop the slot's GC root -- the value stays reachable from the container it was read out of, and nothing in its live range can move it. Conservative in both directions: anything the walk does not recognise answers "no". */ static int aon_value(Compiler *c, int v, int depth); /* What a node on a local's write list means to the scans below: 1 for a write that assigns its `value` (`x = v`, `x ||= v`, `x &&= v`), -1 for one that assigns what an operator answered (`x += v`), which nothing here reads, 0 for a write in a branch that was pruned, which assigns nothing. */ static int aon_write_kind(const NodeTable *nt, int id) { switch (nt_kind(nt, id)) { case NK_LocalVariableWriteNode: case NK_LocalVariableOrWriteNode: case NK_LocalVariableAndWriteNode: return 1; case NK_LocalVariableOperatorWriteNode: return -1; default: return 0; } } /* A multiple assignment, a `for`, a rescue or a pattern binds a local through a target, to a value the write list does not hold. */ static int aon_local_is_target(Compiler *c, Scope *sc, const char *name) { NT_FOREACH_KIND(c->nt, NK_LocalVariableTargetNode, t) { const char *tn = nt_str(c->nt, t, "name"); if (tn && sp_streq(tn, name) && comp_scope_of(c, t) == sc) return 1; } return 0; } /* Is every ELEMENT of the container-valued expression `v` an array or nil? */ static int aon_container(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 8) return 0; if (nt_kind(nt, v) == NK_ArrayNode) { int en = 0; const int *els = nt_arr(nt, v, "elements", &en); for (int k = 0; els && k < en; k++) if (!aon_value(c, els[k], depth + 1)) return 0; return en > 0; } if (nt_kind(nt, v) == NK_CallNode) { const char *nm = nt_str(nt, v, "name"); int rc = nt_ref(nt, v, "receiver"); /* `[nil] * n` and `Array.new(n)` fill with nil; `xs.map { [..] }` fills with whatever the block's tail builds. */ if (nm && rc >= 0 && (sp_streq(nm, "*") || sp_streq(nm, "freeze") || sp_streq(nm, "dup") || sp_streq(nm, "clone") || sp_streq(nm, "to_a"))) return aon_container(c, rc, depth + 1); if (nm && (is_map_alias(nm))) { int blk = nt_ref(nt, v, "block"); int body = blk >= 0 ? nt_ref(nt, blk, "body") : -1; if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) return 0; int bn = 0; const int *bs = nt_arr(nt, body, "body", &bn); return bs && bn > 0 && aon_value(c, bs[bn - 1], depth + 1); } if (nm && sp_streq(nm, "new") && rc >= 0 && nt_kind(nt, rc) == NK_ConstantReadNode) { const char *rn = nt_str(nt, rc, "name"); int blk = nt_ref(nt, v, "block"); if (rn && sp_streq(rn, "Array") && blk < 0) { int ca = nt_ref(nt, v, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; return an == 1 || (an >= 2 && aon_value(c, av[1], depth + 1)); } } return 0; } if (nt_kind(nt, v) == NK_InstanceVariableReadNode) { ClassInfo *ci = NULL; int iv = nullable_elem_ivar(c, v, &ci); return iv >= 0 && ci->ivar_arr_elem_arr_or_nil[iv]; } /* a container LOCAL: the same scan as the ivar case, over its own writes and over every store into it. `entries[key] ||= [..]` fills a hash this way, and an `.map` over it is what the ivar above ends up holding. */ if (nt_kind(nt, v) == NK_LocalVariableReadNode) { Scope *sc = comp_scope_of(c, v); const char *nm = nt_str(nt, v, "name"); if (!sc || !nm) return 0; int saw = 0; for (int r = lw_shared_first(c, nm, (int)(sc - c->scopes)); r >= 0; r = lw_shared_next(r)) { int id = lw_shared_node(r); const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, nm) || comp_scope_of(c, id) != sc) continue; int wk = aon_write_kind(nt, id); if (wk < 0) return 0; if (!wk) continue; int wv = nt_ref(nt, id, "value"); /* an empty literal carries no element evidence of its own; the stores below are what fill it */ if (wv >= 0 && (nt_kind(nt, wv) == NK_HashNode || nt_kind(nt, wv) == NK_ArrayNode)) { int en = 0; nt_arr(nt, wv, "elements", &en); if (en == 0) { saw = 1; continue; } } if (!aon_container(c, wv, depth + 1)) return 0; saw = 1; } if (!saw || aon_local_is_target(c, sc, nm)) return 0; /* every store into it must put an array or nil there */ NT_FOREACH_KIND(nt, NK_CallNode, w) { const char *wn2 = nt_str(nt, w, "name"); if (!wn2 || (!sp_streq(wn2, "[]=") && !sp_streq(wn2, "push") && !sp_streq(wn2, "<<") && !sp_streq(wn2, "unshift") && !sp_streq(wn2, "store"))) continue; int wr = nt_ref(nt, w, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode) continue; const char *rn2 = nt_str(nt, wr, "name"); if (!rn2 || !sp_streq(rn2, nm) || comp_scope_of(c, wr) != sc) continue; int ca = nt_ref(nt, w, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; int val = an >= 1 ? av[an - 1] : -1; if (!aon_value(c, val, depth + 1)) return 0; } NT_FOREACH_KIND(nt, NK_IndexOrWriteNode, w) { int wr = nt_ref(nt, w, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_LocalVariableReadNode) continue; const char *rn2 = nt_str(nt, wr, "name"); if (!rn2 || !sp_streq(rn2, nm) || comp_scope_of(c, wr) != sc) continue; if (!aon_value(c, nt_ref(nt, w, "value"), depth + 1)) return 0; } return 1; } return 0; } /* Is the value of `v` an array or nil? */ static int aon_value(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (v < 0 || depth > 8) return 0; switch (nt_kind(nt, v)) { case NK_NilNode: case NK_ArrayNode: return 1; /* `a[i] = x = `: an assignment's value is what it assigned */ case NK_LocalVariableWriteNode: case NK_InstanceVariableWriteNode: return aon_value(c, nt_ref(nt, v, "value"), depth + 1); case NK_LocalVariableReadNode: { Scope *sc = comp_scope_of(c, v); const char *nm = nt_str(nt, v, "name"); LocalVar *lv = nm && sc ? scope_local(sc, nm) : NULL; return lv && lv->arr_or_nil == 1; } case NK_CallNode: { const char *nm = nt_str(nt, v, "name"); int rc = nt_ref(nt, v, "receiver"); if (!nm || rc < 0) return 0; /* one element out of a container whose elements are all arrays or nil */ if (sp_streq(nm, "[]") || sp_streq(nm, "at") || sp_streq(nm, "fetch")) return aon_container(c, rc, depth + 1); return 0; } default: return 0; } } /* Public form of the array-or-nil proof, for codegen to emit an index without the poly dispatch. Deliberately narrower than aon_value: only a LOCAL the fixpoint marked, not the `container[i]` case aon_value also admits. That case is sound for what it was written for -- the root elision asks whether a read can allocate or re-enter, and reading a non-array through the general helper does neither -- but not for choosing the helper, where being wrong about the receiver's kind is a wrong VALUE. A container whose elements are "all arrays or nil" by the container proof turned out to include one that was neither (test/rbs-seed/nilable_scalar_paths). */ int expr_is_arr_or_nil(Compiler *c, int v) { if (v < 0 || nt_kind(c->nt, v) != NK_LocalVariableReadNode) return 0; Scope *sc = comp_scope_of(c, v); const char *nm = nt_str(c->nt, v, "name"); LocalVar *lv = nm && sc ? scope_local(sc, nm) : NULL; return lv && lv->arr_or_nil == 1 && lv->type == TY_POLY; } /* Prove the flags to a fixpoint: a local reads out of an ivar whose elements come from another ivar, so one pass is not enough. Monotone (flags only get set), bounded by the number of flags. */ static void mark_array_or_nil_slots(Compiler *c) { const NodeTable *nt = c->nt; long rounds = c->nscopes + 2; for (int ci = 0; ci < c->nclasses; ci++) rounds += c->classes[ci].nivars; for (long round = 0; round < rounds; round++) { int changed = 0; /* container ivars: every write must fill with arrays or nil */ NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, id) { ClassInfo *ci = NULL; int iv = nullable_elem_ivar(c, id, &ci); if (iv < 0 || ci->ivar_arr_elem_arr_or_nil[iv]) continue; if (ci->ivar_types[iv] != TY_POLY_ARRAY) continue; int v = nt_ref(nt, id, "value"); if (v < 0 || !aon_container(c, v, 0)) continue; /* every element STORE into it must agree too */ int ok = 1; NT_FOREACH_KIND(nt, NK_CallNode, w) { const char *wn = nt_str(nt, w, "name"); if (!wn || (!sp_streq(wn, "[]=") && !sp_streq(wn, "push") && !sp_streq(wn, "<<") && !sp_streq(wn, "unshift"))) continue; int wr = nt_ref(nt, w, "receiver"); if (wr < 0 || nt_kind(nt, wr) != NK_InstanceVariableReadNode) continue; ClassInfo *wc = NULL; int wiv = nullable_elem_ivar(c, wr, &wc); if (wc != ci || wiv != iv) continue; int ca = nt_ref(nt, w, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; int val = sp_streq(wn, "[]=") ? (an >= 2 ? av[an - 1] : -1) : (an >= 1 ? av[0] : -1); if (!aon_value(c, val, 0)) { ok = 0; break; } } if (ok) { ci->ivar_arr_elem_arr_or_nil[iv] = 1; changed = 1; } } /* locals: every write must be an array, nil, or such an element read */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if (lv->arr_or_nil || lv->type != TY_POLY || lv->is_param || lv->is_block_param) continue; if (lv->is_cell || lv->rbs_seeded) continue; int saw = 0, ok = 1; for (int r = lw_shared_first(c, lv->name, s); r >= 0 && ok; r = lw_shared_next(r)) { int id = lw_shared_node(r); const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, lv->name) || comp_scope_of(c, id) != sc) continue; int wk = aon_write_kind(nt, id); if (wk < 0) { ok = 0; break; } if (!wk) continue; saw = 1; if (!aon_value(c, nt_ref(nt, id, "value"), 0)) ok = 0; } if (saw && ok && !aon_local_is_target(c, sc, lv->name)) { lv->arr_or_nil = 1; changed = 1; } } } if (!changed) break; } } /* Can one of the values a block site passes be an Integer's or a Float's nil? Asked of every site before its block is typed again below, so the common program -- no site passes one -- pays for this walk alone. */ /* Under --int-overflow=promote an Integer variable that can hold nil is widened to the box (an ivar, a local, a parameter, a global, a class variable): such a read handed to a block parameter, which keeps its Integer type, can be nil. */ int promote_boxed_var_read(Compiler *c, int v) { if (v < 0) return 0; switch (nt_kind(c->nt, v)) { case NK_InstanceVariableReadNode: case NK_GlobalVariableReadNode: case NK_ClassVariableReadNode: return c->ntype[v] == TY_POLY; case NK_LocalVariableReadNode: { const char *nm = nt_str(c->nt, v, "name"); Scope *s = nm ? comp_scope_of(c, v) : NULL; LocalVar *lv = s ? scope_local(s, nm) : NULL; return (lv && lv->type == TY_POLY) || c->ntype[v] == TY_POLY; } default: return 0; } } static int site_args_may_be_nil(Compiler *c, const int *av, int an) { const NodeTable *nt = c->nt; for (int k = 0; k < an; k++) { int v = av[k]; if (v >= 0 && nt_kind(nt, v) == NK_SplatNode) { int x = nt_ref(nt, v, "expression"); int en = 0; const int *el = x >= 0 && nt_kind(nt, x) == NK_ArrayNode ? nt_arr(nt, x, "elements", &en) : NULL; if (el && site_args_may_be_nil(c, el, en)) return 1; continue; } TyKind t = v >= 0 ? c->ntype[v] : TY_UNKNOWN; if ((t == TY_INT || t == TY_FLOAT) && nullable_int_value(c, v)) return 1; /* under promote a boxed ivar (an Integer member) can hand its nil too */ if (g_promote_mode && promote_boxed_var_read(c, v)) return 1; } return 0; } /* The marks a call's arguments hand the parameters of method `mi` it binds: an object parameter handed nil, an array one handed an array holding a nil, an Integer or Float one handed a value that can be nil -- a keyword parameter by the value its key names (`k: nil`). Returns 1 if any mark was set. */ /* ---- strict parameters ---- A parameter is strict when the method's first evaluated statement raises on it being nil before anything observable runs: the receiver of an arithmetic or ordering operator (NoMethodError "undefined method 'op' for nil"), or the operand of an arithmetic one on an Integer / Float receiver (TypeError "nil can't be coerced into Integer"). Its slot is then plain: a caller handing a value that can be nil evaluates every argument, then raises that same error in the callee's order (param_strict's ord) where the value is nil. Nothing is trusted that a wrong answer could hide: a nil still raises its own error, at the call rather than in the callee. Refused: a method reached other than by a plain positional call resolved to it (a boxed receiver, a Symbol or String naming it, a splat, a keyword, a block argument), one redefined, overridden or aliased, method_missing / respond_to_missing? anywhere, a first use under a rescue or ensure, and an operator the program reopens on nil or the number. */ typedef struct { signed char kind[16]; const char *what[16]; signed char ord[16]; int done; } PStrict; static PStrict *g_pstrict = NULL; static int g_pstrict_n = 0; static const NodeTable *g_pstrict_nt = NULL; static int ps_pure(Compiler *c, Scope *m, int n) { const NodeTable *nt = c->nt; switch (nt_kind(nt, n)) { case NK_IntegerNode: case NK_FloatNode: case NK_LocalVariableReadNode: case NK_SelfNode: case NK_TrueNode: case NK_FalseNode: case NK_NilNode: return 1; case NK_ConstantReadNode: { LocalVar *cv = comp_const(c, nt_str(nt, n, "name")); return cv != NULL; } default: (void)m; return 0; } } static int ps_param_index(Compiler *c, Scope *m, int n) { const NodeTable *nt = c->nt; if (nt_kind(nt, n) != NK_LocalVariableReadNode || comp_scope_of(c, n) != m) return -1; const char *nm = nt_str(nt, n, "name"); for (int k = 0; nm && k < m->nparams && k < 16; k++) if (m->pnames[k] && sp_streq(m->pnames[k], nm)) return k; return -1; } static int ps_op_reopened(Compiler *c, const char *op) { int nc = comp_core_class(c, CORE_NILCLASS); if (nc >= 0 && comp_method_in_chain(c, nc, op, NULL) >= 0) return 1; return comp_builtin_kind_reopen_mi(c, TY_INT, op) >= 0 || comp_builtin_kind_reopen_mi(c, TY_FLOAT, op) >= 0; } /* Walk n in evaluation order, recording each parameter's first use; 0 once something not understood is reached (every parameter not yet seen is then not strict) */ static int ps_walk(Compiler *c, Scope *m, int n, PStrict *ps, char *seen, int *ord) { const NodeTable *nt = c->nt; if (n < 0) return 1; NodeKind k = nt_kind(nt, n); if (k == NK_ParenthesesNode) { /* `(expr)`: its one expression; a sequence stops at its first */ int b = nt_ref(nt, n, "body"); if (b < 0) return 0; if (nt_kind(nt, b) != NK_StatementsNode) return ps_walk(c, m, b, ps, seen, ord); int bn = 0; const int *bs = nt_arr(nt, b, "body", &bn); if (bn == 1) return ps_walk(c, m, bs[0], ps, seen, ord); if (bn > 1) (void)ps_walk(c, m, bs[0], ps, seen, ord); return 0; } if (k == NK_StatementsNode) { /* the method's body: its first statement, then no further */ int bn = 0; const int *bs = nt_arr(nt, n, "body", &bn); if (bn >= 1) (void)ps_walk(c, m, bs[0], ps, seen, ord); return 0; } int pi = ps_param_index(c, m, n); if (pi >= 0) { seen[pi] = 1; return 1; } /* a bare use: not strict */ if (ps_pure(c, m, n)) return 1; if (k != NK_CallNode) return 0; const char *op = nt_str(nt, n, "name"); int r = nt_ref(nt, n, "receiver"), a = nt_ref(nt, n, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!op || r < 0 || an != 1 || nt_ref(nt, n, "block") >= 0) return 0; static const char *const RECV_OPS[] = { "+", "-", "*", "/", "%", "**", "<", ">", "<=", ">=", "<<", ">>", NULL }; static const char *const ARG_OPS[] = { "+", "-", "*", "/", "%", "**", NULL }; if (!str_in(op, RECV_OPS) || ps_op_reopened(c, op)) return 0; TyKind rt = comp_ntype(c, r); if (rt != TY_INT && rt != TY_FLOAT) return 0; /* the receiver evaluates first, then the argument, then the operator raises for a nil receiver: the argument must run nothing */ int rp = ps_param_index(c, m, r); if (rp >= 0) { if (!ps_pure(c, m, av[0]) && ps_param_index(c, m, av[0]) < 0) return 0; if (!seen[rp]) { seen[rp] = 1; ps->kind[rp] = 1; ps->what[rp] = op; ps->ord[rp] = (signed char)(*ord)++; } } else if (!ps_walk(c, m, r, ps, seen, ord)) return 0; int ap = ps_param_index(c, m, av[0]); if (ap >= 0) { if (!seen[ap]) { seen[ap] = 1; TyKind at = comp_ntype(c, av[0]); if (str_in(op, ARG_OPS) && (at == TY_INT || at == TY_FLOAT)) { ps->kind[ap] = 2; ps->what[ap] = rt == TY_FLOAT ? "Float" : "Integer"; ps->ord[ap] = (signed char)(*ord)++; } } return 1; } return ps_walk(c, m, av[0], ps, seen, ord); } /* The program's names, sorted once so each method looks its own up rather than rescanning the program: the scopes by name, the Symbol / String values, and the calls by name (in node order within a name). The node names are copied into the index's own block: a rewrite that renames a call for a moment (emit_str_mutator_call emits `s.upcase!` through `upcase` and puts the name back) frees the string the node held, and the index read it after. */ typedef struct { const char *name; int id; } PsName; static PsName *g_ps_scopes, *g_ps_lits, *g_ps_calls; static int g_ps_nscopes, g_ps_nlits, g_ps_ncalls, g_ps_mm, g_ps_ncount = -1; static int ps_name_cmp(const void *a, const void *b) { const PsName *x = a, *y = b; int r = strcmp(x->name, y->name); return r ? r : x->id - y->id; } /* the first entry of v (sorted) named name, or n */ static int ps_name_first(const PsName *v, int n, const char *name) { int lo = 0, hi = n; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (strcmp(v[mid].name, name) < 0) lo = mid + 1; else hi = mid; } return lo; } /* n entries and `bytes` of name text after them, *pool at the text */ static PsName *ps_names_alloc(int n, size_t bytes, char **pool) { size_t cnt = (size_t)(n > 0 ? n : 1); PsName *v = malloc(sizeof *v * cnt + bytes); if (!v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (pool) *pool = (char *)(v + cnt); return v; } static const char *ps_name_copy(char **pool, const char *s) { size_t n = strlen(s) + 1; char *d = *pool; memcpy(d, s, n); *pool += n; return d; } static const char *ps_lit_name(const NodeTable *nt, int id) { if (nt_kind(nt, id) == NK_SymbolNode) return nt_str(nt, id, "value"); const char *v = nt_str(nt, id, "unescaped"); return v ? v : nt_content(nt, id); } static void ps_index_build(Compiler *c) { const NodeTable *nt = c->nt; free(g_ps_scopes); free(g_ps_lits); free(g_ps_calls); g_ps_ncount = nt->count; g_ps_scopes = ps_names_alloc(c->nscopes, 0, NULL); g_ps_nscopes = 0; g_ps_mm = 0; for (int k = 0; k < c->nscopes; k++) { const char *sn = c->scopes[k].name; if (!sn) continue; /* method_missing / respond_to_missing? anywhere: a call can land anywhere */ if (sp_streq(sn, "method_missing") || sp_streq(sn, "respond_to_missing?")) g_ps_mm = 1; g_ps_scopes[g_ps_nscopes++] = (PsName){ sn, k }; } int nl = 0, ncall = 0; size_t lbytes = 0, cbytes = 0; NT_FOREACH_KIND(nt, NK_SymbolNode, id) { const char *v = ps_lit_name(nt, id); if (v) { nl++; lbytes += strlen(v) + 1; } } NT_FOREACH_KIND(nt, NK_StringNode, id) { const char *v = ps_lit_name(nt, id); if (v) { nl++; lbytes += strlen(v) + 1; } } NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *cn = nt_str(nt, id, "name"); if (cn) { ncall++; cbytes += strlen(cn) + 1; } } char *lpool, *cpool; g_ps_lits = ps_names_alloc(nl, lbytes, &lpool); g_ps_calls = ps_names_alloc(ncall, cbytes, &cpool); g_ps_nlits = 0; g_ps_ncalls = 0; NT_FOREACH_KIND(nt, NK_SymbolNode, id) { const char *v = ps_lit_name(nt, id); if (v) g_ps_lits[g_ps_nlits++] = (PsName){ ps_name_copy(&lpool, v), id }; } NT_FOREACH_KIND(nt, NK_StringNode, id) { const char *v = ps_lit_name(nt, id); if (v) g_ps_lits[g_ps_nlits++] = (PsName){ ps_name_copy(&lpool, v), id }; } NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *cn = nt_str(nt, id, "name"); if (cn) g_ps_calls[g_ps_ncalls++] = (PsName){ ps_name_copy(&cpool, cn), id }; } qsort(g_ps_scopes, (size_t)g_ps_nscopes, sizeof *g_ps_scopes, ps_name_cmp); qsort(g_ps_lits, (size_t)g_ps_nlits, sizeof *g_ps_lits, ps_name_cmp); qsort(g_ps_calls, (size_t)g_ps_ncalls, sizeof *g_ps_calls, ps_name_cmp); } /* Can method mi be reached only by plain positional calls resolved to it? */ static int ps_reached_plainly(Compiler *c, int mi) { const NodeTable *nt = c->nt; Scope *m = &c->scopes[mi]; const char *nm = m->name; if (!nm || g_ps_mm) return 0; /* redefined in its class, or overridden in a subclass */ for (int i = ps_name_first(g_ps_scopes, g_ps_nscopes, nm); i < g_ps_nscopes && sp_streq(g_ps_scopes[i].name, nm); i++) { int k = g_ps_scopes[i].id; if (k != mi && c->scopes[k].is_cmethod == m->is_cmethod && c->scopes[k].class_id >= 0 && m->class_id >= 0 && (c->scopes[k].class_id == m->class_id || is_descendant(c, c->scopes[k].class_id, m->class_id))) return 0; } /* named by a Symbol or String (send, method, alias, define_method) */ int li = ps_name_first(g_ps_lits, g_ps_nlits, nm); if (li < g_ps_nlits && sp_streq(g_ps_lits[li].name, nm)) return 0; for (int i = ps_name_first(g_ps_calls, g_ps_ncalls, nm); i < g_ps_ncalls && sp_streq(g_ps_calls[i].name, nm); i++) { int id = g_ps_calls[i].id; const char *cn = g_ps_calls[i].name; int r = nt_ref(nt, id, "receiver"); int t = -1; if (r < 0) t = comp_self_call_mi(c, id, cn); else if (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode) { int rc = const_node_class_index(c, r); t = rc >= 0 ? call_target(c, id, rc, cn, CT_CMETHOD, NULL) : -1; } else { TyKind rt = infer_type(c, r); if (rt == TY_POLY || rt == TY_UNKNOWN) return 0; if (ty_is_object(rt)) t = call_target(c, id, ty_object_class(rt), cn, 0, NULL); else continue; /* a builtin's own method of the name */ } if (t < 0 && r >= 0 && ty_is_object(infer_type(c, r))) return 0; if (t != mi) continue; int a = nt_ref(nt, id, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an != m->nparams || nt_ref(nt, id, "block") >= 0) return 0; if (nt_argv_has(nt, av, an, NT_ARG_SPLAT | NT_ARG_KEYWORDS | NT_ARG_BLOCK | NT_ARG_FORWARDING)) return 0; } return 1; } static void ps_compute(Compiler *c, int mi) { PStrict *ps = &g_pstrict[mi]; ps->done = 1; Scope *m = &c->scopes[mi]; if (m->def_node < 0 || m->nparams <= 0 || m->nparams > 16 || m->rest_idx >= 0 || m->kwrest_idx >= 0 || (m->blk_param && m->blk_param[0]) || m->yields) return; for (int k = 0; k < m->nparams; k++) { if (!m->pnames[k] || (m->pdefault && m->pdefault[k] >= 0) || callee_param_is_declared_kwarg(c, m, m->pnames[k])) return; } int body = m->body; /* a method-level rescue / ensure would catch what the call raises */ if (body < 0 || nt_kind(c->nt, body) == NK_BeginNode) return; if (!ps_reached_plainly(c, mi)) return; char seen[16] = {0}; int ord = 0; (void)ps_walk(c, m, body, ps, seen, &ord); /* only a number parameter's nil is decided this way */ for (int k = 0; k < m->nparams; k++) { LocalVar *lv = scope_local(m, m->pnames[k]); if (!lv || (lv->type != TY_INT && lv->type != TY_FLOAT) || lv->is_cell) ps->kind[k] = 0; } } /* Parameter k of method mi is strict: 1 (receiver of `what`), 2 (operand, coerced into `what`), else 0; *ord its place among the checks */ int param_strict(Compiler *c, int mi, int k, const char **what, int *ord) { if (mi < 0 || mi >= c->nscopes || k < 0 || k >= 16) return 0; /* A later widening can box a parameter whose strictness was cached. */ Scope *m = &c->scopes[mi]; LocalVar *p = k < m->nparams && m->pnames[k] ? scope_local(m, m->pnames[k]) : NULL; if (!p || (p->type != TY_INT && p->type != TY_FLOAT)) return 0; if (!g_pstrict || g_pstrict_n < c->nscopes || g_pstrict_nt != c->nt) { free(g_pstrict); g_pstrict = calloc((size_t)c->nscopes + 1, sizeof *g_pstrict); if (!g_pstrict) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } g_pstrict_n = c->nscopes; g_pstrict_nt = c->nt; ps_index_build(c); } /* nodes cloned since; a local variable read appended since (a writer's value temporary) is none of the nodes the index lists */ else if (g_ps_ncount != c->nt->count) { if (g_ps_ncount < c->nt->count && nt_appended_only_local_reads(c->nt, g_ps_ncount)) g_ps_ncount = c->nt->count; else ps_index_build(c); } PStrict *ps = &g_pstrict[mi]; if (!ps->done) ps_compute(c, mi); if (what) *what = ps->what[k]; if (ord) *ord = ps->ord[k]; return ps->kind[k]; } /* Is argument `a` a read of a parameter whose default is nil (`port: nil`)? The value it hands on can be that nil. */ static int arg_reads_nil_default_param(Compiler *c, int a) { const NodeTable *nt = c->nt; if (a < 0 || nt_kind(nt, a) != NK_LocalVariableReadNode) return 0; const char *n = nt_str(nt, a, "name"); Scope *s = comp_scope_of(c, a); for (int k = 0; n && s && s->pdefault && k < s->nparams; k++) if (s->pnames[k] && sp_streq(s->pnames[k], n)) return s->pdefault[k] >= 0 && nt_kind(nt, s->pdefault[k]) == NK_NilNode; return 0; } static int mark_nullable_params_of_call(Compiler *c, int id, int mi) { const NodeTable *nt = c->nt; int changed = 0; Scope *m = &c->scopes[mi]; int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; ArgLayout L; call_layout(c, m, av, an, &L); for (int k = 0; k < m->nparams; k++) { int a = layout_plain_arg(c, m, av, &L, k); LocalVar *p = a >= 0 && m->pnames[k] ? scope_local(m, m->pnames[k]) : NULL; if (!p) continue; /* an object parameter handed nil, or handed one that was (#5088) */ if (ty_is_object(p->type) && !p->obj_nilable) { int nilarg = nt_kind(nt, a) == NK_NilNode; if (!nilarg && nt_kind(nt, a) == NK_LocalVariableReadNode) { Scope *as = comp_scope_of(c, a); const char *an2 = nt_str(nt, a, "name"); LocalVar *al = as && an2 ? scope_local(as, an2) : NULL; nilarg = al && al->is_param && al->obj_nilable; } if (nilarg) { p->obj_nilable = 1; changed = 1; } } /* A boxed Array argument converts into the typed parameter (sp_poly_as_int_array and its kin), a nil element carried as the slot's nil, and nothing marks a box's elements. Only a late widening makes the two differ: --int-overflow=promote boxes `[f(1), f(-1)]` once f answers poly, after the copy min_by runs in took its parameter's type from it, and the nil element read as an Integer. */ if ((p->type == TY_INT_ARRAY || p->type == TY_FLOAT_ARRAY) && !(p->nullable_int_elem & NIE_MARKED) && (nullable_int_elem_expr(c, a, 0) || infer_type(c, a) == TY_POLY_ARRAY || infer_type(c, a) == TY_POLY)) { p->nullable_int_elem |= NIE_MARKED; changed = 1; } /* a nil some other name put into the argument's array is in the parameter's: the proved loop read asks (NIE_REACH) */ if ((p->type == TY_INT_ARRAY || p->type == TY_FLOAT_ARRAY) && !(p->nullable_int_elem & NIE_REACH) && elem_nil_reach_expr(c, a)) { p->nullable_int_elem |= NIE_REACH; changed = 1; } /* ... and the other way: the parameter IS the caller's array, so a nil the method stores into it (`def add(x, k) = x << h[k]`) is in the argument's array too */ if ((p->type == TY_INT_ARRAY || p->type == TY_FLOAT_ARRAY) && (p->nullable_int_elem & NIE_STORED)) changed |= mark_elem_slot(c, a, NIE_STORED | NIE_REACH); /* a mixed parameter boxes the argument's array (mark_boxed_arrays) */ if (p->type == TY_POLY && nie_boxed_store) changed |= mark_boxed_value(c, a); if ((p->type != TY_INT && p->type != TY_FLOAT) || p->nullable_int) continue; /* a strict parameter's nil raises at the call: its slot stays plain */ if (param_strict(c, mi, k, NULL, NULL)) continue; if (nullable_int_value(c, a) || arg_reads_nil_default_param(c, a)) { p->nullable_int = 1; changed = 1; continue; } /* an ivar that can be read before anything assigned it, or a parameter already carrying one: boxing the parameter has to answer nil (#5085) */ if (!p->box_nullable && box_nullable_arg(c, a)) { p->box_nullable = 1; changed = 1; } } /* a keyword parameter takes the value its key names (`k: nil`) */ int kwh = nt_argv_kwh(nt, av, an); for (int k = 0; kwh >= 0 && k < m->nparams; k++) { const char *pk = m->pnames[k]; LocalVar *p = pk ? scope_local(m, pk) : NULL; if (!p || (p->type != TY_INT && p->type != TY_FLOAT) || p->nullable_int || !callee_param_is_declared_kwarg(c, m, pk)) continue; if (nullable_int_value(c, ie_kwhash_value(c, kwh, pk))) { p->nullable_int = 1; changed = 1; } } arg_layout_free(&L); return changed; } /* A bare `super` hands each parameter of the method on as the argument of the parent's matching one, so a parameter that can hold nil (`b = nil`) makes the parent's Integer or Float slot hold its nil too, as the same call spelled `super(a, b)` does through mark_nullable_params_of_call. */ static int zsuper_param_nilable(Scope *s, int idx) { LocalVar *src = idx >= 0 && s->pnames[idx] ? scope_local(s, s->pnames[idx]) : NULL; return src && src->nullable_int; } static int mark_nullable_params_of_zsuper(Compiler *c, int id, int mi) { Scope *s = comp_scope_of(c, id), *m = &c->scopes[mi]; int changed = 0, srest = s->rest_idx, opt_seen = 0; int gather = srest >= 0 && s->pnames[srest]; for (int k = 0; k < m->nparams; k++) { int own = zsuper_param_source(c, s, m, k); int nil = zsuper_param_nilable(s, own); /* A method with a rest passes its positionals as one gathered list, and which of them a parameter of the parent takes depends on how many there are: the layout the types follow (bind_zsuper_params). With the rest empty, the last fixed parameters are the ones a parameter counted from the end takes, and a parent's parameter after an optional takes any of them. */ if (gather && k != m->rest_idx && k != m->kwrest_idx && m->pnames[k] && !callee_param_is_declared_kwarg(c, m, m->pnames[k])) { int optional = m->pdefault && m->pdefault[k] >= 0; if (own > srest) opt_seen |= optional; else if ((m->rest_idx >= 0 && k > m->rest_idx) || (opt_seen && !optional)) { for (int j = 0; j < srest; j++) nil |= zsuper_param_nilable(s, j); } else { opt_seen |= optional; if (k < srest && (m->rest_idx < 0 || k < m->rest_idx)) nil |= zsuper_param_nilable(s, k); } } LocalVar *p = m->pnames[k] ? scope_local(m, m->pnames[k]) : NULL; if (!nil || !p || (p->type != TY_INT && p->type != TY_FLOAT) || p->nullable_int || param_strict(c, mi, k, NULL, NULL)) continue; p->nullable_int = 1; changed = 1; } return changed; } /* Mark the int locals that can hold nil, so codegen gives them a nil flag (an oint) and boxes them as nil rather than as a number. Boxing every int through the nil check costs ~8% on optcarrot -- every pixel goes through it -- so the marking is static and the hot path keeps the plain box. */ /* ---- a scalar local read before any write ---- A Ruby local exists from its first assignment in the source, and reads nil until a write runs. An Integer or Float slot started at 0 / 0.0, so a read no write has reached yet (`x = 5 if c; p x` with c false, a multiple assignment under a modifier `if`) answered 0. A read is covered when an earlier statement of a list around it writes the local on every path; any other read may see the slot unassigned. */ static int du_writes(const NodeTable *nt, int n, const char *nm, int depth); static int du_list_writes(const NodeTable *nt, int stmts, const char *nm, int depth) { int bn = 0; const int *b = stmts >= 0 ? nt_arr(nt, stmts, "body", &bn) : NULL; for (int i = 0; i < bn; i++) if (du_writes(nt, b[i], nm, depth + 1)) return 1; return 0; } /* Does pattern `n` bind local `nm` when it matches? A pattern that binds a name binds it in every alternative it matches by (CRuby refuses a binding under `|`), so any target of the name in it is one. */ static int du_pattern_binds(const NodeTable *nt, int n, const char *nm, int depth) { if (n < 0 || depth > 64) return 0; if (nt_kind(nt, n) == NK_LocalVariableTargetNode) { const char *tn = nt_str(nt, n, "name"); return tn && sp_streq(tn, nm); } for (int i = 0; i < nt_num_refs(nt, n); i++) if (du_pattern_binds(nt, nt_ref_at(nt, n, i), nm, depth + 1)) return 1; for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int k = 0; k < an; k++) if (du_pattern_binds(nt, av[k], nm, depth + 1)) return 1; } return 0; } /* Does evaluating node `n` write local `nm` on every path through it? */ static int du_writes(const NodeTable *nt, int n, const char *nm, int depth) { if (n < 0 || depth > 64) return 0; switch (nt_kind(nt, n)) { case NK_LocalVariableWriteNode: case NK_LocalVariableOrWriteNode: case NK_LocalVariableAndWriteNode: case NK_LocalVariableOperatorWriteNode: case NK_LocalVariableTargetNode: { const char *wn = nt_str(nt, n, "name"); NodeKind wk = nt_kind(nt, n); /* `x &&= v` writes only an x already truthy: an unassigned one stays nil. (`x ||= v` writes a nil x, and `x += v` raises on one.) */ if (wn && sp_streq(wn, nm)) return wk != NK_LocalVariableAndWriteNode; /* the value of `||=` and `&&=` runs only on one side of the test */ if (wk == NK_LocalVariableOrWriteNode || wk == NK_LocalVariableAndWriteNode) return 0; return du_writes(nt, nt_ref(nt, n, "value"), nm, depth + 1); } case NK_MultiWriteNode: case NK_MultiTargetNode: { const char *keys[] = { "lefts", "rights" }; for (int k = 0; k < 2; k++) { int ln = 0; const int *l = nt_arr(nt, n, keys[k], &ln); for (int i = 0; i < ln; i++) if (du_writes(nt, l[i], nm, depth + 1)) return 1; } int r = nt_ref(nt, n, "rest"); if (r >= 0 && du_writes(nt, nt_kind(nt, r) == NK_SplatNode ? nt_ref(nt, r, "expression") : r, nm, depth + 1)) return 1; return du_writes(nt, nt_ref(nt, n, "value"), nm, depth + 1); } case NK_StatementsNode: return du_list_writes(nt, n, nm, depth); /* `case v in ...` (and `v => pat`, lowered to one) writes the name when every arm's pattern binds it or its body writes it, and the case has no else -- CRuby raises NoMatchingPatternError then -- or an else that writes it or raises */ case NK_CaseMatchNode: { if (du_writes(nt, nt_ref(nt, n, "predicate"), nm, depth + 1)) return 1; int wn = 0; const int *w = nt_arr(nt, n, "conditions", &wn); if (wn < 1) return 0; for (int i = 0; i < wn; i++) if (!du_pattern_binds(nt, nt_ref(nt, w[i], "pattern"), nm, depth + 1) && !du_writes(nt, nt_ref(nt, w[i], "statements"), nm, depth + 1)) return 0; int els = nt_ref(nt, n, "else_clause"); if (els < 0) return 1; if (du_writes(nt, els, nm, depth + 1)) return 1; int es = nt_ref(nt, els, "statements"); int en = 0; const int *eb = es >= 0 ? nt_arr(nt, es, "body", &en) : NULL; if (!eb || en < 1 || nt_kind(nt, eb[en - 1]) != NK_CallNode) return 0; const char *rn = nt_str(nt, eb[en - 1], "name"); return rn && sp_streq(rn, "raise") && nt_ref(nt, eb[en - 1], "receiver") < 0; } /* `v => [a, b]` binds the pattern's names, or raises */ case NK_MatchRequiredNode: return du_writes(nt, nt_ref(nt, n, "value"), nm, depth + 1) || du_pattern_binds(nt, nt_ref(nt, n, "pattern"), nm, depth + 1); case NK_ParenthesesNode: return du_writes(nt, nt_ref(nt, n, "body"), nm, depth + 1); case NK_BeginNode: { /* an ensure always runs; a rescue can finish the begin without the body's write, but without one an exception skips what follows */ int en = nt_ref(nt, n, "ensure_clause"); if (en >= 0 && du_writes(nt, nt_ref(nt, en, "statements"), nm, depth + 1)) return 1; if (nt_ref(nt, n, "rescue_clause") >= 0) return 0; return du_writes(nt, nt_ref(nt, n, "statements"), nm, depth + 1); } case NK_IfNode: case NK_UnlessNode: { if (du_writes(nt, nt_ref(nt, n, "predicate"), nm, depth + 1)) return 1; int els = nt_ref(nt, n, nt_kind(nt, n) == NK_IfNode ? "subsequent" : "else_clause"); return els >= 0 && du_writes(nt, nt_ref(nt, n, "statements"), nm, depth + 1) && du_writes(nt, els, nm, depth + 1); } case NK_ElseNode: return du_writes(nt, nt_ref(nt, n, "statements"), nm, depth + 1); case NK_CaseNode: { if (du_writes(nt, nt_ref(nt, n, "predicate"), nm, depth + 1)) return 1; int els = nt_ref(nt, n, "else_clause"); if (els < 0 || !du_writes(nt, els, nm, depth + 1)) return 0; int wn = 0; const int *w = nt_arr(nt, n, "conditions", &wn); for (int i = 0; i < wn; i++) if (!du_writes(nt, nt_ref(nt, w[i], "statements"), nm, depth + 1)) return 0; return 1; } case NK_AndNode: case NK_OrNode: return du_writes(nt, nt_ref(nt, n, "left"), nm, depth + 1); case NK_WhileNode: case NK_UntilNode: return du_writes(nt, nt_ref(nt, n, "predicate"), nm, depth + 1); case NK_CallNode: { if (du_writes(nt, nt_ref(nt, n, "receiver"), nm, depth + 1)) return 1; int a = nt_ref(nt, n, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; for (int i = 0; i < an; i++) if (du_writes(nt, av[i], nm, depth + 1)) return 1; return 0; } default: return 0; } } /* Can the read `rd` of local `nm` run before any write of it? Walks out through the lists around it to the method (a def is the boundary: a local does not cross it), asking each list's statements ahead of the one holding the read. A block's body is walked through to the statements around the call that carries it. */ /* The first statement of list `stmts` that writes `nm` on every path, or the list's length: memoized per (list, name), so a long list read many times is scanned once per local (scale-test's flat programs). */ typedef struct { int stmts; const char *nm; int first; } DUMemo; static DUMemo *du_memo; static int du_memo_cap, du_memo_n; static int du_first_write(const NodeTable *nt, int stmts, const char *nm) { unsigned h = 2166136261u ^ (unsigned)stmts; for (const char *q = nm; *q; q++) { h ^= (unsigned char)*q; h *= 16777619u; } if (du_memo_n * 2 >= du_memo_cap) { int ncap = du_memo_cap ? du_memo_cap * 2 : 1024; DUMemo *nm2 = calloc((size_t)ncap, sizeof *nm2); for (int k = 0; k < ncap; k++) nm2[k].stmts = -1; for (int k = 0; k < du_memo_cap; k++) { if (du_memo[k].stmts < 0) continue; unsigned h2 = 2166136261u ^ (unsigned)du_memo[k].stmts; for (const char *q = du_memo[k].nm; *q; q++) { h2 ^= (unsigned char)*q; h2 *= 16777619u; } unsigned j = h2 & (unsigned)(ncap - 1); while (nm2[j].stmts >= 0) j = (j + 1) & (unsigned)(ncap - 1); nm2[j] = du_memo[k]; } free(du_memo); du_memo = nm2; du_memo_cap = ncap; } unsigned j = h & (unsigned)(du_memo_cap - 1); while (du_memo[j].stmts >= 0) { if (du_memo[j].stmts == stmts && sp_streq(du_memo[j].nm, nm)) return du_memo[j].first; j = (j + 1) & (unsigned)(du_memo_cap - 1); } int bn = 0; const int *b = nt_arr(nt, stmts, "body", &bn); int first = bn; for (int i = 0; i < bn; i++) if (du_writes(nt, b[i], nm, 0)) { first = i; break; } du_memo[j].stmts = stmts; du_memo[j].nm = nm; du_memo[j].first = first; du_memo_n++; return first; } void du_memo_free(void) { free(du_memo); du_memo = NULL; du_memo_cap = du_memo_n = 0; } /* The parent of each node as the program's tree reaches it: a desugar can leave a detached node that still names a live child (the parentheses around a rewritten expression), and an_parent_map, which takes whichever referrer comes last, then walked a read up into that dead copy and found no write ahead of it. A node the program does not reach keeps an_parent_map's answer. */ int *du_parent_map(const NodeTable *nt) { int *par = an_parent_map(nt); if (!par) return NULL; char *seen = calloc((size_t)nt->count + 1, 1); int *stack = malloc(sizeof(int) * ((size_t)nt->count + 1)); if (!seen || !stack) { free(seen); free(stack); return par; } for (int root = 0; root < nt->count; root++) { NodeKind ty_k = nt_kind(nt, root); if (ty_k != NK_ProgramNode || seen[root]) continue; int sp = 0; seen[root] = 1; par[root] = -1; stack[sp++] = root; while (sp > 0) { int n = stack[--sp]; for (int i = 0; i < nt_num_refs(nt, n); i++) { int ch = nt_ref_at(nt, n, i); if (ch < 0 || ch >= nt->count || seen[ch]) continue; seen[ch] = 1; par[ch] = n; stack[sp++] = ch; } for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int k = 0; k < an; k++) { int ch = av[k]; if (ch < 0 || ch >= nt->count || seen[ch]) continue; seen[ch] = 1; par[ch] = n; stack[sp++] = ch; } } } } free(seen); free(stack); return par; } /* Does block or lambda `blk` take `nm` as a parameter? Its body reads the value the call binds; a block-local (`|x; t|`) starts nil, so it is not one. */ static int du_param_binds(const NodeTable *nt, int n, const char *nm, int depth) { if (n < 0 || depth > 32) return 0; const char *ty = nt_type(nt, n); NodeKind ty_k = nt_kind(nt, n); if (ty_k == NK_BlockLocalVariableNode) return 0; size_t tl = ty ? strlen(ty) : 0; if (tl > 13 && sp_streq(ty + tl - 13, "ParameterNode")) { const char *pn = nt_str(nt, n, "name"); if (pn && sp_streq(pn, nm)) return 1; } for (int i = 0; i < nt_num_refs(nt, n); i++) if (du_param_binds(nt, nt_ref_at(nt, n, i), nm, depth + 1)) return 1; for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int k = 0; k < an; k++) if (du_param_binds(nt, av[k], nm, depth + 1)) return 1; } return 0; } /* Where each statement sits in the list it belongs to (its parent in `par`), filled one list at a time on first need: asked by a scan of the list for every read, a long body read many times cost its length per read. -1 for a node not in its parent's list. */ static int du_stmt_index(const NodeTable *nt, const int *par, DUPos *dp, int p, int cur) { if (!dp->done[p]) { int bn = 0; const int *b = nt_arr(nt, p, "body", &bn); for (int i = 0; i < bn; i++) if (b[i] >= 0 && b[i] < nt->count && par[b[i]] == p && dp->pos[b[i]] < 0) dp->pos[b[i]] = i; dp->done[p] = 1; } return dp->pos[cur]; } int du_read_maybe_unset(const NodeTable *nt, const int *par, DUPos *dp, int rd, const char *nm) { int cur = rd, below = -1; for (int guard = 0; guard < 4096; guard++) { int p = par[cur]; if (p < 0) return 1; NodeKind pk = nt_kind(nt, p); if (pk == NK_DefNode || pk == NK_ClassNode || pk == NK_ModuleNode || pk == NK_SingletonClassNode) return 1; /* a block's body reads what the call bound to its parameters, the parameter a loop the block was lowered into writes */ if ((pk == NK_BlockNode || pk == NK_LambdaNode) && nt_ref(nt, p, "body") == cur && du_param_binds(nt, nt_ref(nt, p, "parameters"), nm, 0)) return 0; /* `if x in [a, b]`: the then-branch runs after the match bound */ if (pk == NK_IfNode && nt_ref(nt, p, "statements") == cur) { int pr = nt_ref(nt, p, "predicate"); if (pr >= 0 && nt_kind(nt, pr) == NK_MatchPredicateNode && du_pattern_binds(nt, nt_ref(nt, pr, "pattern"), nm, 0)) return 0; } /* an `in` arm's body, and its guard (`in n if n > 5`), run after its pattern bound */ if (pk == NK_InNode && du_pattern_binds(nt, nt_ref(nt, p, "pattern"), nm, 0)) { int pat = nt_ref(nt, p, "pattern"); if (nt_ref(nt, p, "statements") == cur) return 0; if (pat == cur && (nt_kind(nt, pat) == NK_IfNode || nt_kind(nt, pat) == NK_UnlessNode) && nt_ref(nt, pat, "predicate") == below) return 0; } if (pk == NK_StatementsNode) { int bn = 0; nt_arr(nt, p, "body", &bn); int first = du_first_write(nt, p, nm); /* the read inside or before that write's own statement is not covered */ int at = du_stmt_index(nt, par, dp, p, cur); if (at >= 0 && at <= first) first = -1; if (first >= 0 && first < bn) return 0; /* a write ahead of the read's statement */ } below = cur; cur = p; } return 1; } /* The instance methods of a program, by name: a boxed receiver's call binds every one of its name (mark_nullable_int_locals), looked up once per call in every round, so they are sorted once rather than scanned per call. */ typedef struct { const char *name; int mi; } NamedMethod; static int named_method_cmp(const void *a, const void *b) { const NamedMethod *x = a, *y = b; int r = strcmp(x->name, y->name); return r ? r : x->mi - y->mi; } static NamedMethod *instance_methods_by_name(Compiler *c, int *count) { NamedMethod *v = malloc(sizeof *v * (size_t)(c->nscopes + 1)); int n = 0; for (int k = 1; v && k < c->nscopes; k++) if (c->scopes[k].name && !c->scopes[k].is_cmethod && c->scopes[k].class_id >= 0) { v[n].name = c->scopes[k].name; v[n].mi = k; n++; } if (v) qsort(v, (size_t)n, sizeof *v, named_method_cmp); *count = v ? n : 0; return v; } /* the first entry of `v` (sorted) named `name`, or n */ static int named_method_first(const NamedMethod *v, int n, const char *name) { int lo = 0, hi = n; while (lo < hi) { int mid = lo + (hi - lo) / 2; if (strcmp(v[mid].name, name) < 0) lo = mid + 1; else hi = mid; } return lo; } /* A5: a Hash literal whose Integer or Float key or value can be nil is the boxed-keyed / boxed-valued kind, as a literal nil makes it. The nil marks settle after the types, so the literal (and a local it is written to, with that local's reads) widens here; a typed key or value slot has no nil (nil out of band). */ void infer_subtree(Compiler *c, int id); /* analyze_infer.c */ static int *wnh_bodies, wnh_n, wnh_cap; static void wnh_note(int body) { for (int i = 0; i < wnh_n; i++) if (wnh_bodies[i] == body) return; if (wnh_n == wnh_cap) { wnh_cap = wnh_cap ? wnh_cap * 2 : 256; wnh_bodies = realloc(wnh_bodies, sizeof(int) * (size_t)wnh_cap); } wnh_bodies[wnh_n++] = body; } /* Does a read of local `name` in scope sc hand the hash on whole -- a statement list's last value (a method's or block's answer), a `return`, another variable's write, an argument of a call other than an output one? Its type then reaches slots the late widening cannot retype. */ static int wnh_last_stmt(Compiler *c, int id) { const NodeTable *nt = c->nt; const int *par = comp_parent_map(c); int st = id >= 0 && id < nt->count ? par[id] : -1; /* not a statement: its value is used */ if (st < 0 || nt_kind(nt, st) != NK_StatementsNode) return 1; if (!vp_is_value_edge(nt, st, id, VPS_NONE)) return 0; /* the last statement of the program's own body is run for its effect */ return !prog_is_root_stmts(c, st); } /* Is node n method mi's answer: its body's last statement, or the last statement of an `if` / `unless` / `else` arm in that place, or of a `case` or `case`/`in` clause, or of a `begin` (its body, each rescue clause, or its else, which replaces the body), or the value of a rescue modifier, or an operand of an `||` or `&&` (the right operand of an `&&`: its left is the value only when falsy, which a Hash is not)? Read through the iterator with every shape. */ static int wnh_answer_arm(const VpLeaf *leaf, void *ud) { return leaf->kind == VPK_EXPR && leaf->node == *(int *)ud; } static int wnh_answer_in(Compiler *c, int at, int n) { return at >= 0 && for_each_value_position(c->nt, at, VPS_ALL, wnh_answer_arm, &n); } static int wnh_is_answer(Compiler *c, Scope *sc, int n) { if (!sc || sc->def_node < 0 || sc->body < 0) return 0; return wnh_answer_in(c, sc->body, n); } /* A read of `name` that is a value position of sc's body answers the method, whether or not it ends a list: an operand of `||`, the expression of a rescue modifier. Asked only where the method's return is a Hash, the one the widening has to follow; a return of another kind (`seen || {}`) widens the local alone. */ typedef struct { Compiler *c; Scope *sc; const char *name; } WnhReads; static int wnh_answering_read(const VpLeaf *leaf, void *ud) { WnhReads *r = ud; const NodeTable *nt = r->c->nt; if (leaf->kind != VPK_EXPR || nt_kind(nt, leaf->node) != NK_LocalVariableReadNode) return 0; const char *rn = nt_str(nt, leaf->node, "name"); return rn && sp_streq(rn, r->name) && comp_scope_of(r->c, leaf->node) == r->sc; } /* Is the variable the write `w` stores into a box (a local or an ivar of the poly kind)? */ static int wnh_boxed_target(Compiler *c, int w) { const NodeTable *nt = c->nt; const char *wn = nt_str(nt, w, "name"); Scope *ws = comp_scope_of(c, w); if (!wn || !ws) return 0; if (nt_kind(nt, w) == NK_LocalVariableWriteNode) { LocalVar *lv = scope_local(ws, wn); return lv && lv->type == TY_POLY; } if (nt_kind(nt, w) == NK_InstanceVariableWriteNode) { int cid = ws->class_id >= 0 ? ws->class_id : c->toplevel_class; int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], wn) : -1; return iv >= 0 && c->classes[cid].ivar_types[iv] == TY_POLY; } return 0; } /* Is the write `w` of the value `v` a copy of local `name` into a local of the same scope typed as the Hash is (`h = seen`), which hands nothing on whole itself? The widening then retypes the copy with it (wnh_widen_copies). */ static int wnh_local_escapes_ex(Compiler *c, Scope *sc, const char *name, int *answered, int copies); static int wnh_copy_target(Compiler *c, int w, const char *name, Scope *sc) { const NodeTable *nt = c->nt; int v = nt_paren_single(nt, nt_ref(nt, w, "value")); const char *wn = nt_str(nt, w, "name"); if (nt_kind(nt, w) != NK_LocalVariableWriteNode || v < 0 || nt_kind(nt, v) != NK_LocalVariableReadNode || comp_scope_of(c, w) != sc || !wn || !sp_streq(nt_str(nt, v, "name"), name)) return 0; LocalVar *y = scope_local(sc, wn); if (!y || y->type != c->ntype[v] || !ty_is_hash(y->type)) return 0; /* the copy's own reads, a copy of it not allowed to chain */ return !wnh_local_escapes_ex(c, sc, wn, NULL, 0); } /* The locals the late widening asks about, sorted by (scope, name), so that a walk of the program answers for all of them at once: asked for each local in turn, a method called from many methods cost a walk per call. */ typedef struct { Scope *sc; const char *name; int escapes, target, go; } WnhSlot; typedef struct { WnhSlot *v; int n, cap; } WnhSlots; static int wnh_slot_cmp(const void *a, const void *b) { const WnhSlot *x = a, *y = b; if (x->sc != y->sc) return x->sc < y->sc ? -1 : 1; return strcmp(x->name, y->name); } static WnhSlot *wnh_slot_find(const WnhSlots *s, const Scope *sc, const char *name) { if (!s->n || !name || !sc) return NULL; WnhSlot key = { (Scope *)sc, name, 0, 0, 0 }; return bsearch(&key, s->v, (size_t)s->n, sizeof(WnhSlot), wnh_slot_cmp); } static void wnh_slots_add(WnhSlots *s, Scope *sc, const char *name) { if (s->n == s->cap) { s->cap = s->cap ? s->cap * 2 : 16; s->v = (WnhSlot *)realloc(s->v, sizeof(WnhSlot) * (size_t)s->cap); if (!s->v) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } s->v[s->n++] = (WnhSlot){ sc, name, 0, 0, 0 }; } /* sorts the slots and drops the repeats */ static void wnh_slots_seal(WnhSlots *s) { if (s->n > 1) qsort(s->v, (size_t)s->n, sizeof(WnhSlot), wnh_slot_cmp); int k = 0; for (int i = 0; i < s->n; i++) if (k == 0 || wnh_slot_cmp(&s->v[k - 1], &s->v[i])) s->v[k++] = s->v[i]; s->n = k; } /* A leaf of the value written by `w` that reads a slot hands the Hash on whole, unless the write is a copy the widening retypes with it */ typedef struct { Compiler *c; WnhSlots *slots; int w, copies; } WnhScan; static int wnh_scan_write_leaf(const VpLeaf *leaf, void *ud) { WnhScan *r = ud; const NodeTable *nt = r->c->nt; if (leaf->kind != VPK_EXPR || nt_kind(nt, leaf->node) != NK_LocalVariableReadNode) return 0; Scope *rs = comp_scope_of(r->c, leaf->node); const char *rn = nt_str(nt, leaf->node, "name"); WnhSlot *sl = wnh_slot_find(r->slots, rs, rn); if (!sl || sl->escapes) return 0; if (r->copies && wnh_copy_target(r->c, r->w, rn, rs)) return 0; sl->escapes = 1; return 0; } /* Marks each of the slots a read of which hands the hash on whole. `answered`, when given (a scan of one slot), lets a read that is its scope's own answer (the method's last value, wnh_is_answer; a `return` still escapes) pass, and reports that one did: the caller then widens the method's return with the local */ static void wnh_escape_scan(Compiler *c, WnhSlots *slots, int *answered, int copies) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_StatementsNode, st) { int n = 0; const int *b = nt_arr(nt, st, "body", &n); if (n <= 0 || nt_kind(nt, b[n - 1]) != NK_LocalVariableReadNode) continue; Scope *rs = comp_scope_of(c, b[n - 1]); WnhSlot *sl = wnh_slot_find(slots, rs, nt_str(nt, b[n - 1], "name")); if (!sl || sl->escapes) continue; if (answered && wnh_is_answer(c, rs, b[n - 1])) { *answered = 1; continue; } /* a read whose value is dropped (the body of a begin with an `else`, which the else's value replaces) hands nothing on */ if (an_value_dropped(nt, comp_parent_map(c), b[n - 1])) continue; /* or one a box takes: `keep = (seen) || {}` with `keep` a box */ VpUp up; if (vp_climb(nt, comp_parent_map(c), b[n - 1], VP_AND_LEFT | VPS_ALL, &up) == VPU_ENDED && up.parent >= 0 && wnh_boxed_target(c, up.parent)) continue; sl->escapes = 1; } static const NodeKind wk[] = { NK_ReturnNode, NK_LocalVariableWriteNode, NK_InstanceVariableWriteNode, NK_GlobalVariableWriteNode, NK_ClassVariableWriteNode, NK_CallNode }; for (size_t q = 0; q < sizeof wk / sizeof wk[0]; q++) { NT_FOREACH_KIND(nt, wk[q], w) { const int *av = NULL; int an = 0; if (wk[q] == NK_ReturnNode || wk[q] == NK_CallNode) { if (wk[q] == NK_CallNode) { const char *cn = nt_str(nt, w, "name"); if (cn && (sp_streq(cn, "puts") || sp_streq(cn, "print"))) continue; /* `p` and `pp` answer their argument: a statement's, but not a method's or block's answer (its last statement) */ if (cn && (sp_streq(cn, "p") || sp_streq(cn, "pp")) && !wnh_last_stmt(c, w)) continue; } int a = nt_ref(nt, w, "arguments"); av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; } else { int v = nt_ref(nt, w, "value"); an = 0; /* a write of the Hash itself or of a join with it (`keep = (seen) || {}`) keeps it typed, unless the variable is a box, which takes the widened Hash as the read now types it */ if (v >= 0 && !wnh_boxed_target(c, w)) { WnhScan r = { c, slots, w, copies }; for_each_value_position(nt, v, VP_AND_LEFT | VPS_ALL, wnh_scan_write_leaf, &r); } } for (int k = 0; k < an; k++) if (nt_kind(nt, av[k]) == NK_LocalVariableReadNode) { WnhSlot *sl = wnh_slot_find(slots, comp_scope_of(c, av[k]), nt_str(nt, av[k], "name")); if (sl) sl->escapes = 1; } } } } /* Does a read of local `name` in scope sc hand the hash on whole? */ static int wnh_local_escapes_ex(Compiler *c, Scope *sc, const char *name, int *answered, int copies) { WnhSlots one = { NULL, 0, 0 }; wnh_slots_add(&one, sc, name); wnh_escape_scan(c, &one, answered, copies); int esc = one.v[0].escapes; free(one.v); if (!esc && answered && sc->def_node >= 0 && sc->body >= 0 && ty_is_hash(sc->ret)) { WnhReads r = { c, sc, name }; if (for_each_value_position(c->nt, sc->body, VPS_ALL, wnh_answering_read, &r)) *answered = 1; } return esc; } static int wnh_local_escapes(Compiler *c, Scope *sc, const char *name) { return wnh_local_escapes_ex(c, sc, name, NULL, 1); } /* The block parameters of a key/value iteration over a local the widening retyped (`g.each { |k, v| }`) were bound from the typed kind: a key or value that can be nil is now boxed, so is the parameter */ static void wnh_widen_block_params_of(Compiler *c, const WnhSlots *slots, int kn, int vn) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, call) { int r = nt_ref(nt, call, "receiver"), blk = nt_ref(nt, call, "block"); if (r < 0 || blk < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode || nt_kind(nt, blk) != NK_BlockNode) continue; const char *rn = nt_str(nt, r, "name"), *nm = nt_str(nt, call, "name"); if (!rn || !nm || !wnh_slot_find(slots, comp_scope_of(c, r), rn)) continue; int pk = -1, pv = -1; if (sp_streq(nm, "each_key")) pk = 0; else if (sp_streq(nm, "each_value")) pv = 0; else if (sp_streq(nm, "each") || sp_streq(nm, "each_pair") || sp_streq(nm, "select") || sp_streq(nm, "filter") || sp_streq(nm, "reject") || sp_streq(nm, "map") || sp_streq(nm, "collect") || sp_streq(nm, "any?") || sp_streq(nm, "all?") || sp_streq(nm, "none?") || sp_streq(nm, "count") || sp_streq(nm, "delete_if") || sp_streq(nm, "keep_if") || sp_streq(nm, "select!") || sp_streq(nm, "reject!") || sp_streq(nm, "filter!")) { if (!block_param_name(c, blk, 1)) continue; /* a solo parameter is the boxed pair */ pk = 0; pv = 1; } else continue; Scope *bs = comp_scope_of(c, blk); for (int w = 0; w < 2; w++) { int idx = w == 0 ? pk : pv; if (idx < 0 || !(w == 0 ? kn : vn)) continue; const char *pn = block_param_name(c, blk, idx); LocalVar *plv = pn && bs ? scope_local(bs, pn) : NULL; if (plv && (plv->type == TY_INT || plv->type == TY_FLOAT)) lv_set_type(plv, TY_POLY); } if (bs && bs->body >= 0) wnh_note(bs->body); } } static void wnh_widen_block_params(Compiler *c, Scope *sc, const char *ln, int kn, int vn) { WnhSlots one = { NULL, 0, 0 }; wnh_slots_add(&one, sc, ln); wnh_widen_block_params_of(c, &one, kn, vn); free(one.v); } /* The method a call resolves to statically, or -1 (a boxed receiver: -2) */ static int wnh_call_target(Compiler *c, int call) { const NodeTable *nt = c->nt; const char *cn = nt_str(nt, call, "name"); int r = nt_ref(nt, call, "receiver"); if (!cn) return -1; if (r < 0) return comp_self_call_mi(c, call, cn); if (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode) { int rc = const_node_class_index(c, r); return rc >= 0 ? call_target(c, call, rc, cn, CT_CMETHOD, NULL) : -1; } TyKind rt = infer_type(c, r); if (rt == TY_POLY || rt == TY_UNKNOWN) return -2; return ty_is_object(rt) ? call_target(c, call, ty_object_class(rt), cn, 0, NULL) : -1; } /* A value of a local the widening retypes is a call of method `mi` or an empty Hash literal (`h = first_seen(a) || {}`): the literal is built at the kind the local takes. Answers nonzero for a leaf that is neither. */ typedef struct { Compiler *c; int mi; TyKind want; } WnhSlotLeaf; static int wnh_slot_leaf_other(const VpLeaf *leaf, void *ud) { WnhSlotLeaf *s = ud; const NodeTable *nt = s->c->nt; if (leaf->kind != VPK_EXPR) return 1; if (nt_kind(nt, leaf->node) == NK_HashNode) { int en = 0; nt_arr(nt, leaf->node, "elements", &en); return en != 0; } return nt_kind(nt, leaf->node) != NK_CallNode || wnh_call_target(s->c, leaf->node) != s->mi; } static int wnh_slot_retype_leaf(const VpLeaf *leaf, void *ud) { WnhSlotLeaf *s = ud; if (leaf->kind == VPK_EXPR && nt_kind(s->c->nt, leaf->node) == NK_HashNode) { comp_set_ntype(s->c, leaf->node, s->want); if (s->c->hash_want && leaf->node < s->c->node_cap) comp_set_hash_want(s->c, leaf->node, s->want); } return 0; } /* The slot (a local of a scope) a call is a value of (`h = first_seen(a)`, or `h = first_seen(a) || {}`), or NULL when it is no local write's value or the slot is not among the facts. */ static WnhSlot *wnh_call_write_slot(Compiler *c, int call, const WnhSlots *facts) { const NodeTable *nt = c->nt; VpUp up; if (vp_climb(nt, comp_parent_map(c), call, VP_AND_LEFT | VPS_ALL, &up) != VPU_ENDED || up.parent < 0 || nt_kind(nt, up.parent) != NK_LocalVariableWriteNode) return NULL; return wnh_slot_find(facts, comp_scope_of(c, up.parent), nt_str(nt, up.parent, "name")); } /* The facts of the locals the calls of method `mi` are written to, in one walk of the program for all of them: those a read of which hands the Hash on whole, and those a multiple assignment or a rescue writes */ static void wnh_call_slots_scan(Compiler *c, int mi, WnhSlots *facts) { const NodeTable *nt = c->nt; const char *nm = c->scopes[mi].name; if (!nm) return; NT_FOREACH_KIND(nt, NK_CallNode, call) { const char *cn = nt_str(nt, call, "name"); if (!cn || !sp_streq(cn, nm) || wnh_call_target(c, call) != mi) continue; VpUp up; if (vp_climb(nt, comp_parent_map(c), call, VP_AND_LEFT | VPS_ALL, &up) != VPU_ENDED || up.parent < 0 || nt_kind(nt, up.parent) != NK_LocalVariableWriteNode) continue; const char *ln = nt_str(nt, up.parent, "name"); if (ln) wnh_slots_add(facts, comp_scope_of(c, up.parent), ln); } wnh_slots_seal(facts); if (!facts->n) return; wnh_escape_scan(c, facts, NULL, 1); NT_FOREACH_KIND(nt, NK_LocalVariableTargetNode, t) { /* a multiple assignment's or rescue's target */ WnhSlot *sl = wnh_slot_find(facts, comp_scope_of(c, t), nt_str(nt, t, "name")); if (sl) sl->target = 1; } } /* The local a call of method `mi` is a value of (`h = first_seen(a)`, or `h = first_seen(a) || {}`), when that local holds only calls of `mi` and empty literals and hands nothing on whole: it is retyped with the method's return. */ static WnhSlot *wnh_call_slot(Compiler *c, int mi, int call, const WnhSlots *facts) { const NodeTable *nt = c->nt; WnhSlot *sl = wnh_call_write_slot(c, call, facts); if (!sl || sl->escapes || sl->target) return NULL; WnhSlotLeaf lf = { c, mi, TY_UNKNOWN }; for (int o = an_local_writes_named_first(c, sl->name); o >= 0; o = an_local_writes_named_next(o)) { const char *on = nt_str(nt, o, "name"); if (!on || !sp_streq(on, sl->name) || comp_scope_of(c, o) != sl->sc) continue; int ov = nt_kind(nt, o) == NK_LocalVariableWriteNode ? nt_ref(nt, o, "value") : -1; if (ov < 0 || for_each_value_position(nt, ov, VP_AND_LEFT | VPS_ALL, wnh_slot_leaf_other, &lf)) return NULL; } return sl; } /* Can method mi's return widen with the literal it answers? Every call of it must resolve to it, and its value be read only where the reader re-derives the kind: a builtin's receiver or argument (`f(v) == h`, `p f(v)`), or dropped. One that lands in a slot (a variable, a return, a program method's argument, a literal's element) keeps the typed kind and its store refuses, as before. */ static int wnh_ret_widenable(Compiler *c, int mi, const WnhSlots *facts) { const NodeTable *nt = c->nt; const char *nm = c->scopes[mi].name; if (!nm) return 0; NT_FOREACH_KIND(nt, NK_CallNode, call) { const char *cn = nt_str(nt, call, "name"); if (!cn || !sp_streq(cn, nm)) continue; int t = wnh_call_target(c, call); if (t == -2) return 0; if (t != mi) continue; if (comp_value_dropped(c, call)) continue; if (wnh_call_slot(c, mi, call, facts)) continue; int rp = comp_recv_parent(c, call); if (rp >= 0) { if (wnh_call_target(c, rp) >= 0) return 0; continue; } /* an argument of a builtin call */ int found = 0, owner = -1; for (int pc = 0; pc < nt->count && !found; pc++) { if (nt_kind(nt, pc) != NK_CallNode) continue; int a = nt_ref(nt, pc, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; for (int i = 0; i < an && !found; i++) if (nt_paren_single(nt, av[i]) == call) { found = 1; owner = pc; } } if (found && wnh_call_target(c, owner) >= 0) return 0; if (!found) return 0; } return 1; } /* Widen method mi's return to hash kind `want`, and each call of it, when wnh_ret_widenable allows; 1 when done */ static int wnh_widen_ret(Compiler *c, int mi, TyKind want) { const NodeTable *nt = c->nt; WnhSlots facts = { NULL, 0, 0 }; wnh_call_slots_scan(c, mi, &facts); if (!wnh_ret_widenable(c, mi, &facts)) { free(facts.v); return 0; } scope_set_ret(&c->scopes[mi], want); WnhSlots go = { NULL, 0, 0 }; NT_FOREACH_KIND(nt, NK_CallNode, call) { if (wnh_call_target(c, call) != mi) continue; comp_set_ntype(c, call, want); Scope *cs = comp_scope_of(c, call); if (cs && cs->body >= 0) wnh_note(cs->body); /* the local the call is written to is the Hash the caller holds */ WnhSlot *sl = wnh_call_slot(c, mi, call, &facts); LocalVar *lv = sl ? scope_local(sl->sc, sl->name) : NULL; if (!lv || lv->type == want) continue; lv_set_type(lv, want); sl->go = 1; } for (int i = 0; i < facts.n; i++) if (facts.v[i].go) wnh_slots_add(&go, facts.v[i].sc, facts.v[i].name); if (go.n) { wnh_widen_block_params_of(c, &go, ty_hash_key(want) == TY_POLY, ty_hash_val(want) == TY_POLY); WnhSlotLeaf sl = { c, mi, want }; for (int i = 0; i < go.n; i++) { const char *ln = go.v[i].name; for (int o = an_local_writes_named_first(c, ln); o >= 0; o = an_local_writes_named_next(o)) { if (comp_scope_of(c, o) != go.v[i].sc || !sp_streq(nt_str(nt, o, "name"), ln)) continue; comp_set_ntype(c, o, want); int ov = nt_ref(nt, o, "value"); /* the join and its empty literals are the Hash the local is */ if (ov >= 0) { comp_set_ntype(c, ov, want); for_each_value_position(nt, ov, VP_AND_LEFT | VPS_ALL, wnh_slot_retype_leaf, &sl); } } } NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, rr) if (wnh_slot_find(&go, comp_scope_of(c, rr), nt_str(nt, rr, "name"))) comp_set_ntype(c, rr, want); } free(go.v); free(facts.v); return 1; } /* The locals of `sc` written a copy of local `name` (`h = seen`) and typed as the Hash was (`ht`) are retyped with it. */ static void wnh_widen_copies(Compiler *c, Scope *sc, const char *name, TyKind ht, TyKind want, int kn, int vn) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, w) { const char *wn = nt_str(nt, w, "name"); if (!wn || !wnh_copy_target(c, w, name, sc)) continue; LocalVar *y = scope_local(sc, wn); if (!y || y->type != ht) continue; lv_set_type(y, want); wnh_widen_block_params(c, sc, wn, kn, vn); comp_set_ntype(c, w, want); NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, rr) { const char *rn = nt_str(nt, rr, "name"); if (rn && sp_streq(rn, wn) && comp_scope_of(c, rr) == sc) comp_set_ntype(c, rr, want); } } } static void widen_nullable_keyed_hash_literals(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_HashNode, id) { TyKind ht = c->ntype[id]; if (!ty_is_hash(ht)) continue; TyKind kt = ty_hash_key(ht), vt = ty_hash_val(ht); int kn = 0, vn = 0; int n = 0; const int *els = nt_arr(nt, id, "elements", &n); for (int j = 0; j < n; j++) { if (nt_kind(nt, els[j]) != NK_AssocNode) continue; int k = nt_ref(nt, els[j], "key"), v = nt_ref(nt, els[j], "value"); /* (a boxed key or value -- a promote-widened local -- can be nil too) */ if ((kt == TY_INT || kt == TY_FLOAT) && (nullable_int_value(c, k) || (k >= 0 && c->ntype[k] == TY_POLY))) kn = 1; if ((vt == TY_INT || vt == TY_FLOAT) && (nullable_int_value(c, v) || (v >= 0 && c->ntype[v] == TY_POLY))) vn = 1; } if (!kn && !vn) continue; TyKind nk = kn ? TY_POLY : kt, nv = vn ? TY_POLY : vt; TyKind want = nk == TY_SYMBOL ? TY_SYM_POLY_HASH : ty_hash_of(nk, nv); if (!ty_is_hash(want)) want = nk == TY_STRING ? TY_STR_POLY_HASH : TY_POLY_POLY_HASH; if (want == ht) continue; comp_set_ntype(c, id, want); { Scope *ls = comp_scope_of(c, id); if (ls && ls->body >= 0) wnh_note(ls->body); } /* the literal a method answers (`def f(v) = { "k" => v }`): the method's return widens with it, and each call of it, where its value is read only by builtins that re-derive the kind; else it stays typed */ { Scope *ms = comp_scope_of(c, id); int mi = ms ? (int)(ms - c->scopes) : -1; if (ms && ms->def_node >= 0 && ms->ret == ht && nt_paren_single(nt, scope_body_last(c, mi)) == id) { if (!wnh_widen_ret(c, mi, want)) { comp_set_ntype(c, id, ht); continue; } } } /* the local it is written to, and that local's reads */ NT_FOREACH_KIND(nt, NK_LocalVariableWriteNode, w) { if (nt_ref(nt, w, "value") != id) continue; const char *wn = nt_str(nt, w, "name"); Scope *sc = comp_scope_of(c, w); LocalVar *lv = wn && sc ? scope_local(sc, wn) : NULL; if (!lv || lv->type != ht) continue; if (wnh_local_escapes(c, sc, wn)) { comp_set_ntype(c, id, ht); break; } /* left typed: the store refuses */ lv_set_type(lv, want); comp_set_ntype(c, w, want); wnh_widen_block_params(c, sc, wn, kn, vn); NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, r) { const char *rn = nt_str(nt, r, "name"); if (rn && sp_streq(rn, wn) && comp_scope_of(c, r) == sc) comp_set_ntype(c, r, want); } } } /* A method answering a Hash literal of a boxed kind its declared return is not (a parameter boxed by --int-overflow=promote after the types settled): the return follows the literal, as above */ for (int mi = 1; mi < c->nscopes; mi++) { Scope *ms = &c->scopes[mi]; if (ms->def_node < 0 || !ty_is_hash(ms->ret)) continue; int last = nt_paren_single(nt, scope_body_last(c, mi)); if (last < 0 || nt_kind(nt, last) != NK_HashNode || comp_scope_of(c, last) != ms) continue; TyKind lt = c->ntype[last]; if (!ty_is_hash(lt) || lt == ms->ret || ty_hash_val(lt) != TY_POLY || ty_hash_val(ms->ret) == TY_POLY) continue; if (ty_hash_key(lt) != ty_hash_key(ms->ret) && ty_hash_key(lt) != TY_POLY) continue; if (wnh_widen_ret(c, mi, lt) && ms->body >= 0) wnh_note(ms->body); } /* ... and a local Hash a `[]=` / `store` stores such a key or value into (`h = Hash.new; h["k"] = a[1]`): the local, its reads, and its writes whose value is an empty literal or a bare Hash.new built at its kind */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "[]=") && !sp_streq(nm, "store"))) continue; int r = nt_ref(nt, id, "receiver"); if (r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode) continue; TyKind ht = c->ntype[r]; if (!ty_is_hash(ht)) continue; int a = nt_ref(nt, id, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!av || an != 2) continue; TyKind kt = ty_hash_key(ht), vt = ty_hash_val(ht); int kn = (kt == TY_INT || kt == TY_FLOAT) && (nullable_int_value(c, av[0]) || c->ntype[av[0]] == TY_POLY); int vn = (vt == TY_INT || vt == TY_FLOAT) && (nullable_int_value(c, av[1]) || c->ntype[av[1]] == TY_POLY); if (!kn && !vn) continue; TyKind nk = kn ? TY_POLY : kt, nv = vn ? TY_POLY : vt; TyKind want = nk == TY_SYMBOL ? TY_SYM_POLY_HASH : ty_hash_of(nk, nv); if (!ty_is_hash(want)) want = nk == TY_STRING ? TY_STR_POLY_HASH : TY_POLY_POLY_HASH; if (want == ht) continue; const char *ln = nt_str(nt, r, "name"); Scope *sc = comp_scope_of(c, r); LocalVar *lv = ln && sc ? scope_local(sc, ln) : NULL; if (!lv || lv->type != ht || lv->is_param || lv->rbs_seeded) continue; int answered = 0; if (wnh_local_escapes_ex(c, sc, ln, &answered, 1)) continue; /* every write must be one the widening can rebuild */ int ok = 1; for (int w = an_local_writes_named_first(c, ln); w >= 0; w = an_local_writes_named_next(w)) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ln) || comp_scope_of(c, w) != sc) continue; int v = nt_ref(nt, w, "value"); NodeKind vk = v >= 0 ? nt_kind(nt, v) : NK_NilNode; int en = 0; if (vk == NK_HashNode) nt_arr(nt, v, "elements", &en); int bare_new = vk == NK_CallNode && sp_streq(nt_str(nt, v, "name"), "new") && nt_ref(nt, v, "arguments") < 0 && nt_ref(nt, v, "block") < 0; if (!((vk == NK_HashNode && en == 0) || bare_new)) { ok = 0; break; } } if (!ok) continue; /* the method answers the local (`counts = {}; ...; counts`, tally in builtins/): its return widens with it, and each call of it, where wnh_ret_widenable allows; else the local stays typed and the store refuses, as before */ if (answered) { /* only for a key that can be nil: a nil value is the typed hash's own (its per-entry value nil bits), not a reason to widen the return */ if (!kn) continue; if (vn) { want = ty_hash_of(TY_POLY, vt); if (!ty_is_hash(want)) want = TY_POLY_POLY_HASH; vn = 0; } /* a method answering something other than this Hash (`seen || {}`, a conditional beside a `{}` arm: the return is the box that joins them) hands the box the local's Hash as it is, so the local widens alone; one answering another Hash kind keeps both typed */ int mi = (int)(sc - c->scopes); if (ty_is_hash(sc->ret) ? (sc->ret != ht || !wnh_widen_ret(c, mi, want)) : sc->ret != TY_POLY) continue; } lv_set_type(lv, want); wnh_widen_block_params(c, sc, ln, kn, vn); wnh_widen_copies(c, sc, ln, ht, want, kn, vn); for (int w = an_local_writes_named_first(c, ln); w >= 0; w = an_local_writes_named_next(w)) { const char *wn = nt_str(nt, w, "name"); if (!wn || !sp_streq(wn, ln) || comp_scope_of(c, w) != sc) continue; comp_set_ntype(c, w, want); int v = nt_ref(nt, w, "value"); if (v >= 0) { comp_set_ntype(c, v, want); if (nt_kind(nt, v) == NK_HashNode && c->hash_want && v < c->node_cap) comp_set_hash_want(c, v, want); } } NT_FOREACH_KIND(nt, NK_LocalVariableReadNode, rr) { const char *rn = nt_str(nt, rr, "name"); if (rn && sp_streq(rn, ln) && comp_scope_of(c, rr) == sc) comp_set_ntype(c, rr, want); } if (sc->body >= 0) wnh_note(sc->body); } /* A constant written from an Integer / Float that can be nil (`X = define_method(:c) { next 3 if f; 4 }` over a method answering its proc's value) holds it boxed: a constant has no nil flag of its own. Its reads, anywhere, re-infer against the box. */ int const_widened = 0; NT_FOREACH_KIND(nt, NK_ConstantWriteNode, id) { const char *cn = nt_str(nt, id, "name"); int v = nt_ref(nt, id, "value"); LocalVar *cv = cn ? comp_const(c, cn) : NULL; if (!cv || v < 0 || (cv->type != TY_INT && cv->type != TY_FLOAT)) continue; if (!nullable_int_value(c, v)) continue; lv_set_type(cv, TY_POLY); const_widened = 1; comp_set_ntype(c, id, TY_POLY); } /* ...and a constant that is only another's name (`TEXT = Theme::TEXT`) holds the same box: its own type came from the read before the widening, and a plain slot assigned the box did not build (#8575) */ for (int again = const_widened; again; ) { again = 0; NT_FOREACH_KIND(nt, NK_ConstantWriteNode, id) { const char *cn = nt_str(nt, id, "name"); int v = nt_paren_single(nt, nt_ref(nt, id, "value")); LocalVar *cv = cn ? comp_const(c, cn) : NULL; if (!cv || v < 0 || (cv->type != TY_INT && cv->type != TY_FLOAT)) continue; NodeKind vk = nt_kind(nt, v); if (vk != NK_ConstantReadNode && vk != NK_ConstantPathNode) continue; LocalVar *src = nt_str(nt, v, "name") ? comp_const(c, nt_str(nt, v, "name")) : NULL; if (!src || src->type != TY_POLY) continue; lv_set_type(cv, TY_POLY); comp_set_ntype(c, id, TY_POLY); comp_set_ntype(c, v, TY_POLY); again = 1; } } if (const_widened) for (int si = 0; si < c->nscopes; si++) { if (c->scopes[si].body >= 0) wnh_note(c->scopes[si].body); /* a parameter default is outside the body, and may read the box too */ for (int i = 0; c->scopes[si].pdefault && i < c->scopes[si].nparams; i++) if (c->scopes[si].pdefault[i] >= 0) wnh_note(c->scopes[si].pdefault[i]); } /* the expressions over a widened hash (its reads' `[]`, a call it is an argument of) re-infer against it */ for (int i = 0; i < wnh_n; i++) infer_subtree(c, wnh_bodies[i]); wnh_n = 0; } /* A block or lambda reads its enclosing scope's variable through one cell (or a copy of it): the slot is one, so whether it holds a nil is one property -- the enclosing local's and the body's same-named local agree (the cell's C type is the owner's, the reads in the body ask theirs). */ static void sync_captured_nullable(Compiler *c) { const NodeTable *nt = c->nt; for (int round = 0; round < 8; round++) { int changed = 0; static const NodeKind pk[] = { NK_LambdaNode, NK_BlockNode }; for (int q = 0; q < 2; q++) { NT_FOREACH_KIND(nt, pk[q], id) { int body = nt_ref(nt, id, "body"); if (body < 0) continue; Scope *bs = comp_scope_of(c, body), *encl = comp_scope_of(c, id); if (!bs || !encl || bs == encl) continue; for (int i = 0; i < bs->nlocals; i++) { LocalVar *in = &bs->locals[i]; if (!in->name || in->is_param || in->is_block_param) continue; LocalVar *out = scope_local(encl, in->name); if (!out || out == in || out->type != in->type) continue; if (in->type != TY_INT && in->type != TY_FLOAT) continue; if (in->nullable_int != out->nullable_int) { in->nullable_int = out->nullable_int = 1; changed = 1; } } } } if (!changed) break; } } /* The per-kind flag of a typed Integer-valued Hash kind, or NULL */ static unsigned char *hash_vnil_flag(Compiler *c, TyKind t) { return t == TY_STR_INT_HASH ? &c->hash_vnil_str_int : t == TY_INT_INT_HASH ? &c->hash_vnil_int_int : NULL; } int hash_vals_nullable(Compiler *c, int node) { if (node < 0) return 0; TyKind t = c->ntype && node < c->node_cap && c->ntype[node] != TY_UNKNOWN ? c->ntype[node] : infer_type(c, node); unsigned char *f = hash_vnil_flag(c, t); return f && *f; } /* A value a store hands an Integer-valued typed Hash that can be nil: a nil literal, an Integer that can be nil, or a boxed value (the store unboxes it with its nil) */ static int hash_store_val_may_nil(Compiler *c, int v) { if (v < 0) return 0; if (nt_kind(c->nt, nt_paren_single(c->nt, v)) == NK_NilNode) return 1; return nullable_int_value(c, v) || infer_type(c, v) == TY_POLY; } /* The block parameters a walk over a Hash binds its key and value to (-1: none), as `each { |k, v| }` / `each_value { |v| }`; 0 for a call that is no such walk, or binds the pair as one boxed parameter */ static int hash_walk_params(Compiler *c, int call, int *pk, int *pv) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); int blk = nt_ref(nt, call, "block"); *pk = *pv = -1; if (!nm || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) return 0; if (sp_streq(nm, "each_key")) { *pk = 0; return 1; } if (sp_streq(nm, "each_value") || sp_streq(nm, "transform_values") || sp_streq(nm, "transform_values!")) { *pv = 0; return 1; } static const char *const two[] = { "each", "each_pair", "select", "filter", "reject", "map", "collect", "flat_map", "filter_map", "any?", "all?", "none?", "count", "delete_if", "keep_if", "select!", "reject!", "filter!", "find", "detect", "find_all", "partition", "group_by", "sum", "sort_by", "min_by", "max_by", "to_h", "each_with_index" }; for (size_t i = 0; i < sizeof two / sizeof two[0]; i++) { if (!sp_streq(nm, two[i])) continue; if (!block_param_name(c, blk, 1)) return 0; /* a solo parameter is the boxed pair */ if (sp_streq(nm, "each_with_index")) return 0; /* |(k, v), i|: the pair is destructured */ *pk = 0; *pv = 1; return 1; } return 0; } /* D3b-ii, W2: a store that can put a nil value into a typed Integer-valued Hash marks that kind program-wide (a first cut: per kind, not per slot), and every walk over a Hash of a marked kind binds its value parameter with the nil (an oint slot). 1 when something changed. */ static int mark_hash_nil_values(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || (!sp_streq(nm, "[]=") && !sp_streq(nm, "store"))) continue; unsigned char *f = hash_vnil_flag(c, infer_type(c, nt_ref(nt, id, "receiver"))); if (!f || *f) continue; int a = nt_ref(nt, id, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (av && an == 2 && hash_store_val_may_nil(c, av[1])) { *f = 1; changed = 1; } } /* `h[k] ||= v` / `h[k] &&= v` store v (an op-write stores the op's Integer) */ for (int q = 0; q < 2; q++) { NodeKind wk = q ? NK_IndexAndWriteNode : NK_IndexOrWriteNode; NT_FOREACH_KIND(nt, wk, id) { unsigned char *f = hash_vnil_flag(c, infer_type(c, nt_ref(nt, id, "receiver"))); if (f && !*f && hash_store_val_may_nil(c, nt_ref(nt, id, "value"))) { *f = 1; changed = 1; } } } if (!c->hash_vnil_str_int && !c->hash_vnil_int_int) return changed; NT_FOREACH_KIND(nt, NK_CallNode, id) { int recv = nt_ref(nt, id, "receiver"), pk, pv; if (recv < 0 || !hash_vals_nullable(c, recv)) continue; int blk = nt_ref(nt, id, "block"); int pis[2] = { -1, -1 }; const char *nm = nt_str(nt, id, "name"); /* merge / update with a block: |key, old, new| are both hashes' values */ if (nm && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && (sp_streq(nm, "merge") || sp_streq(nm, "merge!") || sp_streq(nm, "update"))) { pis[0] = 1; pis[1] = 2; } else if (hash_walk_params(c, id, &pk, &pv) && pv >= 0) pis[0] = pv; else continue; Scope *bs = comp_scope_of(c, blk); for (int q = 0; q < 2; q++) { const char *pn = pis[q] >= 0 ? block_param_name(c, blk, pis[q]) : NULL; LocalVar *plv = pn && bs ? scope_local(bs, pn) : NULL; if (!plv || plv->type != TY_INT || plv->nullable_int) continue; plv->nullable_int = 1; changed = 1; } } return changed; } /* The block parameters that are another name of an array -- the receiver or the memo by the iterator row (`a.tap { |x| }`, `each_with_object(b) { |e, m| }`), or the argument a method's `yield` passes (`def give(a) = yield(a)`) -- marked as a local assigned from it is: a nil stored through the parameter marks the array, and the array's NIE_REACH reaches the parameter. For mark_nullable_int_locals' rounds; 1 when a mark changed. */ static int mark_block_param_arrays(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; /* `a.tap { |x| x << v }`, `each_with_object(b) { |e, m| m[i] = v }`: the block parameter IS the receiver or the argument (block_param_alias), another name of that array, as a local assigned from it is */ NT_FOREACH_KIND(nt, NK_CallNode, id) { int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; Scope *bsc = comp_scope_of(c, blk); for (int pk = 0; pk < 3; pk++) { int src = block_param_alias(c, id, pk); const char *pnm = src >= 0 ? block_param_name(c, blk, pk) : NULL; LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT_ARRAY && plv->type != TY_FLOAT_ARRAY)) continue; if (plv->nullable_int_elem & NIE_STORED) changed |= mark_elem_slot(c, src, NIE_STORED | NIE_REACH); if (!(plv->nullable_int_elem & NIE_REACH) && elem_nil_reach_expr(c, src)) { plv->nullable_int_elem |= NIE_REACH; changed = 1; } } } /* `def give(a) = yield(a)` with `give(b) { |m| m << v }`: a block parameter bound by a yield's argument is that argument's array */ NT_FOREACH_KIND(nt, NK_YieldNode, y) { Scope *ys = comp_scope_of(c, y); int ymi = ys ? (int)(ys - c->scopes) : -1; int ya = nt_ref(nt, y, "arguments"), yan = 0; const int *yav = ya >= 0 ? nt_arr(nt, ya, "arguments", &yan) : NULL; int *sites = NULL, ns = 0; for (int max = 64; ymi > 0 && yan > 0; max *= 2) { int *grown = realloc(sites, sizeof(int) * (size_t)max); if (!grown) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } sites = grown; ns = yield_block_calls(c, ymi, sites, max); if (ns < max) break; } /* a list not known to be complete: every array counts as one a nil can reach */ if (ns < 0) { if (!nie_gave_up) { nie_gave_up = 1; changed = 1; } ns = 0; } for (int si = 0; si < ns; si++) { int blk = nt_ref(nt, sites[si], "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; Scope *bsc = comp_scope_of(c, blk); /* past a splat (`yield(*xs, a)`) a parameter can be bound to any of the arguments from it on */ int sp = -1; for (int k = 0; k < yan && sp < 0; k++) if (nt_kind(nt, yav[k]) == NK_SplatNode) sp = k; for (int pk = 0; ; pk++) { const char *pnm = block_param_name(c, blk, pk); if (!pnm) break; LocalVar *plv = bsc ? scope_local(bsc, pnm) : NULL; int typed = plv && (plv->type == TY_INT_ARRAY || plv->type == TY_FLOAT_ARRAY); int lo = sp >= 0 && pk >= sp ? sp : pk, hi = sp >= 0 && pk >= sp ? yan : pk + 1; for (int k = lo; plv && k < hi && k < yan; k++) { int av = yav[k]; if (nt_kind(nt, av) == NK_SplatNode) { /* an element of the splatted array is bound: an array of numbers holds none of an array, anything else may */ TyKind st = infer_type(c, nt_ref(nt, av, "expression")), et = ty_array_elem(st); int scalars = ty_is_array(st) && (et == TY_INT || et == TY_FLOAT || et == TY_STRING || et == TY_SYMBOL || et == TY_BOOL); if (typed && !scalars && !nie_gave_up) { nie_gave_up = 1; changed = 1; } continue; } /* a mixed parameter boxes the argument's array (mark_boxed_arrays) */ if (plv->type == TY_POLY && nie_boxed_store) changed |= mark_boxed_value(c, av); if (!typed) continue; if (plv->nullable_int_elem & NIE_STORED) changed |= mark_elem_slot(c, av, NIE_STORED | NIE_REACH); if (!(plv->nullable_int_elem & NIE_REACH) && elem_nil_reach_expr(c, av)) { plv->nullable_int_elem |= NIE_REACH; changed = 1; } } } } free(sites); } return changed; } /* `[[1, 2], [3]].each { |a, b| }`: a literal table whose row is shorter than the block's parameters (or is no literal row) leaves the parameters past it nil. A table of Integer or Float rows built any other way can hold a row of any length, or a nil row. */ static int nullable_table_row_params(Compiler *c) { const NodeTable *nt = c->nt; int changed = 0; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *rn2 = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int blk = nt_ref(nt, id, "block"); if (!rn2 || recv < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; TyKind rtt = infer_type(c, recv); int built = nt_kind(nt, recv) != NK_ArrayNode && (rtt == TY_INT_ARRAY_ARRAY || rtt == TY_FLOAT_ARRAY_ARRAY); if (nt_kind(nt, recv) != NK_ArrayNode && !built) continue; if (!sp_streq(rn2, "each") && !sp_streq(rn2, "each_entry") && !sp_streq(rn2, "map") && !sp_streq(rn2, "collect") && !sp_streq(rn2, "flat_map") && !sp_streq(rn2, "select") && !sp_streq(rn2, "filter") && !sp_streq(rn2, "reject") && !sp_streq(rn2, "each_with_object") && !sp_streq(rn2, "filter_map") && !sp_streq(rn2, "sum") && !sp_streq(rn2, "count")) continue; int bp = nt_ref(nt, blk, "parameters"); int params = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int pn = 0; const int *reqs = params >= 0 ? nt_arr(nt, params, "requireds", &pn) : NULL; if (!reqs || pn < 2) continue; int en = 0; const int *els = built ? NULL : nt_arr(nt, recv, "elements", &en); int minlen = built ? 0 : 1 << 30; for (int e = 0; els && e < en; e++) { if (nt_kind(nt, els[e]) != NK_ArrayNode) { minlen = 0; break; } int rl = 0; const int *rel = nt_arr(nt, els[e], "elements", &rl); for (int q = 0; rel && q < rl; q++) if (nt_kind(nt, rel[q]) == NK_SplatNode) rl = 0; if (rl < minlen) minlen = rl; } if (minlen >= pn) continue; Scope *bsc = comp_scope_of(c, blk); for (int pk = minlen; pk < pn; pk++) { const char *pnm = nt_kind(nt, reqs[pk]) == NK_RequiredParameterNode ? nt_str(nt, reqs[pk], "name") : NULL; LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int) continue; plv->nullable_int = 1; changed = 1; } } return changed; } /* a value position that reads a local holding only nil (for_each_value_position) */ static int nn_nil_only_leaf(const VpLeaf *leaf, void *ud) { Compiler *c = ud; if (leaf->kind != VPK_EXPR || nt_kind(c->nt, leaf->node) != NK_LocalVariableReadNode) return 0; const char *nm = nt_str(c->nt, leaf->node, "name"); Scope *sc = nm ? comp_scope_of(c, leaf->node) : NULL; LocalVar *lv = sc ? scope_local(sc, nm) : NULL; return lv && lv->nil_only; } static void mark_nullable_int_locals(Compiler *c) { const NodeTable *nt = c->nt; /* A scalar local a read can reach before any write starts as its nil and carries it (du_read_maybe_unset); the rounds below spread the mark */ { int *par = NULL; DUPos dp = { NULL, NULL }; /* `x &&= v` and `x += v` read x before they write it: an unassigned x there is nil, which `&&=` keeps and `+=` raises on, where the zero start was truthy and counted */ static const NodeKind rk[] = { NK_LocalVariableReadNode, NK_LocalVariableAndWriteNode, NK_LocalVariableOperatorWriteNode }; for (int q = 0; q < 3; q++) for (int r = comp_kind_first(c, rk[q]); r >= 0; r = comp_kind_next(c, r)) { if (nt_kind(nt, r) != rk[q]) continue; const char *nm = nt_str(nt, r, "name"); Scope *rs = nm ? comp_scope_of(c, r) : NULL; LocalVar *lv = rs ? scope_local(rs, nm) : NULL; if (!lv || lv->is_param || lv->is_block_param || lv->maybe_unset || (lv->type != TY_INT && lv->type != TY_FLOAT)) continue; if (!par) { par = du_parent_map(nt); if (!par) break; dp.pos = (int *)malloc(sizeof(int) * ((size_t)nt->count + 1)); dp.done = (char *)calloc((size_t)nt->count + 1, 1); if (!dp.pos || !dp.done) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int k = 0; k < nt->count; k++) dp.pos[k] = -1; } if (du_read_maybe_unset(nt, par, &dp, r, nm)) { lv->maybe_unset = 1; lv->nullable_int = 1; } } free(par); free(dp.pos); free(dp.done); du_memo_free(); } /* A method whose tail reads a local that only ever holds nil (nil_only, the late widening's boxed nil) returns nil: known before any round, so seeded here rather than found by the first round, which then ran one more to see nothing else change -- a round over every node, which the builtins' per-site helpers (__enum_find__N's `found`) cost every program (scale-test). The rule in the rounds would set the same. */ for (int mi = 1; mi < c->nscopes; mi++) { Scope *s = &c->scopes[mi]; if (s->ret_nullable_int || (s->ret != TY_INT && s->ret != TY_FLOAT && !((s->ret == TY_POLY || s->ret == TY_UNKNOWN) && s->yields))) continue; int tail = scope_body_last(c, mi); if (tail >= 0 && for_each_value_position(nt, tail, VPS_ALL, nn_nil_only_leaf, c)) s->ret_nullable_int = 1; } /* An --rbs `Integer?` return is the seeded form of the same property the rounds below infer, so start the propagation from it. */ for (int mi = 1; mi < c->nscopes; mi++) /* nullable_int is the SCALAR nil-flag property; a nilable string is recorded by ret_rbs_nilable alone (its nil is NULL, not a nil flag), so it must not seed this flag. */ if (c->scopes[mi].ret_rbs_nilable && (c->scopes[mi].ret == TY_INT || c->scopes[mi].ret == TY_FLOAT)) c->scopes[mi].ret_nullable_int = 1; /* A Float ivar initialize need not assign holds the constructor's nil seed (its nil byte, emit_ivar_nil_inits) until its first write, so a local copied from it can hold nil too. */ int toplevel_cid = c->toplevel_class; for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; if (ci->is_struct) continue; /* the top level has no initialize: its ivars are statics whose reads the nil marks of their writes decide */ int is_toplevel = k == toplevel_cid; for (int iv = 0; iv < ci->nivars; iv++) /* an Integer one too: nil out of band, its read is nil until the first write (the nil bit), and "assigned in initialize" means on every path (a top-level statement of initialize) */ if ((ci->ivar_types[iv] == TY_FLOAT || (ci->ivar_types[iv] == TY_INT && !is_toplevel)) && !ivar_assigned_in_initialize(c, k, ci->ivars[iv])) { ci->ivar_nullable_int[iv] = 1; comp_touch_class(ci->idx); } } /* Method returns propagate through this fixpoint too (a pass-through method is nilable because its callee is), so the cap has to clear a chain of them rather than the single hop the local marking used to need. Every sub-pass below skips what is already marked and only ever sets a flag, so `changed` is the real exit and the cap is a runaway backstop: measured over the 2636 test + benchmark programs, 2563 settle in one round, 72 in two and one in three. A chain deeper than the cap would stop early and silently under-mark -- the unsafe direction -- hence the did-not-converge check after the loop rather than a silent stop. Which is why the cap is derived from the program rather than fixed. A round that changes anything sets at least one of these flags and never clears one, so the rounds cannot outnumber the flags: methods propagate in definition order, so a chain of pass-through methods written in reverse advances one link per round, and a fixed cap of 32 refused to compile a legal 40-deep one. Past this bound the pass is not monotone, which is the only thing the check is here to catch. */ long rounds_max = c->nscopes + 2 + c->ngvars; for (int s = 0; s < c->nscopes; s++) rounds_max += c->scopes[s].nlocals; int converged = 0; block_sites_index(c); /* the block arm reads each method's yields */ int nimeth = 0; NamedMethod *imeth = instance_methods_by_name(c, &nimeth); for (long round = 0; round < rounds_max; round++) { int changed = 0; em_reset(); /* the marking walks' memo lasts a round */ nn_compute(c, (int)round); nie_boxed_store = boxed_nil_store(c); /* A method whose own return expression can be nil hands it to every caller. Without this, only an --rbs-seeded signature made a method nilable, so `def pass(x) = x.p_` silently laundered the nil into a plain int at the caller (#3505). */ for (int mi = 1; mi < c->nscopes; mi++) { Scope *s = &c->scopes[mi]; /* (a yielding method's boxed or untyped return too: a call site narrows it to a number, its block's, and asks this mark) */ if (s->ret_nullable_int || (s->ret != TY_INT && s->ret != TY_FLOAT && !((s->ret == TY_POLY || s->ret == TY_UNKNOWN) && s->yields))) continue; int tail = scope_body_last(c, mi); /* a body with rescue / else / ensure clauses: any arm's value */ if (tail < 0 && s->body >= 0 && nt_kind(nt, s->body) == NK_BeginNode) tail = s->body; if (tail >= 0 && nullable_int_value(c, tail)) { s->ret_nullable_int = 1; changed = 1; } } /* an explicit `return e` exits the method just as its tail does. Blocks share the enclosing method's scope, so a `return` inside one attributes to the method it returns from. */ NT_FOREACH_KIND(nt, NK_ReturnNode, id) { Scope *rs = comp_scope_of(c, id); int rmi = rs ? (int)(rs - c->scopes) : -1; if (rmi < 1 || rmi >= c->nscopes) continue; Scope *s = &c->scopes[rmi]; if (s->ret_nullable_int || (s->ret != TY_INT && s->ret != TY_FLOAT && !((s->ret == TY_POLY || s->ret == TY_UNKNOWN) && s->yields))) continue; if (nullable_int_value(c, id)) { s->ret_nullable_int = 1; changed = 1; } } for (int id = 0; id < nt->count; id++) { NodeKind wk = nt_kind(nt, id); if (wk != NK_LocalVariableWriteNode && wk != NK_LocalVariableAndWriteNode && wk != NK_LocalVariableOrWriteNode) continue; int v = nt_ref(nt, id, "value"); const char *ln = nt_str(nt, id, "name"); if (v < 0 || !ln) continue; Scope *sc = comp_scope_of(c, id); LocalVar *lv = sc ? scope_local(sc, ln) : NULL; if (!lv || (lv->type != TY_INT && lv->type != TY_FLOAT) || lv->nullable_int) continue; /* An outright `i = nil` on a slot the other writes make an int leaves nil in it just as a search miss does, and so does a local only nil is written to, here or through `&&=` / `||=`; a boxed value (`z = f(nil)` answering poly) can be nil too, and unboxes into the slot with it */ if (nullable_int_value(c, v) || infer_type(c, v) == TY_POLY) { lv->nullable_int = 1; changed = 1; } } /* A proc's Integer / Float parameter: a call with fewer arguments, or a nil one, binds nil, so the slot holds its nil beside the value (the proc function binds `(argc > k) ? ... : nil`). */ for (int id = 0; id < nt->count; id++) { if (!is_proc_create(c, id)) continue; Scope *ps = comp_scope_of(c, id); for (int k = 0; k < 16; k++) { const char *pn = proc_param_name(c, id, k); if (!pn) break; LocalVar *plv = ps ? scope_local(ps, pn) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int) continue; plv->nullable_int = 1; changed = 1; } } /* A global written from such a value carries it to every reader, just as a local does: `$g = a[i]` boxed the sentinel as a number (a Float's printed NaN) wherever $g was read. */ NT_FOREACH_KIND(nt, NK_GlobalVariableWriteNode, id) { int v = nt_ref(nt, id, "value"); const char *gn = nt_str(nt, id, "name"); const char *rn = gn ? comp_resolve_gvar(c, gn + 1) : NULL; LocalVar *g = rn ? comp_gvar(c, rn) : NULL; if (v < 0 || !g || (g->type != TY_INT && g->type != TY_FLOAT) || g->nullable_int) continue; if (nullable_int_value(c, v) || infer_type(c, v) == TY_POLY) { g->nullable_int = 1; changed = 1; } } /* A PARAMETER whose DEFAULT is the nil literal carries nil on every defaulted call even when each explicit call site passes a real number -- the number is what narrowed the slot to sp_int, and the call-site propagation below never sees the default. `of(path, line = nil)` stored the old nil sentinel boxed as an Integer: truthy, non-nil?, class Integer, while inspect still said nil (#4212). Only the boxing has to know, as everywhere in this family. */ for (int si2 = 1; si2 < c->nscopes; si2++) { Scope *sc2 = &c->scopes[si2]; if (!sc2->pdefault) continue; for (int pk2 = 0; pk2 < sc2->nparams; pk2++) { int dv2 = sc2->pdefault[pk2]; if (dv2 < 0) continue; LocalVar *p2 = sc2->pnames[pk2] ? scope_local(sc2, sc2->pnames[pk2]) : NULL; if (!p2 || (p2->type != TY_INT && p2->type != TY_FLOAT) || p2->nullable_int) continue; /* ... and so does any default that can be nil (`b = xs.first`, an ivar initialize never assigns): the slot holds its nil beside the value */ if (nt_kind(nt, dv2) != NK_NilNode && !nullable_int_value(c, dv2)) continue; p2->nullable_int = 1; changed = 1; } } /* A block's optional or keyword parameter whose default can be nil (`|k: @d|` under an instance_exec whose receiver has no @d) binds that nil whenever the call leaves it out */ for (int q = 0; q < 2; q++) { NodeKind pk = q ? NK_OptionalKeywordParameterNode : NK_OptionalParameterNode; NT_FOREACH_KIND(nt, pk, id) { int dv = nt_ref(nt, id, "value"); const char *pn = nt_str(nt, id, "name"); if (dv < 0 || !pn) continue; Scope *ps = comp_scope_of(c, id); LocalVar *pv = ps ? scope_local(ps, pn) : NULL; if (!pv || (pv->type != TY_INT && pv->type != TY_FLOAT) || pv->nullable_int) continue; if (nt_kind(nt, dv) != NK_NilNode && !nullable_int_value(c, dv)) continue; pv->nullable_int = 1; changed = 1; } } /* An int/float ARRAY local holding such a value hands it to every element read and to the block parameter of every iteration over it. The array's C element type is unchanged: only the boxing has to know (#3505). */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_LocalVariableWriteNode) continue; int v = nt_ref(nt, id, "value"); if (v < 0) continue; LocalVar *lv = nullable_elem_local(c, id, nt_str(nt, id, "name")); /* the value is the array itself, however it was built: a literal, an alias of another marked local, a chain, a method that returns one */ if (lv && nullable_int_elem_expr(c, v, 0)) { lv->nullable_int_elem |= NIE_MARKED; changed = 1; } /* ... and a marked local marks the array it was assigned from (`b = a; b << nil` puts the nil in a) */ Scope *ws = comp_scope_of(c, id); LocalVar *wl = ws && nt_str(nt, id, "name") ? scope_local(ws, nt_str(nt, id, "name")) : NULL; if (wl && (wl->type == TY_INT_ARRAY || wl->type == TY_FLOAT_ARRAY)) { if (wl->nullable_int_elem & NIE_STORED) changed |= mark_elem_slot(c, v, NIE_STORED | NIE_REACH); if (!(wl->nullable_int_elem & NIE_REACH) && elem_nil_reach_expr(c, v)) { wl->nullable_int_elem |= NIE_REACH; changed = 1; } } } /* the same slot written through an ivar */ NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, id) { int v = nt_ref(nt, id, "value"); if (v < 0) continue; ClassInfo *ci = NULL; int iv = nullable_elem_ivar(c, id, &ci); if (iv < 0) continue; if (ci->ivar_types[iv] != TY_INT_ARRAY && ci->ivar_types[iv] != TY_FLOAT_ARRAY) continue; /* (a marked ivar marks the array it was assigned from, as a local does) */ if (ci->ivar_nullable_int_elem[iv] & NIE_STORED) changed |= mark_elem_slot(c, v, NIE_STORED | NIE_REACH); if (!(ci->ivar_nullable_int_elem[iv] & NIE_REACH) && elem_nil_reach_expr(c, v)) { ci->ivar_nullable_int_elem[iv] |= NIE_REACH; changed = 1; } if (!(ci->ivar_nullable_int_elem[iv] & NIE_MARKED) && nullable_int_elem_expr(c, v, 0)) { ci->ivar_nullable_int_elem[iv] |= NIE_MARKED; changed = 1; } } /* A mutation that stores such a value -- or copies in an array that can hold one -- marks the array it mutates, whether it names a local, an ivar or the ivar an object call hands out (`k.arr << v`, `k.arr[i] = v`). */ NT_FOREACH_KIND(nt, NK_CallNode, id) { int recv = nt_ref(nt, id, "receiver"); if (recv >= 0 && nullable_elem_mutation(c, id, 0)) changed |= mark_elem_slot(c, recv, NIE_MARKED | NIE_STORED | NIE_REACH); } changed |= mark_boxed_arrays(c); /* `ks.each { |k| h[k] = ... }`: the block parameter IS the element (elem_block_params). A poly parameter is marked too: the typed emitters (map, select, reject, inject, ...) shadow it with the element's type, which then holds the nil. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { int blk = nt_ref(nt, id, "block"), recv = -1; int np = elem_block_params(c, id, &recv); if (!np || !nullable_int_elem_expr(c, recv, 0)) continue; Scope *bsc = comp_scope_of(c, blk); for (int pk = 0; pk < np; pk++) { const char *pnm = block_param_name(c, blk, pk); LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT && plv->type != TY_POLY) || plv->nullable_int) continue; plv->nullable_int = 1; changed = 1; } } changed |= mark_block_param_arrays(c); /* `a.zip(b) { |x, y| }`: x is an element of a, y one of b */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *znm = nt_str(nt, id, "name"); int blk = nt_ref(nt, id, "block"), recv = nt_ref(nt, id, "receiver"); if (!znm || !sp_streq(znm, "zip") || blk < 0 || recv < 0) continue; int za = nt_ref(nt, id, "arguments"), zn = 0; const int *zv = za >= 0 ? nt_arr(nt, za, "arguments", &zn) : NULL; Scope *bsc = comp_scope_of(c, blk); for (int pk = 0; pk <= zn; pk++) { const char *pnm = block_param_name(c, blk, pk); LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int || !nullable_int_elem_expr(c, pk ? zv[pk - 1] : recv, 0)) continue; plv->nullable_int = 1; changed = 1; } } /* `h.fetch(k) { |key| }`: a missing key is handed to the block, so a key that can be nil reaches it as nil (the typed slot unwrapped it with a TypeError) */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *fnm = nt_str(nt, id, "name"); int blk = nt_ref(nt, id, "block"), recv = nt_ref(nt, id, "receiver"); if (!fnm || !sp_streq(fnm, "fetch") || blk < 0 || recv < 0 || nt_kind(nt, blk) != NK_BlockNode || !ty_is_hash(infer_type(c, recv))) continue; int fa = nt_ref(nt, id, "arguments"), fn = 0; const int *fv = fa >= 0 ? nt_arr(nt, fa, "arguments", &fn) : NULL; /* (a boxed key can hold nil too: --int-overflow=promote boxes every Integer local) */ if (fn != 1 || !(nullable_int_value(c, fv[0]) || infer_type(c, fv[0]) == TY_POLY)) continue; Scope *bsc = comp_scope_of(c, blk); const char *pnm = block_param_name(c, blk, 0); LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int) continue; plv->nullable_int = 1; changed = 1; } /* a nil value stored into a typed Integer-valued Hash, and the walks that bind it (D3b-ii) */ if (mark_hash_nil_values(c)) changed = 1; /* `x.then { |y| }`, `x.tap { |y| }`: the block parameter IS the receiver, nil included. A boxed receiver (every Integer local under --int-overflow=promote) is unboxed into it with its nil kept in the nil flag (emit_tap_then_expr), so it can leave one there too. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *tn = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"), blk = nt_ref(nt, id, "block"); if (!tn || recv < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; if (!is_tap_alias(tn)) continue; int bp = nt_ref(nt, blk, "parameters"); int params = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int rn = 0; const int *reqs = params >= 0 ? nt_arr(nt, params, "requireds", &rn) : NULL; const char *pnm = reqs && rn >= 1 ? nt_str(nt, reqs[0], "name") : NULL; Scope *bsc = pnm ? comp_scope_of(c, blk) : NULL; LocalVar *plv = bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int) continue; if (nullable_int_value(c, recv) || infer_type(c, recv) == TY_POLY) { plv->nullable_int = 1; changed = 1; } } /* `a.each_slice(n) { |w, x| }` and `a.each_cons(n) { |w, x| }` bind a row's elements: a parameter past a short last row or past n is nil, and so is any parameter when an element can be nil or the row is boxed (every element of a poly array can be). Only a literal n over a literal array it divides, with no parameter past n, has no nil. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *rn2 = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"), blk = nt_ref(nt, id, "block"), win = id; if (!rn2 || recv < 0 || blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; /* ... and so does an iteration over the rows it hands on (`a.each_cons(2).map { |w, x| }`) */ if (strbuf_elem_first_iterator(rn2) && !sp_streq(rn2, "each_with_index") && !sp_streq(rn2, "each_with_object") && nt_kind(nt, recv) == NK_CallNode && nt_ref(nt, recv, "block") < 0 && nt_str(nt, recv, "name")) { win = recv; rn2 = nt_str(nt, win, "name"); recv = nt_ref(nt, win, "receiver"); if (recv < 0) continue; } int cons = sp_streq(rn2, "each_cons"); if (!cons && !sp_streq(rn2, "each_slice")) continue; int ca = nt_ref(nt, win, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (an != 1) continue; int bp = nt_ref(nt, blk, "parameters"); int params = bp >= 0 ? nt_ref(nt, bp, "parameters") : -1; int pn = 0; const int *reqs = params >= 0 ? nt_arr(nt, params, "requireds", &pn) : NULL; if (!reqs || pn < 2) continue; long long n = nt_kind(nt, av[0]) == NK_IntegerNode ? nt_int(nt, av[0], "value", 0) : 0; int en = -1; if (nt_kind(nt, recv) == NK_ArrayNode) { const int *els = nt_arr(nt, recv, "elements", &en); for (int e = 0; els && e < en; e++) if (nt_kind(nt, els[e]) == NK_SplatNode) en = -1; } int any_nil = nullable_int_elem_expr(c, recv, 0) || infer_type(c, recv) == TY_POLY_ARRAY; int full_rows = n > 0 && (cons || (en >= 0 && en % n == 0)); Scope *bsc = comp_scope_of(c, blk); for (int pk = 0; pk < pn; pk++) { const char *pnm = nt_str(nt, reqs[pk], "name"); LocalVar *plv = pnm && bsc ? scope_local(bsc, pnm) : NULL; if (!plv || (plv->type != TY_INT && plv->type != TY_FLOAT) || plv->nullable_int) continue; int past_n = n > 0 ? pk >= n : pk > 0; /* a size not written out may be 1 */ if (any_nil || past_n || (pk > 0 && !full_rows)) { plv->nullable_int = 1; changed = 1; } } } changed |= nullable_table_row_params(c); /* An IVAR written from such a value hands its nil to every reader, including the attr_reader a caller goes through (`W.new(r.p_).v`). The slot keeps its scalar C type, its nil in its nil byte, so only the boxing has to know (#3505). */ NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, id) { int v = nt_ref(nt, id, "value"); if (v < 0) continue; Scope *s = comp_scope_of(c, id); int cid = s ? s->class_id : -1; /* inside `r.instance_eval { @x = nil }` the ivar is the receiver's */ if (ie_class_of(c, id) >= 0) cid = ie_class_of(c, id); if (cid < 0) cid = c->toplevel_class; if (cid < 0 || cid >= c->nclasses) continue; ClassInfo *ci = &c->classes[cid]; int iv = comp_ivar_index(ci, nt_str(nt, id, "name")); if (iv < 0 || ci->ivar_nullable_int[iv]) continue; if (ci->ivar_types[iv] != TY_INT && ci->ivar_types[iv] != TY_FLOAT) continue; if (nullable_int_value(c, v) || infer_type(c, v) == TY_POLY) { { ci->ivar_nullable_int[iv] = 1; comp_touch_class(ci->idx); } changed = 1; } } /* ... and through a multiple assignment: a target the right side can leave nil (`@@g, @@h = 7`, `bx.v, bx.w = nil, 6`, `o.x, o.y = 3`) holds that nil -- a class variable, an ivar, an attribute's ivar */ NT_FOREACH_KIND(nt, NK_MultiWriteNode, mw) { int mv = nt_ref(nt, mw, "value"); int ln = 0; const int *ls = nt_arr(nt, mw, "lefts", &ln); int rn2 = 0; const int *rs2 = nt_arr(nt, mw, "rights", &rn2); int mrest = nt_ref(nt, mw, "rest"); int lit = mv >= 0 && nt_kind(nt, mv) == NK_ArrayNode; int en = 0; const int *ev = lit ? nt_arr(nt, mv, "elements", &en) : NULL; int has_splat = 0; for (int e = 0; e < en; e++) if (nt_kind(nt, ev[e]) == NK_SplatNode) has_splat = 1; TyKind mvt = mv >= 0 ? infer_type(c, mv) : TY_UNKNOWN; int scalar_rhs = !lit && mv >= 0 && !ty_is_array(mvt) && mvt != TY_POLY && mvt != TY_UNKNOWN; for (int pass = 0; pass < 2; pass++) { int n = pass ? rn2 : ln; const int *tg = pass ? rs2 : ls; for (int j = 0; j < n; j++) { int t = tg[j]; NodeKind tk = nt_kind(nt, t); if (tk != NK_ClassVariableTargetNode && tk != NK_InstanceVariableTargetNode && tk != NK_CallTargetNode) continue; /* can this position be nil */ int may_nil; if (lit && !has_splat) { /* a left takes element j; a right after the rest counts from the end, and an element the lefts took leaves it nil */ int pos = pass ? (en - rn2 + j) : j; may_nil = (pos < 0 || pos >= en || (pass && pos < ln)) ? 1 : nullable_int_value(c, ev[pos]); } else if (scalar_rhs) may_nil = pass || j > 0 || nullable_int_value(c, mv); /* a constant Array of a known length (or a conditional of such): a left within it takes that element of each literal the constant can be */ else if (!pass && mrest < 0) may_nil = masgn_const_pos_may_nil(c, mv, j); else may_nil = 1; /* an array of unknown length, or a boxed value */ if (!may_nil) continue; ClassInfo *ci = NULL; int iv = -1, is_cv = 0; if (tk == NK_ClassVariableTargetNode) { iv = cvar_slot(c, t, &ci); is_cv = 1; } else if (tk == NK_InstanceVariableTargetNode) { Scope *s = comp_scope_of(c, t); int cid = s ? s->class_id : -1; if (ie_class_of(c, t) >= 0) cid = ie_class_of(c, t); if (cid < 0) cid = c->toplevel_class; if (cid >= 0 && cid < c->nclasses) { ci = &c->classes[cid]; iv = comp_ivar_index(ci, nt_str(nt, t, "name")); } } else { int trc = nt_ref(nt, t, "receiver"); const char *wn = nt_str(nt, t, "name"); size_t wl = wn ? strlen(wn) : 0; TyKind rt = trc >= 0 ? infer_type(c, trc) : TY_UNKNOWN; if (!ty_is_object(rt) || wl < 2 || wl > 255) continue; int cid = ty_object_class(rt), defc = -1; char base[256]; if (!setter_base_name(wn, base, sizeof base)) memcpy(base, wn, wl + 1); if (comp_writer_in_chain(c, cid, base, &defc)) { char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, cid, base)); ci = &c->classes[defc >= 0 ? defc : cid]; iv = comp_ivar_index(ci, ivb); } else iv = struct_member_slot(c, cid, base, &ci); } if (iv < 0 || !ci) continue; if (is_cv) { if (ci->cvar_nullable_int[iv]) continue; if (ci->cvar_types[iv] != TY_INT && ci->cvar_types[iv] != TY_FLOAT) continue; ci->cvar_nullable_int[iv] = 1; changed = 1; } else { if (ci->ivar_nullable_int[iv]) continue; if (ci->ivar_types[iv] != TY_INT && ci->ivar_types[iv] != TY_FLOAT) continue; { ci->ivar_nullable_int[iv] = 1; comp_touch_class(ci->idx); } changed = 1; } } } } /* ... and `o.send(:x=, v)`: the attr writer's ivar takes v */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *sn = nt_str(nt, id, "name"); if (!sn || (!sp_streq(sn, "send") && !sp_streq(sn, "public_send") && !sp_streq(sn, "__send__"))) continue; int recv = nt_ref(nt, id, "receiver"); int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; if (recv < 0 || an != 2 || nt_kind(nt, av[0]) != NK_SymbolNode) continue; const char *wn = nt_str(nt, av[0], "value"); char base[256]; if (!setter_base_name(wn, base, sizeof base)) continue; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) continue; int cid = ty_object_class(rt), defc = -1; ClassInfo *ci = NULL; int iv = -1; if (comp_writer_in_chain(c, cid, base, &defc)) { char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, cid, base)); ci = &c->classes[defc >= 0 ? defc : cid]; iv = comp_ivar_index(ci, ivb); } if (iv < 0 || !ci || ci->ivar_nullable_int[iv]) continue; if (ci->ivar_types[iv] != TY_INT && ci->ivar_types[iv] != TY_FLOAT) continue; if (nullable_int_value(c, av[1])) { { ci->ivar_nullable_int[iv] = 1; comp_touch_class(ci->idx); } changed = 1; } } /* ... and through a setter that is no ivar write in the program: an attr_writer's or a Struct member's `o.x = v` */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *wn = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); char base[256]; /* a writer called by name (`o.send(:x=, v)` desugars to it) as well as the assignment syntax; the comparison operators are no writers */ if (recv < 0 || !setter_base_name(wn, base, sizeof base)) continue; int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; TyKind rt = infer_type(c, recv); if (an != 1 || (!ty_is_object(rt) && rt != TY_POLY)) continue; if (!nullable_int_value(c, av[0])) continue; /* a boxed receiver's write reaches the member of every class that takes it, as `s[:x] = v` below does (#8769) */ for (int cid = ty_is_object(rt) ? ty_object_class(rt) : 0; cid < c->nclasses; cid++) { if (ty_is_object(rt) && cid != ty_object_class(rt)) break; int defc = -1; ClassInfo *ci = NULL; int iv = -1; if (comp_writer_in_chain(c, cid, base, &defc)) { char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", comp_resolve_alias(c, cid, base)); ci = &c->classes[defc >= 0 ? defc : cid]; iv = comp_ivar_index(ci, ivb); } else if (call_target(c, id, cid, wn, 0, NULL) < 0) iv = struct_member_slot(c, cid, base, &ci); if (iv < 0 || ci->ivar_nullable_int[iv]) continue; if (ci->ivar_types[iv] != TY_INT && ci->ivar_types[iv] != TY_FLOAT) continue; { ci->ivar_nullable_int[iv] = 1; comp_touch_class(ci->idx); } changed = 1; } } /* A Struct's or Data's custom initialize forwarding its parameters through `super`: a parameter that can hold nil (a `song: nil` default) reaches the member of its name, or of its position */ for (int si = 1; si < c->nscopes; si++) { Scope *is = &c->scopes[si]; if (!is->name || !sp_streq(is->name, "initialize") || is->is_cmethod || is->class_id < 0) continue; int k = is->class_id, sc = -1; for (int hop = 0; k >= 0 && hop < 64; k = c->classes[k].parent, hop++) if (c->classes[k].is_struct || c->classes[k].is_data) { sc = k; break; } if (sc < 0) continue; ClassInfo *sci = &c->classes[sc]; for (int pi = 0; pi < is->nparams; pi++) { const char *pn = is->pnames ? is->pnames[pi] : NULL; LocalVar *pv = pn ? scope_local(is, pn) : NULL; if (!pv || !pv->nullable_int) continue; char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", pn); int m = comp_ivar_index(sci, ivb); if (m < 0 || m >= sci->nmembers) m = pi < sci->nmembers ? pi : -1; if (m < 0 || sci->ivar_nullable_int[m]) continue; if (sci->ivar_types[m] != TY_INT && sci->ivar_types[m] != TY_FLOAT) continue; { sci->ivar_nullable_int[m] = 1; comp_touch_class(sci->idx); } changed = 1; } } /* `s[:x] = v` / `s["x"] = v` / `s[0] = v` on a Struct (or a boxed value that can be one): the member takes v, nil included */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *sn = nt_str(nt, id, "name"); int srecv = nt_ref(nt, id, "receiver"); if (!sn || !sp_streq(sn, "[]=") || srecv < 0) continue; int sa = nt_ref(nt, id, "arguments"); int san = 0; const int *sav = sa >= 0 ? nt_arr(nt, sa, "arguments", &san) : NULL; if (san != 2) continue; if (nt_kind(nt, sav[1]) != NK_NilNode && !nullable_int_value(c, sav[1])) continue; NodeKind kk = nt_kind(nt, sav[0]); if (kk != NK_SymbolNode && kk != NK_StringNode && kk != NK_IntegerNode) continue; TyKind srt = infer_type(c, srecv); int only = ty_is_object(srt) ? ty_object_class(srt) : -1; if (only < 0 && srt != TY_POLY) continue; for (int k = 0; k < c->nclasses; k++) { if (only >= 0 && k != only) continue; ClassInfo *sci = &c->classes[k]; if (!sci->is_struct || sci->nmembers <= 0) continue; int m = -1; if (kk == NK_IntegerNode) { long long ix = nt_int(nt, sav[0], "value", 0); if (ix < 0) ix += sci->nmembers; if (ix >= 0 && ix < sci->nmembers) m = (int)ix; } else { const char *mn = kk == NK_SymbolNode ? nt_str(nt, sav[0], "value") : nt_str(nt, sav[0], "content"); char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", mn ? mn : ""); int iv = mn ? comp_ivar_index(sci, ivb) : -1; if (iv >= 0 && iv < sci->nmembers) m = iv; } if (m < 0 || sci->ivar_nullable_int[m]) continue; if (sci->ivar_types[m] != TY_INT && sci->ivar_types[m] != TY_FLOAT) continue; { sci->ivar_nullable_int[m] = 1; comp_touch_class(sci->idx); } changed = 1; } } /* `d.with(x: v)` on a Struct or Data: the copy's member takes v, nil included (#8508) */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *wn = nt_str(nt, id, "name"); int wrecv = nt_ref(nt, id, "receiver"); if (!wn || !sp_streq(wn, "with") || wrecv < 0) continue; int wa = nt_ref(nt, id, "arguments"); int wan = 0; const int *wav = wa >= 0 ? nt_arr(nt, wa, "arguments", &wan) : NULL; if (wan != 1 || nt_kind(nt, wav[0]) != NK_KeywordHashNode) continue; TyKind wrt = infer_type(c, wrecv); if (!ty_is_object(wrt)) continue; ClassInfo *sci = &c->classes[ty_object_class(wrt)]; if (!sci->is_struct || sci->nmembers <= 0) continue; int kn = 0; const int *ke = nt_arr(nt, wav[0], "elements", &kn); for (int e = 0; e < kn; e++) { if (nt_kind(nt, ke[e]) != NK_AssocNode) continue; int key = nt_ref(nt, ke[e], "key"), v = nt_ref(nt, ke[e], "value"); const char *mn = key >= 0 && nt_kind(nt, key) == NK_SymbolNode ? nt_str(nt, key, "value") : NULL; if (!mn || v < 0 || (nt_kind(nt, v) != NK_NilNode && !nullable_int_value(c, v))) continue; char ivb[300]; snprintf(ivb, sizeof ivb, "@%s", mn); int m = comp_ivar_index(sci, ivb); if (m < 0 || m >= sci->nmembers || sci->ivar_nullable_int[m]) continue; if (sci->ivar_types[m] != TY_INT && sci->ivar_types[m] != TY_FLOAT) continue; { sci->ivar_nullable_int[m] = 1; comp_touch_class(sci->idx); } changed = 1; } } /* `o.instance_variable_set(:@x, v)` with a nil, or a nullable number, stores it in the ivar as a plain write would. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *sn = nt_str(nt, id, "name"); int srecv = nt_ref(nt, id, "receiver"); if (!sn || !sp_streq(sn, "instance_variable_set") || srecv < 0) continue; int sa = nt_ref(nt, id, "arguments"); int san = 0; const int *sav = sa >= 0 ? nt_arr(nt, sa, "arguments", &san) : NULL; if (san != 2 || nt_kind(nt, sav[0]) != NK_SymbolNode) continue; TyKind srt = infer_type(c, srecv); int scid = ty_is_object(srt) ? ty_object_class(srt) : -1; if (scid < 0 && nt_kind(nt, srecv) == NK_SelfNode) { Scope *ss = comp_scope_of(c, id); scid = ss ? ss->class_id : -1; } const char *ivn0 = nt_str(nt, sav[0], "value"); if (!ivn0 || !ivn0[0]) continue; char ivn[300]; snprintf(ivn, sizeof ivn, "%s%s", ivn0[0] == '@' ? "" : "@", ivn0); /* the symbol's text, with or without its `@` */ /* a boxed receiver can be an instance of any class: every class with an Integer or Float ivar of that name can be handed the nil */ if (scid < 0 && srt == TY_POLY && (nt_kind(nt, sav[1]) == NK_NilNode || nullable_int_value(c, sav[1]))) { for (int k = 0; k < c->nclasses; k++) { ClassInfo *pci = &c->classes[k]; int piv = comp_ivar_index(pci, ivn); if (piv < 0 || pci->ivar_nullable_int[piv]) continue; if (pci->ivar_types[piv] != TY_INT && pci->ivar_types[piv] != TY_FLOAT) continue; { pci->ivar_nullable_int[piv] = 1; comp_touch_class(pci->idx); } changed = 1; } continue; } if (scid < 0 || scid >= c->nclasses) continue; ClassInfo *sci = NULL; int siv = -1; for (int k = scid; k >= 0 && siv < 0; k = c->classes[k].parent) { sci = &c->classes[k]; siv = comp_ivar_index(sci, ivn); } if (siv < 0 || sci->ivar_nullable_int[siv]) continue; if (sci->ivar_types[siv] != TY_INT && sci->ivar_types[siv] != TY_FLOAT) continue; if (nt_kind(nt, sav[1]) == NK_NilNode || nullable_int_value(c, sav[1])) { { sci->ivar_nullable_int[siv] = 1; comp_touch_class(sci->idx); } changed = 1; } } /* A Struct's generated constructor sets its members: from a nil argument, or to nil when the construction does not supply one (`S.new(1)` of two members; a Data raises instead). */ NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *cn = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!cn || !sp_streq(cn, "new")) continue; NodeKind rk = recv >= 0 ? nt_kind(nt, recv) : NK_SelfNode; /* the classes the receiver can be: a class value chosen at run time (`(c ? A : B).new`, `[A, B][i].new`) is each of them */ int ks[8], nk = 0; if (rk == NK_SelfNode) { /* `new(...)` / `self.new(...)` in a class method of the Struct / Data itself (`def self.of(path, line = nil) = new(path:, line:)`) */ Scope *ns = comp_scope_of(c, id); if (ns && ns->is_cmethod && ns->class_id >= 0) ks[nk++] = ns->class_id; } else nk = new_recv_classes(c, recv, ks, 8); /* a class value no analysis names (`Object.const_get(x).new`): the run-time switch constructs any Struct of the program with the arguments given and nil for the rest (#9093: a typed member got 0.0 where CRuby has nil); an argument that may be nil makes its member nullable there too, as the arguments' types reach the members (struct_new_types_members_at) */ int every = nk == 0 && new_recv_every_class(c, recv); int nloop = every ? c->nclasses : nk; for (int q = 0; q < nloop; q++) { int k = every ? q : ks[q]; if (k < 0 || !(c->classes[k].is_struct || c->classes[k].is_data) || comp_method_in_chain(c, k, "initialize", NULL) >= 0) continue; ClassInfo *ci = &c->classes[k]; int ca = nt_ref(nt, id, "arguments"); int an = 0; const int *av = ca >= 0 ? nt_arr(nt, ca, "arguments", &an) : NULL; /* a splat or a `**` member is boxed (struct_new_types_members) */ int plain = !nt_argv_has(nt, av, an, NT_ARG_SPLAT | NT_ARG_BLOCK); int kwh = an == 1 && nt_kind(nt, av[0]) == NK_KeywordHashNode && ci->kw_init != -1 ? av[0] : -1; int kn = 0; const int *ke = kwh >= 0 ? nt_arr(nt, kwh, "elements", &kn) : NULL; for (int e = 0; e < kn && plain; e++) plain = nt_kind(nt, ke[e]) == NK_AssocNode; if (!plain) continue; for (int m = 0; m < ci->nivars; m++) { if (ci->ivar_nullable_int[m] || (ci->ivar_types[m] != TY_INT && ci->ivar_types[m] != TY_FLOAT)) continue; int vnode = kwh < 0 && m < an ? av[m] : -1; for (int e = 0; e < kn; e++) { int key = nt_ref(nt, ke[e], "key"); const char *kv = nt_kind(nt, key) == NK_SymbolNode ? nt_str(nt, key, "value") : NULL; if (kv && sp_streq(kv, ci->ivars[m] + 1)) vnode = nt_ref(nt, ke[e], "value"); } if (vnode < 0 ? !ci->is_data : nullable_int_value(c, vnode)) { { ci->ivar_nullable_int[m] = 1; comp_touch_class(ci->idx); } changed = 1; } } } } /* ... and through a class variable */ NT_FOREACH_KIND(nt, NK_ClassVariableWriteNode, id) { int v = nt_ref(nt, id, "value"); ClassInfo *ci = NULL; int cv = v >= 0 ? cvar_slot(c, id, &ci) : -1; if (cv < 0 || ci->cvar_nullable_int[cv]) continue; if (ci->cvar_types[cv] != TY_INT && ci->cvar_types[cv] != TY_FLOAT) continue; if (nullable_int_value(c, v)) { ci->cvar_nullable_int[cv] = 1; changed = 1; } } /* `@@x ||= v` reads the slot before any write set it: that read is nil, so the slot holds a nil (the `||=` / `&&=` test is on it) */ NT_FOREACH_KIND(nt, NK_ClassVariableOrWriteNode, id) { ClassInfo *ci = NULL; int cv = cvar_slot(c, id, &ci); if (cv < 0 || ci->cvar_nullable_int[cv]) continue; if (ci->cvar_types[cv] != TY_INT && ci->cvar_types[cv] != TY_FLOAT) continue; ci->cvar_nullable_int[cv] = 1; changed = 1; } NT_FOREACH_KIND(nt, NK_ClassVariableAndWriteNode, id) { ClassInfo *ci = NULL; int cv = cvar_slot(c, id, &ci); if (cv < 0 || ci->cvar_nullable_int[cv]) continue; if (ci->cvar_types[cv] != TY_INT && ci->cvar_types[cv] != TY_FLOAT) continue; ci->cvar_nullable_int[cv] = 1; changed = 1; } /* A PARAMETER bound from such a value carries nil into the callee, where boxing it (`other.inspect`, `x == other`) has the same problem the local marking exists to prevent. */ NT_FOREACH_KIND(nt, NK_CallNode, id) { int recv = nt_ref(nt, id, "receiver"); int mi = recv < 0 ? comp_self_call_mi(c, id, nt_str(nt, id, "name")) : call_target(c, id, -1, nt_str(nt, id, "name"), CT_TOP, NULL); /* a bare `new(...)` in a class method or a class body builds an instance of that class: initialize's parameters take the nil, as `W.new(k)`'s do below (#8572) */ if (mi < 0 && recv < 0 && nt_str(nt, id, "name") && sp_streq(nt_str(nt, id, "name"), "new")) { Scope *bs = comp_scope_of(c, id); int bk = bs->def_node >= 0 ? (bs->is_cmethod ? bs->class_id : -1) : (c->node_cbody && id < c->node_cap ? c->node_cbody[id] : -1); if (bk >= 0) mi = comp_method_in_chain(c, bk, "initialize", NULL); } if (mi < 0 && recv >= 0) { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) mi = call_target(c, id, ty_object_class(rt), nt_str(nt, id, "name"), 0, NULL); /* a builtin-typed receiver calling a method the program adds to Object that no builtin row serves: codegen's Object fallback reaches it, so the nil its arguments carry marks it (`12.enc(nil)`) */ else if (rt != TY_POLY && rt != TY_UNKNOWN && rt != TY_VOID && rt != TY_NIL && nt_kind(nt, recv) != NK_ConstantReadNode && nt_kind(nt, recv) != NK_ConstantPathNode && nt_str(nt, id, "name")) { int oci = comp_core_class(c, CORE_OBJECT); int omi = oci >= 0 ? call_target(c, id, oci, nt_str(nt, id, "name"), CT_OWN, NULL) : -1; int ba = nt_ref(nt, id, "arguments"); int bac = 0; if (ba >= 0) nt_arr(nt, ba, "arguments", &bac); if (omi >= 0 && !bop_find(rt, nt_str(nt, id, "name"), bac, nt_ref(nt, id, "block") >= 0)) mi = omi; /* ... and a method the program defines on the builtin class itself (`class Float; def <(other)`): the reopen answers the call, so the nil an argument can be reaches its parameter, which holds it beside the value */ int bcn = builtin_class_of_type(rt) ? core_of_type(rt) : -1; int bci = bcn >= 0 ? comp_core_class(c, bcn) : -1; int bmi = bci >= 0 ? call_target(c, id, bci, nt_str(nt, id, "name"), CT_OWN, NULL) : -1; if (bmi >= 0) mi = bmi; } /* `W.new(k)` binds initialize's parameters, and `W.build(k)` a class method's: neither receiver is an instance, so the arm above cannot see them and the nil stopped at the constructor (#3505). A scoped receiver (`Views::Inbox.label(@page)`) resolves by its leaf name, the key classes are indexed under (#5091). */ else if (nt_kind(nt, recv) == NK_ConstantReadNode || nt_kind(nt, recv) == NK_ConstantPathNode) { const char *cnm = nt_str(nt, id, "name"); int rci = const_node_class_index(c, recv); if (rci >= 0) mi = sp_streq(cnm, "new") ? comp_method_in_chain(c, rci, "initialize", NULL) : call_target(c, id, rci, cnm, CT_CMETHOD, NULL); } } /* `m.call(v)` on a Method, or on the Proc its to_proc made, is the only site of a method reached that way: it binds each method the receiver may hold (method_recv_nodes). A bound builtin's wrapper carries its receiver in the first parameter and is left alone. */ const char *cn = nt_str(nt, id, "name"); /* a boxed receiver binds whichever method of the name its class has: the dispatch's arms are every one of them, which the lookups above cannot name, and each takes the nil an argument can be */ int kf = mi < 0 && recv >= 0 && cn && nt_ref(nt, id, "arguments") >= 0 ? named_method_first(imeth, nimeth, cn) : nimeth; if (kf < nimeth && sp_streq(imeth[kf].name, cn) && infer_type(c, recv) == TY_POLY) { /* a call with no arguments hands no parameter a nil, and a name no instance method has reaches no arm */ for (int k = kf; k < nimeth && sp_streq(imeth[k].name, cn); k++) if (method_takes_call_args(c, &c->scopes[imeth[k].mi], id) && /* only a method that takes the call's arguments is its callee */ !cls_fact_excludes(c, recv, c->scopes[imeth[k].mi].class_id)) /* ... of a class the receiver can hold (#8530) */ changed |= mark_nullable_params_of_call(c, id, imeth[k].mi); } else if (mi >= 0) changed |= mark_nullable_params_of_call(c, id, mi); /* a bare call where self is main reaches the method of a module the top level includes, a copy of its own where that module took it from another (`include IM` in M, `include M` at the top): the first method of the name, which the line above marks, is not that copy, and the nil reached a parameter the call never binds (#9193) */ if (recv < 0 && cn && nt_ref(nt, id, "arguments") >= 0 && self_is_main(c, id)) for (int k = 0; k < c->ntoplevel_includes; k++) { int tmi = call_target(c, id, c->toplevel_includes[k], cn, 0, NULL); if (tmi >= 0 && tmi != mi) changed |= mark_nullable_params_of_call(c, id, tmi); } /* a class value, boxed or not (`[Q, R].each { |o| o.cm(k: nil) }`), binds the class method of the name any class has */ else if (recv >= 0 && cn && nt_ref(nt, id, "arguments") >= 0 && (infer_type(c, recv) == TY_CLASS || infer_type(c, recv) == TY_POLY)) { for (int k = 0; k < c->nclasses; k++) { int cmi = call_target(c, id, k, cn, CT_OWN | CT_CMETHOD, NULL); /* an inherited one is its definer's */ if (cmi >= 0) changed |= mark_nullable_params_of_call(c, id, cmi); } } if (recv >= 0 && cn && is_call_alias(cn) && mi < 0) { TyKind rt = infer_type(c, recv); int *mns = NULL; int nmn = rt == TY_METHOD ? method_recv_nodes(c, recv, &mns) : rt == TY_PROC ? proc_to_proc_method_nodes(c, recv, &mns) : 0; for (int j = 0; j < nmn; j++) { int tmi = method_obj_target_mi(c, mns[j]); if (tmi >= 0 && !method_call_param_shift(c, mns[j], tmi)) changed |= mark_nullable_params_of_call(c, id, tmi); } free(mns); } } /* ... and a `super` with arguments binds the method it reaches */ NT_FOREACH_KIND(nt, NK_SuperNode, id) { Scope *ss = comp_scope_of(c, id); if (!ss || ss->class_id < 0 || !ss->name || comp_super_is_class_new(c, id)) continue; int sp = comp_super_parent(c, ss->class_id, ss->is_cmethod); const char *sn = sp >= 0 ? comp_super_name(c, sp, comp_scope_def_name(c, ss), ss->is_cmethod) : NULL; if (!sn) continue; int smi = ss->is_cmethod ? comp_cmethod_in_chain(c, sp, sn, NULL) : comp_method_in_chain(c, sp, sn, NULL); if (smi >= 0) changed |= mark_nullable_params_of_call(c, id, smi); } NT_FOREACH_KIND(nt, NK_ForwardingSuperNode, id) { Scope *ss = comp_scope_of(c, id); if (!ss || ss->class_id < 0 || !ss->name || comp_super_is_class_new(c, id)) continue; int smi = a_super_target(c, ss); if (smi >= 0) changed |= mark_nullable_params_of_zsuper(c, id, smi); } /* A BLOCK parameter bound from such a value: the sites that bind the block -- each yield of the method the call reaches, or an instance_exec's own arguments -- typed again by the binding plan (block_site_types), which records a value that may be nil as BS_NIL. An Integer or a Float required or optional stays that type, its binders handing the nil through, so it is marked here as block_settle_types marks one during inference: a value the rounds above made nilable since (a local copied from a missed read) was not one yet then, and `yield(i == 0 ? nil : i)` left `Integer === a` true. */ { char *hot = NULL; for (int si = 1; si < c->nscopes; si++) { if (!c->scopes[si].yields) continue; const int *sites = NULL; int ns = block_sites(c, si, &sites); for (int k = 0; k < ns; k++) { int a = nt_ref(nt, sites[k], "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!av || !site_args_may_be_nil(c, av, an)) continue; if (!hot) hot = calloc((size_t)c->nscopes, 1); hot[si] = 1; break; } } NT_FOREACH_KIND(nt, NK_CallNode, u) { int blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; const char *un = nt_str(nt, u, "name"); int iex = un && sp_streq(un, "instance_exec"); int ia = iex ? nt_ref(nt, u, "arguments") : -1; int ian = 0; const int *iav = ia >= 0 ? nt_arr(nt, ia, "arguments", &ian) : NULL; if (iex ? !(iav && site_args_may_be_nil(c, iav, ian)) : !hot) continue; Scope *bs = comp_scope_of(c, blk); BlockSig bsig; block_sig(c, nt_ref(nt, blk, "parameters"), 0, &bsig); int np = bsig.P + bsig.O + bsig.Q; if (!bs || (np == 0 && bsig.nk == 0)) continue; TyKind *pos = calloc((size_t)(np + bsig.nk + 1), sizeof(TyKind)); char *absent = calloc((size_t)np + 1, 1); if (iex) block_site_types(c, &bsig, iav, ian, pos, absent, pos + np); else for (int si = 1; si < c->nscopes; si++) { if (!hot[si] || !an_call_targets_scope(c, u, si, &c->scopes[si])) continue; const int *sites = NULL; int ns = block_sites(c, si, &sites); for (int k = 0; k < ns; k++) { int a = block_site_args(c, si, sites[k], u); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; block_site_types(c, &bsig, av, an, pos, absent, pos + np); } } /* the posts after a splat too: `|a, *r, b|` binds b from the last argument */ for (int i = 0; i < np; i++) { if (!(absent[i] & BS_NIL)) continue; const char *pnm = block_sig_name(c, &bsig, i); LocalVar *pv = pnm ? scope_local(bs, pnm) : NULL; if (!pv || (pv->type != TY_INT && pv->type != TY_FLOAT) || pv->nullable_int) continue; pv->nullable_int = 1; changed = 1; } /* a keyword the sites bind (`yield(k: a)`) from arguments that may be nil: the binding plan keeps no per-key nil, so the slot holds its nil beside the value whenever a site may pass one */ for (int ki = 0; ki < bsig.nk; ki++) { const char *kn = block_keyword_name(c, blk, ki); LocalVar *pv = kn ? scope_local(bs, kn) : NULL; if (!pv || (pv->type != TY_INT && pv->type != TY_FLOAT) || pv->nullable_int) continue; pv->nullable_int = 1; changed = 1; } free(pos); free(absent); } free(hot); } /* A destructuring target the right side cannot supply gets nil, through a target node rather than a write node of its own -- `a, b, *c, d, e = 1` leaves nil in every int slot after the first. */ NT_FOREACH_KIND(nt, NK_MultiWriteNode, id) { int v = nt_ref(nt, id, "value"); /* how many values the right side statically supplies: an array literal supplies its elements, a scalar supplies exactly one, and anything else (an array-valued call or local) is only known at runtime. */ int supply = 0; if (masgn_tuple_rhs(nt, v)) nt_arr(nt, v, "elements", &supply); else if (v >= 0 && nt_kind(nt, v) != NK_NilNode && !ty_is_array(infer_type(c, v))) supply = 1; int tn2 = 0; const int *tv2 = nt_arr(nt, id, "lefts", &tn2); int en2 = 0; const int *ev2 = masgn_tuple_rhs(nt, v) ? nt_arr(nt, v, "elements", &en2) : NULL; for (int k = 0; tv2 && k < tn2; k++) { if (nt_kind(nt, tv2[k]) != NK_LocalVariableTargetNode) continue; /* A target the right side supplies takes that value, which can be nil as a plain write's can: `a, b = z, 0` with z an Integer that may be nil left `a >= 0` comparing it as a number. */ if (k < supply) { int sv2 = ev2 ? (k < en2 ? ev2[k] : -1) : v; if (sv2 < 0 || !nullable_int_value(c, sv2)) continue; } const char *tn = nt_str(nt, tv2[k], "name"); Scope *ts = tn ? comp_scope_of(c, tv2[k]) : NULL; LocalVar *tl = ts ? scope_local(ts, tn) : NULL; if (tl && (tl->type == TY_INT || tl->type == TY_FLOAT) && !tl->nullable_int) { tl->nullable_int = 1; changed = 1; } } /* a target after the splat is supplied only when the right side is long enough, which a static count almost never proves */ int rn2 = 0; const int *rv2 = nt_arr(nt, id, "rights", &rn2); for (int k = 0; rv2 && k < rn2; k++) { if (nt_kind(nt, rv2[k]) != NK_LocalVariableTargetNode) continue; const char *tn = nt_str(nt, rv2[k], "name"); Scope *ts = tn ? comp_scope_of(c, rv2[k]) : NULL; LocalVar *tl = ts ? scope_local(ts, tn) : NULL; if (tl && (tl->type == TY_INT || tl->type == TY_FLOAT) && !tl->nullable_int) { tl->nullable_int = 1; changed = 1; } } } if (!changed) { converged = 1; break; } } /* Running out of rounds means some slot that CAN hold nil is still unmarked, and codegen would box it as an ordinary number -- a Hash key no literal nil matches, with no error at compile or run time (#3505). That is the exact silent-wrong-output this pass exists to prevent, so refuse to emit rather than emit something quietly wrong. Unreachable for a monotone predicate whatever the program (the bound counts the flags themselves), so this firing means a marking arm that is not monotone -- worth a bug report. */ free(imeth); if (!converged) { fprintf(stderr, "spinel: internal: nilable-scalar marking did not converge in " "%ld rounds; refusing to emit (a slot that can hold nil would be " "read as a plain number). Please report this with the source.\n", rounds_max); exit(1); } } /* `f(*a)` where the splatted array's length is known statically expands to `f(a[0], a[1], ...)`, whatever the target. A user-defined method already handles the unexpanded form -- codegen reads the array into its declared params -- but a builtin's arity is its C function's, so the array arrived as a single argument: `h.fetch(*k)` either failed to build or silently ran the one-argument overload and answered the wrong thing (#3515, #3516). Runs after walk_scope, because deciding whether a local's length is static means asking which writes are in ITS scope, and the synthesized nodes need a scope of their own for inference to read the right slot. */ static int splat_lit_len(Compiler *c, int ex) { NodeTable *nt = (NodeTable *)c->nt; NodeKind t_k = nt_kind(nt, ex); if (t_k != NK_ArrayNode) return -1; int n = 0; const int *el = nt_arr(nt, ex, "elements", &n); if (n > 0 && !el) return -1; for (int i = 0; i < n; i++) { NodeKind ek = nt_kind(nt, el[i]); if ((ek == NK_SplatNode || ek == NK_KeywordHashNode || ek == NK_AssocSplatNode)) return -1; } return n; } /* A local qualifies when its scope assigns it an array literal exactly once and never touches it again: no second write, no operator write, no use as a receiver (any call on it could be a push, which changes the length). Every read is an index (`a[i]`) or a splat: a read handed on (`b = a`, `f(a)`, `[a]`) is an alias a push through changes the length too. */ static int splat_local_len(Compiler *c, int ex, int callid) { NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, ex, "name"); if (!nm || callid >= c->node_cap) return -1; int sc = c->nscope[callid]; int len = -1, writes = 0, reads = 0, seen = 0; /* (the scope first: the scan is of every node of the program, once per splat of a local, and reading each node's type was most of it) */ for (int id = 0; id < nt->count && id < c->node_cap; id++) { if (c->nscope[id] != sc) continue; const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); if (!ty) continue; if (nt_kind(nt, id) == NK_LocalVariableReadNode) { const char *rn = nt_str(nt, id, "name"); if (rn && sp_streq(rn, nm)) reads++; continue; } if (nt_kind(nt, id) == NK_SplatNode) { int e = nt_ref(nt, id, "expression"); const char *en = e >= 0 && nt_kind(nt, e) == NK_LocalVariableReadNode ? nt_str(nt, e, "name") : NULL; if (en && sp_streq(en, nm)) seen++; continue; } if (k == NK_LocalVariableWriteNode) { const char *wn = nt_str(nt, id, "name"); if (!wn || !sp_streq(wn, nm)) continue; if (++writes > 1) return -1; int v = nt_ref(nt, id, "value"); len = v >= 0 ? splat_lit_len(c, v) : -1; if (len < 0) return -1; continue; } if (k == NK_LocalVariableTargetNode || k == NK_LocalVariableOrWriteNode || k == NK_LocalVariableAndWriteNode || k == NK_LocalVariableOperatorWriteNode) { const char *wn = nt_str(nt, id, "name"); if (wn && sp_streq(wn, nm)) return -1; } if (k == NK_CallNode) { int r = nt_ref(nt, id, "receiver"); if (r < 0) continue; NodeKind rk = nt_kind(nt, r); if (rk != NK_LocalVariableReadNode) continue; const char *rn = nt_str(nt, r, "name"); if (!rn || !sp_streq(rn, nm)) continue; /* `a[i]` cannot change the length, and excluding it matters beyond tidiness: expanding one call synthesizes exactly that shape, so without this the first expansion disqualifies every later one in the same scope. */ const char *cn = nt_str(nt, id, "name"); if (is_aref_name(cn)) { seen++; continue; } return -1; } } return writes == 1 && reads == seen ? len : -1; } /* The array literal a splat of local `x` spreads, when its length is sure: the method's own body assigns it that literal once, in a statement ahead of the one holding the splat, and every other mention of it only indexes it (`a[i]`) or spreads it again. Anything else -- another write, a call on it, handing it to a method or to another local, which could all push onto it -- and the length is the run time's. -1 then. Stricter than splat_local_len, which does not ask that the write run first or that no alias reaches the array; the binders read the elements without a length test on the strength of this. */ int splat_local_sure_lit(Compiler *c, int x) { const NodeTable *nt = c->nt; if (x < 0 || x >= c->node_cap || nt_kind(nt, x) != NK_LocalVariableReadNode) return -1; const char *nm = nt_str(nt, x, "name"); int sc = c->nscope[x]; Scope *s = sc >= 0 && sc < c->nscopes ? &c->scopes[sc] : NULL; if (!nm || !s || s->def_node < 0 || s->body < 0 || nt_kind(nt, s->body) != NK_StatementsNode) return -1; LocalVar *lv = scope_local(s, nm); if (!lv || lv->is_param || lv->is_block_param) return -1; int wn = -1, lit = -1; for (int w = comp_lvw_first_sc(c, sc, nm); w >= 0; w = comp_lvw_next_sc(c, w)) { if (w >= c->node_cap || c->nscope[w] != sc) continue; const char *n2 = nt_str(nt, w, "name"); if (!n2 || !sp_streq(n2, nm)) continue; if (wn >= 0 || nt_kind(nt, w) != NK_LocalVariableWriteNode) return -1; wn = w; lit = nt_ref(nt, w, "value"); } if (wn < 0 || splat_lit_len(c, lit) < 0) return -1; /* the write is a statement of the body itself, the splat in a later one */ int bn = 0; const int *bs = nt_arr(nt, s->body, "body", &bn); int wi = -1, after = 0; for (int j = 0; j < bn && !after; j++) { if (bs[j] == wn) wi = j; else if (wi >= 0 && a_subtree_contains(nt, bs[j], x, 0)) after = 1; } if (!after) return -1; int reads = 0, seen = 0; for (int r = comp_kind_first(c, NK_LocalVariableReadNode); r >= 0; r = comp_kind_next(c, r)) { if (r >= c->node_cap || c->nscope[r] != sc) continue; const char *n2 = nt_str(nt, r, "name"); if (n2 && sp_streq(n2, nm)) reads++; } for (int sp = comp_kind_first(c, NK_SplatNode); sp >= 0; sp = comp_kind_next(c, sp)) { int e = nt_ref(nt, sp, "expression"); if (e < 0 || e >= c->node_cap || c->nscope[e] != sc || nt_kind(nt, e) != NK_LocalVariableReadNode) continue; const char *n2 = nt_str(nt, e, "name"); if (n2 && sp_streq(n2, nm)) seen++; } for (int u = comp_scall_first(c, sc); u >= 0; u = comp_scall_next(c, u)) { int r = nt_ref(nt, u, "receiver"); if (r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode) continue; const char *n2 = nt_str(nt, r, "name"), *cn = nt_str(nt, u, "name"); if (n2 && sp_streq(n2, nm) && cn && sp_streq(cn, "[]")) seen++; } return reads == seen ? lit : -1; } /* A binary operator takes its one operand whatever the receiver, and no block moves that count. A splat into one -- `a.==(*rest)`, an operator arm of `public_send(*args, &block)` -- passed the array itself as the operand: == answered false and + raised TypeError. */ static int splat_binary_operator(const char *name) { static const char *const ops[] = { "==", "!=", "eql?", "equal?", "===", "=~", "<=>", "<", "<=", ">", ">=", "+", "-", "*", "/", "%", "**", "<<", ">>", "&", "|", "^", NULL }; for (int i = 0; ops[i]; i++) if (sp_streq(name, ops[i])) return 1; return 0; } /* How many arguments the builtin requires, for a splat whose length only the run time knows and that splat_dispatch_on_length declined (a block, `&.`, a user method of the name that cannot take every count). Only the required count: an optional trailing parameter (`fetch`'s default, `split`'s limit) cannot be chosen without knowing the length, and reading one that the array may not carry would pass nil where Ruby passes nothing. -1 for a name with no single answer. */ static int splat_builtin_arity(const char *name) { static const struct { const char *name; int arity; } tab[] = { /* slice is NOT here: it is the one name in this list whose arity depends on the receiver -- Hash#slice is variadic, Array#slice and String#slice take one argument or two -- so no single number is right. Expanding to 1 dropped every Hash key after the first and dropped Array/String's length argument (#4164). A slice splat is expanded where the length is statically known and dispatched on it otherwise (splat_dispatch_on_length). */ { "fetch", 1 }, { "store", 2 }, { "insert", 2 }, { "delete", 1 }, { "sub", 2 }, { "sub!", 2 }, { "gsub", 2 }, { "gsub!", 2 }, { "[]", 1 }, { "[]=", 2 }, /* the predicate and search families: each takes its argument as a scalar in C, so an unexpanded array reached the slot as a pointer and the call answered from a comparison against garbage */ { "key?", 1 }, { "has_key?", 1 }, { "include?", 1 }, { "member?", 1 }, { "value?", 1 }, { "has_value?", 1 }, { "index", 1 }, { "rindex", 1 }, { "count", 1 }, { "split", 1 }, { "join", 1 }, { "start_with?", 1 }, { "end_with?", 1 }, { "tr", 2 }, { "tr_s", 2 }, /* the counts take none (splat_builtin_range) */ { "size", 0 }, { "length", 0 }, { "empty?", 0 }, { NULL, 0 } }; for (int i = 0; tab[i].name; i++) if (sp_streq(name, tab[i].name)) return tab[i].arity; if (splat_binary_operator(name)) return 1; return -1; } /* Whether some user method of this name could not take `n` positional arguments, which is what makes expanding unsafe: the call might be that method's, and then the elements would not fill its parameters. A negative `n` means the count is unknown, so any definition of the name rejects. */ static int splat_user_method_rejects(Compiler *c, const char *name, int n) { for (int si = 0; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (!s->name || !sp_streq(s->name, name)) continue; if (n < 0) return 1; if (s->rest_idx >= 0) continue; /* takes any count */ if (n < s->nrequired || n > s->nparams) return 1; } return 0; } /* A default constructor consumes the expanded value, not the Array enclosing it. Its share row identifies that argument flow in both builds, independently of String sharing; its positional range is zero to one. */ static int splat_constructor_arity(const NodeTable *nt, int id, const char *name) { if (!is_new_name(name)) return -1; int recv = nt_ref(nt, id, "receiver"); const char *cn = nt_kind(nt, recv) == NK_ConstantReadNode ? nt_str(nt, recv, "name") : NULL; int sh = cn ? bop_share_named(BOP_CLASS_NEW, cn) : 0; if (sh == BSH_NEW_DEFAULT) return 1; /* A fill constructor consumes size and fill separately. */ if (sh == BSH_NEW_FILL) return 2; return -1; } /* Is scope s a class method `new` of a class other than the constructor call's receiver class (and other than Object or BasicObject, whose singleton methods every class inherits)? Such a method never answers that call. */ static int splat_ctor_other_cnew(Compiler *c, const Scope *s, int recv) { const char *cn = nt_str(c->nt, recv, "name"); const char *kn = s->class_id >= 0 ? c->classes[s->class_id].name : NULL; return s->is_cmethod && cn && kn && !sp_streq(kn, cn) && !is_object_root(kn); } /* A literal receiver is the builtin's own, whatever user classes share the method name. */ static int splat_recv_is_builtin_literal(NodeTable *nt, int id) { int r = nt_ref(nt, id, "receiver"); const char *t = r >= 0 ? nt_type(nt, r) : NULL; NodeKind k = nt_kind(nt, r); if (!t) return 0; return k == NK_StringNode || k == NK_InterpolatedStringNode || k == NK_ArrayNode || k == NK_HashNode || k == NK_IntegerNode || k == NK_FloatNode || k == NK_SymbolNode || k == NK_RangeNode; } /* The argument counts a builtin takes, over every builtin receiver that has the method: `fetch` is Hash's and Array's (key, default), `[]` is Hash's one argument and Array's or String's two. A variadic method (String#count, #delete, #start_with?) is dispatched up to a cap. 0 for an unlisted name. */ static int splat_builtin_range(const char *name, int *lo, int *hi, int *variadic) { /* variadic 2 marks a name some receiver takes any count of while the others take lo..hi: Array's and String's slice take one argument or two, Hash#slice any number of keys. The receiver is not typed yet, so the counts in range get their arms and every other count keeps the original splat call, which Hash#slice iterates and the one-or-two-argument forms refuse with the arity error (emit_int_expr_ex). */ static const struct { const char *name; int lo, hi, variadic; } tab[] = { { "fetch", 1, 2, 0 }, { "store", 2, 2, 0 }, { "delete", 1, 8, 1 }, { "insert", 1, 8, 1 }, { "sub", 2, 2, 0 }, { "sub!", 2, 2, 0 }, { "gsub", 1, 2, 0 }, { "gsub!", 1, 2, 0 }, { "[]", 1, 2, 0 }, { "[]=", 2, 3, 0 }, { "key?", 1, 1, 0 }, { "has_key?", 1, 1, 0 }, { "include?", 1, 1, 0 }, { "member?", 1, 1, 0 }, { "value?", 1, 1, 0 }, { "has_value?", 1, 1, 0 }, { "index", 0, 2, 0 }, { "rindex", 0, 2, 0 }, { "count", 0, 8, 1 }, { "split", 0, 2, 0 }, { "join", 0, 1, 0 }, { "start_with?", 0, 8, 1 }, { "end_with?", 0, 8, 1 }, { "tr", 2, 2, 0 }, { "tr_s", 2, 2, 0 }, { "first", 0, 1, 0 }, { "last", 0, 1, 0 }, { "sum", 0, 1, 0 }, { "ljust", 1, 2, 0 }, { "rjust", 1, 2, 0 }, { "center", 1, 2, 0 }, { "to_s", 0, 1, 0 }, { "slice", 1, 2, 2 }, { "byteslice", 1, 2, 0 }, { "rotate", 0, 1, 0 }, { "digits", 0, 1, 0 }, { "pow", 1, 2, 0 }, { "sample", 0, 1, 0 }, { "pop", 0, 1, 0 }, { "shift", 0, 1, 0 }, { "round", 0, 1, 0 }, { "floor", 0, 1, 0 }, { "ceil", 0, 1, 0 }, { "truncate", 0, 1, 0 }, { "max", 0, 1, 0 }, { "min", 0, 1, 0 }, /* none is the no-pattern form, one the pattern: the Array whole was taken as the pattern (`any?(*[])` asked `[] === e`) */ { "all?", 0, 1, 0 }, { "any?", 0, 1, 0 }, { "none?", 0, 1, 0 }, { "one?", 0, 1, 0 }, /* the readers a delegator forwards `*args` to, which take none: an empty splat is the plain call, any other is the arity error */ { "size", 0, 0, 0 }, { "length", 0, 0, 0 }, { "empty?", 0, 0, 0 }, { "keys", 0, 0, 0 }, { "values", 0, 0, 0 }, { "to_a", 0, 0, 0 }, { NULL, 0, 0, 0 } }; for (int i = 0; tab[i].name; i++) if (sp_streq(name, tab[i].name)) { if (lo) *lo = tab[i].lo; if (hi) *hi = tab[i].hi; if (variadic) *variadic = tab[i].variadic; return 1; } if (splat_binary_operator(name)) { if (lo) *lo = 1; if (hi) *hi = 1; if (variadic) *variadic = 0; return 1; } return 0; } /* The counts CRuby's tables give every class with a builtin instance method of this name, where they are few (find_index, assoc, casecmp, divmod), for a blockless call `id` the list above leaves out. 0 for a name no builtin has, or one taking any number, which keeps its splat call. */ static int splat_builtin_spec_range(Compiler *c, int id, const char *name, int *lo, int *hi) { const NodeTable *nt = c->nt; int l, h; /* nor is a method of the program's own (a class's `add`) a builtin's: its splat call reaches it as before */ for (int si = 0; si < c->nscopes; si++) if (c->scopes[si].name && sp_streq(c->scopes[si].name, name)) return 0; /* ...nor a reader it declares of the name */ for (int k = 0; k < c->nclasses; k++) if (comp_reader_in_chain(c, k, name, NULL)) return 0; /* the tables are instance methods': a class's (`File.unlink(*paths)`) is no row of theirs */ int r = nt_ref(nt, id, "receiver"); if (r >= 0 && (nt_kind(nt, r) == NK_ConstantReadNode || nt_kind(nt, r) == NK_ConstantPathNode)) return 0; /* (between?'s arms compare the receiver with the elements' boxes as integers, which the C build rejects: it keeps its splat call) */ if (sp_streq(name, "between?")) return 0; if (!builtin_name_arity_span(name, 0, &l, &h) || h < 0 || h > l + 3) return 0; if (lo) *lo = l; if (hi) *hi = h; return 1; } /* A name whose call takes a value or a block (`find_index(v)`, `find_index { |x| .. }`), never both: a value beside a block leaves the block unused. */ static int splat_value_or_block_name(const char *name) { return sp_streq(name, "index") || sp_streq(name, "rindex") || sp_streq(name, "find_index"); } static int splat_builtin_keeps_kwsplat(const char *name) { static const char *const kw[] = { "round", "encode", "encode!", "each_line", "lines", "readlines", "readline", "gets", "unpack", "unpack1", "clone", "Integer", "Float", "Rational", "Complex", "update", "merge!", "sample", "shuffle", "shuffle!", "step", NULL }; for (int i = 0; kw[i]; i++) if (sp_streq(name, kw[i])) return 1; return 0; } /* An arm of a dynamic send (`public_send(*args)`) on a receiver whose class only the run time knows takes the rest of the list into whatever builtin its name is: the counts come from CRuby's arity tables, over every class with the name, so the arm dispatches on the length as the names above do. A name taking any number is dispatched up to a cap, and a longer list keeps the splat call (variadic 2, as slice's other counts do): `push` with five elements reached the builtin that way before these arms. */ static int splat_dyn_arm_range(Compiler *c, int id, const char *name, int with_block, int *lo, int *hi, int *variadic) { if (!nt_int((NodeTable *)c->nt, id, "dyn_arm", 0)) return 0; int l, h; if (!(name[0] >= 'A' && name[0] <= 'Z' && builtin_kernel_fn_span(name, with_block, &l, &h)) && !builtin_name_arity_span(name, with_block, &l, &h)) return 0; int v = 0; if (h < 0 || h > l + 3) { h = l + 3; v = 2; } if (lo) *lo = l; if (hi) *hi = h; if (variadic) *variadic = v; return 1; } /* A node that can be copied into each arm of the dispatch without being evaluated more than once, or out of order, in the arm that runs. */ static int splat_leaf_node(NodeTable *nt, int id) { switch (nt_kind(nt, id)) { case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: case NK_ConstantReadNode: case NK_SelfNode: case NK_SymbolNode: case NK_IntegerNode: case NK_FloatNode: case NK_StringNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: return 1; default: return 0; } } /* Node builders for the length dispatch's per-class arms below. */ static int sd_call(NodeTable *nt, const char *name, int recv, const int *args, int n) { int an = -1; if (n > 0) { an = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, an, "arguments", args, n); } int cl = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, cl, "name", name); nt_node_set_ref(nt, cl, "receiver", recv); nt_node_set_ref(nt, cl, "arguments", an); nt_node_set_ref(nt, cl, "block", -1); return cl; } /* The same, for a builtin the desugar asks of a value it built or knows (a splat's Array, its length): never a reopened class's own method of the name */ static void sd_builtin(NodeTable *nt, int cl) { nt_node_set_int(nt, cl, "builtin_only", 1); nt_node_set_int(nt, cl, "desugar_builtin", 1); /* typed as the builtin alone (infer_call) */ } static int sd_bcall(NodeTable *nt, const char *name, int recv, const int *args, int n) { int cl = sd_call(nt, name, recv, args, n); sd_builtin(nt, cl); return cl; } static int sd_str(NodeTable *nt, const char *s) { int n = nt_new_node(nt, "StringNode"); nt_node_set_str(nt, n, "content", s); return n; } static int sd_if(NodeTable *nt, int pred, int then_, int else_) { int ts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, ts, "body", &then_, 1); int es = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, es, "body", &else_, 1); int en = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, en, "statements", es); int ifn = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, ifn, "predicate", pred); nt_node_set_ref(nt, ifn, "statements", ts); nt_node_set_ref(nt, ifn, "subsequent", en); return ifn; } /* `recv.is_a?(String)`, over a copy of the receiver (a leaf or its temp) */ static int sd_is_string(NodeTable *nt, int recv) { int k = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, k, "name", "String"); return sd_call(nt, "is_a?", nt_clone_subtree(nt, recv), &k, 1); } static const char *const kwb_rows[][3] = { { "lines", "chomp", "FalseNode" }, { "each_line", "chomp", "FalseNode" }, { "readlines", "chomp", "FalseNode" }, { "readline", "chomp", "FalseNode" }, { "gets", "chomp", "FalseNode" }, { "unpack", "offset", "IntegerNode" }, { "unpack1", "offset", "IntegerNode" }, { "clone", "freeze", "NilNode" }, { "read_nonblock", "exception", "TrueNode" }, { "write_nonblock", "exception", "TrueNode" }, { "recv_nonblock", "exception", "TrueNode" }, { "accept_nonblock", "exception", "TrueNode" }, { "connect_nonblock", "exception", "TrueNode" }, { NULL, NULL, NULL } }; static int kwb_is(const char *a, const char *b) { return a && sp_streq(a, b); } static int kwb_row(const char *name) { for (int i = 0; name && kwb_rows[i][0]; i++) if (sp_streq(name, kwb_rows[i][0])) return i; return -1; } static int kwb_lv(NodeTable *nt, Scope *sc, const char *nm, int value) { if (sc) scope_local_intern(sc, nm); int n = nt_new_node(nt, value >= 0 ? "LocalVariableWriteNode" : "LocalVariableReadNode"); nt_node_set_str(nt, n, "name", nm); nt_node_set_int(nt, n, "depth", 0); if (value >= 0) nt_node_set_ref(nt, n, "value", value); return n; } static int kwb_const(NodeTable *nt, const char *name) { int k = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, k, "name", name); return k; } static int kwb_seq(NodeTable *nt, const int *st, int n) { int s = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, s, "body", st, n); int p = nt_new_node(nt, "ParenthesesNode"); nt_node_set_ref(nt, p, "body", s); return p; } static int kwb_conv(NodeTable *nt, Scope *sc, const char *hn) { int hc = kwb_const(nt, "Hash"), eh = nt_new_node(nt, "HashNode"); nt_node_set_arr(nt, eh, "elements", NULL, 0); return sd_if(nt, sd_call(nt, "nil?", kwb_lv(nt, sc, hn, -1), NULL, 0), eh, sd_if(nt, sd_call(nt, "is_a?", kwb_lv(nt, sc, hn, -1), &hc, 1), kwb_lv(nt, sc, hn, -1), sd_call(nt, "to_hash", kwb_lv(nt, sc, hn, -1), NULL, 0))); } static int kwb_reject(NodeTable *nt, Scope *sc, const char *xn) { int one = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, one, "value", 1); int pl = sd_if(nt, sd_bcall(nt, ">", sd_bcall(nt, "size", kwb_lv(nt, sc, xn, -1), NULL, 0), &one, 1), sd_str(nt, "s"), sd_str(nt, "")); int lb = sd_str(nt, "["), rb = sd_str(nt, "]"), co = sd_str(nt, ": "); int ls = sd_call(nt, "delete_suffix", sd_call(nt, "delete_prefix", sd_call(nt, "inspect", kwb_lv(nt, sc, xn, -1), NULL, 0), &lb, 1), &rb, 1); int ra[2] = { kwb_const(nt, "ArgumentError"), sd_call(nt, "+", sd_call(nt, "+", sd_call(nt, "+", sd_str(nt, "unknown keyword"), &pl, 1), &co, 1), &ls, 1) }; return sd_if(nt, sd_call(nt, "empty?", kwb_lv(nt, sc, xn, -1), NULL, 0), nt_new_node(nt, "NilNode"), sd_call(nt, "raise", -1, ra, 2)); } static int kwb_explicit(NodeTable *nt, int row, int h, Scope *sc) { char hn[32], vn[32], xn[32]; snprintf(hn, sizeof hn, "__kwbh%d", nt->count); snprintf(vn, sizeof vn, "__kwbv%d", nt->count); snprintf(xn, sizeof xn, "__kwbx%d", nt->count); int key = nt_new_node(nt, "SymbolNode"); nt_node_set_str(nt, key, "value", kwb_rows[row][1]); int conv = kwb_conv(nt, sc, hn); int ka = nt_new_node(nt, "ArrayNode"), kc = nt_clone_subtree(nt, key); nt_node_set_arr(nt, ka, "elements", &kc, 1); int fa[2] = { nt_clone_subtree(nt, key), nt_new_node(nt, kwb_rows[row][2]) }; if (nt_kind(nt, fa[1]) == NK_IntegerNode) nt_node_set_int(nt, fa[1], "value", 0); int st[5] = { kwb_lv(nt, sc, hn, nt_clone_subtree(nt, h)), kwb_lv(nt, sc, vn, conv), kwb_lv(nt, sc, xn, sd_call(nt, "-", sd_call(nt, "keys", kwb_lv(nt, sc, vn, -1), NULL, 0), &ka, 1)), kwb_reject(nt, sc, xn), sd_call(nt, "fetch", kwb_lv(nt, sc, vn, -1), fa, 2) }; int as = nt_new_node(nt, "AssocNode"); nt_node_set_ref(nt, as, "key", key); nt_node_set_ref(nt, as, "value", kwb_seq(nt, st, 5)); int kh = nt_new_node(nt, "KeywordHashNode"); nt_node_set_arr(nt, kh, "elements", &as, 1); return kh; } /* A literal keyword hash naming a key the builtin does not take, as the Hash kwb_explicit checks: its values run in order and the call raises CRuby's `unknown keyword(s)`, as a `**` operand holding that key does. Before, the arm read the key it knows and the call answered as if the others were not there. A hash of the key the method takes, or one with a `**` or a key that is no Symbol, stays as it is (-1). So do the line readers: IO's gets, each_line and readlines accept a keyword they do not take, and File.readlines takes the open options (`encoding:`, `mode:`) too. The *_nonblock calls take `exception:` alone. */ static int kwb_unknown_literal(NodeTable *nt, int row, int kh) { const char *m = kwb_rows[row][0]; if (!kwb_is(m, "unpack") && !kwb_is(m, "unpack1") && !kwb_is(m, "lines") && !kwb_is(m, "clone") && !kwb_is(m, "read_nonblock") && !kwb_is(m, "write_nonblock") && !kwb_is(m, "recv_nonblock") && !kwb_is(m, "accept_nonblock") && !kwb_is(m, "connect_nonblock")) return -1; int en = 0; const int *el = nt_arr(nt, kh, "elements", &en); if (en < 1) return -1; int unknown = 0; for (int i = 0; i < en; i++) { int key = nt_kind(nt, el[i]) == NK_AssocNode ? nt_ref(nt, el[i], "key") : -1; if (key < 0 || nt_kind(nt, key) != NK_SymbolNode) return -1; if (!kwb_is(nt_str(nt, key, "value"), kwb_rows[row][1])) unknown = 1; } if (!unknown) return -1; int h = nt_new_node(nt, "HashNode"); nt_node_set_arr(nt, h, "elements", el, en); return h; } /* Split `call` (`x.clone(freeze: v)`) into the sequence that checks the keyword. The sequence is built of clones and new nodes and swapped into the call's place; the call itself, with every node under it, is at the id this answers (-1: not split). */ static int kwb_clone_split(Compiler *c, int call, Scope *sc) { NodeTable *nt = (NodeTable *)c->nt; int an = nt_ref(nt, call, "arguments"), n = 0, en = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; if (!kwb_is(nt_str(nt, call, "name"), "clone") || n != 1 || nt_kind(nt, av[0]) != NK_KeywordHashNode) return -1; const int *el = nt_arr(nt, av[0], "elements", &en); if (en != 1 || nt_kind(nt, el[0]) != NK_AssocNode) return -1; int key = nt_ref(nt, el[0], "key"), v = nt_ref(nt, el[0], "value"); NodeKind vk = nt_kind(nt, v); if (nt_kind(nt, key) != NK_SymbolNode || !kwb_is(nt_str(nt, key, "value"), "freeze")) return -1; if (vk == NK_TrueNode || vk == NK_FalseNode || vk == NK_NilNode) return -1; char rn[32], vn[32]; snprintf(rn, sizeof rn, "__kwcr%d", nt->count); snprintf(vn, sizeof vn, "__kwcv%d", nt->count); int rcv = nt_ref(nt, call, "receiver"), st[3], ns = 0; NodeKind rk = rcv >= 0 ? nt_kind(nt, rcv) : NK_SelfNode; int sn = nt_safe_nav(nt, call); int bind = rcv >= 0 && (sn || (rk != NK_SelfNode && rk != NK_ConstantReadNode && (subtree_has_side_effect(c, v) || (rk != NK_LocalVariableReadNode && rk != NK_InstanceVariableReadNode)))); if (bind) st[ns++] = kwb_lv(nt, sc, rn, nt_clone_subtree(nt, rcv)); int vs = kwb_lv(nt, sc, vn, nt_clone_subtree(nt, v)); if (!sn) st[ns++] = vs; int bare = nt_clone_subtree(nt, call), arms[2]; nt_node_set_ref(nt, bare, "arguments", -1); if (bind) nt_node_set_ref(nt, bare, "receiver", kwb_lv(nt, sc, rn, -1)); for (int i = 0; i < 2; i++) { arms[i] = nt_clone_subtree(nt, call); int m = 0, e2n = 0; int kh = nt_arr(nt, nt_ref(nt, arms[i], "arguments"), "arguments", &m)[0]; int as = nt_arr(nt, kh, "elements", &e2n)[0]; int lit = nt_new_node(nt, i ? "FalseNode" : "TrueNode"); nt_node_set_ref(nt, as, "value", lit); if (bind) nt_node_set_ref(nt, arms[i], "receiver", kwb_lv(nt, sc, rn, -1)); } int tn = nt_new_node(nt, "TrueNode"), fn = nt_new_node(nt, "FalseNode"); int cn = sd_call(nt, "to_s", sd_call(nt, "class", kwb_lv(nt, sc, vn, -1), NULL, 0), NULL, 0); int ra[2] = { kwb_const(nt, "ArgumentError"), sd_call(nt, "+", sd_str(nt, "unexpected value for freeze: "), &cn, 1) }; int rz = sd_call(nt, "raise", -1, ra, 2); int ch = sd_if(nt, sd_call(nt, "nil?", kwb_lv(nt, sc, vn, -1), NULL, 0), bare, sd_if(nt, sd_call(nt, "==", kwb_lv(nt, sc, vn, -1), &tn, 1), arms[0], sd_if(nt, sd_call(nt, "==", kwb_lv(nt, sc, vn, -1), &fn, 1), arms[1], rz))); if (sn) { int g[2] = { vs, ch }; ch = sd_if(nt, sd_call(nt, "nil?", kwb_lv(nt, sc, rn, -1), NULL, 0), nt_new_node(nt, "NilNode"), kwb_seq(nt, g, 2)); } st[ns++] = ch; int seq = kwb_seq(nt, st, ns); nt_swap_nodes(nt, call, seq); return seq; } static const char *const kwo_rows[][3] = { { "step", "by", "to" }, { "sample", "random", NULL }, { "shuffle", "random", NULL }, { "shuffle!", "random", NULL }, { NULL, NULL, NULL } }; static int kwo_row(const char *name) { for (int i = 0; name && kwo_rows[i][0]; i++) if (sp_streq(name, kwo_rows[i][0])) return i; return -1; } static int kwo_sym(NodeTable *nt, const char *s) { int k = nt_new_node(nt, "SymbolNode"); nt_node_set_str(nt, k, "value", s); return k; } static int kwo_tree(NodeTable *nt, int call, int row, int i, int mask, const char *vn, Scope *sc) { if (i < 2 && kwo_rows[row][i + 1]) { int k = kwo_sym(nt, kwo_rows[row][i + 1]); return sd_if(nt, sd_call(nt, "key?", kwb_lv(nt, sc, vn, -1), &k, 1), kwo_tree(nt, call, row, i + 1, mask | 1 << i, vn, sc), kwo_tree(nt, call, row, i + 1, mask, vn, sc)); } int cl = nt_clone_subtree(nt, call), an = nt_ref(nt, cl, "arguments"), n = 0, m = 0, ne = 0, args[16], el[2]; const int *av = nt_arr(nt, an, "arguments", &n); for (int j = 0; j + 1 < n; j++) args[m++] = av[j]; for (int j = 0; j < 2; j++) { if (!(mask & 1 << j)) continue; int ix = kwo_sym(nt, kwo_rows[row][j + 1]); el[ne] = nt_new_node(nt, "AssocNode"); nt_node_set_ref(nt, el[ne], "key", kwo_sym(nt, kwo_rows[row][j + 1])); nt_node_set_ref(nt, el[ne++], "value", sd_call(nt, "[]", kwb_lv(nt, sc, vn, -1), &ix, 1)); } if (ne) { args[m] = nt_new_node(nt, "KeywordHashNode"); nt_node_set_arr(nt, args[m++], "elements", el, ne); } if (m) nt_node_set_arr(nt, an, "arguments", args, m); else nt_node_set_ref(nt, cl, "arguments", -1); return cl; } /* The same for `x.step(1, **h)` and the builtins that take `by:`, `to:` or `random:`. */ static int kwb_optional_split(NodeTable *nt, int call, int row, Scope *sc) { int an = nt_ref(nt, call, "arguments"), n = 0, en = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; if (n < 1 || n > 16 || nt_argv_kwh(nt, av, n) < 0) return -1; const int *el = nt_arr(nt, av[n - 1], "elements", &en); int h = en == 1 && nt_kind(nt, el[0]) == NK_AssocSplatNode ? nt_ref(nt, el[0], "value") : -1; if (h < 0) return -1; char hn[32], vn[32], xn[32], rn[32]; snprintf(hn, sizeof hn, "__kwoh%d", nt->count); snprintf(vn, sizeof vn, "__kwov%d", nt->count); snprintf(xn, sizeof xn, "__kwox%d", nt->count); snprintf(rn, sizeof rn, "__kwor%d", nt->count); int sn = nt_safe_nav(nt, call), rw = -1; if (sn) { rw = kwb_lv(nt, sc, rn, nt_clone_subtree(nt, nt_ref(nt, call, "receiver"))); nt_node_set_ref(nt, call, "receiver", kwb_lv(nt, sc, rn, -1)); } int ks[2], nk = 0; for (int i = 1; i < 3 && kwo_rows[row][i]; i++) ks[nk++] = kwo_sym(nt, kwo_rows[row][i]); int ka = nt_new_node(nt, "ArrayNode"); nt_node_set_arr(nt, ka, "elements", ks, nk); int st[5] = { kwb_lv(nt, sc, hn, nt_clone_subtree(nt, h)), kwb_lv(nt, sc, vn, kwb_conv(nt, sc, hn)), kwb_lv(nt, sc, xn, sd_call(nt, "-", sd_call(nt, "keys", kwb_lv(nt, sc, vn, -1), NULL, 0), &ka, 1)), kwb_reject(nt, sc, xn), kwo_tree(nt, call, row, 0, 0, vn, sc) }; int seq = kwb_seq(nt, st, 5); if (sn) { int g[2] = { rw, sd_if(nt, sd_call(nt, "nil?", kwb_lv(nt, sc, rn, -1), NULL, 0), nt_new_node(nt, "NilNode"), seq) }; seq = kwb_seq(nt, g, 2); } nt_swap_nodes(nt, call, seq); return seq; } static int kwo_leaves(NodeTable *nt, int call) { int an = nt_ref(nt, call, "arguments"), n = 0, r = nt_ref(nt, call, "receiver"); const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; if (r < 0 || !splat_leaf_node(nt, r)) return 0; for (int i = 0; i + 1 < n; i++) if (!splat_leaf_node(nt, av[i])) return 0; return 1; } /* The defs of the program, up to 4096, a call to the builtins this row splits is left alone beside a def of the name of */ int desugar_builtin_kwsplat_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; int nd = 0; const int *defs = nt_nodes_of_kind(nt, NK_DefNode, &nd); lc->ids = malloc(sizeof(int) * (size_t)(nd < 4096 ? (nd > 0 ? nd : 1) : 4096)); if (!lc->ids) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int i = 0; i < nd && lc->nids < 4096; i++) lc->ids[lc->nids++] = defs[i]; return 1; } /* The call a split replaced is at `orphan` now, no tree's. Nothing under it is in the tree either: kwb_clone_split builds the sequence from clones of the call (kwb_clone_split's bare and arm calls, the receiver and the value it binds) and new nodes, and kwb_optional_split from a clone of the hash, of the receiver and, in kwo_tree, of the call; nothing is moved. SPINEL_LOWER_CHECK asserts it before the subtree is blanked. */ static void kwb_drop_orphan(Compiler *c, LowerCtx *lc, int orphan) { if (orphan < 0) return; if (lc->check) lower_check_unreachable(c, lc, "desugar_builtin_kwsplat", orphan); nt_subtree_blank((NodeTable *)c->nt, orphan); } int desugar_builtin_kwsplat_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int orow = nt_kind(nt, id) == NK_CallNode ? kwo_row(nt_str(nt, id, "name")) : -1; for (int i = 0; orow >= 0 && i < lc->nids; i++) if (kwb_is(nt_str(nt, lc->ids[i], "name"), kwo_rows[orow][0])) orow = -1; if (orow >= 0 && kwo_leaves(nt, id)) kwb_drop_orphan(c, lc, kwb_optional_split(nt, id, orow, NULL)); int row = nt_kind(nt, id) == NK_CallNode ? kwb_row(nt_str(nt, id, "name")) : -1; for (int i = 0; row >= 0 && i < lc->nids; i++) if (kwb_is(nt_str(nt, lc->ids[i], "name"), kwb_rows[row][0])) row = -1; int an = row >= 0 ? nt_ref(nt, id, "arguments") : -1, n = 0, en = 0, args[16]; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; if (n < 1 || n > 16 || nt_argv_kwh(nt, av, n) < 0) return 0; for (int i = 0; i < n; i++) args[i] = av[i]; const int *el = nt_arr(nt, args[n - 1], "elements", &en); int lit = -1; if (en == 1 && nt_kind(nt, el[0]) == NK_AssocSplatNode && nt_ref(nt, el[0], "value") >= 0) { args[n - 1] = kwb_explicit(nt, row, nt_ref(nt, el[0], "value"), NULL); nt_node_set_arr(nt, an, "arguments", args, n); } else if ((lit = kwb_unknown_literal(nt, row, args[n - 1])) >= 0) { args[n - 1] = kwb_explicit(nt, row, lit, NULL); nt_node_set_arr(nt, an, "arguments", args, n); } kwb_drop_orphan(c, lc, kwb_clone_split(c, id, NULL)); return 1; } static void desugar_begin_args(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int an = nt_ref(nt, id, "arguments"), n = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; for (int i = 0; i < n; i++) { if (nt_kind(nt, av[i]) != NK_BeginNode || (i == 0 && nt_ref(nt, id, "receiver") < 0)) continue; int s = nt_new_node(nt, "StatementsNode"), one[1] = { av[i] }; nt_node_set_arr(nt, s, "body", one, 1); int p = nt_new_node(nt, "ParenthesesNode"); nt_node_set_ref(nt, p, "body", s); comp_grow_node_arrays(c); c->nscope[s] = c->nscope[p] = c->nscope[av[i]]; int *args = malloc(sizeof(int) * (size_t)n); if (!args) continue; for (int j = 0; j < n; j++) args[j] = j == i ? p : av[j]; nt_node_set_arr(nt, an, "arguments", args, n); free(args); av = nt_arr(nt, an, "arguments", &n); } } } static void desugar_diverging_args(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_safe_nav(nt, id)) continue; int an = nt_ref(nt, id, "arguments"), n = 0, k = -1; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &n) : NULL; for (int i = 0; i < n && k < 0; i++) { if (nt_kind(nt, av[i]) == NK_SplatNode) break; const char *dn = nt_kind(nt, av[i]) == NK_CallNode && nt_ref(nt, av[i], "receiver") < 0 ? nt_str(nt, av[i], "name") : NULL; if (dn && is_diverging_call(dn)) k = i; } int own = k < 0 || an_bare_call_class_owned(c, av[k]); for (int i = 0; !own && i < c->nscopes; i++) own = c->scopes[i].class_id < 0 && c->scopes[i].def_node >= 0 && c->scopes[i].name && sp_streq(c->scopes[i].name, nt_str(nt, av[k], "name")); if (own) continue; int *st = malloc(sizeof(int) * (size_t)(k + 2)), ns = 0, recv = nt_ref(nt, id, "receiver"); if (!st) continue; if (recv >= 0 && subtree_has_side_effect(c, recv)) st[ns++] = recv; for (int i = 0; i < k; i++) if (subtree_has_side_effect(c, av[i])) st[ns++] = av[i]; st[ns++] = av[k]; int s = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, s, "body", st, ns); free(st); nt_node_set_type(nt, id, "ParenthesesNode"); nt_node_set_ref(nt, id, "body", s); nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_ref(nt, id, "arguments", -1); nt_node_set_ref(nt, id, "block", -1); comp_grow_node_arrays(c); c->nscope[s] = c->nscope[id]; } } /* A read of the splat operand local `anm`. Where the local itself is the operand (`view`), the read is marked splat_view: a boxed one is read as the Array a splat makes of it (emit_local_ivar_write_expr), where its length read raised NoMethodError for a boxed Integer (`{5 => 1}.fetch(*s)`). A typed Array reads as it did. */ static int splat_operand_read(Compiler *c, NodeTable *nt, const char *anm, long long depth, int view) { int rd = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rd, "name", anm); nt_node_set_int(nt, rd, "depth", depth); if (view) { nt_node_set_int(nt, rd, "splat_view", 1); int *sv = realloc(c->splat_views, sizeof(int) * (size_t)(c->nsplat_views + 1)); if (!sv) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } c->splat_views = sv; c->splat_views[c->nsplat_views++] = rd; } return rd; } /* The splat_view reads once their local is typed: a builtin scalar spreads as itself, so the read becomes the one-element list `[x]` (nil the empty one) the arms measure and index; `h.fetch(*k)` with k an Integer local read k's length and its C did not compile. An Array or a boxed value reads as it is (the box through sp_splat_to_array); anything else (a Hash, a Range, an object's to_a) keeps the read. */ static int desugar_splat_view_scalars(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; for (int r = 0; r < c->nsplat_views; r++) { int id = c->splat_views[r]; if (id < 0) continue; TyKind t = infer_type(c, id); if (t == TY_UNKNOWN) continue; c->splat_views[r] = -1; int scalar = t == TY_INT || t == TY_BIGINT || t == TY_FLOAT || t == TY_STRING || t == TY_SYMBOL || t == TY_BOOL || t == TY_RATIONAL || t == TY_COMPLEX; if (!scalar && t != TY_NIL) continue; int el = -1; if (scalar) { el = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, el, "name", nt_str(nt, id, "name")); nt_node_set_int(nt, el, "depth", nt_int(nt, id, "depth", 0)); } nt_node_set_type(nt, id, "ArrayNode"); nt_node_set_arr(nt, id, "elements", &el, scalar ? 1 : 0); comp_grow_node_arrays(c); if (el >= 0) c->nscope[el] = c->nscope[id]; comp_set_ntype(c, id, TY_UNKNOWN); changed = 1; } return changed; } /* `recv.m(pre, *a, post)` where a's length is only known at run time and m is a builtin taking lo..hi arguments. Expanding to the required count dropped every optional argument (`h.fetch(*[:k, 0])` ran fetch(:k) and raised KeyError), so branch on the length instead: (t = recv; a.length == 1 ? t.m(a[0]) : a.length == 2 ? t.m(a[0], a[1]) : raise ArgumentError, "...") The receiver, and any fixed argument that is not a leaf, is bound to a temporary first so it is evaluated once. The range also covers the counts a user method of the name takes; one with a rest parameter takes every count, so the arms run up to the cap of 8 arguments. Returns 0, leaving the call to the old expansion, when such a method takes a keyword rest or more than 8 arguments. */ static int splat_dispatch_on_length(Compiler *c, int id, const int *argv, int argc, int sp_at, int ex) { NodeTable *nt = (NodeTable *)c->nt; const char *cnm = nt_str(nt, id, "name"); int lo, hi, variadic; if (!cnm) return 0; int blk = nt_ref(nt, id, "block"); /* a dynamic send's arm (`public_send(*args)`) takes its counts from CRuby's tables, the block-carrying call's where there is a block */ int odyn = 0, spec = 0; int ctor = splat_constructor_arity(nt, id, cnm); if (ctor >= 0) { lo = 0; hi = ctor; variadic = 0; } else if (!splat_builtin_range(cnm, &lo, &hi, &variadic)) { if (splat_dyn_arm_range(c, id, cnm, blk >= 0, &lo, &hi, &variadic)) odyn = 1; /* another builtin name the list above leaves out */ else if ((blk >= 0 && !splat_value_or_block_name(cnm)) || !splat_builtin_spec_range(c, id, cnm, &lo, &hi)) return 0; else { variadic = 0; spec = 1; } } /* index, rindex and find_index take a value or a block: the empty splat is the block's form and one element the value's, which leaves the block unused. So the block goes on the arm with no arguments (a `&b` read is kept on the others for its TypeError, below). */ int vob = blk >= 0 && splat_value_or_block_name(cnm); /* A `&b` may hold nil, and then the call with no arguments answers an Enumerator: that arm tests it, `b ? r.m(&b) : r.m`, or block_given? for an anonymous `&`. Only nil is no block: `&false` raises TypeError at any count, so the falsy arm raises it for false, and a `&b` stays on the value's arms too, where it is not called. A `&` of anything but a local keeps its splat. */ int vob_test = 0, bxl = 0; if (vob && nt_kind(nt, blk) == NK_BlockArgumentNode) { int bx = nt_ref(nt, blk, "expression"); if (bx < 0 || nt_kind(nt, bx) == NK_LocalVariableReadNode) vob_test = 1; else vob = 0; bxl = bx >= 0 && vob_test; } /* A block goes on one arm only. The substitutions take it at one argument, the pattern alone (`s.gsub(*a) { |m| .. }`), and ignore it beside a replacement, as CRuby does, so their two-argument arm is the blockless call. An operator's one arm takes it as it stands, and a dynamic send's arms share their one block node. Any other name keeps its splat. */ int subst = sp_streq(cnm, "sub") || sp_streq(cnm, "sub!") || sp_streq(cnm, "gsub") || sp_streq(cnm, "gsub!"); /* ...except the block a method's own forwarded block became (`{ |x| yield x }`, desugar_value_callable_forwards): its parameters live in the method's scope, so every arm takes a copy of it */ int fwd_blk = blk >= 0 && !subst && nt_kind(nt, blk) == NK_BlockNode && nt_int(nt, blk, "fwd_yield", 0); /* ...and the `&blk` a super into Array or Hash hands on (builtin_only, the subclass rewrites): a read of the block, which every arm shares */ if (blk >= 0 && !subst && nt_kind(nt, blk) == NK_BlockArgumentNode && nt_int(nt, id, "builtin_only", 0)) fwd_blk = 1; /* ...and a method's own `&block` handed on to an element read, which ignores a block (`def [](*args, &block) = expressions.[](*args, &block)`, regexp_parser's forwarders): a read of a local, which every arm shares too. So does fetch's (`def fetch(key, *args, &block) = @hash.fetch(key, *args, &block)`, thor's HashWithIndifferentAccess), the key fixed ahead of the splat: each arm takes the block a missing key calls, and a nil one leaves the default or the KeyError, where the kept splat call handed the whole Array over as the default. A name a block changes (`index { }`) keeps its splat. */ if (blk >= 0 && !subst && nt_kind(nt, blk) == NK_BlockArgumentNode && (sp_streq(cnm, "[]") || sp_streq(cnm, "slice") || (sp_streq(cnm, "fetch") && sp_at == 1))) { int bx = nt_ref(nt, blk, "expression"); if (bx >= 0 && nt_kind(nt, bx) == NK_LocalVariableReadNode) fwd_blk = 1; } if (blk >= 0 && !subst && !vob && !splat_binary_operator(cnm) && !odyn && !fwd_blk && ctor < 0) return 0; if (blk >= 0 && subst) lo = 1; /* insert(i, *objs) spreads at run time (emit_array_splat_mutator) */ if (sp_streq(cnm, "insert") && sp_at > 0) return 0; if (nt_safe_nav(nt, id)) return 0; /* A splat of anything but a local -- a constant, a method's result -- is bound to a temporary as `[*ex]` (nil spreads to no arguments, a Range to its elements), in its turn among the arguments, and the arms read that. */ /* (a local splat into one of the names the lists leave out is bound so too: the local need not hold an Array, `*n` spreads an Integer) */ int ex_local = !spec && nt_kind(nt, ex) == NK_LocalVariableReadNode; const char *anm = ex_local ? nt_str(nt, ex, "name") : NULL; int aview = ex_local; /* anm is the operand local itself (splat_operand_read) */ if (ex_local && !anm) return 0; long long adepth = ex_local ? nt_int(nt, ex, "depth", 0) : 0; int recv = nt_ref(nt, id, "receiver"); /* a receiverless call is self's method, never one of these builtins */ if (recv < 0) return 0; /* A String's builtins take each argument in a slot of their own and none spreads a splat: a dynamic send's capped name kept its splat call past the arms, which handed concat or squeeze the Array as one argument (a C error, or the Array's inspect appended). Past the cap it raises as the listed variadic names do. */ if (variadic == 2 && odyn && infer_type(c, recv) == TY_STRING) variadic = 1; /* the builtin's own counts, before a user method's widen them: what the kept splat call of a variadic-2 name (slice) reports as its arity */ int blo = lo, bhi = hi; int nuser = 0, ukw = 0, urest = 0, mhi = -1; /* a user method of the name may own the call: the range widens to the counts it takes, so each arm calls whichever method the receiver has with the arguments it was given. One with a rest parameter takes any count, so the arms run up to the cap and a longer list raises as a variadic name's does. Leaving it to the old expansion read only the builtin's required count and dropped the rest: with any `def index(*a)` in the program (a def_delegators line makes one), `s.index(*a)` on a String with a = ["b", 2] answered `s.index("b")`. A variadic-2 name (slice) needs no widening: its other counts keep the splat call, which reaches the user method as any splat call does. */ /* a call marked as the builtin's (a super into Array or Hash) is never a user method's */ if (variadic != 2 && !splat_recv_is_builtin_literal(nt, id) && !nt_int(nt, id, "builtin_only", 0)) { for (int si = 0; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; if (!s->name || !sp_streq(s->name, cnm)) continue; /* A constructor's receiver names its class: another class's own `def self.new` is not the one `Hash.new` reaches. */ if (ctor >= 0 && splat_ctor_other_cnew(c, s, recv)) continue; if (s->kwrest_idx >= 0) return 0; /* Only where the old expansion would cut the list: a splat of a local, into a name it cuts to its arity. Any other keeps its splat call, which reaches a rest parameter whole, as does a dynamic send's arm (the n < 0 dyn_arm case in the caller) */ if (s->rest_idx >= 0 && (odyn || !ex_local || splat_builtin_arity(cnm) < 0)) return 0; if (s->rest_idx >= 0) urest = 1; nuser++; int pn = s->def_node >= 0 ? nt_ref(nt, s->def_node, "parameters") : -1, kn = 0; if (pn >= 0) nt_arr(nt, pn, "keywords", &kn); if (kn > 0) ukw = 1; int req = 0; for (int p = 0; p < s->nparams; p++) if (s->pdefault && s->pdefault[p] < 0 && p != s->rest_idx) req++; if (req < lo) lo = req; if (s->nparams > hi) hi = s->nparams; } if (hi > 8) return 0; /* a rest parameter's arms run to the cap; the arity message keeps the counts the receivers have, and only a list past the cap raises the variadic names' NotImplementedError */ if (urest) { mhi = hi; hi = 8; } /* a method of the program's own of the name may read the block at any count: the call keeps its splat */ if (vob && nuser > 0) return 0; } int idx = (is_string_index(cnm)) && call_target(c, id, -1, cnm, CT_TOP, NULL) < 0; for (int si = 0; idx && si < c->nscopes; si++) if (c->scopes[si].name && sp_streq(c->scopes[si].name, cnm)) idx = 0; int fixed = argc - 1; for (int k = 0; k < argc; k++) if (nt_kind(nt, argv[k]) == NK_KeywordHashNode) fixed--; int base = nt->count; int encl = c->nscope[id]; int pre[32]; int npre = 0; char tn[64]; if (!splat_leaf_node(nt, recv)) { snprintf(tn, sizeof tn, "__splr%s", comp_node_tag(c, id)); scope_local_intern(comp_scope_of(c, id), tn); int w = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, w, "name", tn); nt_node_set_int(nt, w, "depth", 0); nt_node_set_ref(nt, w, "value", recv); pre[npre++] = w; recv = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, recv, "name", tn); nt_node_set_int(nt, recv, "depth", 0); } int fargs[24]; char snm[64]; for (int k = 0; k < argc; k++) { fargs[k] = argv[k]; if (k == sp_at && !ex_local) { snprintf(snm, sizeof snm, "__spls%s", comp_node_tag(c, id)); scope_local_intern(comp_scope_of(c, id), snm); int arr = nt_new_node(nt, "ArrayNode"); nt_node_set_arr(nt, arr, "elements", &argv[k], 1); int w = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, w, "name", snm); nt_node_set_int(nt, w, "depth", 0); nt_node_set_ref(nt, w, "value", arr); pre[npre++] = w; anm = snm; aview = 0; continue; } if (k == sp_at || splat_leaf_node(nt, argv[k])) continue; /* Hash.new's capacity lowering reads the keyword's key. Hold its value once, but keep the explicit pair for that lowering in each arm. */ if (ctor == 1 && nt_kind(nt, argv[k]) == NK_KeywordHashNode) { int nk = 0; const int *kv = nt_arr(nt, argv[k], "elements", &nk); if (nk == 1 && nt_kind(nt, kv[0]) == NK_AssocNode) { fargs[k] = nt_clone_subtree(nt, argv[k]); int pair = nt_arr(nt, fargs[k], "elements", &nk)[0]; snprintf(tn, sizeof tn, "__spla%s_%d", comp_node_tag(c, id), k); pre[npre++] = kwb_lv(nt, comp_scope_of(c, id), tn, nt_ref(nt, pair, "value")); nt_node_set_ref(nt, pair, "value", kwb_lv(nt, comp_scope_of(c, id), tn, -1)); continue; } } snprintf(tn, sizeof tn, "__spla%s_%d", comp_node_tag(c, id), k); scope_local_intern(comp_scope_of(c, id), tn); int w = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, w, "name", tn); nt_node_set_int(nt, w, "depth", 0); nt_node_set_ref(nt, w, "value", argv[k]); pre[npre++] = w; fargs[k] = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, fargs[k], "name", tn); nt_node_set_int(nt, fargs[k], "depth", 0); } char knm[64]; if (!ukw && (nuser > 0 || (ctor != 1 && !splat_builtin_keeps_kwsplat(cnm))) && argc > 1 && nt_argv_kwh(nt, argv, argc) >= 0) { snprintf(knm, sizeof knm, "__splk%s", comp_node_tag(c, id)); scope_local_intern(comp_scope_of(c, id), knm); int ar = splat_operand_read(c, nt, anm, adepth, aview); int one = nt_new_node(nt, "ArrayNode"); int kv = nt_clone_subtree(nt, fargs[argc - 1]); nt_node_set_arr(nt, one, "elements", &kv, 1); int cat = sd_call(nt, "+", nt_clone_subtree(nt, ar), &one, 1); int w = nt_new_node(nt, "LocalVariableWriteNode"); nt_node_set_str(nt, w, "name", knm); nt_node_set_int(nt, w, "depth", 0); nt_node_set_ref(nt, w, "value", sd_if(nt, sd_call(nt, "empty?", nt_clone_subtree(nt, fargs[argc - 1]), NULL, 0), ar, cat)); pre[npre++] = w; anm = knm; aview = 0; adepth = 0; argc--; } int krow = nuser ? -1 : kwb_row(cnm), orow = nuser ? -1 : kwo_row(cnm); for (int k = 0; k < argc; k++) { if (nt_kind(nt, argv[k]) != NK_KeywordHashNode) continue; if (ctor == 1 && nt_kind(nt, fargs[k]) == NK_KeywordHashNode) continue; if (krow >= 0) { fargs[k] = kwb_explicit(nt, krow, fargs[k], comp_scope_of(c, id)); continue; } int as = nt_new_node(nt, "AssocSplatNode"); nt_node_set_ref(nt, as, "value", fargs[k]); fargs[k] = nt_new_node(nt, "KeywordHashNode"); nt_node_set_arr(nt, fargs[k], "elements", &as, 1); } /* the else arm: a count outside the range */ char msg[160]; if (variadic) snprintf(msg, sizeof msg, "spinel: a splat of more than %d arguments into %s is not supported", hi - fixed, cnm); else if (lo == (mhi >= 0 ? mhi : hi)) snprintf(msg, sizeof msg, ", expected %d)", lo); else snprintf(msg, sizeof msg, ", expected %d..%d)", lo, mhi >= 0 ? mhi : hi); int ecn = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, ecn, "name", variadic ? "NotImplementedError" : "ArgumentError"); int emn = nt_new_node(nt, "StringNode"); nt_node_set_str(nt, emn, "content", msg); /* index and rindex take 1..2 on a String and 0..1 on an Array (0 is the block's form, or an Enumerator): the arms cover both, each call's own guard refusing the count its receiver does not take, and a count past both names the receiver's range */ if (idx) emn = sd_if(nt, sd_is_string(nt, recv), sd_str(nt, ", expected 1..2)"), sd_str(nt, ", expected 0..1)")); if (!variadic) { /* CRuby's message counts what was given: "wrong number of arguments (given " + (a.length + fixed).to_s + msg */ int ard = splat_operand_read(c, nt, anm, adepth, aview); int alen = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, alen, "name", "length"); nt_node_set_ref(nt, alen, "receiver", ard); nt_node_set_ref(nt, alen, "arguments", -1); nt_node_set_ref(nt, alen, "block", -1); sd_builtin(nt, alen); int given = alen; if (fixed > 0) { int fl = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, fl, "value", fixed); int fa = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, fa, "arguments", &fl, 1); given = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, given, "name", "+"); nt_node_set_ref(nt, given, "receiver", alen); nt_node_set_ref(nt, given, "arguments", fa); nt_node_set_ref(nt, given, "block", -1); sd_builtin(nt, given); } int ts = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, ts, "name", "to_s"); nt_node_set_ref(nt, ts, "receiver", given); nt_node_set_ref(nt, ts, "arguments", -1); nt_node_set_ref(nt, ts, "block", -1); sd_builtin(nt, ts); int head = nt_new_node(nt, "StringNode"); nt_node_set_str(nt, head, "content", "wrong number of arguments (given "); int pa = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, pa, "arguments", &ts, 1); int p1 = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, p1, "name", "+"); nt_node_set_ref(nt, p1, "receiver", head); nt_node_set_ref(nt, p1, "arguments", pa); nt_node_set_ref(nt, p1, "block", -1); sd_builtin(nt, p1); int pb = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, pb, "arguments", &emn, 1); int p2 = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, p2, "name", "+"); nt_node_set_ref(nt, p2, "receiver", p1); nt_node_set_ref(nt, p2, "arguments", pb); nt_node_set_ref(nt, p2, "block", -1); sd_builtin(nt, p2); emn = p2; } if (urest && !variadic) { /* one raise, its class and message picked by the length: raise(a.length > cap ? NotImplementedError : ArgumentError, a.length > cap ? "..." : ) */ char cmsg[160]; snprintf(cmsg, sizeof cmsg, "spinel: a splat of more than %d arguments into %s is not supported", hi - fixed, cnm); int over[2]; for (int k = 0; k < 2; k++) { int ard = splat_operand_read(c, nt, anm, adepth, aview); int cap = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, cap, "value", hi - fixed); over[k] = sd_bcall(nt, ">", sd_bcall(nt, "length", ard, NULL, 0), &cap, 1); } int ncn = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, ncn, "name", "NotImplementedError"); ecn = sd_if(nt, over[0], ncn, ecn); emn = sd_if(nt, over[1], sd_str(nt, cmsg), emn); } int ea[2] = { ecn, emn }; int eargs = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, eargs, "arguments", ea, 2); int arm = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, arm, "name", "raise"); nt_node_set_ref(nt, arm, "receiver", -1); nt_node_set_ref(nt, arm, "arguments", eargs); nt_node_set_ref(nt, arm, "block", -1); if (variadic == 2) { /* any other count keeps the splat call (see splat_builtin_range) */ int args[32]; for (int k = 0; k < argc; k++) { if (k != sp_at) { args[k] = nt_clone_subtree(nt, fargs[k]); continue; } int rd = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rd, "name", anm); nt_node_set_int(nt, rd, "depth", adepth); int spn = nt_new_node(nt, "SplatNode"); nt_node_set_ref(nt, spn, "expression", rd); /* a dynamic send's capped name takes any count past the arms */ if (!odyn) { nt_node_set_int(nt, spn, "splat_lo", blo); nt_node_set_int(nt, spn, "splat_hi", bhi); } args[k] = spn; } int an = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, an, "arguments", args, argc); arm = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, arm, "name", cnm); if (nt_str(nt, id, "send_blind")) nt_node_set_str(nt, arm, "send_blind", "1"); if (nt_str(nt, id, "vis_enforce")) nt_node_set_str(nt, arm, "vis_enforce", "1"); nt_node_set_int(nt, arm, "dyn_arm", nt_int(nt, id, "dyn_arm", 0)); nt_node_set_ref(nt, arm, "receiver", nt_clone_subtree(nt, recv)); nt_node_set_ref(nt, arm, "arguments", an); nt_node_set_ref(nt, arm, "block", odyn ? blk : -1); } /* built from the longest count down, each arm the else of the next */ int first_len = lo - fixed < 0 ? 0 : lo - fixed; for (int len = hi - fixed; len >= first_len; len--) { int args[32]; int m = 0; for (int k = 0; k < argc; k++) { if (k != sp_at) { args[m++] = nt_clone_subtree(nt, fargs[k]); continue; } for (int i = 0; i < len; i++) { int rd = splat_operand_read(c, nt, anm, adepth, aview); int ix = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, ix, "value", i); int ia = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, ia, "arguments", &ix, 1); int el = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, el, "name", "[]"); nt_node_set_ref(nt, el, "receiver", rd); nt_node_set_ref(nt, el, "arguments", ia); nt_node_set_ref(nt, el, "block", -1); sd_builtin(nt, el); args[m++] = el; } } int cl = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, cl, "name", cnm); if (nt_str(nt, id, "send_blind")) nt_node_set_str(nt, cl, "send_blind", "1"); if (nt_str(nt, id, "vis_enforce")) nt_node_set_str(nt, cl, "vis_enforce", "1"); nt_node_set_int(nt, cl, "dyn_arm", nt_int(nt, id, "dyn_arm", 0)); nt_node_set_ref(nt, cl, "receiver", nt_clone_subtree(nt, recv)); nt_node_set_ref(nt, cl, "block", fwd_blk ? nt_clone_subtree(nt, blk) : vob ? (m == 0 ? blk : vob_test && bxl ? nt_clone_subtree(nt, blk) : -1) : blk >= 0 && (m == 1 || odyn || ctor >= 0) ? blk : -1); int an = -1; if (m > 0) { an = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, an, "arguments", args, m); } nt_node_set_ref(nt, cl, "arguments", an); /* the block's arm is the call the site's copy of the Enumerable method serves (desugar_builtin_enum_calls ran before this) */ if (vob && m == 0 && nt_int(nt, id, "enum_copy", -1) >= 0) { int cp = (int)nt_int(nt, id, "enum_copy", -1); nt_node_set_int(nt, cl, "enum_copy", cp); if (nt_int(nt, cp, "enum_site", -1) == id) nt_node_set_int(nt, cp, "enum_site", cl); } if (krow >= 0) kwb_clone_split(c, cl, comp_scope_of(c, id)); if (orow >= 0) kwb_optional_split(nt, cl, orow, comp_scope_of(c, id)); if (vob_test && m == 0) { int bx = nt_ref(nt, blk, "expression"); int test = bx >= 0 ? nt_clone_subtree(nt, bx) : sd_call(nt, "block_given?", -1, NULL, 0); int bare = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, bare, "name", cnm); if (nt_str(nt, id, "send_blind")) nt_node_set_str(nt, bare, "send_blind", "1"); if (nt_str(nt, id, "vis_enforce")) nt_node_set_str(nt, bare, "vis_enforce", "1"); nt_node_set_ref(nt, bare, "receiver", nt_clone_subtree(nt, recv)); nt_node_set_ref(nt, bare, "arguments", -1); nt_node_set_ref(nt, bare, "block", -1); /* a falsy `&b` is nil or false, and false raises TypeError */ if (bx >= 0) { int isn = sd_bcall(nt, "nil?", nt_clone_subtree(nt, bx), NULL, 0); int te = nt_new_node(nt, "ConstantReadNode"); nt_node_set_str(nt, te, "name", "TypeError"); int rargs[2] = { te, sd_str(nt, "no implicit conversion of FalseClass into Proc") }; int chk = sd_if(nt, isn, nt_new_node(nt, "NilNode"), sd_call(nt, "raise", -1, rargs, 2)); int body[2] = { chk, bare }; int st = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, st, "body", body, 2); bare = nt_new_node(nt, "ParenthesesNode"); nt_node_set_ref(nt, bare, "body", st); } cl = sd_if(nt, test, cl, bare); } int ard = splat_operand_read(c, nt, anm, adepth, aview); int alen = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, alen, "name", "length"); nt_node_set_ref(nt, alen, "receiver", ard); nt_node_set_ref(nt, alen, "arguments", -1); nt_node_set_ref(nt, alen, "block", -1); sd_builtin(nt, alen); int lit = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, lit, "value", len); int la = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, la, "arguments", &lit, 1); int pred = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, pred, "name", "=="); nt_node_set_ref(nt, pred, "receiver", alen); nt_node_set_ref(nt, pred, "arguments", la); nt_node_set_ref(nt, pred, "block", -1); sd_builtin(nt, pred); int ts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, ts, "body", &cl, 1); int es = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, es, "body", &arm, 1); int en = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, en, "statements", es); int ifn = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, ifn, "predicate", pred); nt_node_set_ref(nt, ifn, "statements", ts); nt_node_set_ref(nt, ifn, "subsequent", en); arm = ifn; } pre[npre++] = arm; int stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, stmts, "body", pre, npre); int paren = nt_new_node(nt, "ParenthesesNode"); nt_node_set_ref(nt, paren, "body", stmts); /* the call becomes a transparent alias of the dispatch's value */ nt_node_set_str(nt, id, "name", "itself"); nt_node_set_ref(nt, id, "receiver", paren); nt_node_set_ref(nt, id, "block", -1); nt_node_set_ref(nt, id, "arguments", -1); comp_grow_node_arrays(c); for (int j = base; j < nt->count; j++) c->nscope[j] = encl; return 1; } void expand_static_splat_args(Compiler *c, int from, int count) { NodeTable *nt = (NodeTable *)c->nt; for (int id = from; id < count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int an = nt_ref(nt, id, "arguments"); if (an < 0) continue; int argc = 0; const int *argv0 = nt_arr(nt, an, "arguments", &argc); if (!argv0 || argc < 1 || argc > 24) continue; /* copied out: synthesizing a node can realloc the table and leave the pointer nt_arr returned dangling */ int argv[24]; for (int k = 0; k < argc; k++) argv[k] = argv0[k]; int sp_at = -1; for (int k = 0; k < argc; k++) { NodeKind ak = nt_kind(nt, argv[k]); if (ak == NK_SplatNode) { if (sp_at >= 0) { sp_at = -1; break; } sp_at = k; } } if (sp_at < 0) continue; /* Only the fixed-arity builtins, which is the whole population that needs this: a user-defined method reads the array into its declared params (and reports the arity error the expansion would hide), and the variadic builtins take the array correctly as it stands -- `puts(*a)`, `format(*args)`, `Struct.new(*syms)` all rely on that, and expanding them types each element separately or loses the compile-time member names. They are splat_builtin_arity's names, and slice and fill, which expand only where the length is static. */ const char *cnm = nt_str(nt, id, "name"); if (!cnm) continue; int listed = splat_builtin_arity(cnm) >= 0 || sp_streq(cnm, "slice") || sp_streq(cnm, "fill"); if (!listed && splat_constructor_arity(nt, id, cnm) < 0 && !splat_builtin_range(cnm, NULL, NULL, NULL) && !splat_dyn_arm_range(c, id, cnm, nt_ref(nt, id, "block") >= 0, NULL, NULL, NULL) && ((nt_ref(nt, id, "block") >= 0 && !splat_value_or_block_name(cnm)) || !splat_builtin_spec_range(c, id, cnm, NULL, NULL))) continue; /* ...but the name has to BE the builtin. A receiverless call to a top-level `def count(*args)` is the user's own variadic method, and expanding its splat to the builtin's arity handed it one element where the program passed none: `count(*[])` answered 1 (#4298). A call with a receiver still reaches the builtin, so only the bare form declines. */ if (nt_ref(nt, id, "receiver") < 0 && call_target(c, id, -1, cnm, CT_TOP, NULL) >= 0) continue; /* Nor is `File.delete(*paths)` the listed `delete` (Hash#delete's): it takes any number of paths, and its File arm spreads a lone splat */ if (argc == 1 && sp_streq(cnm, "delete")) { int fr = nt_ref(nt, id, "receiver"); const char *frn = fr >= 0 && nt_kind(nt, fr) == NK_ConstantReadNode ? nt_str(nt, fr, "name") : NULL; if (frn && sp_streq(frn, "File")) continue; } /* ...and `File.join(*parts)` is not Array#join, which takes one separator: a length known only at run time keeps the splat, which the File arm flattens (a static length still expands to its parts) */ int keep_splat = 0; if (sp_streq(cnm, "join")) { int fr = nt_ref(nt, id, "receiver"); const char *frn = fr >= 0 && nt_kind(nt, fr) == NK_ConstantReadNode ? nt_str(nt, fr, "name") : NULL; keep_splat = frn && sp_streq(frn, "File"); } /* Dir[*pats] / Dir.glob(*pats) takes any count: its emitter unions the patterns a splat holds, so one whose length is the run time's is left whole rather than dispatched over glob's (pattern, flags) */ if (sp_streq(cnm, "glob") || sp_streq(cnm, "[]")) { int fr = nt_ref(nt, id, "receiver"); const char *frn = fr >= 0 && nt_kind(nt, fr) == NK_ConstantReadNode ? nt_str(nt, fr, "name") : NULL; if (frn && sp_streq(frn, "Dir")) keep_splat = 1; } int ex = nt_ref(nt, argv[sp_at], "expression"); if (ex < 0) continue; const char *ext = nt_type(nt, ex); NodeKind exk = nt_kind(nt, ex); if (!ext) continue; int is_lit = exk == NK_ArrayNode; int lit_n = 0, lit_el[24]; int n; if (is_lit) { n = splat_lit_len(c, ex); if (n < 0 || n > 24) continue; const int *el0 = nt_arr(nt, ex, "elements", &lit_n); if ((n > 0 && !el0) || lit_n != n) continue; for (int i = 0; i < n; i++) lit_el[i] = el0[i]; } else if (exk == NK_LocalVariableReadNode) n = splat_local_len(c, ex, id); else { /* any other operand's length is the run time's: dispatch on it, or leave the call as it stands */ if (!keep_splat) splat_dispatch_on_length(c, id, argv, argc, sp_at, ex); continue; } if (keep_splat && n < 0) continue; int n_static = n >= 0; if (n < 0 && splat_dispatch_on_length(c, id, argv, argc, sp_at, ex)) continue; /* the optional-argument builtins added for the dispatch keep their old unexpanded form where it declines */ if (n < 0 && !listed) continue; if (n < 0) { /* The length is not static -- the usual case being a splat forwarded from a parameter, `def f(keys); h.fetch(*keys); end`. The builtin's own arity still says how many elements to read, so expand to that many `a[i]`: the array reaching a scalar slot is what breaks, not the count. Reading past the end yields nil rather than Ruby's ArgumentError, and an element beyond the required arity is not passed on. */ /* Array#insert is variadic past its index: a splat there spreads at run time (emit_array_splat_mutator), where two reads would drop or nil-pad the elements. */ if (sp_streq(cnm, "insert") && sp_at > 0) continue; /* A block moves the required count (`sub(pat) { .. }` takes one argument, not two), so leave those alone. */ /* A method's own forwarded block (`{ |x| yield x }`) only takes what the call yields: outside the substitutions it moves nothing. */ int sblk = nt_ref(nt, id, "block"); int fwd_sblk = sblk >= 0 && nt_kind(nt, sblk) == NK_BlockNode && nt_int(nt, sblk, "fwd_yield", 0) && !sp_streq(cnm, "sub") && !sp_streq(cnm, "sub!") && !sp_streq(cnm, "gsub") && !sp_streq(cnm, "gsub!"); n = sblk >= 0 && !splat_binary_operator(cnm) && !fwd_sblk ? -1 : splat_builtin_arity(cnm); /* slice has no arity to expand to on purpose (see the table). Leave the splat as it stands rather than refusing the program: Hash#slice's emitter iterates it, which is what the call means. */ if (n < 0 && sp_streq(cnm, "slice")) continue; /* (a builtin alias's forwarder keeps its splat too: a call whose receiver is known names the builtin's method instead, desugar_builtin_alias_calls) */ if (n < 0 && (nt_int(nt, id, "dyn_arm", 0) || nt_int(nt, id, "alias_fwd", 0) || splat_user_method_rejects(c, cnm, n))) continue; if (n < 0) { /* No arity to expand to (a name we list for its literal-splat form only). Say so here rather than letting the C compiler report it against generated code the user never wrote. (Variadic builtins never get here -- they take the array as it stands and were filtered out above.) */ unsupported_feature(c, id, "splat whose length is not known at compile time, " "into a fixed-arity builtin"); } } if (n > 8) continue; /* keep the synthesized list small */ /* A user method of the same name can own the call, and these names are ordinary ones to define (`index`, `count`, `include?`). Expanding is still right when that method takes exactly this many positional arguments, since the elements land on the same parameters either way; when it cannot, leave the call alone -- codegen reads the array into the declared params -- unless the receiver is a builtin literal, which no user method is reachable from, or the length is static: then the expanded call is the call, and a receiver the method's own raises the ArgumentError while a builtin one still gets its arm. */ if (!n_static && splat_user_method_rejects(c, cnm, n) && !splat_recv_is_builtin_literal(nt, id)) continue; const char *lnm = is_lit ? NULL : nt_str(nt, ex, "name"); if (!is_lit && !lnm) continue; int nargs[32]; int m = 0; for (int k = 0; k < sp_at; k++) nargs[m++] = argv[k]; for (int i = 0; i < n; i++) { if (is_lit) { nargs[m++] = lit_el[i]; continue; } /* a fresh read per element: the original node is shared by the write detection above, and each synthesized node needs its own scope entry */ int rd = nt_new_node(nt, "LocalVariableReadNode"); nt_node_set_str(nt, rd, "name", lnm); int ix = nt_new_node(nt, "IntegerNode"); nt_node_set_int(nt, ix, "value", i); int ia = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, ia, "arguments", &ix, 1); int cl = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, cl, "name", "[]"); nt_node_set_ref(nt, cl, "receiver", rd); nt_node_set_ref(nt, cl, "arguments", ia); comp_grow_node_arrays(c); c->nscope[rd] = c->nscope[id]; c->nscope[ix] = c->nscope[id]; c->nscope[ia] = c->nscope[id]; c->nscope[cl] = c->nscope[id]; nargs[m++] = cl; } for (int k = sp_at + 1; k < argc; k++) nargs[m++] = argv[k]; /* a FRESH ArgumentsNode: the existing one's id array came from the parser and is not ours to free, which nt_node_set_arr on it would try to do */ int na = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, na, "arguments", nargs, m); nt_node_set_ref(nt, id, "arguments", na); comp_grow_node_arrays(c); c->nscope[na] = c->nscope[id]; } } /* Whether a synthesized `__bam_N` wrapper binds a RECEIVER in its first parameter (`def __bam_N(__bam_r, ...) = __bam_r.sym(...)`), as opposed to the receiverless Kernel wrapper (`def __bam_N(__bam_r, ...) = Integer(__bam_r, ...)`) whose first parameter is an ordinary argument. */ static int bam_wrapper_binds_receiver(Compiler *c, Scope *sc) { const NodeTable *nt = c->nt; if (!sc->name || strncmp(sc->name, "__bam_", 6) != 0 || sc->body < 0) return 0; int n = 0; const int *st = nt_arr(nt, sc->body, "body", &n); if (n != 1 || nt_kind(nt, st[0]) != NK_CallNode) return 0; int recv = nt_ref(nt, st[0], "receiver"); if (recv < 0) return 0; NodeKind rk = nt_kind(nt, recv); const char *rnm = nt_str(nt, recv, "name"); return rk == NK_LocalVariableReadNode && rnm && sp_streq(rnm, "__bam_r"); } int is_descendant(Compiler *c, int k, int anc); /* Does a call of `name` on an object of class k already reach the method def node `def` as the class stood at p, ahead of a later include? Then that include changes no call on k's objects (`class A; include M2; end` ... `class A; include M3; end`, where M3 includes M2, and both reach M1's method). Not known (a mixin that may not have run): no. */ static int mixin_reached_before(Compiler *c, int k, const char *name, LoadPoint p, int def) { const LrDef *d = NULL; return elab_method_at(c, k, name, 0, p, &d) == ELAB_YES && d && d->node == def; } /* Does every method call `name` reaches on a receiver of type rt, among class cid and its subclasses, guard itself as a later prepend's copy (Scope.mixin_guarded)? */ static int mixin_call_guarded(Compiler *c, TyKind rt, int cid, const char *name) { int any = 0; for (int k = 0; k < c->nclasses; k++) { if (k != cid && !is_descendant(c, k, cid)) continue; if (ty_is_object(rt) ? ty_object_class(rt) != k : rt != TY_POLY) continue; int mi = comp_method_in_chain(c, k, name, NULL); if (mi < 0) continue; if (!c->scopes[mi].mixin_guarded) return 0; any = 1; } return any; } /* A module the program mixes into a class after a top-level statement that calls a method on the class's objects (`d.speak; class Dog; prepend Loud; end`) is not there yet when that call runs: the call reaches the method the class had then. Spinel has one method chain per class, the last, so a call that this chain resolves into the module would reach the module's method early. Such a call is refused: one written in a top-level statement that ran ahead of the mixin's, and, where one of those statements runs code at all, one in a method body defined ahead of it, which may run there (`def talk(d) = d.speak; talk(d)`). A receiverless call in an instance method is a call on self. */ typedef struct { int cls, mod, prep, node, i; long long key; } MixinEvent; static int mixin_event_cmp(const void *a, const void *b) { const MixinEvent *x = a, *y = b; if (x->key != y->key) return x->key < y->key ? -1 : 1; return y->i - x->i; /* one call's modules as written (the record has them last first) */ } /* Does a statement that runs code have a rank strictly between lo and hi? */ static int mixin_runs_between(const int *run, int n, int lo, int hi) { for (int i = 0; i < n; i++) if (run[i] > lo && run[i] < hi) return 1; return 0; } /* Where a file of another region can start to run, from the requires that name it: blk, the least rank of a top-level statement holding one outside a method body (in a block kept for later), and def, of one holding a method whose body requires it; INT_MAX for none. any: a require that stands in no ranked statement (in another region's file), or a file no require names (a computed path), which may run at any time. */ typedef struct { int blk, def, any; } MixinFileFrom; static MixinFileFrom *mixin_files_from(Compiler *c) { const NodeTable *nt = c->nt; int nf = nt_nroots(nt); MixinFileFrom *ff = malloc(sizeof *ff * (size_t)(nf > 0 ? nf : 1)); if (!ff) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int f = 0; f < nf; f++) ff[f] = (MixinFileFrom){ INT_MAX, INT_MAX, 1 }; NT_FOREACH_KIND(nt, NK_LoadNode, id) { int t = (int)nt_int(nt, id, "target_file", -1), sf, sr; if (t <= 0 || t >= nf) continue; if (ff[t].any == 1) ff[t].any = 0; /* a require names it */ elab_top_place(c, id, &sf, &sr); Scope *ss = comp_scope_of(c, id); if (sr < 0) ff[t].any = 2; else if (ss && ss->def_node >= 0) { if (sr < ff[t].def) ff[t].def = sr; } else if (sr < ff[t].blk) ff[t].blk = sr; } return ff; } static void refuse_mixin_after_calls(Compiler *c) { const NodeTable *nt = c->nt; /* the classes' includes and prepends, as the load record has them, in the order they run; one that may not run is left out, as is a core class's reopening */ MixinEvent *ev = NULL; int nev = 0, cev = 0; for (int cid = 0; cid < c->nclasses; cid++) { if (is_builtin_reopen_name(c->classes[cid].name)) continue; int nmx = 0, nb = 0, bn = 0; const LrMixin *mx = elab_class_mixins(c, cid, &nmx); const LrOpen *ob = nmx ? elab_class_bodies(c, cid, &nb) : NULL; /* (a class's one body: its own statements' mixins are no reopening's, known before the points are asked) */ int b0 = nb == 1 ? nt_ref(nt, ob[0].node, "body") : -1; const int *bv = b0 >= 0 && nt_kind(nt, b0) == NK_StatementsNode ? nt_arr(nt, b0, "body", &bn) : NULL; for (int i = 0; i < nmx; i++) { int own = 0; for (int q = 0; q < bn && !own; q++) own = bv[q] == mx[i].node; if (own || elab_mix_on_singleton(mx[i].kind) || mx[i].mod < 0 || elab_point_may(c, mx[i].node)) continue; if (nev == cev) { cev = cev ? cev * 2 : 16; ev = realloc(ev, sizeof *ev * (size_t)cev); if (!ev) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } ev[nev++] = (MixinEvent){ cid, mx[i].mod, mx[i].kind == LR_PREPEND, mx[i].node, i, elab_sort_key(c, mx[i].node) }; } } if (nev > 1) qsort(ev, (size_t)nev, sizeof *ev, mixin_event_cmp); /* the ranks of the top-level statements that run code, in load order: the record holds definitions, not which statements run code (a load's file's statements follow it, and run or not themselves) */ int n = 0, *st = nev ? prog_top_seq(c, &n, NULL) : NULL, nrun = 0; int *run = n ? malloc(sizeof(int) * (size_t)n) : NULL; if (n && !run) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int q = 0; q < n; q++) { NodeKind qk = nt_kind(nt, st[q]); if (qk == NK_ClassNode || qk == NK_ModuleNode || qk == NK_DefNode || qk == NK_LoadNode) continue; int f, r; elab_top_place(c, st[q], &f, &r); if (r >= 0) run[nrun++] = r; } free(st); MixinFileFrom *ff = nev ? mixin_files_from(c) : NULL; for (int e = 0; e < nev; e++) { int cid = ev[e].cls, mod = ev[e].mod, jf, jrank; elab_top_place(c, ev[e].node, &jf, &jrank); if (jrank < 0) continue; /* a reopening: the class's first body ran in an earlier top-level statement; before it no object of the class exists to call on */ int nb = 0, of, orank; const LrOpen *ob = elab_class_bodies(c, cid, &nb); if (nb < 1) continue; elab_top_place(c, ob[0].node, &of, &orank); if (orank < 0 || orank >= jrank) continue; LoadPoint pm = elab_point_of(c, ev[e].node); NT_FOREACH_KIND(nt, NK_CallNode, id) { /* a call in a top-level statement that ran ahead of the mixin's, in a method body too (the statement defining it); one in a file of another region, whose points the record does not order against the mixin's, may run ahead of it once a require of the file may: one in a statement ahead of the mixin's, or in a method body that may run there, as a call in it may */ int f, rank; elab_top_place(c, id, &f, &rank); int other = f >= 0 && rank < 0; if (!other && (rank < 0 || rank >= jrank)) continue; if (other && !ff[f].any && ff[f].blk >= jrank && (ff[f].def == INT_MAX || !mixin_runs_between(run, nrun, ff[f].def > orank ? ff[f].def : orank, jrank))) continue; const char *name = nt_str(nt, id, "name"); if (!name) continue; /* an arm of a lowered `send` is no statement of the program: which method the send runs is the run time's, and CRuby finds a module's method through it only once the module is mixed in (Rails' Module#redefine_method sends the visibility it computed, and the arm for `concerning` stopped the program at `Module.include Concerning`) */ if (nt_int(nt, id, "dyn_arm", 0)) continue; int recv = nt_ref(nt, id, "receiver"); Scope *ms = comp_scope_of(c, id); int in_def = ms && ms->def_node >= 0; /* a method body runs ahead of the mixin only under a statement that runs after both its def and the class's first body, which made the class whose object it is called on; one ahead of either may store a block that calls it, but a later statement runs that block. One in a file of another region may run at any time. */ if (in_def && !other && !mixin_runs_between(run, nrun, rank > orank ? rank : orank, jrank)) continue; /* the classes the receiver can be an object of: its type's, or for a receiverless call in an instance method, self's */ TyKind rt = TY_UNKNOWN; if (recv >= 0) rt = comp_ntype(c, recv); else if (in_def && !ms->is_cmethod && ms->class_id >= 0) rt = ty_object(ms->class_id); else continue; int mm = comp_method_in_chain(c, mod, name, NULL), hit = 0; for (int k = 0; mm >= 0 && k < c->nclasses && !hit; k++) { if (k != cid && !is_descendant(c, k, cid)) continue; if (ty_is_object(rt) ? ty_object_class(rt) != k : rt != TY_POLY) continue; int d = -1, mi = comp_method_in_chain(c, k, name, &d); /* an include changes the call where the chain finds the module's method (the class's copy of it), unless the class reached it already where the include runs; a prepend, where the chain finds the name at the class or above it, or not at all */ hit = !ev[e].prep ? mi >= 0 && c->scopes[mi].def_node == c->scopes[mm].def_node && !mixin_reached_before(c, k, name, pm, c->scopes[mm].def_node) : mi < 0 || d == cid || is_descendant(c, cid, d); } if (!hit) continue; /* the module's own methods run only once it is mixed in */ if (in_def && ms->class_id == mod) continue; /* a copy whose guard entry hands the call to the class's own method until the prepend has run (Scope.mixin_guarded) */ if (mixin_call_guarded(c, rt, cid, name)) continue; char msg[600]; snprintf(msg, sizeof msg, "a later statement mixes module %s into class %s (include or prepend), after this call, so " "the call would reach the module's method before the module is there. Include or prepend " "the module where the class is defined, ahead of the call.", c->classes[mod].name, c->classes[cid].name); free(ev); free(run); free(ff); unsupported_feature(c, id, msg); } } free(ev); free(run); free(ff); } /* Can evaluating `e` do anything observable? A read of a local, an ivar or a constant, a literal, self, `&:sym`, and a lambda or `proc { }` literal cannot; anything else (a call, above all) may. */ static int bo_may_act(const NodeTable *nt, int e) { if (e < 0) return 0; switch (nt_kind(nt, e)) { case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: case NK_ConstantReadNode: case NK_SelfNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_StringNode: case NK_LambdaNode: return 0; /* a literal container of such values, and parentheses around one */ case NK_ArrayNode: case NK_HashNode: case NK_KeywordHashNode: { int en = 0; const int *el = nt_arr(nt, e, "elements", &en); for (int k = 0; k < en; k++) if (bo_may_act(nt, el[k])) return 1; return 0; } case NK_AssocNode: return bo_may_act(nt, nt_ref(nt, e, "key")) || bo_may_act(nt, nt_ref(nt, e, "value")); case NK_RangeNode: return bo_may_act(nt, nt_ref(nt, e, "left")) || bo_may_act(nt, nt_ref(nt, e, "right")); case NK_ParenthesesNode: case NK_StatementsNode: { int b = nt_kind(nt, e) == NK_ParenthesesNode ? nt_ref(nt, e, "body") : e; if (b < 0) return 0; if (b != e) return bo_may_act(nt, b); int bn = 0; const int *bb = nt_arr(nt, b, "body", &bn); for (int k = 0; k < bn; k++) if (bo_may_act(nt, bb[k])) return 1; return 0; } case NK_CallNode: { const char *nm = nt_str(nt, e, "name"); int blk = nt_ref(nt, e, "block"); if (nt_ref(nt, e, "receiver") < 0 && nm && (is_proc_constructor(nm)) && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && nt_ref(nt, e, "arguments") < 0) return 0; /* `method(:m)` / `obj.method(:m)` only builds the Method object, and the block-argument emitters lower that spelling to the method itself */ if (nm && sp_streq(nm, "method") && blk < 0 && !bo_may_act(nt, nt_ref(nt, e, "receiver"))) { int ma = nt_ref(nt, e, "arguments"), mn = 0; const int *mv = ma >= 0 ? nt_arr(nt, ma, "arguments", &mn) : NULL; if (mn == 1 && nt_kind(nt, mv[0]) == NK_SymbolNode) return 0; } return 1; } default: return 1; } } static int rr_writes_var(const NodeTable *nt, int n, const char *pfx, const char *name); /* Does `e` read a local in its own evaluation (`s`, `[s]`, `{k: s}`)? A block, a lambda or a def reads its locals when it runs. */ static int bo_reads_local(const NodeTable *nt, int e) { if (e < 0) return 0; NodeKind k = nt_kind(nt, e); if (k == NK_LocalVariableReadNode) return 1; if (k == NK_DefNode || k == NK_LambdaNode || k == NK_BlockNode) return 0; int nr = nt_num_refs(nt, e); for (int i = 0; i < nr; i++) if (bo_reads_local(nt, nt_ref_at(nt, e, i))) return 1; int na = nt_num_arrs(nt, e); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, e, i, &m); for (int j = 0; j < m; j++) if (bo_reads_local(nt, ids[j])) return 1; } return 0; } /* A copy of call `id`'s fields into a fresh node of its kind, a CallNode or a SuperNode (the children are shared, not cloned). Each caller makes `id` the parentheses that run the copy as their last statement, so the copy runs where the call stood, in its frame, once: node_in_place says so to the load record (lr_unplaced), which places it there. */ static int bo_shallow_copy(NodeTable *nt, int id) { int nc = nt_new_node(nt, nt_kind(nt, id) == NK_SuperNode ? "SuperNode" : "CallNode"); if (nc < 0) return -1; const SpNode *nd = &nt->nodes[id]; for (int j = 0; j < nd->ns; j++) nt_node_set_str(nt, nc, nd->s[j].key, nd->s[j].val); nd = &nt->nodes[id]; for (int j = 0; j < nd->ni; j++) nt_node_set_int(nt, nc, nd->i[j].key, nd->i[j].val); nt_node_set_int(nt, nc, "node_in_place", 1); nd = &nt->nodes[id]; for (int j = 0; j < nd->nr; j++) nt_node_set_ref(nt, nc, nd->r[j].key, nd->r[j].ref); nd = &nt->nodes[id]; for (int j = 0; j < nd->na; j++) nt_node_set_arr(nt, nc, nd->a[j].key, nd->a[j].ids, nd->a[j].n); return nc; } /* `recv.m(args, &expr)`: Ruby evaluates the receiver, then the arguments, then the block expression. The emitted C passes the block as one more function argument (or hoists it ahead of a Class-value dispatch, #4992), and C leaves the order of function arguments to the compiler: `run(say(1), &blk(1))` printed the block's side effect first. When the block expression can act and something ahead of it can too, the call becomes (__bo_N_0 = recv; __bo_N_1 = arg; ...; __bo_N_b = expr; __bo_N_0.m(__bo_N_1, ..., &__bo_N_b)) so the order is the statements'. The node itself becomes the parentheses, keeping its id, line and scope. */ static void desugar_block_arg_order(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; int blk = nt_ref(nt, id, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockArgumentNode) continue; int be = nt_ref(nt, blk, "expression"); int pb = nt_kind(nt, be) == NK_ParenthesesNode ? nt_ref(nt, be, "body") : -1; int pn = 0; const int *ps = pb >= 0 ? nt_arr(nt, pb, "body", &pn) : NULL; int seq = ps && pn >= 2 && (nt_kind(nt, ps[pn - 1]) == NK_LambdaNode || is_proc_create(c, ps[pn - 1])); /* `&s` reads s after the operands ahead of it, and one of them may rebind it (`m((s = :downcase), &s)`): a block made from it keeps what it read, so s is read in its turn */ int lread = nt_kind(nt, be) == NK_LocalVariableReadNode; if (!seq && !lread && !bo_may_act(nt, be)) continue; int recv = nt_ref(nt, id, "receiver"); int args = nt_ref(nt, id, "arguments"); int an = 0; const int *av0 = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; int ahead = bo_may_act(nt, recv), plain = 1; for (int k = 0; k < an; k++) { NodeKind ak = nt_kind(nt, av0[k]); if (ak == NK_BlockArgumentNode || ak == NK_ForwardingArgumentsNode) plain = 0; else if (ak == NK_SplatNode) ahead |= bo_may_act(nt, nt_ref(nt, av0[k], "expression")); else if (ak == NK_KeywordHashNode) { int en = 0; const int *el = nt_arr(nt, av0[k], "elements", &en); for (int e = 0; e < en; e++) { if (nt_kind(nt, el[e]) != NK_AssocNode) { plain = 0; break; } ahead |= bo_may_act(nt, nt_ref(nt, el[e], "key")) | bo_may_act(nt, nt_ref(nt, el[e], "value")); } } else ahead |= bo_may_act(nt, av0[k]); } if (lread && !seq && !bo_may_act(nt, be)) { const char *bn = nt_str(nt, be, "name"); int rebinds = bn && rr_writes_var(nt, recv, "LocalVariable", bn); for (int k = 0; bn && k < an && !rebinds; k++) rebinds = rr_writes_var(nt, av0[k], "LocalVariable", bn); if (!rebinds) continue; } else lread = 0; if (!plain || (!ahead && !seq)) continue; Scope *sc = comp_scope_of(c, id); if (!sc) continue; int *av = an > 0 ? malloc(sizeof(int) * (size_t)an) : NULL; if (an > 0 && !av) continue; for (int k = 0; k < an; k++) av[k] = av0[k]; int first = nt->count; int stm[256]; int ns = 0, serial = 0; char tn[64]; /* one `__bo__ = e` statement; answers the read that replaces e */ #define BO_HOIST(E) ({ int _e = (E), _r = _e; if (ns < 255 && _e >= 0 && (seq || bo_may_act(nt, _e) || \ (lread && bo_reads_local(nt, _e)))) { \ snprintf(tn, sizeof tn, "__bo_%s_%d", comp_node_tag(c, id), serial++); \ int _w = nt_new_node(nt, "LocalVariableWriteNode"); \ int _rd = nt_new_node(nt, "LocalVariableReadNode"); \ nt_node_set_str(nt, _w, "name", tn); nt_node_set_ref(nt, _w, "value", _e); \ nt_node_set_str(nt, _rd, "name", tn); \ stm[ns++] = _w; scope_local_intern(sc, tn); _r = _rd; } _r; }) /* A sequence can reassign even a plain read, and so can an operand that rebinds `&s`'s s, under an operand that reads it (`[s]`; a literal with no read it cannot). Capture the receiver and arguments before it, inside the call's own evaluation site. */ int nrecv = BO_HOIST(recv); for (int k = 0; k < an; k++) { NodeKind ak = nt_kind(nt, av[k]); if (ak == NK_SplatNode) { int ex = nt_ref(nt, av[k], "expression"); int nex = BO_HOIST(ex); if (nex != ex) nt_node_set_ref(nt, av[k], "expression", nex); } else if (ak == NK_KeywordHashNode) { int en = 0; const int *el0 = nt_arr(nt, av[k], "elements", &en); int *el = malloc(sizeof(int) * (size_t)(en > 0 ? en : 1)); for (int e = 0; e < en; e++) el[e] = el0[e]; for (int e = 0; e < en; e++) { int key = nt_ref(nt, el[e], "key"), val = nt_ref(nt, el[e], "value"); int nk = BO_HOIST(key); if (nk != key) nt_node_set_ref(nt, el[e], "key", nk); int nv = BO_HOIST(val); if (nv != val) nt_node_set_ref(nt, el[e], "value", nv); } free(el); } else av[k] = BO_HOIST(av[k]); } /* `&(log; proc { ... })`: the sequence's statements run here, in turn, and the proc literal they end in is the block itself -- hoisted whole, the literal sat inside a temp's parentheses where nothing bound its parameters, and they read the elements as Integers */ if (nt_kind(nt, be) == NK_ParenthesesNode) { int pb = nt_ref(nt, be, "body"); int pn = 0; const int *ps = pb >= 0 ? nt_arr(nt, pb, "body", &pn) : NULL; if (ps && pn >= 2 && ns + pn < 255 && (nt_kind(nt, ps[pn - 1]) == NK_LambdaNode || is_proc_create(c, ps[pn - 1]))) { for (int k = 0; k < pn - 1; k++) stm[ns++] = ps[k]; be = ps[pn - 1]; } } int nbe = BO_HOIST(be); #undef BO_HOIST if (ns == 0 || ns >= 255) { free(av); continue; } nt_node_set_ref(nt, blk, "expression", nbe); if (args >= 0) nt_node_set_arr(nt, args, "arguments", av, an); free(av); int nc = bo_shallow_copy(nt, id); if (nc < 0) continue; if (recv >= 0) nt_node_set_ref(nt, nc, "receiver", nrecv); stm[ns++] = nc; int stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, stmts, "body", stm, ns); /* the node keeps its id, line and file: the parentheses */ long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); comp_grow_node_arrays(c); /* the new nodes run in the call's scope and class body */ int encl = c->nscope[id]; for (int j = first; j < nt->count; j++) { c->nscope[j] = encl; c->node_cbody[j] = c->node_cbody[id]; } } } /* Does the subtree under `n` write the variable a read of type-name prefix `pfx` ("LocalVariable", "InstanceVariable", ...) and name `name` reads? A def's body runs when called, under its own locals. */ static int rr_writes_var(const NodeTable *nt, int n, const char *pfx, const char *name) { if (n < 0) return 0; const char *ty = nt_type(nt, n); NodeKind k = nt_kind(nt, n); if (!ty || k == NK_DefNode) return 0; if (!strncmp(ty, pfx, strlen(pfx)) && (strstr(ty, "Write") || strstr(ty, "Target")) && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), name)) return 1; int nr = nt_num_refs(nt, n); for (int i = 0; i < nr; i++) if (rr_writes_var(nt, nt_ref_at(nt, n, i), pfx, name)) return 1; int na = nt_num_arrs(nt, n); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) if (rr_writes_var(nt, ids[j], pfx, name)) return 1; } return 0; } /* `v << (v = +"b")`: Ruby evaluates the receiver before the arguments, so the String the call mutates and answers is the one v held then, and the argument's write stands -- `r = v << (v = +"b")` gives r "ab" and v "b". The String mutators read their receiver's variable again after the arguments, to write the result back or to hand its handle on, so they mutated or answered the String the argument bound, and a value-semantics write-back undid the argument's write. When an argument or the block of such a call, or of an append it chains onto (`v << x << (v = y)`), writes the variable its receiver chain starts from, the call becomes (__rr_N = v; __rr_N << (v = +"b")) so the mutation and the value follow the String the receiver read, and the write stands. The node itself becomes the parentheses, keeping its id, line and scope. This runs after the type fixpoint converges; a rewrite resumes inference so the new temporary takes its receiver's type. */ static int desugar_mutator_recv_rebind(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, changed = 0; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !is_string_rebind_mutator(nm)) continue; /* the variable the receiver chain starts from, through the appends that answer their receiver */ int base = nt_ref(nt, id, "receiver"); while (base >= 0 && nt_kind(nt, base) == NK_CallNode && nt_ref(nt, base, "receiver") >= 0 && nt_str(nt, base, "name") && is_string_append(nt_str(nt, base, "name"))) base = nt_ref(nt, base, "receiver"); const char *pfx = NULL; switch (nt_kind(nt, base)) { case NK_LocalVariableReadNode: pfx = "LocalVariable"; break; case NK_InstanceVariableReadNode: pfx = "InstanceVariable"; break; case NK_GlobalVariableReadNode: pfx = "GlobalVariable"; break; case NK_ClassVariableReadNode: pfx = "ClassVariable"; break; default: continue; } const char *vn = nt_str(nt, base, "name"); if (!vn) continue; int hit = 0; for (int cur = id; cur >= 0 && cur != base && !hit; cur = nt_ref(nt, cur, "receiver")) hit = rr_writes_var(nt, nt_ref(nt, cur, "arguments"), pfx, vn) || rr_writes_var(nt, nt_ref(nt, cur, "block"), pfx, vn); if (!hit) continue; /* These names also belong to Arrays, Hashes and user objects. Only a String receiver needs the String emitters' receiver snapshot. */ TyKind rt = infer_type(c, base); if (rt != TY_STRING && rt != TY_STRBUF) continue; Scope *sc = comp_scope_of(c, id); if (!sc) continue; int first = nt->count; char tn[64]; snprintf(tn, sizeof tn, "__rr_%s", comp_node_tag(c, id)); /* `__rr_N = v`, its value a fresh read of v; the chain's base becomes the read of __rr_N */ char vty[48], vnm[256]; snprintf(vty, sizeof vty, "%sReadNode", pfx); snprintf(vnm, sizeof vnm, "%s", vn); int w = nt_new_node(nt, "LocalVariableWriteNode"); int vr = nt_new_node(nt, vty); if (w < 0 || vr < 0) continue; nt_node_set_str(nt, vr, "name", vnm); if (pfx[0] == 'L') nt_node_set_int(nt, vr, "depth", nt_int(nt, base, "depth", 0)); nt_node_set_str(nt, w, "name", tn); nt_node_set_ref(nt, w, "value", vr); int nc = bo_shallow_copy(nt, id); if (nc < 0) continue; long long bl = nt_int(nt, base, "node_line", 0), bf = nt_int(nt, base, "node_file", 0), bc = nt_int(nt, base, "node_col", 0); nt_node_reset(nt, base, "LocalVariableReadNode"); nt_node_set_str(nt, base, "name", tn); nt_node_set_int(nt, base, "node_line", bl); nt_node_set_int(nt, base, "node_file", bf); nt_node_set_int(nt, base, "node_col", bc); nt_node_set_int(nt, vr, "node_line", bl); nt_node_set_int(nt, vr, "node_file", bf); nt_node_set_int(nt, vr, "node_col", bc); scope_local_intern(sc, tn); int stm[2] = { w, nc }; int stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, stmts, "body", stm, 2); long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); comp_grow_node_arrays(c); int encl = c->nscope[id]; for (int j = first; j < nt->count; j++) c->nscope[j] = encl; changed = 1; } return changed; } /* `m(&v)` where v is a boxed value that can be a Symbol: `&:name` is lowered to the block `{ |_spx| _spx.name }` by the parser, but a Symbol that a poly value holds is known only at run time, and sp_poly_to_block refused it with TypeError. The Symbols the value can hold are the literals that flow into it, as far as psb_collect reads them off the source; each gets the proc `&:name` would have made, and the value picks its own at run time. The call becomes, as desugar_block_arg_order's does, (__psym_N = v; __psblk_N = :a == __psym_N ? proc { |x| x.a } : ... : __psym_N; m(&__psblk_N)) so the value is read where the call runs -- in its branch, each time round its loop -- and every site that takes a boxed block argument reads a plain local. A value that is not one of those Symbols is passed on as it was: a Proc, a Method or nil converts as before, and a Symbol that arrived some other way (a parameter, an ivar, a method's result) still raises TypeError. A Symbol-typed value that is not a local (`&(c ? :a : :b)`) takes the same rewrite; with no literal in sight it is only bound to __psym_N, which desugar_symbol_var_block_arg then sends as it sends any Symbol local. */ #define PSB_MAX 64 typedef struct { const char *v[PSB_MAX]; int n; } PsbSyms; /* The names the `&:name` shorthand lowers (see spinel_parse.c): an identifier with an optional `?` or `!`, and the unary operators, whose `-x` is the call `x.-@` once parsed. */ static void psb_add(PsbSyms *s, const char *v) { size_t i = 0; if (!v || (v[0] >= '0' && v[0] <= '9') || s->n == PSB_MAX) return; while (v[i] == '_' || (v[i] >= 'a' && v[i] <= 'z') || (v[i] >= 'A' && v[i] <= 'Z') || (v[i] >= '0' && v[i] <= '9')) i++; if (i > 0 && (v[i] == '?' || v[i] == '!')) i++; if ((i == 0 || v[i]) && !sp_streq(v, "-@") && !sp_streq(v, "+@") && !sp_streq(v, "~")) return; for (int k = 0; k < s->n; k++) if (sp_streq(s->v[k], v)) return; s->v[s->n++] = v; } /* The Symbol literals `node` can evaluate to -- or, with `elems`, that the Array it evaluates to can hold: a literal, an element read out of an Array literal, a local's or a constant's assignments, and the arms of a conditional. `depth` counts the reads and calls followed, which bounds a local assigned from itself. */ typedef struct { Compiler *c; int elems; PsbSyms *s; int depth; } PsbArms; static void psb_collect(Compiler *c, int node, int elems, PsbSyms *s, int depth); static int psb_arm(const VpLeaf *leaf, void *ud) { PsbArms *a = ud; if (leaf->kind == VPK_EXPR) psb_collect(a->c, leaf->node, a->elems, a->s, a->depth + 1); return 0; } static void psb_collect(Compiler *c, int node, int elems, PsbSyms *s, int depth) { const NodeTable *nt = c->nt; if (node < 0 || depth > 8) return; switch (nt_kind(nt, node)) { case NK_SymbolNode: if (!elems) psb_add(s, nt_str(nt, node, "value")); return; case NK_ArrayNode: if (elems) { int en = 0; const int *els = nt_arr(nt, node, "elements", &en); for (int e = 0; e < en; e++) psb_collect(c, els[e], 0, s, depth + 1); } return; case NK_CallNode: { const char *nm = nt_str(nt, node, "name"); if (!elems && nm && (sp_streq(nm, "[]") || sp_streq(nm, "first") || sp_streq(nm, "last") || sp_streq(nm, "sample") || sp_streq(nm, "fetch") || sp_streq(nm, "at"))) psb_collect(c, nt_ref(nt, node, "receiver"), 1, s, depth + 1); return; } case NK_LocalVariableReadNode: { const char *vn = nt_str(nt, node, "name"); for (int w = an_local_writes_named_first(c, vn); w >= 0; w = an_local_writes_named_next(w)) { const char *wn = nt_str(nt, w, "name"); if (vn && wn && sp_streq(wn, vn) && c->nscope[w] == c->nscope[node]) psb_collect(c, nt_ref(nt, w, "value"), elems, s, depth + 1); } return; } case NK_ConstantReadNode: { const char *cn = nt_str(nt, node, "name"); NT_FOREACH_KIND(nt, NK_ConstantWriteNode, w) { const char *wn = nt_str(nt, w, "name"); if (cn && wn && sp_streq(wn, cn)) psb_collect(c, nt_ref(nt, w, "value"), elems, s, depth + 1); } return; } case NK_IfNode: case NK_UnlessNode: case NK_CaseNode: case NK_CaseMatchNode: case NK_BeginNode: case NK_RescueModifierNode: case NK_ElseNode: case NK_ParenthesesNode: case NK_StatementsNode: case NK_OrNode: case NK_AndNode: { PsbArms a = { c, elems, s, depth }; for_each_value_position(nt, node, VP_AND_LEFT | VPS_ALL, psb_arm, &a); return; } default: return; } } /* A new local read or write of `name`. */ static int psb_local(NodeTable *nt, const char *type, const char *name) { int n = nt_new_node(nt, type); nt_node_set_str(nt, n, "name", name); nt_node_set_int(nt, n, "depth", 0); return n; } /* `proc { |x| x.name }` -- the block `&:name` lowers to -- or, for a call that passes its block `nv` values, `proc { |x, y| x.name(y) }` and so on. Its parameters are boxed: whichever site the value reaches calls it, and through a builtin iterator's forward nothing types them, which then defaulted to an Integer (`[pt].map(&v)` called `x` on one). */ static int psb_symbol_proc(Compiler *c, Scope *sc, const char *name, int nv, const char *pfx) { NodeTable *nt = (NodeTable *)c->nt; int req[8], ys[8], args = -1; char pn[8][64]; if (nv < 1) nv = 1; for (int k = 0; k < nv; k++) { snprintf(pn[k], sizeof pn[k], "%s_%c", pfx, 'a' + k); req[k] = nt_new_node(nt, "RequiredParameterNode"); nt_node_set_str(nt, req[k], "name", pn[k]); LocalVar *lv = scope_local_intern(sc, pn[k]); if (lv) { lv->is_block_param = 1; lv_set_type(lv, TY_POLY); } } int params = nt_new_node(nt, "ParametersNode"); nt_node_set_arr(nt, params, "requireds", req, nv); int bparams = nt_new_node(nt, "BlockParametersNode"); nt_node_set_ref(nt, bparams, "parameters", params); if (nv > 1) { for (int k = 1; k < nv; k++) ys[k] = psb_local(nt, "LocalVariableReadNode", pn[k]); args = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, args, "arguments", ys + 1, nv - 1); } int send = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, send, "name", name); nt_node_set_ref(nt, send, "receiver", psb_local(nt, "LocalVariableReadNode", pn[0])); nt_node_set_ref(nt, send, "arguments", args); int body = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, body, "body", &send, 1); int blk = nt_new_node(nt, "BlockNode"); nt_node_set_ref(nt, blk, "parameters", bparams); nt_node_set_ref(nt, blk, "body", body); int pr = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, pr, "name", "proc"); nt_node_set_ref(nt, pr, "block", blk); return pr; } static int desugar_poly_symbol_block_arg(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int changed = 0; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_CallNode && k != NK_SuperNode) continue; int ba = nt_ref(nt, id, "block"); if (ba < 0 || nt_kind(nt, ba) != NK_BlockArgumentNode) continue; if (nt_int(nt, ba, "psb_done", 0)) continue; /* its own value reads the Symbols again */ /* `r&.m(&v)` evaluates v only when r is not nil, and the parentheses would read it first */ if (nt_safe_nav(nt, id)) continue; int ex = nt_ref(nt, ba, "expression"); if (ex < 0) continue; /* A Symbol-typed value too: a local is desugar_symbol_var_block_arg's and a literal `&:+` the operator lowerings', but anything else had no arm at the sites and ran the method without its block. */ TyKind et = infer_type(c, ex); NodeKind ek = nt_kind(nt, ex); if (et != TY_POLY && (et != TY_SYMBOL || ek == NK_LocalVariableReadNode || ek == NK_SymbolNode)) continue; PsbSyms syms; syms.n = 0; psb_collect(c, ex, 0, &syms, 0); /* a Symbol from nowhere this reads is still a Symbol: named by a local, desugar_symbol_var_block_arg sends it */ if (syms.n == 0 && et != TY_SYMBOL) continue; Scope *sc = comp_scope_of(c, id); if (!sc) continue; int first = nt->count; int nv = k == NK_CallNode ? sym_block_values(c, id) : 0; /* over a call that passes its block several values a value that is only ever a Symbol is sent as a local is: the comparators and a seeded inject run no Proc block argument */ if (nv && et == TY_SYMBOL) syms.n = 0; char vn[48], bn[48]; snprintf(vn, sizeof vn, "__psym_%s", comp_node_tag(c, id)); snprintf(bn, sizeof bn, "__psblk_%s", comp_node_tag(c, id)); int stm[3], ns = 0; int w1 = psb_local(nt, "LocalVariableWriteNode", vn); nt_node_set_ref(nt, w1, "value", ex); stm[ns++] = w1; const char *passed = vn; if (syms.n > 0) { /* built from the last arm up: `... : __psym_N` */ int tail = psb_local(nt, "LocalVariableReadNode", vn); int tst = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, tst, "body", &tail, 1); int arm = nt_new_node(nt, "ElseNode"); nt_node_set_ref(nt, arm, "statements", tst); for (int j = syms.n - 1; j >= 0; j--) { char pfx[64]; snprintf(pfx, sizeof pfx, "__psym_%s_%d", comp_node_tag(c, id), j); int pr = psb_symbol_proc(c, sc, syms.v[j], nv, pfx); /* :name == __psym_N */ int sy = nt_new_node(nt, "SymbolNode"); nt_node_set_str(nt, sy, "value", syms.v[j]); int rv = psb_local(nt, "LocalVariableReadNode", vn); int eqa = nt_new_node(nt, "ArgumentsNode"); nt_node_set_arr(nt, eqa, "arguments", &rv, 1); int eq = nt_new_node(nt, "CallNode"); nt_node_set_str(nt, eq, "name", "=="); nt_node_set_ref(nt, eq, "receiver", sy); nt_node_set_ref(nt, eq, "arguments", eqa); int ast = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, ast, "body", &pr, 1); int ifn = nt_new_node(nt, "IfNode"); nt_node_set_ref(nt, ifn, "predicate", eq); nt_node_set_ref(nt, ifn, "statements", ast); nt_node_set_ref(nt, ifn, "subsequent", arm); arm = ifn; } int w2 = psb_local(nt, "LocalVariableWriteNode", bn); nt_node_set_ref(nt, w2, "value", arm); stm[ns++] = w2; passed = bn; } nt_node_set_ref(nt, ba, "expression", psb_local(nt, "LocalVariableReadNode", passed)); nt_node_set_int(nt, ba, "psb_done", 1); int nc = bo_shallow_copy(nt, id); if (nc < 0) continue; stm[ns++] = nc; int stmts = nt_new_node(nt, "StatementsNode"); nt_node_set_arr(nt, stmts, "body", stm, ns); /* the node keeps its id, line and file: the parentheses */ long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0), col = nt_int(nt, id, "node_col", 0); nt_node_reset(nt, id, "ParenthesesNode"); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); nt_node_set_int(nt, id, "node_col", col); nt_node_set_ref(nt, id, "body", stmts); comp_grow_node_arrays(c); int encl = c->nscope[id]; for (int j = first; j < nt->count; j++) c->nscope[j] = encl; lv_set_type(scope_local_intern(sc, vn), syms.n > 0 ? TY_POLY : TY_SYMBOL); if (syms.n > 0) lv_set_type(scope_local_intern(sc, bn), TY_POLY); changed = 1; } return changed; } /* Propagate ivar types up the inheritance chain: a base-class method runs on subclass instances, so an ivar it reads must carry the union of every subclass's assignments. Without this, an abstract base whose @x is only set to nil/placeholder there sees the wrong type when it calls `@x.foo`, even though every concrete subclass assigns @x a real object. It also keeps the structs cast-compatible: an inherited method is emitted once and called as `sp_Base_m((sp_Base *)self)`, which is sound only while the base struct is a common initial sequence of every subclass struct. An ivar held as `sp_PolyArray *` in the base and `sp_RbVal` in a subclass has a different width there, so every later field sits at another offset. Monotonic (unify only widens), so callers iterate to a fixpoint. Returns whether anything widened. */ static int propagate_ivars_up(Compiler *c) { int prop_changed = 0; for (int k = 0; k < c->nclasses; k++) { ClassInfo *kc = &c->classes[k]; for (int iv = 0; iv < kc->nivars; iv++) { TyKind kt = kc->ivar_types[iv]; if (kt == TY_UNKNOWN) continue; const char *ivn = kc->ivars[iv]; for (int a = kc->parent; a >= 0; a = c->classes[a].parent) { int ai = comp_ivar_index(&c->classes[a], ivn); if (ai < 0) continue; if (class_ivar_pinned(&c->classes[a], ivn)) continue; /* --rbs seed pins it */ TyKind merged = ty_unify(c->classes[a].ivar_types[ai], kt); if (merged != c->classes[a].ivar_types[ai]) { ivar_set_type(&c->classes[a], ai, merged); prop_changed = 1; } } } } return prop_changed; } /* Does method scope `ms` hand its named `&blk` on as a value -- keep it in a container, return it, pass it as a plain argument -- rather than only call it or forward it with `&blk`? Such a method needs the proc itself, so it stays a real function taking it, and its block_given? asks whether it is NULL. Made inline-only for its block_given?, the stored `blk` named a local the splice never declared (Benchmark::Job#item: `@list << [label, blk]`). */ static int scope_has_yield_node(Compiler *c, Scope *ms) { for (int id = 0; id < c->nt->count; id++) if (nt_kind(c->nt, id) == NK_YieldNode && comp_scope_of(c, id) == ms) return 1; return 0; } static int blk_param_escapes(Compiler *c, Scope *ms) { const NodeTable *nt = c->nt; if (!ms || !ms->blk_param || !ms->blk_param[0] || ms->def_node < 0) return 0; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) == NK_YieldNode && comp_scope_of(c, id) == ms) return 0; } for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_LocalVariableReadNode || comp_scope_of(c, id) != ms) continue; const char *n = nt_str(nt, id, "name"); if (!n || !sp_streq(n, ms->blk_param)) continue; int used = 0; for (int k = 0; k < nt->count && !used; k++) { NodeKind kk = nt_kind(nt, k); if (kk == NK_CallNode && nt_ref(nt, k, "receiver") == id) used = 1; else if (kk == NK_BlockArgumentNode && nt_ref(nt, k, "expression") == id) used = 1; } if (!used) return 1; } return 0; } /* Is every value this ivar is assigned or pushed a SCALAR? The re-narrow may reset a boxed ARRAY slot only when nothing it holds is itself an array. A table is boxed for reasons its re-derivation cannot see: `@banks[0] = pattern`, where the row handed back is a boxed row read out of another boxed table, keeps @banks boxed, and re-deriving it from the rows' own kinds narrows it to a table of int arrays -- the boxed row is then taken as a bare sp_IntArray * and read back as garbage (test/ivar_table_boxed_row_store). A slot holding scalars has no such hidden reason: its poly_array is the fold of its pushes, which is exactly what the re-run recomputes. */ /* Per (class, ivar), 1 when some value the ivar is assigned or pushed is not a scalar (see the rule below), in one walk over the nodes: asked per boxed array slot, a walk each made the pass ivars x nodes (scale-test). The table is laid out class by class at off[ci]. */ static int an_ivar_elem_value_nonscalar(Compiler *c, int val) { const NodeTable *nt = c->nt; NodeKind vk = nt_kind(nt, val); if (vk == NK_ArrayNode || vk == NK_SplatNode) return 1; TyKind t = infer_type(c, val); return ty_is_array(t) || t == TY_POLY_ARRAY; } static char *an_ivar_nonscalar_table(Compiler *c, int *off) { const NodeTable *nt = c->nt; int total = 0; for (int ci = 0; ci < c->nclasses; ci++) { off[ci] = total; total += c->classes[ci].nivars; } char *bad = (char *)calloc((size_t)(total > 0 ? total : 1), 1); if (!bad) return NULL; /* A slot the alias propagation widened holds another name's Array (propagate_alias_widening), which its own writes and pushes do not show. Reset with the other name, the alias's push folded before the other's write merged, took the pushed element's kind, and the merge boxed the pair (`@a = [1]; @b = @a; @b << "x"`). */ for (int ci = 0; ci < c->nclasses; ci++) for (int iv = 0; iv < c->classes[ci].nivars; iv++) if (c->classes[ci].ivar_alias_wide[iv]) bad[off[ci] + iv] = 1; for (int u = 0; u < nt->count; u++) { NodeKind k = nt_kind(nt, u); const char *ivn = NULL; int owner_node = -1, val = -1, whole = 0; if (k == NK_InstanceVariableWriteNode) { ivn = nt_str(nt, u, "name"); owner_node = u; val = nt_ref(nt, u, "value"); whole = 1; } else if (k == NK_CallNode) { const char *pn = nt_str(nt, u, "name"); if (!pn || !(sp_streq(pn, "<<") || sp_streq(pn, "push") || sp_streq(pn, "append") || sp_streq(pn, "unshift") || sp_streq(pn, "[]="))) continue; int recv = nt_ref(nt, u, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_InstanceVariableReadNode) continue; ivn = nt_str(nt, recv, "name"); owner_node = recv; int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (!av || an <= 0) continue; val = av[an - 1]; } else continue; if (!ivn || val < 0) continue; int cid = comp_ivar_owner(c, owner_node); if (cid < 0 || cid >= c->nclasses) continue; int iv = comp_ivar_index(&c->classes[cid], ivn); if (iv < 0 || bad[off[cid] + iv]) continue; int nonscalar = 0; if (whole) { /* the CONTAINER, not an element: what disqualifies the slot is an element that is itself an array, so ask what this assignment's array holds rather than that it is one */ NodeKind at0_k = nt_kind(nt, val); if (at0_k == NK_ArrayNode) { int en = 0; const int *els = nt_arr(nt, val, "elements", &en); for (int e = 0; e < en && els && !nonscalar; e++) nonscalar = an_ivar_elem_value_nonscalar(c, els[e]); } if (!nonscalar) { TyKind ct = infer_type(c, val); if (ty_is_array(ct)) { TyKind et2 = ty_array_elem(ct); nonscalar = ty_is_array(et2) || et2 == TY_POLY_ARRAY; } } } else nonscalar = an_ivar_elem_value_nonscalar(c, val); if (nonscalar) bad[off[cid] + iv] = 1; } return bad; } /* A class with an instance in a local some proc captures (is_cell) is a heap class: a cell holds a GC pointer or a builtin struct, not a user struct (cell_is_typed_ptr), so a value type there has no cell to ride. */ static void an_heap_captured_classes(Compiler *c) { for (int si = 0; si < c->nscopes; si++) { Scope *s = &c->scopes[si]; for (int j = 0; j < s->nlocals; j++) { LocalVar *lv = &s->locals[j]; if (!lv->is_cell || !ty_is_object(lv->type)) continue; int q = ty_object_class(lv->type); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } } /* Can boxed value `v` be a Method: a read out of an Array or Hash literal that holds one (`[method(:len), 1][i]`, `{ a: method(:a) }[k]`), out of a local or an instance variable that holds such a literal or has a Method stored into it (`h[k] = method(:m)`, `a << method(:m)`), or a local written from such a read? A Method called through `[]` or `===` there takes its arguments as `.call` does, so the evidence for its parameters has to count the call (below). The containers a Method is stored into are found once (an_method_holders_build): a local by its scope and name, an instance variable by its name. Both walks follow every write of a local, to depth 4, and the writes of one local reach the same values again and again: unmemoized, a function of W writes to one local walked W^4 paths. The answer for a node at a depth is fixed while the holders are, and one found at a depth holds higher up too, where the walk has more depth left: `memo` keeps, per walk and node, one more than the deepest depth it was found to hold at (low nibble) and the shallowest it was found not to (high nibble), and is cleared when a holder is added (`memo_n`). */ typedef struct { int n, cap; int *scope; const char **name; unsigned char *memo[2]; int memo_cap, memo_n; } AnMethodHolders; static void an_method_holders_add(AnMethodHolders *h, int scope, const char *name) { if (!name) return; if (h->n == h->cap) { h->cap = h->cap ? h->cap * 2 : 8; h->scope = (int *)realloc(h->scope, sizeof(int) * (size_t)h->cap); h->name = (const char **)realloc(h->name, sizeof(char *) * (size_t)h->cap); if (!h->scope || !h->name) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } h->scope[h->n] = scope; h->name[h->n++] = name; } static int an_method_holders_has(const AnMethodHolders *h, int scope, const char *name) { for (int i = 0; name && i < h->n; i++) if (h->scope[i] == scope && sp_streq(h->name[i], name)) return 1; return 0; } /* -1 when the memo does not know walk `f`'s answer for node `v` at `depth` */ static int an_method_memo_get(Compiler *c, AnMethodHolders *mh, int f, int v, int depth) { if (mh->memo_n != mh->n || mh->memo_cap < c->nt->count) { if (mh->memo_cap < c->nt->count) { for (int k = 0; k < 2; k++) { free(mh->memo[k]); mh->memo[k] = (unsigned char *)malloc((size_t)c->nt->count); if (!mh->memo[k]) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } } mh->memo_cap = c->nt->count; } for (int k = 0; k < 2; k++) memset(mh->memo[k], 0, (size_t)mh->memo_cap); mh->memo_n = mh->n; } unsigned char m = mh->memo[f][v]; if ((m & 15) && depth <= (m & 15) - 1) return 1; if ((m >> 4) && depth >= (m >> 4) - 1) return 0; return -1; } static int an_method_memo_put(AnMethodHolders *mh, int f, int v, int depth, int got) { unsigned char *m = &mh->memo[f][v]; if (got) { if (depth + 1 > (*m & 15)) *m = (unsigned char)((*m & 0xf0) | (depth + 1)); } else if (!(*m >> 4) || depth + 1 < (*m >> 4)) *m = (unsigned char)((*m & 15) | ((depth + 1) << 4)); return got; } static int an_poly_may_be_method(Compiler *c, AnMethodHolders *mh, int v, int depth); static int an_container_holds_method_1(Compiler *c, AnMethodHolders *mh, int a, int depth); static int an_container_holds_method(Compiler *c, AnMethodHolders *mh, int a, int depth) { if (a < 0 || depth > 4) return 0; int got = an_method_memo_get(c, mh, 0, a, depth); if (got >= 0) return got; return an_method_memo_put(mh, 0, a, depth, an_container_holds_method_1(c, mh, a, depth)); } static int an_container_holds_method_1(Compiler *c, AnMethodHolders *mh, int a, int depth) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, a); if (k == NK_ArrayNode) { int en = 0; const int *ev = nt_arr(nt, a, "elements", &en); for (int e = 0; e < en; e++) if (an_poly_may_be_method(c, mh, ev[e], depth + 1)) return 1; return 0; } if (k == NK_HashNode || k == NK_KeywordHashNode) { int en = 0; const int *ev = nt_arr(nt, a, "elements", &en); for (int e = 0; e < en; e++) if (nt_kind(nt, ev[e]) == NK_AssocNode && an_poly_may_be_method(c, mh, nt_ref(nt, ev[e], "value"), depth + 1)) return 1; return 0; } if (k == NK_InstanceVariableReadNode) return an_method_holders_has(mh, -1, nt_str(nt, a, "name")); if (k == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, a, "name"); Scope *vs = vn ? comp_scope_of(c, a) : NULL; if (!vs) return 0; int vsi = (int)(vs - c->scopes); if (an_method_holders_has(mh, vsi, vn)) return 1; for (int w = comp_lvw_first_sc(c, vsi, vn); w >= 0; w = comp_lvw_next_sc(c, w)) if (nt_kind(nt, w) == NK_LocalVariableWriteNode && an_container_holds_method(c, mh, nt_ref(nt, w, "value"), depth + 1)) return 1; } return 0; } static int an_poly_may_be_method_1(Compiler *c, AnMethodHolders *mh, int v, int depth); static int an_poly_may_be_method(Compiler *c, AnMethodHolders *mh, int v, int depth) { if (v < 0 || depth > 4) return 0; int got = an_method_memo_get(c, mh, 1, v, depth); if (got >= 0) return got; return an_method_memo_put(mh, 1, v, depth, an_poly_may_be_method_1(c, mh, v, depth)); } static int an_poly_may_be_method_1(Compiler *c, AnMethodHolders *mh, int v, int depth) { const NodeTable *nt = c->nt; if (c->ntype[v] == TY_METHOD) return 1; NodeKind k = nt_kind(nt, v); if (k == NK_LocalVariableReadNode) { const char *vn = nt_str(nt, v, "name"); Scope *vs = vn ? comp_scope_of(c, v) : NULL; for (int w = vs ? comp_lvw_first_sc(c, (int)(vs - c->scopes), vn) : -1; w >= 0; w = comp_lvw_next_sc(c, w)) if (nt_kind(nt, w) == NK_LocalVariableWriteNode && an_poly_may_be_method(c, mh, nt_ref(nt, w, "value"), depth + 1)) return 1; return 0; } if (k != NK_CallNode) return 0; const char *nm = nt_str(nt, v, "name"); if (!nm || !(sp_streq(nm, "[]") || sp_streq(nm, "fetch") || sp_streq(nm, "dig") || sp_streq(nm, "at") || sp_streq(nm, "first") || sp_streq(nm, "last") || sp_streq(nm, "sample"))) return 0; return an_container_holds_method(c, mh, nt_ref(nt, v, "receiver"), depth + 1); } static void an_method_holders_build(Compiler *c, AnMethodHolders *mh) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); int r = nt_ref(nt, id, "receiver"); if (!nm || r < 0 || !(sp_streq(nm, "[]=") || sp_streq(nm, "<<") || sp_streq(nm, "push") || sp_streq(nm, "append") || sp_streq(nm, "unshift") || sp_streq(nm, "prepend") || sp_streq(nm, "insert") || sp_streq(nm, "store"))) continue; NodeKind rk = nt_kind(nt, r); if (rk != NK_LocalVariableReadNode && rk != NK_InstanceVariableReadNode) continue; int a = nt_ref(nt, id, "arguments"), an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; int holds = 0; for (int k = 0; k < an && !holds; k++) holds = c->ntype[av[k]] == TY_METHOD; if (!holds) continue; Scope *rs = rk == NK_LocalVariableReadNode ? comp_scope_of(c, r) : NULL; if (rk == NK_LocalVariableReadNode && !rs) continue; an_method_holders_add(mh, rs ? (int)(rs - c->scopes) : -1, nt_str(nt, r, "name")); } NT_FOREACH_KIND(nt, NK_InstanceVariableWriteNode, id) if (an_container_holds_method(c, mh, nt_ref(nt, id, "value"), 0)) an_method_holders_add(mh, -1, nt_str(nt, id, "name")); } /* The method a `super` in s reaches: the next shadow in s's own class (an earlier include's, prepend's or extend's copy), or else the parent's. */ static int super_reach(Compiler *c, Scope *s) { const char *shadow = comp_super_shadow(c, s); if (shadow) return s->is_cmethod ? comp_cmethod_in_class(c, s->class_id, shadow) : comp_method_in_class(c, s->class_id, shadow); int p = comp_super_parent(c, s->class_id, s->is_cmethod); if (p < 0) return -1; return s->is_cmethod ? comp_cmethod_in_chain(c, p, s->name, NULL) : comp_method_in_chain(c, p, s->name, NULL); } static int super_new_init_keeps_block(Compiler *c, Scope *s) { if (super_new_init_yields(c, s)) return 1; if (!s->is_cmethod || !sp_streq(comp_scope_user_name(s), "new") || super_reach(c, s) >= 0) return 0; for (int k = 0; k < c->nclasses; k++) { int mi = k == s->class_id || is_descendant(c, k, s->class_id) ? comp_method_in_chain(c, k, "initialize", NULL) : -1; if (mi >= 0 && c->scopes[mi].blk_param && c->scopes[mi].blk_param[0] && !c->scopes[mi].yields) return 1; } return 0; } /* A node of the graph below: a yielding method, which is spliced where it is called. */ static int ctor_graph_node(Compiler *c, int s) { if (s < 0 || s >= c->nscopes) return 0; Scope *sc = &c->scopes[s]; return sc->yields && !sc->is_proc_form && sc->name; } /* ...and among those a yielding instance initialize, which a `new` site or a super splices. Its copy behind a class's own initialize (`__inc 0 initialize`, an included module's that the class's calls super into) counts too. */ static int ctor_graph_init(Compiler *c, int s) { Scope *sc = &c->scopes[s]; return ctor_graph_node(c, s) && sc->class_id >= 0 && !sc->is_cmethod && sp_streq(comp_scope_user_name(sc), "initialize"); } /* The yielding method node `id` splices where it stands in a yielding method's body: a `new` of a named class runs that class's initialize, a `super` the parent's method, and any other call its target. -1 when it splices none. */ static int ctor_graph_target(Compiler *c, int id) { const NodeTable *nt = c->nt; int t = -1; if (nt_kind(nt, id) == NK_CallNode) { const char *name = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); int rk = recv >= 0 ? nt_kind(nt, recv) : -1; int ci = rk == NK_ConstantReadNode || rk == NK_ConstantPathNode ? const_node_class_index(c, recv) : -1; if (!name) return -1; /* a bare `new` in a class method builds that class */ Scope *sc = &c->scopes[c->nscope[id]]; if ((recv < 0 || rk == NK_SelfNode) && sc->is_cmethod) ci = sc->class_id; /* a `new` runs the class's own `def self.new` when it has one, else its initialize */ if (sp_streq(name, "new")) { t = ci >= 0 ? comp_cmethod_in_chain(c, ci, "new", NULL) : -1; if (t < 0 && ci >= 0) t = comp_method_in_chain(c, ci, "initialize", NULL); } else if (recv < 0 || rk == NK_SelfNode) t = comp_self_call_mi(c, id, name); else if (ci >= 0) t = call_target(c, id, ci, name, CT_CMETHOD, NULL); else { TyKind rt = infer_type(c, recv); if (ty_is_object(rt)) t = call_target(c, id, ty_object_class(rt), name, 0, NULL); } } else t = super_reach(c, &c->scopes[c->nscope[id]]); return ctor_graph_node(c, t) ? t : -1; } /* Edges for calls and supers in a yielding method's body that splice another: counted into cnt[s + 1], or appended to adj at fill[s]. */ static void ctor_graph_edge(Compiler *c, int id, int *cnt, int *adj, int *fill) { int s = c->nscope[id]; if (!ctor_graph_node(c, s)) return; /* A super reaching Class#new splices initialize on the receiving class, including a descendant that inherits this class method. It has no user superclass method for super_reach to find. */ if (comp_super_is_class_new(c, id)) { int ci = c->scopes[s].class_id, nd = 0; const int *ds = comp_descendants(c, ci, &nd); for (int d = -1; d < nd; d++) { int k = d < 0 ? ci : ds[d]; if (comp_cmethod_in_chain(c, k, "new", NULL) != s) continue; int t = comp_method_in_chain(c, k, "initialize", NULL); if (!ctor_graph_node(c, t)) continue; if (cnt) cnt[s + 1]++; else adj[fill[s]++] = t; } return; } int t = ctor_graph_target(c, id); if (t < 0) return; if (cnt) cnt[s + 1]++; else adj[fill[s]++] = t; } static void ctor_graph_edges(Compiler *c, int *cnt, int *adj, int *fill) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) ctor_graph_edge(c, id, cnt, adj, fill); NT_FOREACH_KIND(nt, NK_SuperNode, id) ctor_graph_edge(c, id, cnt, adj, fill); NT_FOREACH_KIND(nt, NK_ForwardingSuperNode, id) ctor_graph_edge(c, id, cnt, adj, fill); } /* The cycles of constructors. A `new` site splices the yielding initialize it runs (emit_ctor_yield_inline), so an initialize whose body reaches itself again -- `Y.new` in Y#initialize, two classes' initializes building each other, a parent's building a subclass whose initialize calls super, or a `new` in a yielding method the initialize calls -- spliced without end. The graph has an edge from each yielding method to the yielding method a call or a `super` in its body splices, and each initialize on a cycle is marked with its strongly connected component (Scope.ctor_cycle), the other methods there too. The initialize gets the proc-form clone, and a `new` site within the component runs the body through the constructor instead (ctor_site_on_cycle). A cycle through an initialize enters it by a `new`, a `super` only going up the class tree, so that ends it; a cycle of other yielding methods alone is not this pass's. Tarjan's algorithm, iterative, over the calls and the supers: linear in the program. */ static void mark_ctor_cycles(Compiler *c) { int n = c->nscopes; for (int s = 0; s < n; s++) c->scopes[s].ctor_cycle = 0; int *start = (int *)calloc((size_t)n + 1, sizeof(int)); if (!start) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } /* the edges, counted per initialize, then filled in */ ctor_graph_edges(c, start, NULL, NULL); for (int s = 0; s < n; s++) start[s + 1] += start[s]; if (start[n] == 0) { free(start); return; } int *adj = (int *)malloc(sizeof(int) * (size_t)start[n]); int *fill = (int *)malloc(sizeof(int) * (size_t)n); if (!adj || !fill) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(fill, start, sizeof(int) * (size_t)n); ctor_graph_edges(c, NULL, adj, fill); /* index[s] is s's visit order from 1, 0 while unvisited; edge[s] the next edge to follow; stack holds the open components, path the DFS */ int *index = (int *)calloc((size_t)n, sizeof(int)), *low = (int *)malloc(sizeof(int) * (size_t)n); int *edge = (int *)malloc(sizeof(int) * (size_t)n), *stack = (int *)malloc(sizeof(int) * (size_t)n); int *path = (int *)malloc(sizeof(int) * (size_t)n); char *on = (char *)calloc((size_t)n, 1); if (!index || !low || !edge || !stack || !path || !on) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int order = 0, sp = 0, ncomp = 0; for (int r = 0; r < n; r++) { if (index[r] || start[r] == start[r + 1]) continue; int np = 0; path[np++] = r; index[r] = low[r] = ++order; edge[r] = start[r]; stack[sp++] = r; on[r] = 1; while (np > 0) { int v = path[np - 1]; if (edge[v] < start[v + 1]) { int w = adj[edge[v]++]; if (!index[w]) { index[w] = low[w] = ++order; edge[w] = start[w]; stack[sp++] = w; on[w] = 1; path[np++] = w; } else if (on[w] && index[w] < low[v]) low[v] = index[w]; continue; } np--; if (np > 0 && low[v] < low[path[np - 1]]) low[path[np - 1]] = low[v]; if (low[v] != index[v]) continue; /* v roots a component: a cycle when it has two methods, or one whose body splices itself, and one of constructors when an initialize is on it */ int self = 0, init = 0, k = sp; for (int e = start[v]; e < start[v + 1]; e++) if (adj[e] == v) self = 1; do init |= ctor_graph_init(c, stack[--k]); while (stack[k] != v); int mark = init && (self || k < sp - 1); if (mark) ncomp++; int w; do { w = stack[--sp]; on[w] = 0; if (mark) c->scopes[w].ctor_cycle = ncomp; } while (w != v); } } free(start); free(adj); free(fill); free(index); free(low); free(edge); free(stack); free(path); free(on); } /* `super` from a method a module prepended into a builtin class put in front of the builtin's own (`class Range; prepend RangeWithFormat; end`, whose to_s calls super): with no program method above it, it is the builtin's method on self. Spelled as that call -- self as the receiver, the method's own name, marked builtin_only so the prepended copy does not take it again -- the way rewrite_builtin_alias_self_calls spells an alias of a builtin. A bare `super` passes the method's parameters on; one with keyword parameters is left alone. Only a prepend's copy: a reopening's own def replaces the builtin's, and its super goes past it. */ static int rpbs_param_args(Compiler *c, Scope *s, int sup) { NodeTable *nt = (NodeTable *)c->nt; int *ids = (int *)malloc(sizeof(int) * (size_t)(s->nparams > 0 ? s->nparams : 1)); if (!ids) return -1; int n = 0; for (int j = 0; j < s->nparams; j++) { if (j == s->kwrest_idx || !s->pnames || !s->pnames[j]) { free(ids); return -1; } int rd = nt_new_node(nt, "LocalVariableReadNode"); if (rd < 0) { free(ids); return -1; } nt_node_set_str(nt, rd, "name", s->pnames[j]); int arg = rd; if (j == s->rest_idx) { arg = nt_new_node(nt, "SplatNode"); if (arg < 0) { free(ids); return -1; } nt_node_set_ref(nt, arg, "expression", rd); } ids[n++] = arg; } int an = nt_new_node(nt, "ArgumentsNode"); if (an >= 0) nt_node_set_arr(nt, an, "arguments", ids, n); free(ids); comp_grow_node_arrays(c); (void)sup; return an; } static void rewrite_prepended_builtin_supers(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_SuperNode && k != NK_ForwardingSuperNode) continue; int si = c->nscope[id]; if (si < 0 || si >= c->nscopes) continue; Scope *s = &c->scopes[si]; if (!s->is_prepend_copy || s->is_cmethod || !s->name || s->class_id < 0) continue; const char *cn = c->classes[s->class_id].name; if (!cn || !is_builtin_reopen_name(cn) || sp_streq(cn, "Object") || s->class_id == c->toplevel_class) continue; if (super_reach(c, s) >= 0) continue; int args = k == NK_SuperNode ? nt_ref(nt, id, "arguments") : -1; if (k == NK_ForwardingSuperNode) { int kw = 0; for (int j = 0; j < s->nparams && !kw; j++) kw = s->pnames && s->pnames[j] && callee_param_is_declared_kwarg(c, s, s->pnames[j]); if (kw) continue; args = rpbs_param_args(c, s, id); if (args < 0) continue; } char *mname = strdup(s->name); int self = nt_new_node(nt, "SelfNode"); comp_grow_node_arrays(c); c->nscope[self] = si; if (args >= 0) { /* the parameter reads are this scope's */ int an = 0; const int *av = nt_arr(nt, args, "arguments", &an); for (int q = 0; q < an; q++) { c->nscope[av[q]] = si; int ex = nt_kind(nt, av[q]) == NK_SplatNode ? nt_ref(nt, av[q], "expression") : -1; if (ex >= 0) c->nscope[ex] = si; } c->nscope[args] = si; } nt_node_set_type(nt, id, "CallNode"); nt_set_self_receiver(nt, id, self); nt_node_set_ref(nt, id, "arguments", args); nt_node_set_str(nt, id, "name", mname); nt_node_set_int(nt, id, "builtin_only", 1); free(mname); } } /* `super` from the program's own instance_variable_get, _set, instance_variable_defined?, instance_variables or remove_instance_variable, with no program method above it: Object's reflection of self, which reads and writes the object's own fields as the builtin does. Spelled as that call -- self as the receiver, the method's name, marked builtin_only so the method does not take it again -- as a super into a prepended builtin is (rewrite_prepended_builtin_supers). A bare `super` passes the parameters on; one with keyword parameters is left alone. The reflection defined on Object itself reaches Kernel's the same way. */ static void rewrite_ivar_reflection_supers(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k != NK_SuperNode && k != NK_ForwardingSuperNode) continue; int si = c->nscope[id]; if (si < 0 || si >= c->nscopes) continue; Scope *s = &c->scopes[si]; if (s->is_cmethod || !s->name || s->class_id < 0 || !is_ivar_reflection_name(s->name) || c->classes[s->class_id].is_native_class || (is_builtin_reopen_name(c->classes[s->class_id].name) && s->class_id != comp_core_class(c, CORE_OBJECT))) continue; if (super_reach(c, s) >= 0) continue; int args = k == NK_SuperNode ? nt_ref(nt, id, "arguments") : -1; if (k == NK_ForwardingSuperNode) { int kw = 0; for (int j = 0; j < s->nparams && !kw; j++) kw = s->pnames && s->pnames[j] && callee_param_is_declared_kwarg(c, s, s->pnames[j]); if (kw) continue; args = rpbs_param_args(c, s, id); if (args < 0) continue; } char *mname = strdup(s->name); int self = nt_new_node(nt, "SelfNode"); comp_grow_node_arrays(c); c->nscope[self] = si; if (args >= 0) { int an = 0; const int *av = nt_arr(nt, args, "arguments", &an); for (int q = 0; q < an; q++) { c->nscope[av[q]] = si; int ex = nt_kind(nt, av[q]) == NK_SplatNode ? nt_ref(nt, av[q], "expression") : -1; if (ex >= 0) c->nscope[ex] = si; } c->nscope[args] = si; } nt_node_set_type(nt, id, "CallNode"); nt_set_self_receiver(nt, id, self); nt_node_set_ref(nt, id, "arguments", args); nt_node_set_str(nt, id, "name", mname); nt_node_set_int(nt, id, "builtin_only", 1); free(mname); } } /* A receiverless call, in a reopened primitive or a program class, of an alias that captured the builtin method (`def plus_with(o) = plus_without(o)` after `alias_method :plus_without, :+`, `def od = original_dup` after `alias original_dup dup`): it is the builtin's call on self. Spelled as one -- self as the receiver, the builtin's name, marked so the class's own method of that name does not take it -- since a receiverless `+` is a call of the class's own `+`, which by then is plus_with. */ /* The class side: a call of a singleton alias that captured a builtin's class method (`class << Time; alias_method :stamp, :now; end`, then `Time.stamp`, `::Time.stamp`, or `stamp` / `self.stamp` in a class method of Time) is the builtin's call of its own name through the constant, `Time.now`, marked so a class method the program defines of that name does not take it. */ /* a class some class of the program inherits from: `self.mk` or a bare `mk` in its class methods runs for the subclass too, which a rewrite to the constant would not */ static int cm_alias_has_subclass(Compiler *c, int ci) { for (int k = 0; k < c->nclasses; k++) { int dn = c->classes[k].def_node; int sc = dn >= 0 && k != ci ? nt_ref(c->nt, dn, "superclass") : -1; const char *sn = sc >= 0 ? nt_str(c->nt, sc, "name") : NULL; if (sn && sp_streq(sn, c->classes[ci].name)) return 1; } return 0; } /* does call id run before the singleton alias `nm` of class ci is made: code of the program's top level or of a class body that comes ahead of the alias statement? (A def's body runs when it is called.) An alias binds for the whole program here, so such a call would find the alias where CRuby raises NoMethodError: it stays unrenamed, and refused (alias_refuse_early_call is the instance side's) */ static int cm_alias_call_precedes(Compiler *c, int ci, int id, const char *nm) { const NodeTable *nt = c->nt; int si = c->nscope[id]; if (si >= 0 && c->scopes[si].def_node >= 0) return 0; for (int i = c->classes[ci].naliases - 1; i >= 0; i--) { int an = c->classes[ci].alias_node[i]; if (!c->classes[ci].alias_sg[i] || an < 0 || !sp_streq(c->classes[ci].alias_new[i], nm)) continue; /* a call a desugar made has no line of its own (0): it is not ahead */ long long cl = nt_int(nt, id, "node_line", 0); return cl > 0 && nt_int(nt, id, "node_file", 0) == nt_int(nt, an, "node_file", 0) && cl < nt_int(nt, an, "node_line", 0); } return 0; } void rewrite_builtin_cm_alias_call(Compiler *c, int id) { if (!c->has_cm_builtin_alias) return; NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"), ci = -1; if (!nm || nt_int(nt, id, "builtin_only", 0)) return; if (recv >= 0 && (nt_kind(nt, recv) == NK_ConstantReadNode || nt_kind(nt, recv) == NK_ConstantPathNode)) ci = const_node_class_index(c, recv); else if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { int si = c->nscope[id]; ci = si >= 0 && c->scopes[si].is_cmethod ? c->scopes[si].class_id : -1; } if (ci < 0 || !c->classes[ci].name || c->classes[ci].naliases == 0) return; int bi = 0; const char *rn = comp_resolve_cm_alias_ex(c, ci, nm, &bi); if (bi != 2 || !rn || sp_streq(rn, nm)) return; if ((recv < 0 || nt_kind(nt, recv) == NK_SelfNode) && cm_alias_has_subclass(c, ci)) return; if (nt_int(nt, id, "cm_alias_unsure", 0)) { char msg[300]; snprintf(msg, sizeof msg, "%s.%s: a call that may run before the singleton alias of a builtin's class method is made " "(written in a block, on the alias's line, or beside an alias that is not plain class-body code) is not " "supported; see docs/limitations.md", c->classes[ci].name, nm); unsupported_feature(c, id, msg); } if (cm_alias_call_precedes(c, ci, id, nm)) return; char *target = strdup(rn); if (!target) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (recv < 0) { recv = nt_new_node(nt, "ConstantReadNode"); comp_grow_node_arrays(c); c->nscope[recv] = c->nscope[id]; nt_node_set_ref(nt, id, "receiver", recv); } else if (nt_kind(nt, recv) == NK_SelfNode) nt_node_reset(nt, recv, "ConstantReadNode"); if (nt_kind(nt, recv) == NK_ConstantReadNode) nt_node_set_str(nt, recv, "name", c->classes[ci].name); nt_node_set_str(nt, id, "name", target); nt_node_set_int(nt, id, "builtin_only", 1); free(target); } /* Can the program's order not prove that call id runs after the singleton alias statement an? A call written in a block, lambda or proc ahead of the alias (or on its line) may run after it, one on the alias's line has no order, and an alias in a loop or a conditional may not run at all. Such a call is refused: renamed, it would answer where CRuby may raise, and left alone, raise where CRuby may answer. */ static int cm_alias_order_unproven(Compiler *c, const int *par, int id, int an) { const NodeTable *nt = c->nt; for (int n = par[an]; n >= 0 && !prog_is_root(c, n); n = par[n]) { /* up to its file's root */ NodeKind k = nt_kind(nt, n); if (k != NK_StatementsNode && k != NK_ClassNode && k != NK_ModuleNode && k != NK_SingletonClassNode) return 1; } long long cl = nt_int(nt, id, "node_line", 0), al = nt_int(nt, an, "node_line", 0); if (cl <= 0) return 0; if (nt_int(nt, id, "node_file", 0) != nt_int(nt, an, "node_file", 0)) { /* two files: the program's order (prog_order); none known is no proof. (The inline model's one spliced text answers as it always did, until it goes.) */ if (!prog_has_files(c)) return 0; int o = prog_order(c, id, an); if (o == PROG_UNKNOWN) return 1; if (o == PROG_AFTER) return 0; } else if (cl == al) return 1; else if (cl > al) return 0; for (int n = par[id]; n >= 0; n = par[n]) { NodeKind k = nt_kind(nt, n); if (k == NK_DefNode) return 0; if (k == NK_BlockNode || k == NK_LambdaNode) return 1; } return 0; } static void rewrite_builtin_cm_alias_calls(Compiler *c) { if (!c->has_cm_builtin_alias) return; NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, nal = 0; for (int k = 0; k < c->nclasses; k++) for (int i = 0; i < c->classes[k].naliases; i++) if (c->classes[k].alias_sg[i] && c->classes[k].alias_node[i] >= 0) nal++; struct { const char *name; int node; } *als = malloc(sizeof *als * (size_t)(nal ? nal : 1)); if (!als) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nal = 0; for (int k = 0; k < c->nclasses; k++) for (int i = 0; i < c->classes[k].naliases; i++) if (c->classes[k].alias_sg[i] && c->classes[k].alias_node[i] >= 0) { als[nal].name = c->classes[k].alias_new[i]; als[nal++].node = c->classes[k].alias_node[i]; } const int *par = comp_parent_map(c); for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); for (int a = 0; nm && a < nal; a++) if (sp_streq(als[a].name, nm) && cm_alias_order_unproven(c, par, id, als[a].node)) { nt_node_set_int(nt, id, "cm_alias_unsure", 1); break; } rewrite_builtin_cm_alias_call(c, id); } free(als); } /* A class body's call on self the desugar spelled with the class (`rand(2)` -> `U.rand(2)`) that runs before the class's own method of its name is defined, while Kernel has a module function of the name, is Kernel's call, as in a class with no def of the name (cplan_self_builtin): bare again. */ static void route_self_builtin_calls(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) if (cplan_self_builtin(c, id)) { nt_node_set_ref(nt, id, "receiver", -1); nt_node_set_int(nt, id, "self_call", 0); } } static void rewrite_builtin_alias_self_calls(Compiler *c, int settled) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || nt_ref(nt, id, "receiver") >= 0) continue; const char *nm = nt_str(nt, id, "name"); int si = c->nscope[id]; int k = si >= 0 ? c->scopes[si].class_id : -1; if (!nm || k < 0 || c->scopes[si].is_cmethod || c->classes[k].naliases == 0) continue; int bi = 0; const char *rn = comp_resolve_alias_ex(c, k, nm, NULL, &bi); /* An Object method's alias in a program class (3) keeps the builtin only once settle_object_builtin_aliases has found no ancestor or module that defined the name before it: those calls are spelled in a second run after that, the rest in the first. */ if (!bi || (bi == 3) != settled || !rn) continue; char *target = strdup(rn); int self = nt_new_node(nt, "SelfNode"); comp_grow_node_arrays(c); c->nscope[self] = c->nscope[id]; nt_set_self_receiver(nt, id, self); nt_node_set_str(nt, id, "name", target); nt_node_set_int(nt, id, "builtin_only", 1); free(target); } } /* ---- Array subclasses (#7449) ---- An instance of a class below Array IS an Array (ClassInfo.ary_root), and these rewrites spell the parts of its protocol that have no call of their own as ordinary calls on it, so every later pass sees Array's methods called on an Array subclass instance and nothing else. */ /* A node for a rewrite of node `at`: its scope, class body, line and file. */ static int arysub_node(Compiler *c, const char *type, int at) { NodeTable *nt = (NodeTable *)c->nt; int n = nt_new_node(nt, type); if (n < 0) return -1; comp_grow_node_arrays(c); c->nscope[n] = c->nscope[at]; c->node_cbody[n] = c->node_cbody[at]; long long ln = nt_int(nt, at, "node_line", 0), fl = nt_int(nt, at, "node_file", 0); if (ln) nt_node_set_int(nt, n, "node_line", ln); if (fl) nt_node_set_int(nt, n, "node_file", fl); return n; } /* Make node `id` (reset in place, so its parent still holds it) the call recv.name(args) { blk }; args is an ArgumentsNode or -1. */ static void arysub_set_call(Compiler *c, int id, int recv, const char *name, int args, int blk) { NodeTable *nt = (NodeTable *)c->nt; long long ln = nt_int(nt, id, "node_line", 0), fl = nt_int(nt, id, "node_file", 0); /* (where the value is read as an Array stays with the node: `[*X.new(a)]`) */ long long op = nt_int(nt, id, "ary_operand", 0); char *nm = strdup(name); int may_private = nt_ignore_visibility(nt, id) || recv < 0; nt_node_reset(nt, id, "CallNode"); if (ln) nt_node_set_int(nt, id, "node_line", ln); if (fl) nt_node_set_int(nt, id, "node_file", fl); if (op) nt_node_set_int(nt, id, "ary_operand", op); nt_node_set_ref(nt, id, "receiver", recv); nt_set_ignore_visibility(nt, id, may_private); nt_node_set_str(nt, id, "name", nm); nt_node_set_ref(nt, id, "arguments", args); nt_node_set_ref(nt, id, "block", blk); free(nm); } static int arysub_new_call(Compiler *c, int at, int recv, const char *name, int args, int blk) { int n = arysub_node(c, "CallNode", at); if (n < 0) return -1; arysub_set_call(c, n, recv, name, args, blk); return n; } static int arysub_args(Compiler *c, int at, const int *ids, int n) { int a = arysub_node(c, "ArgumentsNode", at); if (a >= 0) nt_node_set_arr((NodeTable *)c->nt, a, "arguments", ids, n); return a; } /* `Array.new(args) { blk }` for node `at`, or -1; `Hash.new(...)` and `Hash[...]` (name "[]") for a Hash subclass's, `String.new(...)` for a String subclass's. bn names the builtin. */ static int arysub_builtin_call(Compiler *c, int at, const char *bn, const char *name, int args, int blk) { int k = arysub_node(c, "ConstantReadNode", at); if (k < 0) return -1; nt_node_set_str((NodeTable *)c->nt, k, "name", bn); return arysub_new_call(c, at, k, name, args, blk); } /* The arguments a bare `super` in the method of scope s passes on: its parameters, read by name. -1 for a parameter list this spelling does not reproduce (keywords, a post-rest or destructured parameter). */ static int arysub_zsuper_args(Compiler *c, int at, Scope *s) { const NodeTable *nt = c->nt; int pn = s->def_node >= 0 ? nt_ref(nt, s->def_node, "parameters") : -1; int rn = 0, on = 0, qn = 0, kn = 0; const int *rq = pn >= 0 ? nt_arr(nt, pn, "requireds", &rn) : NULL; const int *op = pn >= 0 ? nt_arr(nt, pn, "optionals", &on) : NULL; if (pn >= 0) { nt_arr(nt, pn, "posts", &qn); nt_arr(nt, pn, "keywords", &kn); } int rest = pn >= 0 ? nt_ref(nt, pn, "rest") : -1; if (qn || kn || (pn >= 0 && nt_ref(nt, pn, "keyword_rest") >= 0)) return -1; int ids[64], n = 0; for (int i = 0; i < rn + on + (rest >= 0); i++) { int p = i < rn ? rq[i] : i < rn + on ? op[i - rn] : rest; const char *pnm = nt_str(nt, p, "name"); if (!pnm || n >= 63 || nt_kind(nt, p) == NK_MultiTargetNode) return -1; int rd = arysub_node(c, "LocalVariableReadNode", at); if (rd < 0) return -1; nt_node_set_str((NodeTable *)c->nt, rd, "name", pnm); if (p == rest) { int sp = arysub_node(c, "SplatNode", at); if (sp < 0) return -1; nt_node_set_ref((NodeTable *)c->nt, sp, "expression", rd); rd = sp; } ids[n++] = rd; } return arysub_args(c, at, ids, n); } /* Node `n` (reset in place) as the call a `super` at `at` makes into Array: Array's method `mname` on self, marked builtin_only so the class's own override does not take it, or in initialize (init 1) `self.clear` for no arguments and no block, else `self.replace(Array.new(args) { blk })`. In the class's own `def self.new` (init 2) it is Class#new with no initialize of the program to run: `self.allocate`, else `self.allocate.replace(Array.new(args) { blk })`. */ static void arysub_super_into(Compiler *c, int at, int n, const char *mname, int init, int args, int blk) { Scope *s = comp_scope_of(c, at); const char *bn = s ? core_class_name(comp_arysub_builtin(c, s->class_id)) : "Array"; int an = 0; if (args >= 0) nt_arr(c->nt, args, "arguments", &an); int self = arysub_node(c, "SelfNode", at); if (self < 0) return; char *mn = strdup(mname); if (init == 2 && an == 0 && blk < 0) { arysub_set_call(c, n, self, "allocate", -1, -1); free(mn); return; } if (init == 2 && (self = arysub_new_call(c, at, self, "allocate", -1, -1)) < 0) { free(mn); return; } if (!init) { arysub_set_call(c, n, self, mn, args, blk); nt_node_set_int((NodeTable *)c->nt, n, "builtin_only", 1); } else if (an == 0 && blk < 0) arysub_set_call(c, n, self, "clear", -1, -1); else { int an_call = arysub_builtin_call(c, at, bn, "new", an ? args : -1, blk); if (an_call >= 0) arysub_set_call(c, n, self, "replace", arysub_args(c, at, &an_call, 1), -1); if (an_call >= 0 && init == 2) nt_node_set_int((NodeTable *)c->nt, n, "builtin_only", 1); } nt_set_ignore_visibility((NodeTable *)c->nt, n, 1); free(mn); } /* `super` in an Array subclass's method that reaches Array. In initialize it is Array#initialize, which makes the receiver what Array.new would build from the same arguments: `self.replace(Array.new(args) { blk })`, and `super()` the empty Array, `self.clear`. Anywhere else it is Array's method of the same name on self, marked builtin_only so the class's own override does not take it. A Hash subclass's is the same with Hash: Hash#replace takes the default and the default proc along, so `self.replace(Hash.new(default) { blk })` is Hash#initialize's. In the class's own `def self.new` it is Class#new, which calls Array.new's construction on a new instance when no initialize of the program can run: none in the class's chain, nor in a descendant's, which self may be, and no allocate of the program, which Class#new does not call. (Those are left to Class#new's own splice.) */ static int arysub_cnew_kept(Compiler *c, int k) { int nd = 0; comp_descendants(c, k, &nd); return nd > 0 || comp_method_in_chain(c, k, "initialize", NULL) >= 0 || comp_cmethod_in_chain(c, k, "allocate", NULL) >= 0; } /* Whether a call with a block may reach class k's `new` (or a descendant's): a super with no block of its own passes that block on. Only a `new` on a constant naming another class, a builtin's, or a bare `new` in another class's class method is known not to. */ /* Whether argument list args passes a `**` splat. */ static int arysub_args_kwsplat(const NodeTable *nt, int args) { int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; for (int i = 0; i < an; i++) { if (nt_kind(nt, av[i]) == NK_AssocSplatNode) return 1; if (nt_kind(nt, av[i]) != NK_KeywordHashNode) continue; int en = 0; const int *ev = nt_arr(nt, av[i], "elements", &en); for (int j = 0; j < en; j++) if (nt_kind(nt, ev[j]) == NK_AssocSplatNode) return 1; } return 0; } static int arysub_new_gets_block(Compiler *c, int k) { static const char *const other[] = { "Array", "Hash", "String", "Struct", "Class", "Module", "Thread", "Proc", "Enumerator", "Fiber", "Data", "Set", "Mutex", "Queue", "Ractor", NULL }; const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm) continue; /* (`T.method(:new)` hands the Method on, and its call may take a block) */ if ((sp_streq(nm, "method") || sp_streq(nm, "public_method")) && nt_ref(nt, id, "arguments") >= 0) { int an = 0; const int *av = nt_arr(nt, nt_ref(nt, id, "arguments"), "arguments", &an); if (an > 0 && (nt_kind(nt, av[0]) != NK_SymbolNode || (nt_str(nt, av[0], "value") && sp_streq(nt_str(nt, av[0], "value"), "new")))) return 1; } if (nt_ref(nt, id, "block") < 0) continue; if (sp_streq(nm, "send") || sp_streq(nm, "public_send") || sp_streq(nm, "__send__")) { int args = nt_ref(nt, id, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an > 0 && nt_kind(nt, av[0]) != NK_SymbolNode) return 1; if (an > 0 && nt_str(nt, av[0], "value") && sp_streq(nt_str(nt, av[0], "value"), "new")) return 1; continue; } if (!sp_streq(nm, "new")) continue; int recv = nt_ref(nt, id, "receiver"), kc = -1; if (recv >= 0 && (nt_kind(nt, recv) == NK_ConstantReadNode || nt_kind(nt, recv) == NK_ConstantPathNode)) { const char *cn = nt_str(nt, recv, "name"); kc = cn ? comp_class_index(c, cn) : -1; if (kc < 0) { int builtin = 0; for (int i = 0; cn && other[i]; i++) builtin |= sp_streq(cn, other[i]); if (builtin) continue; return 1; } } else if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *cs = comp_scope_of(c, id); if (!cs || !cs->is_cmethod || cs->class_id < 0) return 1; kc = cs->class_id; } else return 1; if (kc == k || is_descendant(c, kc, k)) return 1; } return 0; } static void rewrite_array_subclass_super(Compiler *c, int id) { const NodeTable *nt = c->nt; Scope *s = comp_scope_of(c, id); if (!s || !s->name || comp_ary_root(c, s->class_id) < 0) return; int init; const char *mname = comp_scope_user_name(s); if (s->is_cmethod) { /* (only the class's own: not a copy a module's extend, include or prepend in its singleton made) */ if (s->is_extend_copy || !sp_streq(s->name, "new") || s->def_node < 0 || c->node_cbody[s->def_node] != s->class_id || !comp_super_is_class_new(c, id) || arysub_cnew_kept(c, s->class_id)) return; init = 2; } else { if (comp_super_shadow(c, s)) return; int p = c->classes[s->class_id].parent; if (p >= 0 && comp_method_in_chain(c, p, mname, NULL) >= 0) return; init = sp_streq(mname, "initialize"); } /* Array#initialize_copy is a replace, which the copy dup and clone make of an Array subclass instance has done already: super into it is the receiver */ if (sp_streq(mname, "initialize_copy")) { int self = arysub_node(c, "SelfNode", id); if (self >= 0) { arysub_set_call(c, id, self, "itself", -1, -1); nt_set_ignore_visibility((NodeTable *)c->nt, id, 1); } return; } if (!init && !comp_arysub_builtin_name(c, s->class_id, mname)) return; int args = nt_ref(nt, id, "arguments"), blk = nt_ref(nt, id, "block"); if (nt_kind(nt, id) == NK_ForwardingSuperNode) { args = arysub_zsuper_args(c, id, s); if (args < 0 && init == 2) return; if (args < 0) { char msg[256]; snprintf(msg, sizeof msg, "a bare `super` into %s from a method with keyword, " "post-rest or destructured parameters is not supported yet; " "pass the arguments explicitly", core_class_name(comp_arysub_builtin(c, s->class_id))); unsupported_feature(c, id, msg); return; } } /* (a `**` splat among the arguments is left to Class#new) */ if (init == 2 && arysub_args_kwsplat(nt, args)) return; /* (a block of its own in a self.new that a block may reach as well is left to Class#new) */ if (init == 2 && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && arysub_new_gets_block(c, s->class_id)) return; /* `super(&nil)` passes no block */ if (blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode && nt_ref(nt, blk, "expression") >= 0 && nt_kind(nt, nt_ref(nt, blk, "expression")) == NK_NilNode) { arysub_super_into(c, id, id, mname, init, args, -1); return; } /* `super(n, &blk)` in initialize with the method's own block, named or anonymous, is the forward below: a `new` with no block gives none */ int bx = init == 1 && blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode ? nt_ref(nt, blk, "expression") : -2; if (bx == -1 || (bx >= 0 && nt_kind(nt, bx) == NK_LocalVariableReadNode && s->blk_param && nt_str(nt, bx, "name") && sp_streq(nt_str(nt, bx, "name"), s->blk_param))) blk = -1; if (blk >= 0) { arysub_super_into(c, id, id, mname, init, args, blk); return; } /* In self.new, a split on block_given? would make the method a yielding one, which a call through a class value cannot reach: with no block to pass on it passes none, and else it is left to Class#new. */ if (init == 2) { if (!arysub_new_gets_block(c, s->class_id)) arysub_super_into(c, id, id, mname, init, args, -1); return; } /* A super with no block of its own passes the method's: `&blk` names it, and an anonymous `&` (or none, in a method that yields) is the block the method was given (resolve_forwarded_block). Forwarded, an absent block reached Array's method as a block argument, which it does not take for none: the call with no block is the other arm of `block_given?`. A method that names no block takes a synthetic `&blk` to hold it, as a bare super into a parent's `&blk` does (an_phase_class_structure). */ if (!s->blk_param && !scope_has_yield_node(c, s)) { s->blk_param = strdup("__sblk__"); LocalVar *sblk = scope_local_intern(s, s->blk_param); if (sblk) { lv_set_type(sblk, TY_PROC); sblk->is_param = 1; } } NodeTable *wnt = (NodeTable *)nt; int fwd = arysub_node(c, "BlockArgumentNode", id); if (fwd < 0) return; if (s->blk_param && s->blk_param[0]) { int rd = arysub_node(c, "LocalVariableReadNode", id); if (rd < 0) return; nt_node_set_str(wnt, rd, "name", s->blk_param); nt_node_set_ref(wnt, fwd, "expression", rd); } int with = arysub_node(c, "CallNode", id), without = arysub_node(c, "CallNode", id); int bg = arysub_node(c, "CallNode", id), els = arysub_node(c, "ElseNode", id); int sw = arysub_node(c, "StatementsNode", id), so = arysub_node(c, "StatementsNode", id); if (with < 0 || without < 0 || bg < 0 || els < 0 || sw < 0 || so < 0) return; arysub_super_into(c, id, with, mname, init, args, fwd); arysub_super_into(c, id, without, mname, init, args, -1); arysub_set_call(c, bg, -1, "block_given?", -1, -1); nt_node_set_arr(wnt, sw, "body", &with, 1); nt_node_set_arr(wnt, so, "body", &without, 1); nt_node_set_ref(wnt, els, "statements", so); long long ln = nt_int(wnt, id, "node_line", 0), fl = nt_int(wnt, id, "node_file", 0); nt_node_reset(wnt, id, "IfNode"); if (ln) nt_node_set_int(wnt, id, "node_line", ln); if (fl) nt_node_set_int(wnt, id, "node_file", fl); nt_node_set_ref(wnt, id, "predicate", bg); nt_node_set_ref(wnt, id, "statements", sw); nt_node_set_ref(wnt, id, "subsequent", els); } /* A receiverless call in an Array subclass's instance method of a name its class chain does not define but Array does (`size`, `each { }`, `first`): Array's method on self, spelled with self as the receiver, as rewrite_builtin_alias_self_calls spells a reopened primitive's. */ static void rewrite_array_subclass_self_call(Compiler *c, int id) { int si = c->nscope[id]; Scope *s = si >= 0 ? &c->scopes[si] : NULL; const char *nm = nt_str(c->nt, id, "name"); if (!s || !s->name || s->is_cmethod || !nm || comp_ary_root(c, s->class_id) < 0) return; int k = s->class_id; /* a method of the builtin's reopen is the builtin's, called on self boxed */ int dc = -1, ro = comp_arysub_reopen(c, k); if ((call_target(c, id, k, nm, 0, &dc) >= 0 && (ro < 0 || dc != ro)) || comp_reader_in_chain(c, k, nm, NULL)) return; if (!comp_arysub_builtin_name(c, k, nm)) return; int self = arysub_node(c, "SelfNode", id); if (self >= 0) nt_node_set_ref((NodeTable *)c->nt, id, "receiver", self); } /* `X.new(args) { blk }` for an Array subclass none of whose classes defines initialize or self.new is Array.new's construction on an instance of X: `X.allocate.replace(Array.new(args) { blk })`, and `X.new` is `X.allocate`. `X[a, b]` never runs an initialize: `X.allocate.replace([a, b])`. A bare `new` in X's own class method is the same with self as the receiver. */ static void rewrite_array_subclass_new(Compiler *c, int id) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || (!sp_streq(nm, "new") && !sp_streq(nm, "[]"))) return; int k = -1; if (recv >= 0 && (nt_kind(nt, recv) == NK_ConstantReadNode || nt_kind(nt, recv) == NK_ConstantPathNode)) k = const_node_class_index(c, recv); else if (recv < 0 || nt_kind(nt, recv) == NK_SelfNode) { Scope *s = comp_scope_of(c, id); if (s && s->is_cmethod) k = s->class_id; } if (k < 0 || comp_ary_root(c, k) < 0 || call_target(c, id, k, nm, CT_CMETHOD, NULL) >= 0) return; int brackets = sp_streq(nm, "[]"); /* String has no `[]` of its own class: X[...] is a NoMethodError */ if (brackets && comp_ary_is_str(c, k)) return; if (!brackets && comp_method_in_chain(c, k, "initialize", NULL) >= 0) return; int args = nt_ref(nt, id, "arguments"), blk = nt_ref(nt, id, "block"); int an = 0; if (args >= 0) nt_arr(nt, args, "arguments", &an); if (recv < 0) recv = arysub_node(c, "SelfNode", id); if (recv < 0) return; if (!brackets && an == 0 && blk < 0) { arysub_set_call(c, id, recv, "allocate", -1, -1); return; } int alloc = arysub_new_call(c, id, recv, "allocate", -1, -1); int src = -1; int hash = comp_ary_is_hash(c, k); if (brackets && hash) { /* Hash[...] made into the pairs' to_h, as the early pass does to a written one: Hash has no `[]` of its own at run time */ src = arysub_builtin_call(c, id, "Hash", "[]", args, -1); if (src >= 0 && hash_brackets_desugar(c, src, 1) && nt_str(nt, src, "name") && sp_streq(nt_str(nt, src, "name"), "to_h")) { /* one argument: H's own Hash of its pairs, not the argument itself nor its default, and not through a to_h of the program's */ nt_node_set_int((NodeTable *)nt, src, "builtin_only", 1); int empty = arysub_node(c, "HashNode", id); int merged = empty >= 0 ? arysub_new_call(c, id, empty, "merge", arysub_args(c, id, &src, 1), -1) : -1; if (merged >= 0) nt_node_set_int((NodeTable *)nt, merged, "builtin_only", 1); src = merged; } } else if (brackets) { const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; src = arysub_node(c, "ArrayNode", id); if (src >= 0) nt_node_set_arr((NodeTable *)nt, src, "elements", av, an); } else src = arysub_builtin_call(c, id, core_class_name(comp_arysub_builtin(c, k)), "new", args, blk); if (alloc < 0 || src < 0) return; arysub_set_call(c, id, alloc, "replace", arysub_args(c, id, &src, 1), -1); /* Array's and Hash's own construction: a `replace` of the class's own is never called */ nt_node_set_int((NodeTable *)nt, id, "builtin_only", 1); } /* The calls a splat's length dispatch built from nodes m0 on, of a call into the builtin (`self.slice(*keys)`, builtin_only): each is that call, so it is the builtin's too -- unmarked, the class's own override took it and called itself. `name` limits it to one name; NULL takes any call on self whose name is the builtin's. */ static void arysub_keep_builtin_only(Compiler *c, int m0, const char *name) { NodeTable *nt = (NodeTable *)c->nt; for (int n = m0; n < nt->count; n++) { if (nt_kind(nt, n) != NK_CallNode) continue; int r = nt_ref(nt, n, "receiver"); const char *nm = nt_str(nt, n, "name"); if (r < 0 || nt_kind(nt, r) != NK_SelfNode || !nm) continue; if (name ? !sp_streq(nm, name) : 0) continue; if (!name) { Scope *s = comp_scope_of(c, n); if (!s || comp_ary_root(c, s->class_id) < 0 || !comp_arysub_builtin_name(c, s->class_id, nm)) continue; } nt_node_set_int(nt, n, "builtin_only", 1); } } /* A call with a receiver of an alias that captured the builtin's method in an Array or Hash subclass (`alias_method :raw_push, :push` ahead of the class's own `push`): on self it is the builtin's call, as the receiverless one is (rewrite_builtin_alias_self_calls); on any other receiver the alias resolves by name to the class's own method and would run that, so it is refused until the alias is a method of its own. */ static void arysub_builtin_alias_recv_call(Compiler *c, int id) { NodeTable *nt = (NodeTable *)c->nt; const char *nm = nt_str(nt, id, "name"); int recv = nt_ref(nt, id, "receiver"); if (!nm || recv < 0) return; if (nt_kind(nt, recv) == NK_SelfNode) { Scope *s = comp_scope_of(c, id); int k = s && !s->is_cmethod ? s->class_id : -1; int bi = 0; const char *rn = k >= 0 && comp_ary_root(c, k) >= 0 ? comp_resolve_alias_ex(c, k, nm, NULL, &bi) : NULL; if (!bi || !rn) return; char *target = strdup(rn); nt_node_set_str(nt, id, "name", target); nt_node_set_int(nt, id, "builtin_only", 1); free(target); return; } for (int k = 0; k < c->nclasses; k++) { if (comp_ary_root(c, k) < 0 || c->classes[k].naliases == 0) continue; int bi = 0; const char *rn = comp_resolve_alias_ex(c, k, nm, NULL, &bi); if (!bi || !rn) continue; /* only a class with a method of that name of its own runs something else than the builtin's: with none (activesupport's `blank?` over Hash#empty?, which HashWithIndifferentAccess does not redefine) the alias by name is the builtin's */ int dc = -1, ro = comp_arysub_reopen(c, k); if (call_target(c, id, k, rn, 0, &dc) < 0 || dc == ro) continue; char msg[400]; snprintf(msg, sizeof msg, "calling `%s`, an alias of %s#%s in %s, on a receiver is not supported " "yet (it would run the class's own `%s`); call it without a receiver inside the class", nm, core_class_name(comp_arysub_builtin(c, k)), rn, c->classes[k].name, rn); unsupported_feature(c, id, msg); return; } } static void rewrite_array_subclass_calls(Compiler *c) { if (!c->has_arysub) return; const NodeTable *nt = c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { NodeKind k = nt_kind(nt, id); if (k == NK_CallNode) arysub_builtin_alias_recv_call(c, id); /* where an Array is wanted, an Array subclass instance is read as its Array (infer_type, emit_expr): a splat's operand, a destructured value, the collection a `for` walks */ int opnd = k == NK_SplatNode ? nt_ref(nt, id, "expression") : k == NK_MultiWriteNode ? nt_ref(nt, id, "value") : k == NK_ForNode ? nt_ref(nt, id, "collection") : -1; /* ... and the source of a lazy pipeline, which is read off the chain's outermost call rather than dispatched */ if (k == NK_CallNode && nt_str(nt, id, "name") && sp_streq(nt_str(nt, id, "name"), "lazy") && nt_ref(nt, id, "block") < 0) opnd = nt_ref(nt, id, "receiver"); if (opnd >= 0 && nt_kind(nt, opnd) != NK_ArrayNode) nt_node_set_int((NodeTable *)nt, opnd, "ary_operand", 1); /* ... and the argument of `Array.new(x)`, which copies an Array x (3: an Array subclass instance only; a Hash or String one is a size). Only self, a plain read or a call with no block: the view of a value an inlined block, a yield, a begin or a case answers declares its temporary as the Array and stores the subclass pointer in it */ if (k == NK_CallNode && nt_ref(nt, id, "block") < 0) { int r = nt_ref(nt, id, "receiver"), args = nt_ref(nt, id, "arguments"), argc = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; NodeKind ak = argc == 1 ? nt_kind(nt, av[0]) : NK_SplatNode; if (r >= 0 && nt_kind(nt, r) == NK_ConstantReadNode && is_array_new(nt_str(nt, r, "name"), nt_str(nt, id, "name")) && (ak == NK_LocalVariableReadNode || ak == NK_InstanceVariableReadNode || ak == NK_SelfNode || ak == NK_ClassVariableReadNode || ak == NK_GlobalVariableReadNode || (ak == NK_CallNode && nt_ref(nt, av[0], "block") < 0))) nt_node_set_int((NodeTable *)nt, av[0], "ary_operand", 3); } /* ... and the receiver of an element write that is no call -- `a[i] += v`, `a[i] ||= v`, `a[i], b = ...` -- unless the class reads and writes its elements itself (2: infer_type asks) */ if (k == NK_IndexOperatorWriteNode || k == NK_IndexOrWriteNode || k == NK_IndexAndWriteNode || k == NK_IndexTargetNode) { int ir = nt_ref(nt, id, "receiver"); if (ir >= 0) nt_node_set_int((NodeTable *)nt, ir, "ary_operand", 2); } if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { rewrite_array_subclass_super(c, id); /* the call it became spreads a splat by its length as any builtin call's does (expand_static_splat_args ran before it existed); the calls the length dispatch builds stay the builtin's */ if (nt_kind(nt, id) == NK_CallNode && nt_int(nt, id, "builtin_only", 0)) { int m0 = nt->count; expand_static_splat_args(c, id, id + 1); arysub_keep_builtin_only(c, m0, nt_str(nt, id, "name")); } else if (nt_kind(nt, id) == NK_CallNode) expand_static_splat_args(c, id, id + 1); } else if (k == NK_CallNode && nt_ref(nt, id, "receiver") < 0) { rewrite_array_subclass_new(c, id); if (nt_ref(nt, id, "receiver") < 0) rewrite_array_subclass_self_call(c, id); } else if (k == NK_CallNode) rewrite_array_subclass_new(c, id); } if (nt->count > n0) { int m0 = nt->count; expand_static_splat_args(c, n0, nt->count); arysub_keep_builtin_only(c, m0, NULL); } } static int hp_hash_node = -1; /* the Hash the last hp_hash_walk answered yes for */ /* String routes that copy where CRuby hands over the one object (#6179, #6765): two names come to hold the String, the analysis gives each its own copy, and an in-place mutation through one name is lost to the other. Until Strings are shared by default, each such route is refused rather than compiled with the mutation lost. A name is a local of a scope, an ivar of a class or a global. */ typedef struct { NodeKind kind; const char *name; Scope *scope; int cid; } SaName; static int sa_name(Compiler *c, int n, SaName *o) { const NodeTable *nt = c->nt; n = nt_paren_single(nt, n); const char *nm = n >= 0 ? nt_str(nt, n, "name") : NULL; if (!nm) return 0; o->name = nm; o->scope = comp_scope_of(c, n); o->cid = -1; switch (nt_kind(nt, n)) { case NK_LocalVariableReadNode: case NK_LocalVariableWriteNode: o->kind = NK_LocalVariableReadNode; return o->scope != NULL; case NK_InstanceVariableReadNode: case NK_InstanceVariableWriteNode: o->kind = NK_InstanceVariableReadNode; o->cid = comp_ivar_owner(c, n); return o->cid >= 0; case NK_GlobalVariableReadNode: case NK_GlobalVariableWriteNode: o->kind = NK_GlobalVariableReadNode; o->name = comp_resolve_gvar(c, nm + 1); return o->name != NULL; default: return 0; } } static int sa_reads(Compiler *c, int n, const SaName *a) { SaName b; return sa_name(c, n, &b) && nt_kind(c->nt, nt_paren_single(c->nt, n)) == a->kind && sp_streq(b.name, a->name) && (a->kind != NK_LocalVariableReadNode || b.scope == a->scope) && (a->kind != NK_InstanceVariableReadNode || b.cid == a->cid); } /* The name's entries of site kind k, in node order (the variable-site chains, comp_vsite_first; the site is comp_vsite_node): the first, and the next after e. The chains carry hash collisions, so each entry's variable is checked against the name. */ static int sa_site_is(Compiler *c, int e, const SaName *a) { int v = comp_vsite_var(c, e); NodeKind vk = nt_kind(c->nt, v); SaName b; /* a read of the variable, or a write to it (VS_WRITE) */ if (vk != NK_LocalVariableWriteNode && vk != NK_InstanceVariableWriteNode && vk != NK_GlobalVariableWriteNode) return sa_reads(c, v, a); return sa_name(c, v, &b) && b.kind == a->kind && sp_streq(b.name, a->name) && (a->kind != NK_LocalVariableReadNode || b.scope == a->scope) && (a->kind != NK_InstanceVariableReadNode || b.cid == a->cid); } static int sa_site_next(Compiler *c, const SaName *a, int e) { for (e = comp_vsite_next(c, e); e >= 0 && !sa_site_is(c, e, a); e = comp_vsite_next(c, e)) {} return e; } static int sa_site_first(Compiler *c, const SaName *a, VsKind k) { int key = a->kind == NK_LocalVariableReadNode ? (int)(a->scope - c->scopes) : a->kind == NK_InstanceVariableReadNode ? a->cid : -1; int e = comp_vsite_first(c, k, a->kind, a->name, key); return e >= 0 && !sa_site_is(c, e, a) ? sa_site_next(c, a, e) : e; } /* Is the name read anywhere but at node `except`? */ static int sa_read_elsewhere(Compiler *c, const SaName *a, int except) { for (int e = sa_site_first(c, a, VS_READ); e >= 0; e = sa_site_next(c, a, e)) if (comp_vsite_node(c, e) != except) return 1; return 0; } /* A local's only read, with no parameter or captured slot behind it and only fresh values written to it. The indexed sites check exact names. */ int an_local_read_once(Compiler *c, int n) { SaName a; if (nt_kind(c->nt, n) != NK_LocalVariableReadNode || !sa_name(c, n, &a)) return 0; LocalVar *lv = scope_local(a.scope, a.name); if (!lv || lv->is_param || lv->is_block_param || lv->is_cell || sa_read_elsewhere(c, &a, n)) return 0; /* VS_WRITE holds the plain `x = v` writes only: a multiple-assignment target, `x ||= v`, `x += v` hand it a value no freshness test sees. Conditional writes can test their right side just as a plain write does. */ int sc = (int)(a.scope - c->scopes); for (int w = comp_lvw_first_sc(c, sc, a.name); w >= 0; w = comp_lvw_next_sc(c, w)) { const char *wn = nt_str(c->nt, w, "name"); if (w >= c->node_cap || c->nscope[w] != sc || !wn || !sp_streq(wn, a.name)) continue; NodeKind wk = nt_kind(c->nt, w); if (wk == NK_LocalVariableWriteNode) continue; if ((wk == NK_LocalVariableOrWriteNode || wk == NK_LocalVariableAndWriteNode) && share_value_fresh(c, nt_ref(c->nt, w, "value"), 0)) continue; return 0; } for (int e = sa_site_first(c, &a, VS_WRITE); e >= 0; e = sa_site_next(c, &a, e)) { int w = comp_vsite_node(c, e); if (!share_value_fresh(c, nt_ref(c->nt, w, "value"), 0)) return 0; } return 1; } /* A String bang method that answers its receiver (`strip!`, `sub!`): the receiver, or -1. (The four calls without a bang that answer their receiver, `bytesplice`, `append_as_bytes`, and `concat` or `prepend` with other than one argument, are the share rule's.) */ static int sa_bang_receiver(Compiler *c, int call) { const NodeTable *nt = c->nt; call = nt_paren_single(nt, call); if (call < 0 || nt_kind(nt, call) != NK_CallNode) return -1; const char *nm = nt_str(nt, call, "name"); int r = nt_ref(nt, call, "receiver"); size_t len = nm ? strlen(nm) : 0; int argc = call_plain_argc(c, call); /* a keyword hash is one more argument (encode!'s options; a Hash to a method that takes no keywords): a count is known without a splat or a block argument */ if (argc < 0) { int a = nt_ref(nt, call, "arguments"); const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &argc) : NULL; for (int i = 0; i < argc; i++) if (nt_kind(nt, av[i]) == NK_SplatNode || nt_kind(nt, av[i]) == NK_BlockArgumentNode) { argc = -1; break; } } if (r < 0 || len < 2) return -1; if (nm[len - 1] != '!') return -1; TyKind rt = comp_ntype(c, r); if ((rt != TY_STRING && rt != TY_STRBUF) || !(bop_answers_self(TY_STRING, nm, argc, nt_ref(nt, call, "block") >= 0) & (BOPF_SELF | BOPF_SELF_OR_NIL))) return -1; return r; } /* The String arguments a call's method answers as they are (`def id(x) = x`, also through `x.itself` or `x.strip!`, and each arm of `f ? x : y`): parameters the method never reassigns, read as one of its values. Up to `cap` into out; the count. A call the deep-return rule hands the shared handle (strbuf_box) answers none. */ static int sa_returned_args_t(Compiler *c, int call, int *out, int cap, int poly_args); static int sa_returned_args_t(Compiler *c, int call, int *out, int cap, int poly_args) { const NodeTable *nt = c->nt; int got = 0; call = nt_paren_single(nt, call); if (call < 0 || nt_kind(nt, call) != NK_CallNode || c->strbuf_box[call]) return 0; int mi = an_call_target_mi(c, call); if (mi <= 0) { const CallPlan *p = cplan_user_fresh(c, call); if (p->dispatch == CP_DIRECT && p->via == UC_REOPEN) mi = p->mi; } if (mi <= 0) return 0; Scope *m = &c->scopes[mi]; int lv[16]; /* a path answering nil (`if flag; s; end`) hands s over on the other. The narrow shapes: seen through a `begin`, a `&&` or `||`, a rescue modifier or a case/in, the String this refuses (a method that answers its parameter, mutated in the caller) is refused in the default build too, where it was answered, and no String route gets a new default-build refusal before shared Strings are the default. */ int n = method_value_leaves_or_nil_narrow(c, mi, lv, 16); for (int i = 0; i < n; i++) { int l = nt_paren_single(nt, lv[i]); for (int d = 0; d < 8; d++) { int b = str_self_call(nt, l) ? nt_ref(nt, l, "receiver") : sa_bang_receiver(c, l); if (b < 0) break; l = nt_paren_single(nt, b); } const char *pn = nt_kind(nt, l) == NK_LocalVariableReadNode ? nt_str(nt, l, "name") : NULL; if (!pn || comp_scope_of(c, l) != m) continue; /* the write chain is a hash bucket: another local's write can share it */ int rebound = 0; for (int w = comp_lvw_first_sc(c, mi, pn); w >= 0 && !rebound; w = comp_lvw_next_sc(c, w)) rebound = c->nscope[w] == mi && nt_str(nt, w, "name") && sp_streq(nt_str(nt, w, "name"), pn); if (rebound) continue; for (int j = 0; j < m->nparams; j++) { if (!m->pnames[j] || !sp_streq(m->pnames[j], pn)) continue; int a = arg_layout_param_node(c, m, call, j, NULL); TyKind at = a >= 0 ? comp_ntype(c, a) : TY_UNKNOWN; if ((at == TY_STRING || at == TY_STRBUF || (poly_args && at == TY_POLY)) && got < cap) out[got++] = a; } } return got; } /* A String variable a call adds to an Array without the element store (strbuf_container_store_values) seeing it: through `insert`, `prepend` or a `concat` of a literal on any Array, or through a push or `<<` whose receiver is a push or a global (`a << t << s`, `$a << s`). Answers the k-th stored value's node when it is such a name, -1 when it is not, and -2 past the last value or for another call. */ /* What the node table says of each call asked about, read once per table (its version, its edits -- an argument array an edit replaces is freed, and version does not count that edit -- and its node count): the name and receiver on the first ask, and, once the receiver is known to be an Array's, the values, their count and whether the call's shape is one of the walk's. The store walk asks this of every store into a container local, once per outermost walk that reaches the local, and the memo of the last call (#9996) answered only the k asks of one call in a row: on a Rails program the reads were still 6-10% of a sampled window of the share pass, the argument scan most of it. The receiver's type is asked each time, as before. */ static const NodeTable *sau_nt; static unsigned sau_ver; static unsigned long sau_edits; static int sau_cnt = -1, sau_cap; static signed char *sau_state; /* per node: 0 unread, 1 name and receiver read, 2 shape read too */ static const char **sau_nm; static int *sau_r, *sau_an; static signed char *sau_shape; static const int **sau_av; /* refuse_string_alias_copies' answers of sa_array_observed, by the Array variable's first read (0: not asked yet, 1 no, 2 yes), so the reads of an Array many adders fill are climbed once. */ typedef struct { unsigned char *observed; } SaArrayReads; /* refuse_string_alias_copies' order facts, built on the first question: the top-level statements that run once, after every String mutation the program has (a node-indexed mark), or none. */ typedef struct { unsigned char *after_all; int built; } SaOrder; /* ---- Set#compare_by_identity ---- An identity Set finds its members by equal?. Spinel's identity is CRuby's for an object of the program's, an Array, a Hash, a Proc, a class, a Symbol, nil, true and false; a String may be one object here where CRuby has two (the frozen literal pool, the copies between representations), and an Integer, a Float, a Rational, a Complex or a Range is held by value here where CRuby makes a new object of each Bignum and non-flonum Float, as Hash#compare_by_identity's keys are refused for. Every Set shares one class, so in a program that calls compare_by_identity anywhere, a value that may be of one of those kinds -- or of no kind the analysis knows -- entering a Set or looked up in one is refused. A boxed value, or a String, is judged by what the class facts' walk saw reach it (cls_fact_kinds): objects of the program's classes, Symbols, nil, true, false, containers and Procs, and Strings made afresh (an interpolation, a dup, a unary plus), each one object here as in CRuby; a number, a String literal, a builtin's own answer or a value the walk does not follow keeps the refusal. */ static int sid_kind_ok(Compiler *c, TyKind t) { if (t == TY_SYMBOL || t == TY_NIL || t == TY_BOOL || t == TY_PROC || t == TY_CLASS) return 1; return ty_is_object(t) || ty_is_array(t) || ty_is_hash(t); } static int sid_kinds_ok(int k) { return !(k & (CK_UNSAFE | CK_LIT)); } /* Is value v statically one whose identity is CRuby's? A boxed local whose every write in its scope is such a value counts. Parentheses answer their last statement's value, and `()` answers nil, which is one object. */ static int sid_value_ok(Compiler *c, int v, int depth) { const NodeTable *nt = c->nt; if (depth > 8) return 0; switch (vp_one(nt, v, VPS_NONE | VP_PARENS_ONLY, &v)) { case VP1_NODE: break; case VP1_NIL: return 1; case VP1_MANY: return 0; } TyKind t = infer_type(c, v); if (t != TY_POLY && sid_kind_ok(c, t)) return 1; if ((t == TY_POLY || t == TY_STRING || t == TY_STRBUF) && sid_kinds_ok(cls_fact_kinds(c, v))) return 1; if (t != TY_POLY) return 0; if (nt_kind(nt, v) != NK_LocalVariableReadNode) return 0; const char *vn = nt_str(nt, v, "name"); Scope *vs = comp_scope_of(c, v); if (!vn || !vs) return 0; int any = 0; for (int w = comp_lvw_first(c, vn); w >= 0; w = comp_lvw_next(c, w)) { if (comp_scope_of(c, w) != vs) continue; if (nt_kind(nt, w) != NK_LocalVariableWriteNode) return 0; if (!sid_value_ok(c, nt_ref(nt, w, "value"), depth + 1)) return 0; any = 1; } return any; } /* ...and is collection v one whose elements all are? A Set's members were judged when they entered it. A value of a kind that is no collection (an Integer, a String, nil, ...) brings no member: a Set raises ArgumentError for it ("value must be enumerable" / "a set") before anything enters, and the call may as well be that kind's own (`bits & 0xFF` on a boxed value the class facts cannot tell from a Set). */ static int sid_no_collection_kind(TyKind t) { return t == TY_INT || t == TY_BIGINT || t == TY_FLOAT || t == TY_RATIONAL || t == TY_COMPLEX || t == TY_STRING || t == TY_STRBUF || t == TY_SYMBOL || t == TY_NIL || t == TY_BOOL; } /* Is v a call of a reflective list that answers method, variable or constant names, Symbols each (`nil.methods`, `instance_variables`), in a program that defines no method of that name in any form: an instance method, a class method (`def self.constants`, one in `class << self`, a module's that a class extends), a singleton method (`def o.methods`, `define_singleton_method(:methods)`), or one it may define or alias by a name it computes or names by a literal? */ static int sid_symbol_list_call(Compiler *c, int v) { static const char *const names[] = { "methods", "public_methods", "private_methods", "protected_methods", "singleton_methods", "instance_methods", "public_instance_methods", "private_instance_methods", "protected_instance_methods", "instance_variables", "class_variables", "local_variables", "global_variables", "constants", NULL }; const NodeTable *nt = c->nt; const char *nm = nt_kind(nt, v) == NK_CallNode ? nt_str(nt, v, "name") : NULL; if (!nm || nt_ref(nt, v, "block") >= 0) return 0; int hit = 0; for (int k = 0; names[k] && !hit; k++) hit = sp_streq(nm, names[k]); if (!hit || an_user_recv_defines_method(c, nm) || an_prog_names_method(nm)) return 0; NT_FOREACH_KIND(nt, NK_DefNode, d) { const char *dn = nt_str(nt, d, "name"); if (dn && sp_streq(dn, nm)) return 0; } return 1; } static int sid_collection_ok(Compiler *c, int v, int set_cid) { const NodeTable *nt = c->nt; switch (vp_one(nt, v, VPS_NONE | VP_PARENS_ONLY, &v)) { case VP1_NODE: break; case VP1_NIL: return 1; /* nil, which brings no member */ case VP1_MANY: return 0; } /* an Array's to_a holds its elements (`%i[a b].to_set` is `Set.new(%i[a b].to_a)` here): judged by the Array's, unless the program gives Array a to_a of its own (`class Array; def to_a = ...`, or through a module it prepends), which may answer anything */ int arr_cid = comp_core_class(c, CORE_ARRAY); int own_to_a = arr_cid >= 0 && comp_method_in_chain(c, arr_cid, "to_a", NULL) >= 0; for (int d = 0; d < 8 && !own_to_a && nt_kind(nt, v) == NK_CallNode && nt_str(nt, v, "name") && sp_streq(nt_str(nt, v, "name"), "to_a") && nt_ref(nt, v, "arguments") < 0 && nt_ref(nt, v, "block") < 0; d++) { int r = nt_ref(nt, v, "receiver"); if (r < 0 || !ty_is_array(infer_type(c, r))) break; if (vp_one(nt, r, VPS_NONE | VP_PARENS_ONLY, &r) != VP1_NODE) break; v = r; } TyKind t = infer_type(c, v); if (sid_no_collection_kind(t)) return 1; if (ty_is_object(t)) return ty_object_class(t) == set_cid || is_descendant(c, ty_object_class(t), set_cid); if (ty_is_array(t)) { TyKind et = ty_array_elem(t); if (et != TY_POLY && et != TY_UNKNOWN && sid_kind_ok(c, et)) return 1; if (sid_symbol_list_call(c, v)) return 1; /* `a + b` of two Arrays holds the elements of both, unless the program gives Array a `+` of its own */ if (nt_kind(nt, v) == NK_CallNode && nt_str(nt, v, "name") && sp_streq(nt_str(nt, v, "name"), "+") && nt_ref(nt, v, "block") < 0 && !(arr_cid >= 0 && comp_method_in_chain(c, arr_cid, "+", NULL) >= 0)) { int r = nt_ref(nt, v, "receiver"), an = 0; const int *av = nt_arr(nt, nt_ref(nt, v, "arguments"), "arguments", &an); if (r >= 0 && an == 1 && nt_kind(nt, av[0]) != NK_SplatNode && ty_is_array(infer_type(c, r)) && ty_is_array(infer_type(c, av[0]))) return sid_collection_ok(c, r, set_cid) && sid_collection_ok(c, av[0], set_cid); } if (nt_kind(nt, v) == NK_ArrayNode) { int en = 0; const int *el = nt_arr(nt, v, "elements", &en); for (int e = 0; e < en; e++) if (!sid_value_ok(c, el[e], 0)) return 0; return 1; } /* boxed or String elements: what reaches the containers it holds */ return (et == TY_POLY || et == TY_UNKNOWN || et == TY_STRING || et == TY_STRBUF) && sid_kinds_ok(cls_fact_contents_kinds(c, v)); } /* a boxed Array or Set: one with no object of another class of the program's (a Set's members were judged as they entered it) */ if (t == TY_POLY) { const ClsSet *s = cls_fact_node(c, v); if (!s) return 0; for (int k = cls_set_next(s, -1); k >= 0; k = cls_set_next(s, k)) if (k < c->nclasses && k != set_cid && !is_descendant(c, k, set_cid)) return 0; return sid_kinds_ok(cls_fact_kinds(c, v)) && sid_kinds_ok(cls_fact_contents_kinds(c, v)); } return 0; } /* the value a block answers: its last statement. With `own`, the block is handed the Set's own members, and a parameter it answers back is one. */ static int sid_block_ok(Compiler *c, int blk, int own) { const NodeTable *nt = c->nt; if (blk < 0) return 1; if (nt_kind(nt, blk) != NK_BlockNode) return 0; int body = nt_ref(nt, blk, "body"); int bn = 0; const int *bb = body >= 0 ? nt_arr(nt, body, "body", &bn) : NULL; if (bn == 0) return 1; int v = bb[bn - 1], one; if (vp_one(nt, v, VPS_NONE | VP_PARENS_ONLY, &one) == VP1_NODE) v = one; if (own && nt_kind(nt, v) == NK_LocalVariableReadNode && nt_str(nt, v, "name")) for (int k = 0; block_param_name(c, blk, k); k++) if (sp_streq(block_param_name(c, blk, k), nt_str(nt, v, "name"))) return 1; return sid_value_ok(c, v, 0); } /* ---- does an iterator drop its block's value? ---- A call of the each family (each, each_with_index, times, ...) answers its receiver or nil, whatever its block answers: a builtin's does, and so does each method of the program of that name whose every yield sits where its value is dropped and whose block parameter, if any, is only handed on (`&blk`) to such an iterator. A method asked again while it is being asked (a cycle) drops it unless something else keeps it. */ static int iter_disc_call(Compiler *c, int call, int depth); /* Does every yield of the subtree at n sit where its value is dropped (drop: whether n's own value is)? A loop's body is dropped. */ static int iter_disc_yields(Compiler *c, int n, int drop, int depth) { const NodeTable *nt = c->nt; if (n < 0) return 1; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode) return 1; /* a lambda's yield is the method's block's, and the lambda may answer what it got: its body's value is taken as kept */ if (k == NK_LambdaNode) return iter_disc_yields(c, nt_ref(nt, n, "body"), 0, depth); if (k == NK_YieldNode) return drop && iter_disc_yields(c, nt_ref(nt, n, "arguments"), 0, depth); if (k == NK_StatementsNode) { int bn = 0; const int *bb = nt_arr(nt, n, "body", &bn); for (int i = 0; i < bn; i++) if (!iter_disc_yields(c, bb[i], i + 1 < bn ? 1 : drop, depth)) return 0; return 1; } if (k == NK_WhileNode || k == NK_UntilNode) return iter_disc_yields(c, nt_ref(nt, n, "predicate"), 0, depth) && iter_disc_yields(c, nt_ref(nt, n, "statements"), 1, depth); if (k == NK_IfNode || k == NK_UnlessNode) return iter_disc_yields(c, nt_ref(nt, n, "predicate"), 0, depth) && iter_disc_yields(c, nt_ref(nt, n, "statements"), drop, depth) && iter_disc_yields(c, nt_ref(nt, n, k == NK_IfNode ? "subsequent" : "else_clause"), drop, depth); if (k == NK_ElseNode) return iter_disc_yields(c, nt_ref(nt, n, "statements"), drop, depth); if (k == NK_ParenthesesNode) return iter_disc_yields(c, nt_ref(nt, n, "body"), drop, depth); int bk = k == NK_CallNode ? nt_ref(nt, n, "block") : -1; if (bk >= 0 && nt_kind(nt, bk) != NK_BlockNode) bk = -1; for (int i = 0; i < nt_num_refs(nt, n); i++) { int r = nt_ref_at(nt, n, i); if (r != bk && !iter_disc_yields(c, r, 0, depth)) return 0; } for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (!iter_disc_yields(c, av[j], 0, depth)) return 0; } return bk < 0 || iter_disc_yields(c, nt_ref(nt, bk, "body"), iter_disc_call(c, n, depth + 1), depth); } /* Is every use of block parameter bp in the subtree at n a `&bp` handed to a call whose iterator drops its block's value? */ static int iter_disc_blk_fwd(Compiler *c, int n, const char *bp, int depth) { const NodeTable *nt = c->nt; if (n < 0) return 1; NodeKind k = nt_kind(nt, n); if (k == NK_DefNode) return 1; if (k == NK_LocalVariableReadNode && sp_streq(nt_str(nt, n, "name"), bp)) return 0; int skip = -1; if (k == NK_CallNode) { int b = nt_ref(nt, n, "block"); int e = b >= 0 && nt_kind(nt, b) == NK_BlockArgumentNode ? nt_ref(nt, b, "expression") : -1; if (e >= 0 && nt_kind(nt, e) == NK_LocalVariableReadNode && sp_streq(nt_str(nt, e, "name"), bp)) { if (!iter_disc_call(c, n, depth + 1)) return 0; skip = b; } } for (int i = 0; i < nt_num_refs(nt, n); i++) { int r = nt_ref_at(nt, n, i); if (r != skip && !iter_disc_blk_fwd(c, r, bp, depth)) return 0; } for (int i = 0; i < nt_num_arrs(nt, n); i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (!iter_disc_blk_fwd(c, av[j], bp, depth)) return 0; } return 1; } /* Is t the type of a value of a kind the analysis knows that is no Enumerator: an Array, a Hash, a Range, an object of a class of the program's? -1 for a builtin's, its class for an object, -3 for none. */ static int iter_disc_known_kind(TyKind t) { if (ty_is_object(t)) return ty_object_class(t); if (ty_is_array(t) || ty_is_hash(t) || t == TY_RANGE || t == TY_FLOAT_RANGE || t == TY_STR_RANGE) return -1; return -3; } /* The methods receiverless call n in an instance method can run: on each class made whose instances run that method, the one its chain has (a class with none raises NoMethodError); -1 where method_missing may answer instead, or they are more than cap. */ static int iter_disc_self_targets(Compiler *c, int n, int *out, int cap) { Scope *cur = comp_scope_of(c, n); int ci = cur ? (int)(cur - c->scopes) : -1; const char *nm = nt_str(c->nt, n, "name"); if (ci <= 0 || !nm || cur->is_cmethod || cur->class_id < 0 || !cur->name || cur->def_node < 0) return -1; int k = 0; for (int cid = 0; cid < c->nclasses; cid++) { if (!c->classes[cid].instantiated && !c->classes[cid].is_builtin_const) continue; if (comp_method_in_chain(c, cid, cur->name, NULL) != ci) continue; int mi = call_target(c, n, cid, nm, 0, NULL); /* (a class with none raises NoMethodError, unless method_missing answers) */ if (mi < 0) { if (comp_method_in_chain(c, cid, "method_missing", NULL) >= 0) return -1; continue; } int seen = 0; for (int i = 0; i < k && !seen; i++) seen = out[i] == mi; if (seen) continue; if (k >= cap) return -1; out[k++] = mi; } return k; } /* A boxed receiver r that is a call (no block) of methods of the program only -- the targets the plan lists, or a receiverless call's on each class made that runs the method it is in -- each answering a value of a kind the analysis knows that is no Enumerator (iter_disc_known_kind): the class iter_disc_call asks the methods of (-1 for a builtin's, -2 for any, where the answers are of more than one class), or -3. */ static int iter_disc_call_recv(Compiler *c, int r) { const NodeTable *nt = c->nt; r = nt_paren_single(nt, r); if (r < 0 || nt_kind(nt, r) != NK_CallNode || nt_ref(nt, r, "block") >= 0) return -3; const char *nm = nt_str(nt, r, "name"); /* (no builtin answers it: on self, none of Object's or Kernel's; on a receiver, one of a class of the program's) */ int rr = nt_ref(nt, r, "receiver"); if (!nm || (rr < 0 ? object_public_method_name(nm) || bop_share_named(BOP_KERNEL, nm) : !ty_is_object(infer_type(c, rr)))) return -3; int *tg = malloc(sizeof(int) * (size_t)(c->nscopes + 1)); if (!tg) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int n = cplan_targets(c, r, tg, CPT_MAX); if (n == CPT_UNKNOWN && nt_ref(nt, r, "receiver") < 0) n = iter_disc_self_targets(c, r, tg, c->nscopes + 1); int cls = n > 0 ? -4 : -3; for (int i = 0; i < n && cls != -3; i++) { int k = iter_disc_known_kind((TyKind)c->scopes[tg[i]].ret); if (k == -3) cls = -3; else if (cls == -4) cls = k; else if (cls != k) cls = -2; } free(tg); return cls; } static int iter_disc_call(Compiler *c, int call, int depth) { static const char *const names[] = { "each", "each_with_index", "each_with_object", "each_char", "each_byte", "each_line", "each_key", "each_value", "each_pair", "reverse_each", "each_slice", "each_cons", "each_entry", "times", "upto", "downto", "step", NULL }; const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); int k = 0; while (nm && names[k] && !sp_streq(names[k], nm)) k++; if (!nm || !names[k] || depth > 16) return 0; /* the receiver has to be no Enumerator, whose each answers what its method does with the block's values (`a.map.each { }`): one of a kind the analysis knows, or self. A boxed receiver may be one. */ int r = nt_ref(nt, call, "receiver"); /* the methods of that name the call can reach: on a value of a builtin kind (an Array, a Hash, a Range) no instance method of a class of the program's; on an object of the program's, none of a class off its class's line. A builtin class's (reopened), a module's and a class method (whose receiver is a class) are reachable from either; on self, any is */ int rcls = -2; if (r >= 0) { TyKind rt = infer_type(c, r); rcls = ty_is_object(rt) ? ty_object_class(rt) : -1; /* (a boxed receiver that is a call of the program's methods answering each a known kind, no Enumerator: iter_disc_call_recv) */ if (rt == TY_POLY || rt == TY_UNKNOWN) rcls = iter_disc_call_recv(c, r); else if (rt == TY_ENUMERATOR) return 0; if (rcls == -3) return 0; } static int busy[64], nbusy; for (int mi = 0; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; if (!m->name || m->def_node < 0 || !sp_streq(m->name, nm)) continue; if (rcls != -2 && m->class_id >= 0 && !m->is_cmethod) { ClassInfo *mc = &c->classes[m->class_id]; int open = !mc->name || comp_class_is_module(c, mc) || is_builtin_class_name(mc->name) || is_object_root(mc->name); if (!open && (rcls < 0 || (m->class_id != rcls && !is_descendant(c, rcls, m->class_id) && !is_descendant(c, m->class_id, rcls)))) continue; } int inb = 0; for (int q = 0; q < nbusy; q++) inb |= busy[q] == mi; if (inb) continue; if (nbusy >= 64 || nt_kind(nt, m->def_node) != NK_DefNode) return 0; busy[nbusy++] = mi; int ok = 1; /* a `...` forwarder hands its block on unseen (to a super that may yield it): kept */ int prm = nt_ref(nt, m->def_node, "parameters"); int kwr = prm >= 0 ? nt_ref(nt, prm, "keyword_rest") : -1; if (kwr >= 0 && nt_kind(nt, kwr) == NK_ForwardingParameterNode) ok = 0; if (ok && m->blk_param) { int bpn = prm >= 0 ? nt_ref(nt, prm, "block") : -1; const char *bp = bpn >= 0 ? nt_str(nt, bpn, "name") : NULL; ok = bp && iter_disc_blk_fwd(c, nt_ref(nt, m->def_node, "body"), bp, depth); } ok = ok && iter_disc_yields(c, nt_ref(nt, m->def_node, "body"), 0, depth); nbusy--; if (!ok) return 0; } return 1; } /* analyze.h */ int an_iter_discards_block(Compiler *c, int call) { return call >= 0 && nt_kind(c->nt, call) == NK_CallNode && iter_disc_call(c, call, 0); } /* ---- where the identity Sets live ---- A compare_by_identity is followed to the Set it makes an identity one when that Set lives in a place the program reads it from only as a Set method's receiver: an instance variable (`@seen = Set.new .compare_by_identity`, `@seen ||= ...`), or a local of one scope, assigned only Sets made there, and read through the readers that answer it (`def seen = @seen ||= ...`). The identity check then looks at the calls on those reads alone. Anything else -- a Set handed on, a variable assigned another Set, a reader's answer kept, a call answering the Set or a new identity one (`|`, `dup`) used as a value -- returns to the whole program's check, which refuses more, never less. */ #define SID_CAP 64 typedef struct { const char *ivars[SID_CAP]; int nivars; const char *locals[SID_CAP]; const Scope *lscopes[SID_CAP]; int nlocals; const char *readers[SID_CAP]; int nreaders; int *parent; } SidSources; static int sid_name_in(const char *const *names, int n, const char *nm) { for (int i = 0; i < n; i++) if (sp_streq(names[i], nm)) return 1; return 0; } static int sid_add_name(const char **names, int *n, const char *nm) { if (!nm) return 0; if (sid_name_in(names, *n, nm)) return 1; if (*n >= SID_CAP) return 0; names[(*n)++] = nm; return 1; } /* Is v a Set the expression makes: `Set.new(...)`, `Set[...]`, or one of them made an identity Set on the spot? */ static int sid_fresh_set(const NodeTable *nt, int v) { v = nt_paren_single(nt, v); if (v < 0 || nt_kind(nt, v) != NK_CallNode) return 0; const char *nm = nt_str(nt, v, "name"); int r = nt_paren_single(nt, nt_ref(nt, v, "receiver")); if (is_compare_by_identity_name(nm) && nt_ref(nt, v, "arguments") < 0) return sid_fresh_set(nt, r); if (!nm || (!sp_streq(nm, "new") && !sp_streq(nm, "[]"))) return 0; return r >= 0 && nt_kind(nt, r) == NK_ConstantReadNode && nt_str(nt, r, "name") && sp_streq(nt_str(nt, r, "name"), "Set"); } /* The edges sid_tail_owner climbs: the arms of an if, unless, case and case/in, and a begin's body (beside an else too), its rescue clauses and its else; not its ensure. */ enum { SID_CLIMB = VPS_IF | VPS_UNLESS | VPS_CASE | VPS_CASE_IN | VPS_BEGIN | VP_BEGIN_BOTH }; /* The method or block whose value node n is, or -1: n is the last statement of its body, or of an arm of a conditional, a case or a begin that is in turn (vp_climb, over SID_CLIMB). -2 when the climb meets a statement that is not the last of its statements: its value is dropped there (`if a then x.m end; more`). */ static int sid_tail_owner(Compiler *c, const int *parent, int n) { const NodeTable *nt = c->nt; VpUp up; switch (vp_climb(nt, parent, n, SID_CLIMB, &up)) { case VPU_DROPPED: return -2; case VPU_ROOT: { /* a body no node links (one a rewrite moved): its method's, by its scope */ Scope *s = comp_scope_of(c, up.node); return s && s->body == up.node && s->def_node >= 0 && nt_kind(nt, s->def_node) == NK_DefNode ? s->def_node : -1; } case VPU_ENDED: { NodeKind pk = nt_kind(nt, up.parent); return (pk == NK_DefNode || pk == NK_BlockNode) && nt_ref(nt, up.parent, "body") == up.node ? up.parent : -1; } default: return -1; } } /* Is a Set's call named nm one that answers the Set or another Set that may compare by identity? */ static int sid_answers_set(const char *nm) { static const char *const names[] = { "add", "<<", "add?", "delete", "delete?", "merge", "subtract", "replace", "|", "union", "+", "-", "difference", "^", "dup", "clone", "compare_by_identity", "map!", "collect!", "select!", "filter!", "reject!", "keep_if", "delete_if", "freeze", "to_set", "reset", "flatten", "flatten!", "itself", "tap", "then", "yield_self", NULL }; for (int k = 0; names[k]; k++) if (sp_streq(nm, names[k])) return 1; return 0; } /* Does a call named nm hand its receiver on to a block or as a value no Set call is: a block parameter (`tap`, `then`), the block's self (`instance_eval`), a computed call or a Method? */ static int sid_hands_on_self(const char *nm) { static const char *const names[] = { "tap", "then", "yield_self", "instance_eval", "instance_exec", "send", "__send__", "public_send", "method", "public_method", "singleton_method", "define_singleton_method", "extend", "to_enum", "enum_for", NULL }; for (int k = 0; names[k]; k++) if (sp_streq(nm, names[k])) return 1; return 0; } /* A use of alias node a: as a Set call's receiver whose answer is not one of the Set's (or is dropped), or as the value of a method, which is then a reader whose calls are aliases too. 0 for any other use. */ static int sid_alias_use_ok(Compiler *c, SidSources *S, int a) { const NodeTable *nt = c->nt; for (int d = 0; d < 16; d++) { int p = S->parent[a]; while (p >= 0 && nt_kind(nt, p) == NK_ParenthesesNode) { a = p; p = S->parent[a]; } if (p < 0) return 0; if (nt_kind(nt, p) == NK_CallNode && nt_paren_single(nt, nt_ref(nt, p, "receiver")) == nt_paren_single(nt, a)) { const char *nm = nt_str(nt, p, "name"); if (!nm || sid_hands_on_self(nm)) return 0; if (!sid_answers_set(nm) || comp_value_dropped(c, p)) return 1; /* the call answers the Set or a copy that may compare by identity (`@seen.add(n)`): its value is an alias in turn */ a = p; continue; } int def = sid_tail_owner(c, S->parent, a); if (def == -2) return 1; /* a block's value, which an iterator of the each family drops */ if (def >= 0 && nt_kind(nt, def) == NK_BlockNode) { int call = S->parent[def]; return (call >= 0 && nt_kind(nt, call) == NK_CallNode && nt_ref(nt, call, "block") == def && an_iter_discards_block(c, call)) || comp_value_dropped(c, a); } if (def < 0) return comp_value_dropped(c, a); const char *dn = nt_str(nt, def, "name"); if (!dn) return 0; if (sp_streq(dn, "initialize")) return 1; return sid_add_name(S->readers, &S->nreaders, dn); } return 0; } static int sid_maybe_set(Compiler *c, int v, int set_cid) { TyKind t = infer_type(c, v); return (ty_is_object(t) && (ty_object_class(t) == set_cid || is_descendant(c, ty_object_class(t), set_cid))) || (t == TY_POLY && !cls_fact_excludes(c, v, set_cid)); } /* Does reflective call id (`instance_variable_get(name)`, `instance_variable_set(name, v)`) reach a place S holds an identity Set in: by a literal naming one, or by a name no literal spells (a list of names, `instance_variables`)? 1 when it may, 0 when it names another instance variable or is no such call, -1 for one of another shape. */ static int sid_reflect_reaches_place(const NodeTable *nt, const SidSources *S, int id, int *get) { const char *nm = nt_kind(nt, id) == NK_CallNode ? nt_str(nt, id, "name") : NULL; if (!nm || S->nivars == 0) return 0; int g = sp_streq(nm, "instance_variable_get"); if (!g && !sp_streq(nm, "instance_variable_set")) return 0; if (get) *get = g; int ar = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = ar >= 0 ? nt_arr(nt, ar, "arguments", &argc) : NULL; if (argc != (g ? 1 : 2) || nt_ref(nt, id, "block") >= 0) return -1; int a0 = nt_paren_single(nt, argv[0]); NodeKind ak = nt_kind(nt, a0); if (ak == NK_SplatNode || ak == NK_KeywordHashNode || ak == NK_BlockArgumentNode) return -1; if (g == 0 && nt_kind(nt, nt_paren_single(nt, argv[1])) == NK_SplatNode) return -1; const char *iv = ak == NK_SymbolNode ? nt_str(nt, a0, "value") : ak == NK_StringNode ? nt_str(nt, a0, "content") : NULL; return !iv || sid_name_in(S->ivars, S->nivars, iv); } /* Is node v a read of a place S holds an identity Set in? */ static int sid_is_alias(Compiler *c, SidSources *S, int v) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0) return 0; NodeKind k = nt_kind(nt, v); const char *nm = nt_str(nt, v, "name"); if (!nm) return 0; if (k == NK_InstanceVariableReadNode) return sid_name_in(S->ivars, S->nivars, nm); if (k == NK_LocalVariableReadNode) { const Scope *s = comp_scope_of(c, v); for (int i = 0; i < S->nlocals; i++) if (S->lscopes[i] == s && sp_streq(S->locals[i], nm)) return 1; return 0; } if (k != NK_CallNode) return 0; /* a reader's call, a block it ignores or not (`seen { }`) */ if (sid_name_in(S->readers, S->nreaders, nm)) return 1; /* a reflective read that may answer a place's Set */ int get = 0; if (sid_reflect_reaches_place(nt, S, v, &get) == 1 && get) return 1; /* a call on an alias that answers the Set or a copy that compares the same way (`@seen.add(n)`, `@seen | other`, `@seen.dup`) */ int r = nt_ref(nt, v, "receiver"); return r >= 0 && sid_answers_set(nm) && sid_is_alias(c, S, r); } /* Is Set allocation a (`Set.new(...)`, `Set[...]`) one an identity Set starts from: the receiver of a compare_by_identity, or a value written to a place S holds one in, by an assignment or by reflection? */ static int sid_alloc_is_source(Compiler *c, SidSources *S, int a) { const NodeTable *nt = c->nt; int n = a, p = S->parent[n]; while (p >= 0 && nt_kind(nt, p) == NK_ParenthesesNode) { n = p; p = S->parent[n]; } if (p < 0) return 0; NodeKind pk = nt_kind(nt, p); const char *pn = nt_str(nt, p, "name"); if (pk == NK_CallNode && is_compare_by_identity_name(pn) && nt_paren_single(nt, nt_ref(nt, p, "receiver")) == a) return 1; if ((pk == NK_InstanceVariableWriteNode || pk == NK_InstanceVariableOrWriteNode) && pn) return sid_name_in(S->ivars, S->nivars, pn); if (pk == NK_LocalVariableWriteNode && pn) { const Scope *s = comp_scope_of(c, p); for (int i = 0; i < S->nlocals; i++) if (S->lscopes[i] == s && sp_streq(S->locals[i], pn)) return 1; return 0; } /* the value of an instance_variable_set that reaches a place, which sid_sources follows as such a write: through parentheses, their last statement's (`instance_variable_set(:@s, (x += 1; Set[a]))`) */ VpUp up; if (vp_climb(nt, S->parent, a, VPS_NONE | VP_PARENS_ONLY, &up) != VPU_ENDED) return 0; int q = S->parent[up.parent], get = 0, argc = 0; if (q < 0 || nt_ref(nt, q, "arguments") != up.parent || sid_reflect_reaches_place(nt, S, q, &get) != 1 || get) return 0; const int *argv = nt_arr(nt, up.parent, "arguments", &argc); return argc == 2 && argv[1] == up.node; } /* Does the program name each of S's readers only where it declares it (`attr_reader :seen`): no other Symbol or String of its name, no alias of it, and no method named by a name the program computes? And each place's ivar only where it reads or writes it by reflection? */ /* Does the program define a method named nm itself (a def, or a name a define_method or the like may be handed), so that a call of that name may run the program's method? */ static int sid_prog_defines(const NodeTable *nt, const char *nm) { if (an_prog_names_method(nm)) return 1; NT_FOREACH_KIND(nt, NK_DefNode, d) { const char *dn = nt_str(nt, d, "name"); if (dn && sp_streq(dn, nm)) return 1; } return 0; } /* Is literal lit the new name of an alias (`alias take twice`, `alias_method :take, :twice`), whose parent is up and grandparent call? That defines another method of the name, and reaches no reader under another one: the reader's own calls are followed by its name, a call of the alias among them. `alias_method` counts as Module's own only as a statement of a class or module body, in a program that defines no alias_method or method_missing of its own (which could do anything with the name, `send(new_name)`); `alias` is a keyword no method replaces. */ static int sid_alias_new_name(const NodeTable *nt, const int *parent, int up, int call, int lit, int *own_am) { if (up >= 0 && nt_kind(nt, up) == NK_AliasMethodNode) return nt_ref(nt, up, "new_name") == lit; if (call < 0 || nt_kind(nt, call) != NK_CallNode || nt_ref(nt, call, "arguments") != up) return 0; const char *cn = nt_str(nt, call, "name"); if (!cn || !sp_streq(cn, "alias_method") || nt_ref(nt, call, "receiver") >= 0 || nt_ref(nt, call, "block") >= 0) return 0; int an = 0; const int *av = nt_arr(nt, up, "arguments", &an); if (an != 2 || av[0] != lit) return 0; int st = parent[call], body = st >= 0 ? parent[st] : -1; if (st < 0 || nt_kind(nt, st) != NK_StatementsNode || body < 0 || (nt_kind(nt, body) != NK_ClassNode && nt_kind(nt, body) != NK_ModuleNode)) return 0; if (*own_am < 0) *own_am = sid_prog_defines(nt, "alias_method") || sid_prog_defines(nt, "method_missing"); return !*own_am; } static int sid_name_only_member(const NodeTable *nt, const int *parent, int lit); /* Is literal lit only a key of a constant's Hash literal (`K = { take: [1, 2] }.freeze`) that the program reads only to look a key up (`K[k]`, `K.fetch(k)`, `K.key?(k)`)? Such a read answers a value or a truth, never a key, so the name reaches no call. A read of K that does anything else (`K.each { |k, v| send(k) }`, `K.keys`), K handed on, or named by a literal (`const_get(:K)`) may hand the key on, and counts. */ static int sid_name_only_key(const NodeTable *nt, const int *parent, int lit) { int as = parent[lit], hn = as >= 0 ? parent[as] : -1; if (as < 0 || nt_kind(nt, as) != NK_AssocNode || nt_ref(nt, as, "key") != lit) return 0; if (hn < 0 || nt_kind(nt, hn) != NK_HashNode) return 0; int u = hn; for (int d = 0; d < 8; d++) { int q = parent[u]; while (q >= 0 && nt_kind(nt, q) == NK_ParenthesesNode) { u = q; q = parent[u]; } if (q < 0) return 0; if (nt_kind(nt, q) == NK_CallNode && nt_paren_single(nt, nt_ref(nt, q, "receiver")) == u) { const char *m = nt_str(nt, q, "name"); if (!m || !sp_streq(m, "freeze") || nt_ref(nt, q, "arguments") >= 0 || nt_ref(nt, q, "block") >= 0) return 0; u = q; continue; } if (nt_kind(nt, q) != NK_ConstantWriteNode || nt_ref(nt, q, "value") != u) return 0; const char *kn = nt_str(nt, q, "name"); if (!kn) return 0; for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, h ? NK_StringNode : NK_SymbolNode, l) { const char *v = nt_str(nt, l, h ? "content" : "value"); if (v && sp_streq(v, kn)) return 0; } for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, h ? NK_ConstantPathNode : NK_ConstantReadNode, r) { const char *rn = nt_str(nt, r, "name"); if (!rn || !sp_streq(rn, kn)) continue; int w = r, rp = parent[w]; while (rp >= 0 && nt_kind(nt, rp) == NK_ParenthesesNode) { w = rp; rp = parent[w]; } /* the leaf of a longer path (`K::X`) reads no Hash */ if (rp >= 0 && nt_kind(nt, rp) == NK_ConstantPathNode && nt_ref(nt, rp, "parent") == w) return 0; if (rp < 0 || nt_kind(nt, rp) != NK_CallNode || nt_paren_single(nt, nt_ref(nt, rp, "receiver")) != r) return 0; const char *m = nt_str(nt, rp, "name"); if (!m || !(sp_streq(m, "[]") || sp_streq(m, "fetch") || sp_streq(m, "key?") || sp_streq(m, "has_key?") || sp_streq(m, "include?") || sp_streq(m, "member?")) || nt_ref(nt, rp, "block") >= 0) return 0; } return 1; } return 0; } static int sid_readers_only_declared(Compiler *c, const SidSources *S) { const NodeTable *nt = c->nt; int own_am = -1; /* the program's own alias_method or method_missing, asked once */ for (int i = 0; i < S->nreaders; i++) { const char *r = S->readers[i]; if (!an_prog_names_method(r)) continue; if (an_prog_meta_by_value()) return 0; for (int k = 0; k < c->nclasses; k++) for (int j = 0; j < c->classes[k].naliases; j++) if (sp_streq(c->classes[k].alias_old[j], r)) return 0; for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, h ? NK_StringNode : NK_SymbolNode, lit) { if (!sp_streq(nt_str(nt, lit, h ? "content" : "value"), r)) continue; int args = S->parent[lit], call = args >= 0 ? S->parent[args] : -1; if (sid_alias_new_name(nt, S->parent, args, call, lit, &own_am)) continue; /* a name only listed in a constant the program asks membership of, or only a key of a constant's Hash it looks keys up in, reaches no call */ if (sid_name_only_member(nt, S->parent, lit) || sid_name_only_key(nt, S->parent, lit)) continue; if (h || call < 0 || nt_ref(nt, call, "arguments") != args || !attr_decl_call(c, call)) return 0; } } /* a place's ivar named by a Symbol or a String reaches it unseen (`def_delegator :@seen, :add, :put`), unless it is a reflective read or write, followed as one (sid_reflect_reaches_place) */ for (int i = 0; i < S->nivars; i++) for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, h ? NK_StringNode : NK_SymbolNode, lit) { if (!sp_streq(nt_str(nt, lit, h ? "content" : "value"), S->ivars[i])) continue; int args = S->parent[lit], call = args >= 0 ? S->parent[args] : -1; if (call < 0 || nt_ref(nt, call, "arguments") != args || sid_reflect_reaches_place(nt, S, call, NULL) != 1) return 0; } return 1; } /* Follow the compare_by_identity calls (asked) to their places. 0 when one of them, or a read of a place, cannot be followed. */ static int sid_sources(Compiler *c, SidSources *S, int set_cid, int spelled) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!is_compare_by_identity_name(nm) || nt_ref(nt, id, "arguments") >= 0) continue; Scope *s = comp_scope_of(c, id); if (s && s->class_id == set_cid) continue; /* (a computed name's arm, as refuse_set_identity_members skips it) */ if (!spelled && (nt_int(nt, id, "dyn_arm", 0) || (s && s->name && strncmp(s->name, "__bam_", 6) == 0))) continue; /* a receiver in parentheses holding several statements or none counts as none: following its last statement would refuse programs whose identity Sets answer as CRuby's */ int r = nt_paren_single(nt, nt_ref(nt, id, "receiver")); if (r < 0 || nt_kind(nt, r) == NK_ParenthesesNode) { if (s && !s->is_cmethod && s->class_id >= 0 && is_descendant(c, s->class_id, set_cid)) return 0; continue; } if (!sid_maybe_set(c, r, set_cid)) continue; NodeKind rk = nt_kind(nt, r); int w = id; while (S->parent[w] >= 0 && nt_kind(nt, S->parent[w]) == NK_ParenthesesNode) w = S->parent[w]; int p = S->parent[w]; NodeKind pk = p >= 0 ? nt_kind(nt, p) : NK_NONE; const char *place = NULL; int local = 0; if (rk == NK_InstanceVariableReadNode) place = nt_str(nt, r, "name"); else if (rk == NK_LocalVariableReadNode) { place = nt_str(nt, r, "name"); local = 1; } else if (sid_fresh_set(nt, r) && (pk == NK_InstanceVariableWriteNode || pk == NK_InstanceVariableOrWriteNode) && nt_paren_single(nt, nt_ref(nt, p, "value")) == nt_paren_single(nt, w)) place = nt_str(nt, p, "name"); else if (sid_fresh_set(nt, r) && pk == NK_LocalVariableWriteNode && nt_paren_single(nt, nt_ref(nt, p, "value")) == nt_paren_single(nt, w)) { place = nt_str(nt, p, "name"); local = 1; } else return 0; if (!place) return 0; if (local) { /* a parameter holds the caller's Set, which other names see */ LocalVar *lv = s ? scope_local(s, place) : NULL; if (!lv || lv->is_param || lv->is_block_param) return 0; int known = 0; for (int i = 0; i < S->nlocals; i++) if (S->lscopes[i] == s && sp_streq(S->locals[i], place)) known = 1; if (!known) { if (S->nlocals >= SID_CAP) return 0; S->locals[S->nlocals] = place; S->lscopes[S->nlocals++] = s; } } else if (!sid_add_name(S->ivars, &S->nivars, place)) return 0; /* the value of the call itself, where it is no place's write: kept, it is an alias of its own */ if (rk == NK_InstanceVariableReadNode || rk == NK_LocalVariableReadNode) if (!comp_value_dropped(c, w) && !sid_alias_use_ok(c, S, r)) return 0; } /* a reflective read or write reaches an instance variable by a name: one of the places', or one no literal names. A read answers the Set where it may be a place's, and is followed as a read of a place below (sid_is_alias): `instance_variables.map { |v| instance_variable_get(v) .inspect }` uses it as no Set. A write is followed as a write of the place is below: a Set made there (`Set.new`, `Set[...]`) or no Set, whose own value, the Set, is used as an alias may be; one whose value may be another Set is not followed. */ if (S->nivars > 0) { NT_FOREACH_KIND(nt, NK_CallNode, id) { int get = 0, r = sid_reflect_reaches_place(nt, S, id, &get); if (r < 0) return 0; if (r == 0 || get) continue; int ar = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = nt_arr(nt, ar, "arguments", &argc); int v; switch (vp_one(nt, argv[1], VPS_NONE | VP_PARENS_ONLY, &v)) { case VP1_NODE: break; case VP1_NIL: continue; /* `()` writes nil, no Set */ case VP1_MANY: return 0; } if (!sid_fresh_set(nt, v) && sid_maybe_set(c, v, set_cid)) return 0; if (!sid_alias_use_ok(c, S, id)) return 0; } } /* every write of a place holds a Set made there (or no Set) */ for (int k = 0; k < 3; k++) { NodeKind wk = k == 0 ? NK_InstanceVariableWriteNode : k == 1 ? NK_InstanceVariableOrWriteNode : NK_LocalVariableWriteNode; NT_FOREACH_KIND(nt, wk, w) { const char *nm = nt_str(nt, w, "name"); int hit = 0; if (k < 2) hit = nm && sid_name_in(S->ivars, S->nivars, nm); else for (int i = 0; nm && i < S->nlocals; i++) hit |= S->lscopes[i] == comp_scope_of(c, w) && sp_streq(S->locals[i], nm); if (!hit) continue; int v = nt_ref(nt, w, "value"); if (!sid_fresh_set(nt, v) && sid_maybe_set(c, v, set_cid)) return 0; /* the write's own value is the Set too */ if (!sid_alias_use_ok(c, S, w)) return 0; } } NodeKind other[] = { NK_InstanceVariableOperatorWriteNode, NK_InstanceVariableAndWriteNode, NK_LocalVariableOperatorWriteNode, NK_LocalVariableAndWriteNode, NK_LocalVariableOrWriteNode, NK_LocalVariableTargetNode, NK_InstanceVariableTargetNode }; for (int k = 0; k < (int)(sizeof other / sizeof other[0]); k++) NT_FOREACH_KIND(nt, other[k], w) { const char *nm = nt_str(nt, w, "name"); if (!nm) continue; if (sid_name_in(S->ivars, S->nivars, nm)) return 0; for (int i = 0; i < S->nlocals; i++) if (sp_streq(S->locals[i], nm) && S->lscopes[i] == comp_scope_of(c, w)) return 0; } /* a read of a place's local from another scope (a define_method's body captures it) is not followed */ for (int k = 0; k < 2 && S->nlocals > 0; k++) NT_FOREACH_KIND(nt, k ? NK_LocalVariableWriteNode : NK_LocalVariableReadNode, a) { const char *nm = nt_str(nt, a, "name"); if (!nm || nt_int(nt, a, "depth", 0) <= 0) continue; const Scope *as = comp_scope_of(c, a); for (int i = 0; i < S->nlocals; i++) if (sp_streq(S->locals[i], nm) && S->lscopes[i] != as) return 0; } /* a place's ivar an attr reader answers (`attr_reader :seen`): its calls are a reader's */ for (int i = 0; i < S->nivars; i++) for (int k = 0; S->ivars[i][0] == '@' && k < c->nclasses; k++) for (int j = 0; j < c->classes[k].nreaders; j++) if (sp_streq(c->classes[k].readers[j], S->ivars[i] + 1) && !sid_add_name(S->readers, &S->nreaders, c->classes[k].readers[j])) return 0; /* (a class-level one too: `class << self; attr_reader :seen; end`) */ for (int i = 0; i < S->nivars; i++) for (int k = 0; S->ivars[i][0] == '@' && k < c->nclasses; k++) if (comp_is_sg_reader(&c->classes[k], S->ivars[i] + 1) && !sid_add_name(S->readers, &S->nreaders, S->ivars[i] + 1)) return 0; /* every read is used as a Set, and so is every call of a reader; a reader found on the way adds its calls, so this runs to a fixpoint */ int before = -1; for (int round = 0; before != S->nreaders && round < 8; round++) { before = S->nreaders; for (int k = 0; k < 3; k++) { NodeKind rk = k == 0 ? NK_InstanceVariableReadNode : k == 1 ? NK_LocalVariableReadNode : NK_CallNode; NT_FOREACH_KIND(nt, rk, a) { /* (a read of the place's name that is no Set is another place's) */ if (!sid_is_alias(c, S, a) || !sid_maybe_set(c, a, set_cid)) continue; if (!sid_alias_use_ok(c, S, a)) return 0; } } } /* (a reader chain longer than the rounds is not followed) */ if (before != S->nreaders) return 0; /* a reader is reached by calls no read above shows where the program names it other than to declare it: an alias (`alias_method :s2, :seen`), a Method (`b.method(:seen).call`) */ return sid_readers_only_declared(c, S); } /* Is v the answer of an operator no class of the program defines -- not Set, whose operators are its own methods, nor any other (`bits >> 32` on a boxed value)? Such an answer is a builtin's, an Integer's or a Float's, and no Set: the builtins answer one only from a Set's own methods. */ static int sid_operator_answer(Compiler *c, int v) { const NodeTable *nt = c->nt; v = nt_paren_single(nt, v); if (v < 0 || nt_kind(nt, v) != NK_CallNode || nt_ref(nt, v, "receiver") < 0) return 0; const char *nm = nt_str(nt, v, "name"); if (!nm || !nm[0] || (nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z') || nm[0] == '_') return 0; if (sp_streq(nm, "[]") || sp_streq(nm, "[]=")) return 0; /* an element read answers any member */ return !an_user_defines_method(c, nm); } /* Is the name literal lit only a member of a constant's list the program asks membership of (`K = %i[clear compare_by_identity].to_set.freeze`, read only as `K.include?(name)`)? No computed call is handed such a name. A read of K that does anything else, or a list written another way, spells it. The list goes through to_a, to_set, freeze or Set.new on its way to K. */ static int sid_name_only_member(const NodeTable *nt, const int *parent, int lit) { int u = parent[lit]; if (u < 0 || nt_kind(nt, u) != NK_ArrayNode) return 0; for (int d = 0; d < 8; d++) { int q = parent[u]; if (q < 0) return 0; NodeKind qk = nt_kind(nt, q); if (qk == NK_CallNode && nt_paren_single(nt, nt_ref(nt, q, "receiver")) == u) { const char *m = nt_str(nt, q, "name"); if (!m || !(sp_streq(m, "to_set") || sp_streq(m, "to_a") || sp_streq(m, "freeze")) || nt_ref(nt, q, "arguments") >= 0 || nt_ref(nt, q, "block") >= 0) return 0; u = q; continue; } /* `Set.new(list)`, as `list.to_set` is lowered */ if (nt_kind(nt, q) == NK_ArgumentsNode) { int an = 0; const int *av = nt_arr(nt, q, "arguments", &an); int sn = parent[q], sr = sn >= 0 ? nt_ref(nt, sn, "receiver") : -1; if (an != 1 || av[0] != u || sn < 0 || nt_kind(nt, sn) != NK_CallNode || nt_ref(nt, sn, "arguments") != q || !nt_str(nt, sn, "name") || !sp_streq(nt_str(nt, sn, "name"), "new") || nt_ref(nt, sn, "block") >= 0 || sr < 0 || nt_kind(nt, sr) != NK_ConstantReadNode || !sp_streq(nt_str(nt, sr, "name"), "Set")) return 0; u = sn; continue; } if (qk != NK_ConstantWriteNode || nt_ref(nt, q, "value") != u) return 0; const char *kn = nt_str(nt, q, "name"); if (!kn) return 0; for (int h = 0; h < 2; h++) NT_FOREACH_KIND(nt, h ? NK_ConstantPathNode : NK_ConstantReadNode, r) { const char *rn = nt_str(nt, r, "name"); const char *leaf = rn ? strrchr(rn, ':') : NULL; leaf = leaf ? leaf + 1 : rn; if (!leaf || !sp_streq(leaf, kn)) continue; int w = r, rp = parent[w]; while (rp >= 0 && nt_kind(nt, rp) == NK_ParenthesesNode) { w = rp; rp = parent[w]; } if (rp < 0 || nt_kind(nt, rp) != NK_CallNode || nt_paren_single(nt, nt_ref(nt, rp, "receiver")) != r) return 0; const char *m = nt_str(nt, rp, "name"); if (!m || !(sp_streq(m, "include?") || sp_streq(m, "member?") || sp_streq(m, "==="))) return 0; } return 1; } return 0; } const char *const g_sid_refusal_msg = "a value whose identity is not CRuby's (a String, an Integer, a Float, a Rational, a Complex, a Range, " "or one of no static kind) enters or is looked up in a Set, in a program that calls " "Set#compare_by_identity: an identity Set would find it otherwise than CRuby's"; /* Is value v, entering an identity Set or looked up in one at call `call`, one whose identity is CRuby's (1)? Under --defer-refusals a value the proof cannot take is instead checked where it arrives (2): the check (sp_idset_check) raises the refusal for a value of a kind whose identity Spinel does not keep, and passes any other, which sp_poly_equal finds as CRuby's equal? does. The value's C form has to be one the check reads: a box, an Integer, a Float or a String. 0 for a refused value. */ static int sid_value_or_check(Compiler *c, int call, int v) { if (sid_value_ok(c, v, 0)) return 1; int one; if (!defer_refusals_at(c, call) || vp_one(c->nt, v, VPS_NONE | VP_PARENS_ONLY, &one) != VP1_NODE) return 0; TyKind t = infer_type(c, one); if (t != TY_POLY && t != TY_INT && t != TY_FLOAT && t != TY_STRING && t != TY_STRBUF) return 0; nt_node_set_int((NodeTable *)c->nt, v, "idset_check", call + 1); g_idset_marks = 1; return 2; } static void refuse_set_identity_members(Compiler *c) { const NodeTable *nt = c->nt; int set_cid = comp_core_class(c, CORE_SET); if (set_cid < 0 || comp_method_in_class(c, set_cid, "compare_by_identity") < 0) return; /* a compare_by_identity that may make a Set an identity one: on a value that may be a Set, or on self in an instance method of a Set's subclass (a Hash's or another class's is no Set's) */ int asked = 0; /* a computed name's dispatch -- a send's or method()'s arm, or a method(name) wrapper (__bam_*) -- is made for every name the program defines (Rack::Headers defines its own compare_by_identity): such a call is no Set's unless the program spells the name, as `x.method(:compare_by_identity)` would -- and a name only listed in a constant the program asks membership of is not handed to any call */ int spelled = 0, *lit_parent = NULL; for (int k = 0; k < 2 && !spelled; k++) NT_FOREACH_KIND(nt, k ? NK_StringNode : NK_SymbolNode, lit) { const char *v = nt_str(nt, lit, k ? "content" : "value"); if (!is_compare_by_identity_name(v)) continue; if (!lit_parent) lit_parent = an_parent_map(nt); if (!lit_parent) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (!sid_name_only_member(nt, lit_parent, lit)) { spelled = 1; break; } } free(lit_parent); NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!is_compare_by_identity_name(nm) || asked || nt_ref(nt, id, "arguments") >= 0) continue; Scope *s = comp_scope_of(c, id); if (s && s->class_id == set_cid) continue; if (!spelled && (nt_int(nt, id, "dyn_arm", 0) || (s && s->name && strncmp(s->name, "__bam_", 6) == 0))) continue; int r = nt_ref(nt, id, "receiver"); if (r < 0) { if (s && !s->is_cmethod && s->class_id >= 0 && is_descendant(c, s->class_id, set_cid)) asked = 1; continue; } TyKind rt = infer_type(c, r); if ((ty_is_object(rt) && (ty_object_class(rt) == set_cid || is_descendant(c, ty_object_class(rt), set_cid))) || (rt == TY_POLY && !cls_fact_excludes(c, r, set_cid))) asked = 1; } if (!asked) return; an_cls_kinds(c); /* where the identity Sets live, when every one can be followed there: then only the calls on them are checked (sid_sources) */ SidSources S; memset(&S, 0, sizeof S); S.parent = an_parent_map(nt); if (!S.parent) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } int follow = sid_sources(c, &S, set_cid, spelled); static const char *const elem_names[] = { "add", "<<", "add?", "delete", "delete?", "include?", "member?", "===", NULL }; static const char *const coll_names[] = { "merge", "replace", "subtract", "&", "intersection", "|", "union", "+", "-", "difference", "^", "disjoint?", "intersect?", "subset?", "superset?", "proper_subset?", "proper_superset?", "<=", ">=", "<", ">", NULL }; const char *msg = g_sid_refusal_msg; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm) continue; Scope *s = comp_scope_of(c, id); if (s && s->class_id == set_cid) continue; /* the Set's own walks of its members */ int recv = nt_ref(nt, id, "receiver"); int args = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; int blk = nt_ref(nt, id, "block"); if (recv < 0) continue; NodeKind rk = nt_kind(nt, nt_paren_single(nt, recv)); int on_class = (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode) && nt_str(nt, nt_paren_single(nt, recv), "name") && sp_streq(nt_str(nt, nt_paren_single(nt, recv), "name"), "Set"); int ok = 1, checked = 0; /* (followed, a Set made elsewhere is a plain one: only those the identity Sets start from bring members in) */ if (follow && (on_class ? !sid_alloc_is_source(c, &S, id) : !sid_is_alias(c, &S, recv))) continue; if (on_class) { if (sp_streq(nm, "[]")) for (int a = 0; a < argc && ok; a++) { int r = sid_value_or_check(c, id, argv[a]); ok = r != 0; checked |= r == 2; } else if (sp_streq(nm, "new")) ok = (argc == 0 || sid_collection_ok(c, argv[0], set_cid)) && sid_block_ok(c, blk, 0); else continue; } else if (sp_streq(nm, "to_set") && argc == 0) { ok = sid_collection_ok(c, recv, set_cid) && sid_block_ok(c, blk, 0); } else { TyKind rt = infer_type(c, recv); int maybe_set = (ty_is_object(rt) && (ty_object_class(rt) == set_cid || is_descendant(c, ty_object_class(rt), set_cid))) || (rt == TY_POLY && !cls_fact_excludes(c, recv, set_cid) && !sid_operator_answer(c, recv)); if (!maybe_set) continue; int is_elem = 0, is_coll = 0; for (int k = 0; elem_names[k] && !is_elem; k++) is_elem = sp_streq(nm, elem_names[k]); for (int k = 0; coll_names[k] && !is_coll; k++) is_coll = sp_streq(nm, coll_names[k]); int spread = 0; for (int a = 0; a < argc; a++) { NodeKind ak = nt_kind(nt, argv[a]); if (ak == NK_SplatNode || ak == NK_ForwardingArgumentsNode) spread = 1; } /* a member handed in a splat (`@seen.add(*args)`, a def_delegator's forwarding def): every element of the list is one */ if ((is_elem || is_coll) && spread) for (int a = 0; a < argc && ok; a++) { NodeKind ak = nt_kind(nt, argv[a]); if (ak == NK_ForwardingArgumentsNode) ok = 0; else if (ak == NK_SplatNode) ok = sid_collection_ok(c, nt_ref(nt, argv[a], "expression"), set_cid); else ok = is_elem ? sid_value_ok(c, argv[a], 0) : sid_collection_ok(c, argv[a], set_cid); } else if (is_elem && argc == 1) { int r = sid_value_or_check(c, id, argv[0]); ok = r != 0; checked = r == 2; } else if (is_coll) for (int a = 0; a < argc && ok; a++) ok = sid_collection_ok(c, argv[a], set_cid); else if ((sp_streq(nm, "map!") || sp_streq(nm, "collect!")) && rt != TY_POLY) ok = sid_block_ok(c, blk, 1); else continue; } /* --defer-refusals: a value checked where it arrives is a deferred refusal of its own (reported and counted, the call emitted as written); any other refusal stands at the call, which raises it when it runs (refuse_prepass_at_emit) */ if (ok && checked) refusal_note_deferred(c, id, msg); else if (!ok && defer_refusals_at(c, id)) { nt_node_set_int((NodeTable *)nt, id, "idset_refuse", 1); g_idset_marks = 1; } else if (!ok) unsupported_feature(c, id, msg); } free(S.parent); } int builtin_instance_method_known(const char *cls, const char *m); /* Does a String answer `nm` with its built-in method: one String has, that the program defines nowhere on String's chain (a reopen, an include, a prepend) and with no `method_missing` there to answer it instead? A program's own `String#dup` can answer self, its `touch?` can append. */ static int rd_builtin_string_method(Compiler *c, const char *nm) { if (!builtin_instance_method_known("String", nm)) return 0; int sci = comp_core_class(c, CORE_STRING); return sci < 0 || (comp_method_in_chain(c, sci, nm, NULL) < 0 && comp_method_in_chain(c, sci, "method_missing", NULL) < 0); } /* A scan block's matches are new Strings, but CRuby raises "string modified" when the String the scan walks changes under it, and the compiled loop, walking rows taken before the first turn, would carry on. Whether a block reaches the subject cannot be proved from the block (an alias, a helper, a constant, an implicit call, a buffer a builtin writes), so the refusal is lifted only for a subject nothing can name: a new String (a string literal, an interpolation, `+"lit"`, a String method's new answer), or plain locals the block does not mention whose Strings have no other name the block can reach them by (the route's `sole`, asked of the final facts: sh_sole_unreached). Every other subject keeps the refusal. */ typedef struct { int v[8], n; } ScanNamed; /* Is node r's value a new String, no String, or one of the Strings of the plain locals it reads (added to *nm), the facts following each: such a literal, a local, a conditional's arms, an element read off an Array literal of them (`[t, 1].first`, `[t, u][i]`), or a builtin String method's new answer (bop_share's BSH_PURE: `t.dup`, `t.center(5)`)? A call's answer can otherwise be a String another name holds in CRuby (an exception's `to_s` is its message, `gets` sets `$_`), which no share fact knows. */ static int scan_fresh_string(Compiler *c, int r, ScanNamed *nm); typedef struct { Compiler *c; ScanNamed *nm; } ScanFresh; static int scan_fresh_leaf_bad(const VpLeaf *lf, void *ud) { const ScanFresh *f = ud; Compiler *c = f->c; ScanNamed *nm = f->nm; const NodeTable *nt = c->nt; if (lf->kind == VPK_NIL) return 0; /* a conditional's missing arm: nil */ if (lf->kind == VPK_GAVE_UP) return 1; int r = lf->node; NodeKind k = nt_kind(nt, r); if (k == NK_StringNode || k == NK_InterpolatedStringNode || k == NK_IntegerNode || k == NK_FloatNode || k == NK_SymbolNode || k == NK_NilNode || k == NK_TrueNode || k == NK_FalseNode) return 0; if (k == NK_LocalVariableReadNode) { if (nm->n >= 8) return 1; nm->v[nm->n++] = r; return 0; } const char *name = k == NK_CallNode ? nt_str(nt, r, "name") : NULL; int rv = name ? nt_paren_single(nt, nt_ref(nt, r, "receiver")) : -1; if (!name || rv < 0 || nt_ref(nt, r, "block") >= 0) return 1; if (is_unary_plus(name)) return nt_kind(nt, rv) != NK_StringNode; int args = nt_ref(nt, r, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (nt_kind(nt, rv) == NK_ArrayNode) { /* an element read off it (`[t, 1].first`, which the desugaring makes `[](0)`): one of its elements, or nil */ if (!((is_endpoint_query(name) && argc == 0) || (is_aref_name(name) && argc == 1 && nt_kind(nt, argv[0]) != NK_RangeNode))) return 1; int n = 0; const int *el = nt_arr(nt, rv, "elements", &n); for (int i = 0; i < n; i++) if (!scan_fresh_string(c, el[i], nm)) return 1; return 0; } /* the String row, else the any-receiver one (`itself`), else String's default */ TyKind rt = comp_ntype(c, rv); int sh = bop_share_named(TY_STRING, name); if (!sh) sh = bop_share_named(BOP_ANY_RECV, name); if (!sh) sh = bop_share(TY_STRING, name); return !((rt == TY_STRING || rt == TY_STRBUF) && sh == BSH_PURE && rd_builtin_string_method(c, name)); } /* (the arms of an `if` or `unless`, each of which must be such a value) */ static int scan_fresh_string(Compiler *c, int r, ScanNamed *nm) { ScanFresh f = { c, nm }; return !for_each_value_position(c->nt, r, VPS_IF | VPS_UNLESS, scan_fresh_leaf_bad, &f); } /* The locals a chain of writes has followed: one met again adds no String. */ typedef struct ScanChain { const struct ScanChain *up; int root; const char *name; int depth; } ScanChain; static int scan_local_named_fresh(Compiler *c, int l, const ScanChain *up); /* Is every write of local `name` under `node` such a value (scan_fresh_string), each local it reads written so in turn? A method, class or module body has locals of its own. */ static int scan_local_fresh_writes(Compiler *c, int node, const ScanChain *ch) { const NodeTable *nt = c->nt; if (node < 0) return 1; NodeKind k = nt_kind(nt, node); if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) return 1; if (a_is_write_node(nt_type(nt, node))) { const char *wn = nt_str(nt, node, "name"); ScanNamed nm = {{0}, 0}; if (wn && sp_streq(wn, ch->name) && (k != NK_LocalVariableWriteNode || !scan_fresh_string(c, nt_ref(nt, node, "value"), &nm))) return 0; for (int i = 0; wn && sp_streq(wn, ch->name) && i < nm.n; i++) if (!scan_local_named_fresh(c, nm.v[i], ch)) return 0; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (!scan_local_fresh_writes(c, nt_ref_at(nt, node, i), ch)) return 0; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) if (!scan_local_fresh_writes(c, a[j], ch)) return 0; } return 1; } /* Is every write of the local read `l` names, in its scope, such a value? A local the chain already follows is (`s = [t, 1].first; t = s if f`). */ static int scan_local_named_fresh(Compiler *c, int l, const ScanChain *up) { Scope *ss = comp_scope_of(c, l); int root = ss ? (ss->body >= 0 ? ss->body : c->nt->root_id) : -1; const char *name = nt_str(c->nt, l, "name"); for (const ScanChain *p = up; p; p = p->up) if (p->root == root && sp_streq(p->name, name)) return 1; ScanChain ch = { up, root, name, up ? up->depth + 1 : 0 }; return root >= 0 && ch.depth < 8 && scan_local_fresh_writes(c, root, &ch); } /* Does the subtree at n write local `name`? */ static int scan_local_written(const NodeTable *nt, int n, const char *name) { if (n < 0) return 0; if (a_is_write_node(nt_type(nt, n)) && sp_streq(nt_str(nt, n, "name"), name)) return 1; for (int i = 0; i < nt_num_refs(nt, n); i++) if (scan_local_written(nt, nt_ref_at(nt, n, i), name)) return 1; for (int i = 0; i < nt_num_arrs(nt, n); i++) { int m = 0; const int *a = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) if (scan_local_written(nt, a[j], name)) return 1; } return 0; } static int scan_block_changes(Compiler *c, int blk, int node, const char *subj, int *grow, int *clear); /* A call in the scan block (scan_block_changes): a builtin that runs no code of the program's, on the block's own match, the subject's local, or a value of a kind whose builtins call nothing back (ty_runs_no_code, an Array's store or size), handed such values. A mutator (bop_name_mutates, its state ones too) is allowed on a match, and on the subject's local only when it never shortens it and changes its length: `<<`, concat or insert of a non-empty literal, prepend (CRuby raises for none of an empty prepend), or clear alone. */ static int scan_block_call(Compiler *c, int blk, int node, const char *subj, int *grow, int *clear) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, node, "name"); int r = nt_paren_single(nt, nt_ref(nt, node, "receiver")); int args = nt_ref(nt, node, "arguments"), argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; if (!nm || nt_ref(nt, node, "block") >= 0 || cplan_targets(c, node, NULL, 0) != 0 || an_user_defines_or_reads(c, nm)) return 1; for (int i = 0; i < argc; i++) if (scan_block_changes(c, blk, argv[i], subj, grow, clear)) return 1; if (r >= 0 && scan_block_changes(c, blk, r, subj, grow, clear)) return 1; TyKind rt = r >= 0 ? comp_ntype(c, r) : TY_UNKNOWN; const char *rn = r >= 0 && nt_kind(nt, r) == NK_LocalVariableReadNode ? nt_str(nt, r, "name") : NULL; /* the block's own parameter, never rebound, holds a match: a new String */ int own = 0, self = rn && subj && sp_streq(rn, subj); for (int i = 0; rn && block_param_name(c, blk, i); i++) own |= sp_streq(rn, block_param_name(c, blk, i)) && !scan_local_written(nt, nt_ref(nt, blk, "body"), rn); int st = bop_share_named(BOP_ANY_ARRAY, nm); int store = r >= 0 && (ty_is_array(rt)) && (st == BSH_STORE_ALL || st == BSH_STORE_LAST || ((is_count_alias(nm) || is_endpoint_query(nm)) && argc == 0)); for (int i = 0; i < argc && !store; i++) if (!ty_runs_no_code(comp_ntype(c, argv[i]))) return 1; if (r < 0) return !(is_text_print(nm) || is_inspect_print(nm) || is_raise_alias(nm)); if (store) return 0; if (bop_name_mutates(nm, 0) && !own && (self || rt == TY_STRING || rt == TY_STRBUF || rt == TY_POLY || rt == TY_UNKNOWN)) { if (!self) return 1; int int_ok = 0, a0 = str_mutator_str_args(nm, argc, &int_ok), lit = argc > a0; for (int i = a0; i < argc; i++) lit &= (nt_kind(nt, argv[i]) == NK_StringNode && nt_str(nt, argv[i], "content") && nt_str(nt, argv[i], "content")[0]) || (int_ok && nt_kind(nt, argv[i]) == NK_IntegerNode); if (is_clear_name(nm)) *clear = 1; else if (is_prepend_alias(nm) || ((is_string_append_or_prepend(nm) || is_insert_name(nm)) && lit)) *grow = 1; else return 1; return 0; } TyKind fam = own || self || rt == TY_STRING || rt == TY_STRBUF ? TY_STRING : rt; return (!own && !self && !ty_runs_no_code(rt)) || !bop_share(fam, nm); } /* Can scan block blk's code (node) run nothing of the program's and change no String in place but the ones scan_block_call allows? An allow-list of node kinds known to call nothing implicitly: literals, local reads and writes, the control flow, a rescue into a local; any other kind (a Hash literal, an op-assign through a call, an interpolated variable, a nested block, yield, a case's `===`) answers 1, which keeps the refusal. A change that keeps the length is no change the run-time check sees: CRuby scans on over the changed bytes, or raises for a long subject. */ static int scan_block_changes(Compiler *c, int blk, int node, const char *subj, int *grow, int *clear) { const NodeTable *nt = c->nt; if (node < 0) return 0; switch (nt_kind(nt, node)) { case NK_CallNode: return scan_block_call(c, blk, node, subj, grow, clear); case NK_EmbeddedStatementsNode: if (!ty_runs_no_code(comp_ntype(c, node))) return 1; break; case NK_LocalVariableOperatorWriteNode: if (comp_ntype(c, node) != TY_INT && comp_ntype(c, node) != TY_FLOAT) return 1; break; case NK_RescueNode: { int ref = nt_ref(nt, node, "reference"), en = 0; const int *ex = nt_arr(nt, node, "exceptions", &en); if (ref >= 0 && nt_kind(nt, ref) != NK_LocalVariableTargetNode) return 1; for (int i = 0; i < en; i++) if (nt_kind(nt, ex[i]) != NK_ConstantReadNode) return 1; break; } case NK_StatementsNode: case NK_ParenthesesNode: case NK_IfNode: case NK_UnlessNode: case NK_ElseNode: case NK_AndNode: case NK_OrNode: case NK_WhileNode: case NK_UntilNode: case NK_BreakNode: case NK_NextNode: case NK_ArrayNode: case NK_BeginNode: case NK_StringNode: case NK_IntegerNode: case NK_FloatNode: case NK_SymbolNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: case NK_RegularExpressionNode: case NK_InterpolatedStringNode: case NK_LocalVariableReadNode: case NK_LocalVariableWriteNode: case NK_LocalVariableOrWriteNode: case NK_LocalVariableAndWriteNode: case NK_LocalVariableTargetNode: case NK_ConstantReadNode: break; default: /* an ensure clause holds statements, a break's or next's arguments values, which the walk takes in turn */ if (nt_kind(nt, node) != NK_EnsureNode && nt_kind(nt, node) != NK_ArgumentsNode) return 1; } int nr = nt_num_refs(nt, node); for (int i = 0; i < nr; i++) if (scan_block_changes(c, blk, nt_ref_at(nt, node, i), subj, grow, clear)) return 1; int na = nt_num_arrs(nt, node); for (int i = 0; i < na; i++) { int n = 0; const int *a = nt_arr_at(nt, node, i, &n); for (int j = 0; j < n; j++) if (scan_block_changes(c, blk, a[j], subj, grow, clear)) return 1; } return 0; } /* Can the scan's subject be trusted, and through which locals' Strings (*nm, each the route's `sole` once)? */ static int scan_subject_trusted(Compiler *c, int blk, int subj, ScanNamed *nm) { const NodeTable *nt = c->nt; /* CRuby checks the subject only when the block returns to the scan, and both take the first match before the first turn: a block that leaves only by jumping out (share_block_returns) cannot change the subject under the scan, whatever names it */ int returns = share_block_returns(c, blk); if (subj < 0 || !scan_fresh_string(c, subj, nm)) { nm->n = 0; return !returns; } ANameSet used = {0}; if (returns) a_collect_used(c, nt_ref(nt, blk, "body"), &used); int ok = 1; for (int i = 0; returns && ok && i < nm->n; i++) ok = !aname_has(&used, nt_str(nt, nm->v[i], "name")) && scan_local_named_fresh(c, nm->v[i], NULL); free(used.v); return ok; } /* String routes the settled types show copying where CRuby hands over the one object (#6765), refused rather than compiled with the change lost: - `t = obj.text; t << x` on a boxed obj: the reader's dispatch answers a copy of the member, and obj is read again; - a `scan` block parameter that became a String handle (stored and mutated), or a group's boxed one that is mutated: `scan` binds each match as a plain String, and the C did not build; - `kept[i] << x` with a String x, where kept stores a boxed block parameter no element iterator binds (refuse_stored_block_param). */ /* The scans with a block (`s.scan(re) { |m| ... }`) that take the run-time length check (scan_rtcheck): a subject the block could grow or clear under the scan, whose change CRuby reports as "string modified" at the turn the block returns from (emit_scan_rtcheck_open). */ static void mark_scan_rtchecks(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, u) { const char *un = nt_str(nt, u, "name"); int blk = nt_ref(nt, u, "block"), r = nt_ref(nt, u, "receiver"); if (!un) continue; if (is_scan_name(un) && blk >= 0 && nt_kind(nt, blk) == NK_BlockNode && r >= 0 && (comp_ntype(c, r) == TY_STRING || comp_ntype(c, r) == TY_STRBUF || (comp_ntype(c, r) == TY_POLY))) for (int k = 0; k < 4; k++) { const char *bp = block_param_name(c, blk, k); LocalVar *bv = bp ? scope_local(comp_scope_of(c, blk), bp) : NULL; if (bv && (bv->type == TY_STRBUF || (bv->type == TY_POLY && strbuf_any_str_mut(c, bp, comp_scope_of(c, blk))))) { /* The matches are new Strings, each held by the parameter alone. A block that grows or clears a String in place may grow or clear the subject under the scan, which CRuby notices at the turn it returns from ("string modified"): such a scan takes the run-time length check, unless the subject is vouched for with no name the block reaches (scan_subject_trusted) or the block never returns to the scan (share_block_returns). */ ScanNamed nm = {{0}, 0}; int trusted = scan_subject_trusted(c, blk, nt_paren_single(nt, r), &nm); int grow = 0, clear = 0, rs = nt_paren_single(nt, r), rt = 0, rt_grow = 0; const char *subj = nt_kind(nt, rs) == NK_LocalVariableReadNode ? nt_str(nt, rs, "name") : NULL; /* (a subject the block rebinds is another String once it does, and so is one the scan's own pattern rebinds, which runs after the subject is read and before the check is opened) */ if (subj && (scan_local_written(nt, nt_ref(nt, blk, "body"), subj) || scan_local_written(nt, nt_ref(nt, u, "arguments"), subj))) subj = NULL; /* (a raise out of a watched scan leaves $~ at the last match the loop took before the first turn, where CRuby's is the turn's) */ Scope *us = comp_scope_of(c, u); int uroot = us && us->body >= 0 ? us->body : nt->root_id; if (!scan_block_changes(c, blk, nt_ref(nt, blk, "body"), subj, &grow, &clear) && !(grow && clear) && !((grow || clear) && subtree_reads_match_globals(c, uroot))) { rt = 1; rt_grow = grow || clear; } /* (a block that rebinds the subject's variable changes no String: no check; any other takes the length check) */ if (!trusted && !rt && subj) { rt = 1; rt_grow = 1; } if (rt && rt_grow && !(trusted && nm.n == 0) && share_block_returns(c, blk)) nt_node_set_int((NodeTable *)nt, u, "scan_rtcheck", 1); } } } } /* A multiple assignment's index target that a Struct's own `[]=` can take (struct_aset_may_reach) stores through its evidence call (desugar_masgn_store_evidence), as the single `[]=` stores (masgn_struct_store): the target's own store set an Array's element only, which dropped a member's store through a box, and refused one on a receiver typed as the Struct. */ static void mark_struct_aset_targets(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_IndexTargetNode, t) { int ev = (int)nt_int(nt, t, "aset_ev", 0) - 1; if (ev < 0 || !struct_aset_may_reach(c, t)) continue; /* Without --share-strings a String in a member is a copy of the one the value's other name holds, and a later change through either name would show it: on a receiver typed as the Struct, where master refused every such target, a value that can be such a String (not a fresh literal) keeps that refusal (masgn_store's). */ /* A String member keeps its type (infer_struct_aset_call), so a value that may not fit one the key can name would raise TypeError where it runs: on a receiver typed as the Struct, by a key no literal names, master's refusal stays. */ TyKind rt = comp_ntype(c, nt_ref(nt, t, "receiver")); if (ty_is_object(rt) && sp_streq(nt_str(nt, ev, "name"), "[]=")) { int evn = 0, lo, hi, str = 0; const int *eva = nt_arr(nt, nt_ref(nt, ev, "arguments"), "arguments", &evn); TyKind vt = TY_UNKNOWN; if (evn == 2) { int value; switch (vp_one(nt, eva[1], VPS_NONE | VP_PARENS_ONLY, &value)) { case VP1_NODE: vt = comp_ntype(c, value); break; case VP1_NIL: vt = TY_NIL; break; case VP1_MANY: break; } } if (struct_aset_members(c, ev, ty_object_class(rt), &lo, &hi)) for (int m = lo; m < hi; m++) { TyKind mt = c->classes[ty_object_class(rt)].ivar_types[m]; str |= mt == TY_STRING || mt == TY_STRBUF; } if (str && vt != TY_STRING && vt != TY_STRBUF && vt != TY_NIL) continue; } nt_node_set_int((NodeTable *)nt, t, "struct_aset", 1); } } /* --share-strings: a String the rule holds as a handle, stored into a Struct member by `[]=` through a boxed receiver or by a key no literal names, is stored as the handle: the read hands it over (strbuf_box and strbuf_handle_demand), and the member, which infer_struct_aset_call boxed for it, holds it, so a change through the variable or the member reaches the other, as CRuby's one object does. The poly dispatch passes the handle to the Struct's arm. A member that is neither boxed nor a shared String (one an RBS signature pins, a String member nothing shares) would hold a copy and lose the change: refused (#6765). */ static void share_struct_aset_handles(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, u) { /* a member's writer through a box (emit_boxed_writer_arms): a shared String is handed over as its handle, as `[]=` hands it below */ const char *un = nt_str(nt, u, "name"); int ua = nt_ref(nt, u, "arguments"), uan = 0; const int *uav = ua >= 0 ? nt_arr(nt, ua, "arguments", &uan) : NULL; char ub[256] = "", uiv[258] = ""; int boxed_member = uan == 1 && call_is_setter_assign(nt, u) && setter_base_name(un, ub, sizeof ub) && comp_ntype(c, nt_ref(nt, u, "receiver")) == TY_POLY && an_arg_is_shared_handle(c, nt_paren_single(nt, uav[0])); /* only where every attribute the name writes is a boxed Struct member, whose arm boxes the handle (a String slot's arm takes the bytes) */ if (boxed_member) snprintf(uiv, sizeof uiv, "@%s", ub); for (int k = 0; k < c->nclasses && boxed_member; k++) { if (comp_resolve_member(c, k, ub, 1, NULL, NULL) != SP_MEMBER_ATTR) continue; int m = c->classes[k].is_struct ? comp_member_index(&c->classes[k], uiv) : -1; boxed_member = m >= 0 && c->classes[k].ivar_types[m] == TY_POLY; } if (boxed_member) { int v = nt_paren_single(nt, uav[0]); comp_set_strbuf_box(c, v, 1); c->strbuf_handle_demand[v] = 1; continue; } int one, how = struct_aset_receiver(c, u, &one); if (!how) continue; int v = nt_paren_single(nt, nt_arr(nt, nt_ref(nt, u, "arguments"), "arguments", NULL)[1]); if (!an_arg_is_shared_handle(c, v)) continue; int nk = 1; const int *ks = how == 1 ? &one : poly_recv_classes(c, u, &nk); for (int i = 0; i < (ks ? nk : c->nclasses); i++) { int k = ks ? ks[i] : i, lo, hi; if (how == 2 && cls_fact_excludes(c, nt_ref(nt, u, "receiver"), k)) continue; if (!struct_aset_members(c, u, k, &lo, &hi)) continue; for (int m = lo; m < hi; m++) { ClassInfo *ci = &c->classes[k]; /* a boxed member holds the handle, and so does a shared String one */ if (ci->ivar_types[m] == TY_POLY || (ci->ivar_types[m] == TY_STRBUF && ci->ivar_str_shared[m])) continue; unsupported_feature(c, u, "a String stored into a Struct member by `[]=` through a boxed receiver, " "or by a key that is no literal, is mutated in place, and the member holds a copy " "(a String is not yet shared by reference through a Struct's `[]=` into a member " "of another fixed type, or a String member nothing shares). Store it with the " "member's writer on a receiver typed as the Struct."); } } comp_set_strbuf_box(c, v, 1); c->strbuf_handle_demand[v] = 1; } } /* Only an implicit block or the method's own block parameter forwards its caller's block. A literal block or an unrelated proc belongs to super. */ int super_forwards_caller_block(Compiler *c, int id) { NodeKind kind = nt_kind(c->nt, id); if (kind != NK_SuperNode && kind != NK_ForwardingSuperNode) return 0; int block = nt_ref(c->nt, id, "block"); if (block < 0) return 1; if (nt_kind(c->nt, block) != NK_BlockArgumentNode) return 0; Scope *scope = comp_scope_of(c, id); int expr = nt_ref(c->nt, block, "expression"); return scope && scope->blk_param && scope->blk_param[0] && expr >= 0 && nt_kind(c->nt, expr) == NK_LocalVariableReadNode && sp_streq(nt_str(c->nt, expr, "name"), scope->blk_param); } static void poly_ivar_set_reference(Compiler *c, int id, int recv) { TyKind rt = comp_ntype(c, recv); if (ty_is_object(rt)) { int q = ty_object_class(rt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } else if (rt == TY_POLY) { for (int q = 0; q < c->nclasses; q++) if (poly_ivar_set_class(c, q) && poly_ivar_set_reaches(c, id, q)) c->classes[q].is_value_type = 0; } } /* Setup, the AST desugarings and the registration passes: scopes, locals, attrs, aliases, globals and constants, FFI declarations (analyze_program's steps, in their order) */ /* `retry` restarts a begin from its rescue clause only: CRuby's compiler rejects one in an ensure clause ("Invalid retry", a SyntaxError), where Prism, which checks for an enclosing rescue, lets it through (#8377). ctx: 1 in a rescue clause, 2 in an ensure clause; a method, class or block body starts over. */ static void reject_retry_in_ensure(Compiler *c, int id, int ctx, int depth) { const NodeTable *nt = c->nt; if (id < 0 || id >= nt->count || depth > 4000) return; NodeKind k = nt_kind(nt, id); if (k == NK_RetryNode && ctx == 2) { int ln = (int)nt_int(nt, id, "node_line", 0); const char *file = nt_file_path(nt, (int)nt_int(nt, id, "node_file", 0)); if (!file || !*file) file = nt->source_file ? nt->source_file : "source.rb"; fprintf(stderr, "spinel: %s:%d: Invalid retry (SyntaxError)\n", file, ln); exit(1); } if (k == NK_DefNode || k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode || k == NK_BlockNode || k == NK_LambdaNode) ctx = 0; int in_rescue = k == NK_RescueNode ? nt_ref(nt, id, "statements") : -1; NodeKind ty_k = nt_kind(nt, id); int in_ensure = ty_k == NK_EnsureNode ? nt_ref(nt, id, "statements") : -1; const SpNode *nd = &nt->nodes[id]; for (int i = 0; i < nd->nr; i++) { int ch = nd->r[i].ref; reject_retry_in_ensure(c, ch, ch >= 0 && ch == in_rescue ? 1 : ch >= 0 && ch == in_ensure ? 2 : ctx, depth + 1); } for (int i = 0; i < nd->na; i++) for (int j = 0; j < nd->a[i].n; j++) reject_retry_in_ensure(c, nd->a[i].ids[j], ctx, depth + 1); } /* --exceptions=abort: a construct that catches what a frame unwinds has no frame to catch it with -- the runtime is built without setjmp/longjmp (lib/sp_setjmp.h) -- so it is refused at its line, the flag named. A raise with nothing to catch it traps, as the host's failure. */ static void reject_exc_abort_constructs(Compiler *c) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); const char *what = NULL; if (k == NK_RescueNode) what = "rescue"; else if (k == NK_RescueModifierNode) what = "a rescue modifier"; else if (k == NK_RetryNode) what = "retry"; else if (k == NK_CallNode && nt_ref(nt, id, "receiver") < 0) { const char *nm = nt_str(nt, id, "name"); if (nm && (sp_streq(nm, "catch") || sp_streq(nm, "throw"))) what = nm; } if (!what) continue; int ln = (int)nt_int(nt, id, "node_line", 0); const char *file = nt_file_path(nt, (int)nt_int(nt, id, "node_file", 0)); if (!file || !*file) file = nt->source_file ? nt->source_file : "source.rb"; fprintf(stderr, "spinel: %s:%d: %s needs exception handling, which --exceptions=abort leaves out\n", file, ln, what); exit(1); } } /* Does a def's parameter list take no argument it must be given: no required or post-splat positional, no required keyword? */ static int def_params_all_optional(const NodeTable *nt, int pn) { if (pn < 0) return 1; int n = 0; nt_arr(nt, pn, "requireds", &n); if (n) return 0; nt_arr(nt, pn, "posts", &n); if (n) return 0; const int *kws = nt_arr(nt, pn, "keywords", &n); for (int k = 0; k < n; k++) if (nt_kind(nt, kws[k]) == NK_RequiredKeywordParameterNode) return 0; return 1; } /* `def inspect(indent = 0)` / `def to_s(fmt = nil)`: the runtime asks for the String with no argument (`p obj`, `puts obj`, "#{obj}", an element of an inspected Array), and the method's C function takes its parameters. Those callers passed none -- the C did not compile -- or, finding a method with parameters, rendered the default form instead. Each such def in a class or module body gets a sibling, `__noarg inspect` (or `__noarg to_s`): `= inspect`, a plain call whose defaults bind as any other call's, and the runtime's callers call that (display_method_scope). The space keeps the name out of the program's reach, as `__inc` does: no `def` the program writes is taken for it. */ /* Is the statement d a `def inspect(..)` / `def to_s(..)` that takes no argument it must be given, and has parameters to default? */ static int noarg_display_def(const NodeTable *nt, const LowerCtx *lc, int d) { if (nt_kind(nt, d) != NK_DefNode || nt_ref(nt, d, "receiver") >= 0) return 0; const char *dn = nt_str(nt, d, "name"); int pn = nt_ref(nt, d, "parameters"); if (!(dn && (sp_streq(dn, "inspect") || sp_streq(dn, "to_s")) && pn >= 0 && def_params_all_optional(nt, pn))) return 0; /* the sibling already follows it: this def has been through */ int at = lower_stmt_at(nt, lc, d), bn = 0; const int *bb = nt_arr(nt, lc->body, "body", &bn); if (at + 1 < bn && nt_kind(nt, bb[at + 1]) == NK_DefNode && nt_ref(nt, bb[at + 1], "receiver") < 0) { const char *sn = nt_str(nt, bb[at + 1], "name"); if (sn && !strncmp(sn, "__noarg ", 8) && sp_streq(sn + 8, dn)) return 0; } return 1; } int desugar_noarg_display_wrappers_left(Compiler *c, LowerCtx *lc, int id) { return noarg_display_def(c->nt, lc, id); } int desugar_noarg_display_wrappers_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int d = id; if (!noarg_display_def(nt, lc, d)) return 0; int to_s = sp_streq(nt_str(nt, d, "name"), "to_s"); long long line = nt_int(nt, d, "node_line", 0), file = nt_int(nt, d, "node_file", 0); int call = nt_new_node(nt, "CallNode"); int st = nt_new_node(nt, "StatementsNode"); int def = nt_new_node(nt, "DefNode"); if (call < 0 || st < 0 || def < 0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nt_node_set_str(nt, call, "name", to_s ? "to_s" : "inspect"); nt_node_set_ref(nt, call, "receiver", -1); nt_node_set_ref(nt, call, "arguments", -1); nt_node_set_ref(nt, call, "block", -1); nt_node_set_arr(nt, st, "body", &call, 1); nt_node_set_str(nt, def, "name", to_s ? "__noarg to_s" : "__noarg inspect"); nt_node_set_ref(nt, def, "receiver", -1); nt_node_set_ref(nt, def, "parameters", -1); nt_node_set_ref(nt, def, "body", st); int ln[3] = { call, st, def }; for (int j = 0; j < 3 && line > 0; j++) { nt_node_set_int(nt, ln[j], "node_line", line); nt_node_set_int(nt, ln[j], "node_file", file); } /* the sibling follows the def */ int at = lower_stmt_at(nt, lc, id), bn = 0; const int *bb = nt_arr(nt, lc->body, "body", &bn); int *nb = malloc(sizeof(int) * (size_t)(bn + 1)); if (!nb) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(nb, bb, sizeof(int) * (size_t)(at + 1)); nb[at + 1] = def; memcpy(nb + at + 2, bb + at + 1, sizeof(int) * (size_t)(bn - at - 1)); nt_node_set_arr(nt, lc->body, "body", nb, bn + 1); free(nb); return 1; } /* Does the subtree at n hold a node whose meaning depends on being the method it is written in: super, yield, block_given?? */ static int reinit_body_bound(const NodeTable *nt, int n, int depth) { if (n < 0 || depth > 400) return 0; NodeKind k = nt_kind(nt, n); if (k == NK_SuperNode || k == NK_ForwardingSuperNode || k == NK_YieldNode) return 1; if (k == NK_CallNode && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), "block_given?")) return 1; int nr = nt_num_refs(nt, n); for (int i = 0; i < nr; i++) if (reinit_body_bound(nt, nt_ref_at(nt, n, i), depth + 1)) return 1; int na = nt_num_arrs(nt, n); for (int i = 0; i < na; i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) if (reinit_body_bound(nt, av[j], depth + 1)) return 1; } return 0; } /* Renames each receiverless `initialize(...)` call under n to `to`; answers how many. */ static int reinit_rename_calls(NodeTable *nt, int n, const char *to, int depth) { if (n < 0 || depth > 400) return 0; int hit = 0; if (nt_kind(nt, n) == NK_CallNode && nt_ref(nt, n, "receiver") < 0 && nt_str(nt, n, "name") && sp_streq(nt_str(nt, n, "name"), "initialize")) { nt_node_set_str(nt, n, "name", to); hit = 1; } int nr = nt_num_refs(nt, n); for (int i = 0; i < nr; i++) hit += reinit_rename_calls(nt, nt_ref_at(nt, n, i), to, depth + 1); int na = nt_num_arrs(nt, n); for (int i = 0; i < na; i++) { int an = 0; const int *av = nt_arr_at(nt, n, i, &an); for (int j = 0; j < an; j++) hit += reinit_rename_calls(nt, av[j], to, depth + 1); } return hit; } /* The last segment of constant node n's name (`A::B` -> "B"), or NULL. */ static const char *reinit_leaf(const NodeTable *nt, int n) { if (n < 0 || (nt_kind(nt, n) != NK_ConstantReadNode && nt_kind(nt, n) != NK_ConstantPathNode)) return NULL; const char *nm = nt_str(nt, n, "name"); const char *t = nm ? strrchr(nm, ':') : NULL; return t ? t + 1 : nm; } /* `def reload(s) = initialize(s)`: a method of the class calling its own initialize, for its effect and for its value. The emitted initialize is a constructor body: it answers nothing, and on a class laid out by value it takes a pointer the method does not have, so the C did not compile. Each such call is renamed to __spinel_reinit, and each `def initialize` of the program -- in every class and module body, a reopening's and a subclass's too -- gets a copy of itself under that name beside it, which answers the body's value as CRuby's initialize does. The call then finds the copy of the initialize the receiver's class has, as CRuby's lookup finds that initialize. A body bound to being initialize (super, yield, block_given?) gets no copy, and refuse_reinit_mismatch refuses a call that would find another initialize's copy for it. */ int desugar_explicit_initialize_calls_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count, hits = 0; ANameHash near = {0}; /* the classes and modules a renamed call may run on */ for (int cls = 0; cls < n0; cls++) { if (nt_kind(nt, cls) != NK_ClassNode) continue; int body = nt_ref(nt, cls, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); int init = -1; for (int k = 0; k < bn; k++) if (nt_kind(nt, bb[k]) == NK_DefNode && nt_ref(nt, bb[k], "receiver") < 0 && nt_str(nt, bb[k], "name") && sp_streq(nt_str(nt, bb[k], "name"), "initialize")) init = bb[k]; if (init < 0 || reinit_body_bound(nt, nt_ref(nt, init, "body"), 0)) continue; int h = 0; for (int k = 0; k < bn; k++) if (nt_kind(nt, bb[k]) == NK_DefNode && bb[k] != init) h += reinit_rename_calls(nt, nt_ref(nt, bb[k], "body"), "__spinel_reinit", 0); const char *cn = reinit_leaf(nt, nt_ref(nt, cls, "constant_path")); if (h && cn && !anh_has(&near, cn)) anh_add(&near, strdup(cn)); hits += h; } /* the copies go where such a call may find an initialize: the class, its reopenings, its subclasses, and the modules they include. A class left out is one refuse_reinit_mismatch refuses a call for; a copy elsewhere would only be a method nothing calls, with untyped parameters */ for (int grew = hits > 0; grew; ) { grew = 0; for (int cls = 0; cls < n0; cls++) { if (nt_kind(nt, cls) != NK_ClassNode) continue; const char *cn = reinit_leaf(nt, nt_ref(nt, cls, "constant_path")); const char *sn = reinit_leaf(nt, nt_ref(nt, cls, "superclass")); if (!cn || anh_has(&near, cn) || !sn || !anh_has(&near, sn)) continue; anh_add(&near, strdup(cn)); grew = 1; } } for (int cls = 0; hits && cls < n0; cls++) { if (nt_kind(nt, cls) != NK_ClassNode) continue; const char *cn = reinit_leaf(nt, nt_ref(nt, cls, "constant_path")); int body = nt_ref(nt, cls, "body"); if (!cn || !anh_has(&near, cn) || body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb = nt_arr(nt, body, "body", &bn); for (int k = 0; k < bn; k++) { const char *in = nt_kind(nt, bb[k]) == NK_CallNode && nt_ref(nt, bb[k], "receiver") < 0 ? nt_str(nt, bb[k], "name") : NULL; if (!in || (!sp_streq(in, "include") && !sp_streq(in, "prepend"))) continue; int an = 0; const int *av = nt_arr(nt, nt_ref(nt, bb[k], "arguments"), "arguments", &an); for (int a = 0; a < an; a++) { const char *mn = reinit_leaf(nt, av[a]); if (mn && !anh_has(&near, mn)) anh_add(&near, strdup(mn)); } } } if (!hits) { anh_free(&near); return 0; } for (int cls = 0; cls < n0; cls++) { if (nt_kind(nt, cls) != NK_ClassNode && nt_kind(nt, cls) != NK_ModuleNode) continue; const char *cn = reinit_leaf(nt, nt_ref(nt, cls, "constant_path")); if (!cn || !anh_has(&near, cn)) continue; int body = nt_ref(nt, cls, "body"); if (body < 0 || nt_kind(nt, body) != NK_StatementsNode) continue; int bn = 0; const int *bb1 = nt_arr(nt, body, "body", &bn); int *bb = malloc(sizeof(int) * (size_t)(bn * 3 + 1)), *bb0 = malloc(sizeof(int) * (size_t)(bn + 1)); if (!bb || !bb0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } /* (a clone may move the body's array: read a copy) */ memcpy(bb0, bb1, sizeof(int) * (size_t)bn); int m = 0, added = 0; for (int k = 0; k < bn; k++) { int init = bb0[k]; bb[m++] = init; if (nt_kind(nt, init) != NK_DefNode || nt_ref(nt, init, "receiver") >= 0 || !nt_str(nt, init, "name") || !sp_streq(nt_str(nt, init, "name"), "initialize") || reinit_body_bound(nt, nt_ref(nt, init, "body"), 0)) continue; int copy = nt_clone_subtree(nt, init); if (copy < 0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nt_node_set_str(nt, copy, "name", "__spinel_reinit"); nt_node_set_int(nt, copy, "reinit_of", init + 1); /* private, as initialize is: no caller outside the class sees it */ int sym = nt_new_node(nt, "SymbolNode"), args = nt_new_node(nt, "ArgumentsNode"), priv = nt_new_node(nt, "CallNode"); if (sym < 0 || args < 0 || priv < 0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } nt_node_set_str(nt, sym, "value", "__spinel_reinit"); nt_node_set_arr(nt, args, "arguments", &sym, 1); nt_node_set_str(nt, priv, "name", "private"); nt_node_set_ref(nt, priv, "receiver", -1); nt_node_set_ref(nt, priv, "arguments", args); nt_node_set_ref(nt, priv, "block", -1); long long line = nt_int(nt, init, "node_line", 0), file = nt_int(nt, init, "node_file", 0); int ln[3] = { sym, args, priv }; for (int j = 0; j < 3 && line > 0; j++) { nt_node_set_int(nt, ln[j], "node_line", line); nt_node_set_int(nt, ln[j], "node_file", file); } bb[m++] = copy; bb[m++] = priv; added = 1; } if (added) nt_node_set_arr(nt, body, "body", bb, m); free(bb); free(bb0); } anh_free(&near); return 0; } /* A class whose objects run a `__spinel_reinit` call (desugar_explicit_initialize_calls) finds the copy of an initialize: the call is refused where that copy is not of the initialize the class's own lookup finds -- that one has no copy, its body being bound to running as initialize (super, yield, block_given?). */ /* Can the method of scope si, whose body has the `__spinel_reinit` call, run on an object of class k? Its calls are followed: on self, through the caller's own calls; on a receiver, by the receiver's type. */ static int reinit_reaches(Compiler *c, int si, int k, int depth) { const NodeTable *nt = c->nt; const char *mn = c->scopes[si].name; /* (a method run by a name the program spells or computes: any receiver) */ if (!mn || depth > 8 || an_prog_names_method(mn) || an_prog_meta_by_value()) return 1; if (comp_method_in_chain(c, k, mn, NULL) != si) return 0; for (int id = an_calls_named_first(c, mn); id >= 0; id = an_calls_named_next(id)) { int r = nt_ref(nt, id, "receiver"); if (r < 0 || nt_kind(nt, r) == NK_SelfNode) { Scope *s = comp_scope_of(c, id); if (!s || s->is_cmethod || s->class_id < 0) continue; if (s->class_id != k && !is_descendant(c, k, s->class_id)) continue; if (reinit_reaches(c, (int)(s - c->scopes), k, depth + 1)) return 1; continue; } TyKind t = comp_ntype(c, r); if (ty_is_object(t)) { if (ty_object_class(t) == k) return 1; continue; } if (t != TY_POLY) continue; int n = 0; const int *cl = poly_recv_classes(c, id, &n); if (!cl) return 1; for (int q = 0; q < n; q++) if (cl[q] == k) return 1; } return 0; } static void refuse_reinit_mismatch(Compiler *c) { const NodeTable *nt = c->nt; for (int k = 0; k < c->nclasses; k++) { if (!c->classes[k].instantiated) continue; int mr = comp_method_in_chain(c, k, "__spinel_reinit", NULL); if (mr < 0) continue; int mi = comp_method_in_chain(c, k, "initialize", NULL); int of = c->scopes[mr].def_node >= 0 ? (int)nt_int(nt, c->scopes[mr].def_node, "reinit_of", 0) - 1 : -1; if (mi >= 0 && of >= 0 && c->scopes[mi].def_node == of) continue; NT_FOREACH_KIND(nt, NK_CallNode, id) { const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, "__spinel_reinit") || nt_ref(nt, id, "receiver") >= 0) continue; Scope *s = comp_scope_of(c, id); if (!s || s->is_cmethod || s->class_id < 0 || (s->class_id != k && !is_descendant(c, k, s->class_id))) continue; if (!reinit_reaches(c, (int)(s - c->scopes), k, 0)) continue; unsupported_feature(c, id, "a method calls initialize on an object whose class's initialize uses super, yield or block_given?, " "so the call cannot run it as a plain method here. Move the shared code to a method of its own and " "call that from both."); } } } /* Does constant `k` name a class of the program whose chain of superclass constants, through the program's own classes, reaches the builtin Struct, none of them defining a `new` of its own (#9292)? Its `new(:a, :b)` makes a Struct class below it, as Struct.new does below Struct. */ static int struct_subclass_maker(const NodeTable *nt, const char *k, int depth) { if (!k || depth > 16 || sp_streq(k, "Struct")) return 0; int found = 0; NT_FOREACH_KIND(nt, NK_ClassNode, id) { int cp = nt_ref(nt, id, "constant_path"); const char *cn = cp >= 0 ? nt_str(nt, cp, "name") : NULL; if (!cn || !sp_streq(cn, k)) continue; int sc = nt_ref(nt, id, "superclass"); if (sc >= 0) { NodeKind sk = nt_kind(nt, sc); if (sk != NK_ConstantReadNode && sk != NK_ConstantPathNode) return 0; const char *sn = nt_str(nt, sc, "name"); if (!sn || !(sp_streq(sn, "Struct") || struct_subclass_maker(nt, sn, depth + 1))) return 0; found = 1; } /* `def self.new` / `class << self; def new` in a body of k */ int body = nt_ref(nt, id, "body"), bn = 0; const int *bb = body >= 0 && nt_kind(nt, body) == NK_StatementsNode ? nt_arr(nt, body, "body", &bn) : NULL; for (int j = 0; j < bn; j++) { int st = bb[j]; if (nt_kind(nt, st) == NK_DefNode && sp_streq(nt_str(nt, st, "name"), "new") && nt_ref(nt, st, "receiver") >= 0) return 0; if (nt_kind(nt, st) == NK_SingletonClassNode) { int sb = nt_ref(nt, st, "body"), sn2 = 0; const int *ss = sb >= 0 && nt_kind(nt, sb) == NK_StatementsNode ? nt_arr(nt, sb, "body", &sn2) : NULL; for (int q = 0; q < sn2; q++) if (nt_kind(nt, ss[q]) == NK_DefNode && sp_streq(nt_str(nt, ss[q], "name"), "new")) return 0; } } } /* `def K.new` anywhere */ NT_FOREACH_KIND(nt, NK_DefNode, id) { int rv = nt_ref(nt, id, "receiver"); if (rv >= 0 && nt_kind(nt, rv) == NK_ConstantReadNode && sp_streq(nt_str(nt, rv, "name"), k) && sp_streq(nt_str(nt, id, "name"), "new")) return 0; } return found; } /* Is `nm` a constant the program defines: a class or module body, or a constant write, of that name? */ static int lcg_defined(const NodeTable *nt, const char *nm) { for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); const char *n = NULL; if (k == NK_ClassNode || k == NK_ModuleNode) { int cp = nt_ref(nt, id, "constant_path"); n = cp >= 0 ? nt_str(nt, cp, "name") : NULL; } else if (k == NK_ConstantWriteNode) n = nt_str(nt, id, "name"); else if (k == NK_ConstantPathWriteNode) { int t = nt_ref(nt, id, "target"); n = t >= 0 ? nt_str(nt, t, "name") : NULL; } if (n && sp_streq(n, nm)) return 1; } return 0; } /* `Object.const_get("R::Cam")` / `Object.const_get(:Cam)` with a literal name whose every segment the program defines is that constant, `R::Cam`: read as the call, a `new` or `[]` on it took a switch over every class of the program, whose arms typed the members of every Struct (#9093) */ /* Does the subtree at id name the local `nm`, read or written? */ static int tvw_names_local(const NodeTable *nt, int id, const char *nm, int depth) { if (id < 0 || depth > 200) return 0; const char *ty = nt_type(nt, id); if (ty && strncmp(ty, "LocalVariable", 13) == 0) { const char *n = nt_str(nt, id, "name"); if (n && sp_streq(n, nm)) return 1; } int nr = nt_num_refs(nt, id); for (int i = 0; i < nr; i++) if (tvw_names_local(nt, nt_ref_at(nt, id, i), nm, depth + 1)) return 1; int na = nt_num_arrs(nt, id); for (int i = 0; i < na; i++) { int n = 0; const int *ids = nt_arr_at(nt, id, i, &n); for (int j = 0; j < n; j++) if (tvw_names_local(nt, ids[j], nm, depth + 1)) return 1; } return 0; } /* --share-strings: `x.itself` is `x` for a variable x where the program defines no itself of its own: the builtin answers its receiver. Each route a value takes then sees the variable itself; through the call, a String a block parameter held was pushed or answered as a copy, or refused (#9258). */ int desugar_itself_receiver_gate(Compiler *c, LowerCtx *lc) { const NodeTable *nt = c->nt; for (int id = 0; id < lc->n0; id++) { NodeKind k = nt_kind(nt, id); const char *nm = NULL; if (k == NK_DefNode) nm = nt_str(nt, id, "name"); else if (k == NK_AliasMethodNode) { int nn = nt_ref(nt, id, "new_name"); nm = nn >= 0 ? nt_str(nt, nn, "value") : NULL; } else if (k == NK_CallNode && (sp_streq(nt_str(nt, id, "name"), "define_method") || sp_streq(nt_str(nt, id, "name"), "alias_method"))) { int an = nt_ref(nt, id, "arguments"), ac = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &ac) : NULL; if (ac < 1) continue; NodeKind ak = nt_kind(nt, av[0]); nm = ak == NK_SymbolNode ? nt_str(nt, av[0], "value") : ak == NK_StringNode ? nt_str(nt, av[0], "content") : "?"; } if (nm && sp_streq(nm, "itself")) return 0; } return 1; } /* the variable read an `x.itself` call at id stands for, or -1 */ static int itself_base(const NodeTable *nt, int id) { if (!sp_streq(nt_str(nt, id, "name"), "itself") || nt_ref(nt, id, "arguments") >= 0 || nt_ref(nt, id, "block") >= 0 || nt_safe_nav(nt, id)) return -1; int r = nt_ref(nt, id, "receiver"); if (r < 0) return -1; /* (a chain of them is its base: `@s.itself.itself`, whichever link is rewritten first) */ for (int d = 0; d < 16 && r >= 0 && nt_kind(nt, r) == NK_CallNode && sp_streq(nt_str(nt, r, "name"), "itself") && nt_ref(nt, r, "arguments") < 0 && nt_ref(nt, r, "block") < 0 && !nt_safe_nav(nt, r); d++) r = nt_ref(nt, r, "receiver"); if (r < 0) return -1; /* a variable's read becomes that read; any other receiver keeps the call, whose routes the String plan already takes */ NodeKind rk = nt_kind(nt, r); return rk == NK_LocalVariableReadNode || rk == NK_InstanceVariableReadNode ? r : -1; } int desugar_itself_receiver_left(Compiler *c, LowerCtx *lc, int id) { return itself_base(c->nt, id) >= 0; } int desugar_itself_receiver_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; int r = itself_base(nt, id); if (r < 0) return 0; long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0); NodeKind rk = nt_kind(nt, r); const char *vn = nt_str(nt, r, "name"); long long depth = nt_int(nt, r, "depth", 0); nt_node_reset(nt, id, rk == NK_LocalVariableReadNode ? "LocalVariableReadNode" : "InstanceVariableReadNode"); nt_node_set_str(nt, id, "name", vn); if (rk == NK_LocalVariableReadNode) nt_node_set_int(nt, id, "depth", depth); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); return 1; } /* --share-strings: `x = E.tap { |t| ... }` is `x = (x = E).tap { ... }` when the block names no x: tap answers its receiver, so x is E's object either way, and the block runs before the outer write as before. Written that way, the receiver is a variable, whose handle the block parameter and the write share; the value of a tap over a new String handed on as a variable's write was refused (#9258). */ int desugar_tap_value_write_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *nt = (NodeTable *)c->nt; const char *x = nt_str(nt, id, "name"); int v = nt_ref(nt, id, "value"); if (!x || v < 0 || nt_kind(nt, v) != NK_CallNode || !sp_streq(nt_str(nt, v, "name"), "tap") || nt_ref(nt, v, "arguments") >= 0 || nt_safe_nav(nt, v)) return 0; int blk = nt_ref(nt, v, "block"), r = nt_ref(nt, v, "receiver"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode || r < 0) return 0; NodeKind rk = nt_kind(nt, nt_paren_single(c->nt, r)); if (rk == NK_LocalVariableReadNode || rk == NK_SelfNode || rk == NK_NilNode || rk == NK_LocalVariableWriteNode || tvw_names_local(nt, blk, x, 0) || tvw_names_local(nt, r, x, 0)) return 0; long long line = nt_int(nt, r, "node_line", 0), file = nt_int(nt, r, "node_file", 0); int scope_of = c->nscope[id]; int w = nt_new_node(nt, "LocalVariableWriteNode"); int st = w >= 0 ? nt_new_node(nt, "StatementsNode") : -1; int par = st >= 0 ? nt_new_node(nt, "ParenthesesNode") : -1; if (par < 0) return 0; nt_node_set_str(nt, w, "name", x); nt_node_set_int(nt, w, "depth", nt_int(nt, id, "depth", 0)); nt_node_set_ref(nt, w, "value", r); nt_node_set_arr(nt, st, "body", &w, 1); nt_node_set_ref(nt, par, "body", st); int made[3] = { w, st, par }; comp_grow_node_arrays(c); for (int k = 0; k < 3; k++) { nt_node_set_int(nt, made[k], "node_line", line); nt_node_set_int(nt, made[k], "node_file", file); c->nscope[made[k]] = scope_of; } nt_node_set_ref(nt, v, "receiver", par); return 1; } static void desugar_literal_const_get(Compiler *c) { NodeTable *nt = (NodeTable *)c->nt; int n0 = nt->count; for (int id = 0; id < n0; id++) { if (nt_kind(nt, id) != NK_CallNode || !sp_streq(nt_str(nt, id, "name"), "const_get") || nt_ref(nt, id, "block") >= 0) continue; int rv = nt_ref(nt, id, "receiver"); if (rv < 0 || nt_kind(nt, rv) != NK_ConstantReadNode || !sp_streq(nt_str(nt, rv, "name"), "Object")) continue; int an = nt_ref(nt, id, "arguments"), ac = 0; const int *av = an >= 0 ? nt_arr(nt, an, "arguments", &ac) : NULL; if (ac != 1) continue; NodeKind ak = nt_kind(nt, av[0]); const char *lit = ak == NK_SymbolNode ? nt_str(nt, av[0], "value") : ak == NK_StringNode ? nt_str(nt, av[0], "content") : NULL; if (!lit || !*lit || strlen(lit) >= 256) continue; char path[256]; snprintf(path, sizeof path, "%s", lit[0] == ':' && lit[1] == ':' ? lit + 2 : lit); /* each segment a constant name the program defines */ char *segs[16]; int ns = 0, ok = 1; for (char *p = path; ok && *p; ) { if (ns == 16 || !(*p >= 'A' && *p <= 'Z')) { ok = 0; break; } segs[ns++] = p; char *q = p; while (*q && *q != ':') { if (!((*q >= 'A' && *q <= 'Z') || (*q >= 'a' && *q <= 'z') || (*q >= '0' && *q <= '9') || *q == '_')) ok = 0; q++; } if (!*q) break; if (q[1] != ':' || !q[2]) { ok = 0; break; } *q = 0; p = q + 2; } for (int i = 0; ok && i < ns; i++) ok = lcg_defined(nt, segs[i]); if (!ok || ns == 0) continue; long long line = nt_int(nt, id, "node_line", 0), file = nt_int(nt, id, "node_file", 0); int scope_of = c->nscope[id]; /* the prefix as a chain of reads; this node becomes the last segment */ int par = -1; for (int i = 0; i < ns - 1 && ok; i++) { int nn = nt_new_node(nt, i == 0 ? "ConstantReadNode" : "ConstantPathNode"); if (nn < 0) { ok = 0; break; } nt_node_set_str(nt, nn, "name", segs[i]); if (i > 0) nt_node_set_ref(nt, nn, "parent", par); nt_node_set_int(nt, nn, "node_line", line); nt_node_set_int(nt, nn, "node_file", file); comp_grow_node_arrays(c); c->nscope[nn] = scope_of; par = nn; } if (!ok) continue; nt_node_reset(nt, id, ns == 1 ? "ConstantReadNode" : "ConstantPathNode"); nt_node_set_str(nt, id, "name", segs[ns - 1]); if (ns > 1) nt_node_set_ref(nt, id, "parent", par); nt_node_set_int(nt, id, "node_line", line); nt_node_set_int(nt, id, "node_file", file); } } /* A step of an analysis phase: `call` runs with the pass's name `nm` on the node table, the name the nodes it appends are named after (nt_set_pass). The name before the step is back after it, so a step inside a step names what it appends itself. AN_STEP is an int expression, AN_STEP_V a statement. The phase lists call their passes through these rather than through AN_PASS, whose memo boundaries, tree skip and --timing rows are the fixpoint's: around the phase lists' calls they would change what the memo keeps, which passes run, and the --timing report. */ #define AN_STEP(nm, call) ({ const char *_stp = nt_set_pass((NodeTable *)c->nt, (nm)); int _str = (call); \ nt_set_pass((NodeTable *)c->nt, _stp); _str; }) #define AN_STEP_V(nm, call) do { const char *_stp = nt_set_pass((NodeTable *)c->nt, (nm)); call; \ nt_set_pass((NodeTable *)c->nt, _stp); } while (0) /* `&(expr)`: the parentheses carry no meaning for a block argument, but they hide the expression from every shape check downstream (`&blk` as a proc local, a symbol, a literal). Strip them once here (#3054). */ int desugar_paren_block_arg_left(Compiler *c, LowerCtx *lc, int id) { int ex = nt_ref(c->nt, id, "expression"); if (ex < 0) return 0; int un = nt_paren_single(c->nt, ex); return un >= 0 && un != ex; } int desugar_paren_block_arg_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntm = (NodeTable *)c->nt; if (!desugar_paren_block_arg_left(c, lc, id)) return 0; nt_node_set_ref(ntm, id, "expression", nt_paren_single(c->nt, nt_ref(ntm, id, "expression"))); return 1; } /* The same for a call's RECEIVER: `(Hash.new(0)).size` is the same call on the same hash, but the parentheses hid the receiver from every shape check that asks what it is -- the scan for a Hash.new used as a receiver, the anonymous-struct receiver scan, the empty-literal receiver marking -- and each left the value untyped, so its methods read as unresolved calls. They carry no meaning here either, so strip them once, as the block argument above does. The field of a node of that kind that holds the operand, or NULL. */ static const char *paren_operand_field(NodeKind k) { switch (k) { case NK_CallNode: return "receiver"; /* and the value a name is bound to: `h = (Hash.new(7))` hid the shape from the write-scanning resolvers the same way */ case NK_LocalVariableWriteNode: case NK_InstanceVariableWriteNode: case NK_ClassVariableWriteNode: case NK_ConstantWriteNode: return "value"; default: return NULL; } } int desugar_paren_operands_left(Compiler *c, LowerCtx *lc, int id) { const char *field = paren_operand_field(nt_kind(c->nt, id)); int rc = field ? nt_ref(c->nt, id, field) : -1; if (rc < 0) return 0; int un = nt_paren_single(c->nt, rc); return un >= 0 && un != rc; } int desugar_paren_operands_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntm = (NodeTable *)c->nt; if (!desugar_paren_operands_left(c, lc, id)) return 0; const char *field = paren_operand_field(nt_kind(ntm, id)); nt_node_set_ref(ntm, id, field, nt_paren_single(c->nt, nt_ref(ntm, id, field))); return 1; } /* `Struct.send(:new, :a)`, `Data.public_send(:define, :x)`: a class maker reached through a literal-name send. The send is retargeted onto the direct call only inside the fixpoint, after the passes that register the class it makes have run, so `P = Data.send(:define, :x)` left P undefined and its first use raised NameError. Retarget these here, as the later pass would. */ int desugar_send_class_makers_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *ntm = c->nt; const char *nm = nt_str(ntm, id, "name"); if (!nm || !(sp_streq(nm, "send") || sp_streq(nm, "public_send") || sp_streq(nm, "__send__"))) return 0; int rv = nt_ref(ntm, id, "receiver"); if (rv < 0 || nt_kind(ntm, rv) != NK_ConstantReadNode) return 0; const char *rn = nt_str(ntm, rv, "name"); int sa = nt_ref(ntm, id, "arguments"), sac = 0; const int *sav = sa >= 0 ? nt_arr(ntm, sa, "arguments", &sac) : NULL; if (sac < 1 || sac - 1 > 64 || nt_kind(ntm, sav[0]) != NK_SymbolNode) return 0; const char *mn = nt_str(ntm, sav[0], "value"); if (!rn || !mn) return 0; return (sp_streq(mn, "new") && (sp_streq(rn, "Struct") || sp_streq(rn, "Class"))) || (sp_streq(mn, "define") && sp_streq(rn, "Data")); } int desugar_send_class_makers_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntm = (NodeTable *)c->nt; if (!desugar_send_class_makers_left(c, lc, id)) return 0; int sa = nt_ref(ntm, id, "arguments"), sac = 0; const int *sav = nt_arr(ntm, sa, "arguments", &sac); const char *mn = nt_str(ntm, sav[0], "value"); int rest[64], nrest = sac - 1; for (int k = 0; k < nrest; k++) rest[k] = sav[k + 1]; int na = nt_new_node(ntm, "ArgumentsNode"); if (na < 0) return 0; comp_grow_node_arrays(c); c->nscope[na] = c->nscope[id]; nt_node_set_arr(ntm, na, "arguments", rest, nrest); char mbuf[16]; snprintf(mbuf, sizeof mbuf, "%s", mn); nt_node_set_str(ntm, id, "name", mbuf); nt_node_set_ref(ntm, id, "arguments", nrest > 0 ? na : -1); return 1; } /* `class Inner < self` written in a class's body: self is that class, so the superclass names it (activerecord's Promise::Complete < Promise). Classes are registered under their last name, and the name resolves to the class inside its own body. In a module's body, or in `class << self`, self is no superclass and is refused below. */ int desugar_self_superclass_left(Compiler *c, LowerCtx *lc, int id) { const NodeTable *ntc = c->nt; if (nt_kind(ntc, id) != NK_ClassNode) return 0; int cp = nt_ref(ntc, lc->owner, "constant_path"); int sc = nt_ref(ntc, id, "superclass"); return cp >= 0 && nt_str(ntc, cp, "name") && sc >= 0 && nt_kind(ntc, sc) == NK_SelfNode; } int desugar_self_superclass_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntc = (NodeTable *)c->nt; if (!desugar_self_superclass_left(c, lc, id)) return 0; const char *pn = nt_str(ntc, nt_ref(ntc, lc->owner, "constant_path"), "name"); int sc = nt_ref(ntc, id, "superclass"); int cr = nt_new_node(ntc, "ConstantReadNode"); if (cr < 0) return 0; nt_node_set_str(ntc, cr, "name", pn); comp_grow_node_arrays(c); c->nscope[cr] = c->nscope[sc]; nt_node_set_ref(ntc, id, "superclass", cr); return 1; } /* `RetryOptions = Options.new(:max, :interval)` where Options < Struct: the Struct.new class below Options, as in superclass position below */ /* the Struct subclass K a `X = K.new(..)` write at id makes a class below, or NULL */ static const char *struct_new_write_class(const NodeTable *ntc, int id) { int v = nt_ref(ntc, id, "value"); if (v < 0 || nt_kind(ntc, v) != NK_CallNode || !sp_streq(nt_str(ntc, v, "name"), "new")) return NULL; int rv = nt_ref(ntc, v, "receiver"); if (rv < 0 || (nt_kind(ntc, rv) != NK_ConstantReadNode && nt_kind(ntc, rv) != NK_ConstantPathNode)) return NULL; return struct_subclass_maker(ntc, nt_str(ntc, rv, "name"), 0) ? nt_str(ntc, rv, "name") : NULL; } int desugar_struct_subclass_new_write_left(Compiler *c, LowerCtx *lc, int id) { return struct_new_write_class(c->nt, id) != NULL; } int desugar_struct_subclass_new_write_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntc = (NodeTable *)c->nt; if (!struct_new_write_class(ntc, id)) return 0; int v = nt_ref(ntc, id, "value"); int rv = nt_ref(ntc, v, "receiver"); char kbuf[256]; snprintf(kbuf, sizeof kbuf, "%s", nt_str(ntc, rv, "name")); int st = nt_new_node(ntc, "ConstantReadNode"); if (st < 0) return 0; nt_node_set_str(ntc, st, "name", "Struct"); comp_grow_node_arrays(c); c->nscope[st] = c->nscope[rv]; nt_node_set_ref(ntc, v, "receiver", st); nt_node_set_str(ntc, id, "struct_parent", kbuf); return 1; } /* A superclass is a constant or a call (`Struct.new(:a)`); the parser peels parentheses around one expression. Anything else -- `(A rescue B)`, `(x; Base)`, a local variable -- was read as no superclass and the class silently became a subclass of Object: refuse it. Of the calls, only the class makers are read: `Struct.new(..)`, `Data.define(..)`, and `Class.new` / `Class.new(Base)` without a block, whose anonymous class adds nothing, so the class inherits Base directly. Any other call (`class A < f`) was Object as well. */ int refuse_superclass_exprs_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntc = (NodeTable *)c->nt; int sc = nt_ref(ntc, id, "superclass"); if (sc < 0) return 0; NodeKind sk = nt_kind(ntc, sc); if (sk == NK_ConstantReadNode || sk == NK_ConstantPathNode) return 0; const char *what = nt_type(ntc, sc); if (sk == NK_CallNode) { if (is_struct_call(c, sc)) return 0; int rv = nt_ref(ntc, sc, "receiver"); const char *mn = nt_str(ntc, sc, "name"); /* `class RetryOptions < Options.new(:max, :interval)` where Options < Struct: the Struct.new class below Options, which becomes its parent (#9292) */ if (rv >= 0 && (nt_kind(ntc, rv) == NK_ConstantReadNode || nt_kind(ntc, rv) == NK_ConstantPathNode) && mn && sp_streq(mn, "new") && struct_subclass_maker(ntc, nt_str(ntc, rv, "name"), 0)) { char kbuf[256]; snprintf(kbuf, sizeof kbuf, "%s", nt_str(ntc, rv, "name")); int st = nt_new_node(ntc, "ConstantReadNode"); if (st >= 0) { nt_node_set_str(ntc, st, "name", "Struct"); comp_grow_node_arrays(c); c->nscope[st] = c->nscope[rv]; nt_node_set_ref(ntc, sc, "receiver", st); nt_node_set_str(ntc, id, "struct_parent", kbuf); return 1; } } int ca = nt_ref(ntc, sc, "arguments"), ac = 0; const int *av = ca >= 0 ? nt_arr(ntc, ca, "arguments", &ac) : NULL; if (rv >= 0 && nt_kind(ntc, rv) == NK_ConstantReadNode && sp_streq(nt_str(ntc, rv, "name"), "Class") && mn && sp_streq(mn, "new") && nt_ref(ntc, sc, "block") < 0) { /* the anonymous class in between is remembered: reflection that would name it is refused (resolve_parents) */ if (ac == 0) { nt_node_set_ref(ntc, id, "superclass", -1); nt_node_set_int(ntc, id, "anon_super", 1); return 1; } if (ac == 1 && (nt_kind(ntc, av[0]) == NK_ConstantReadNode || nt_kind(ntc, av[0]) == NK_ConstantPathNode)) { nt_node_set_ref(ntc, id, "superclass", av[0]); nt_node_set_int(ntc, id, "anon_super", 1); return 1; } } what = "a call other than Struct.new, Data.define, a Struct subclass's new or a blockless Class.new(Constant)"; } int ln = (int)nt_int(ntc, id, "node_line", 0); const char *file = nt_file_path(ntc, (int)nt_int(ntc, id, "node_file", 0)); if (!file || !*file) file = ntc->source_file; if (!file || !*file) file = "source.rb"; fprintf(stderr, "spinel: %s:%d: unsupported superclass expression (%s): " "write the superclass as a constant\n", file, ln, what); exit(1); } /* A block written with its own rescue clause (`do ... rescue ... end`) has a BeginNode where every other block has a StatementsNode, so the body read as empty and the block's value was nil. Wrap it once here (#3710). */ int desugar_rescue_block_bodies_left(Compiler *c, LowerCtx *lc, int id) { int bd = nt_ref(c->nt, id, "body"); return bd >= 0 && nt_kind(c->nt, bd) != NK_StatementsNode; } int desugar_rescue_block_bodies_at(Compiler *c, LowerCtx *lc, int id) { NodeTable *ntm = (NodeTable *)c->nt; if (!desugar_rescue_block_bodies_left(c, lc, id)) return 0; int bd = nt_ref(ntm, id, "body"); int st = nt_new_node(ntm, "StatementsNode"); if (st < 0) return 0; nt_node_set_arr(ntm, st, "body", &bd, 1); nt_node_set_ref(ntm, id, "body", st); comp_grow_node_arrays(c); return 1; } static void an_phase_desugar_register(Compiler *c) { comp_poly_candidates_reset(); comp_descendants_reset(); comp_scope_index_set_frozen(0); /* scope shape changes during the passes below */ /* scope 0 = top level */ Scope *top = comp_scope_new(c, NULL, -1); { top->body = nt_ref(c->nt, c->nt->root_id, "statements"); comp_touch_scope(top->idx); } prog_file_scopes(c); /* and each required file's (--load-model=graph) */ for (int r = 0; r < nt_nroots(c->nt); r++) reject_retry_in_ensure(c, nt_root(c->nt, r), 0, 0); if (g_exc_abort) reject_exc_abort_constructs(c); LowerCtx lc; lower_begin(c, &lc); HostStep hs; /* class_eval "" -> that code, grafted into the body. First, so every pass below reads the grafted code as it reads the program's */ host_begin(&hs, &lc); AN_STEP_V("desugar_static_class_eval", desugar_static_class_eval(c, &hs)); host_end(c, &hs); /* a class body's loop over a constant table that defines constants and classes, run now: its classes are written ones for every pass below */ AN_STEP_V("desugar_table_loops", desugar_table_loops(c)); /* `m(&nil)` is the blockless call */ lower_run(c, &lc, LW_NIL_BLOCK_ARG); lower_run(c, &lc, LW_CONST_SET_WRITE); /* M.const_set(:N, v) -> M::N = v, a constant write to every pass */ lower_run(c, &lc, LW_THREAD_START); /* Thread.start / .fork -> Thread.new */ lower_run(c, &lc, LW_MARSHAL_RESTORE); /* Marshal.restore -> Marshal.load */ lower_run(c, &lc, LW_SOCKET_OPEN); /* TCPSocket.open(h, p) -> TCPSocket.new(h, p) */ lower_run(c, &lc, LW_SYSTEM_OPTIONS); /* system(..., out:) -> spawn + wait */ lower_run(c, &lc, LW_IO_POPEN_BLOCK); /* IO.popen(cmd) { } -> pipe + spawn + wait */ lower_run(c, &lc, LW_USER_BACKTICK); /* `cmd` -> `("cmd") beside a def of ` */ lower_run(c, &lc, LW_LITERAL_UNDEF_METHOD); /* undef_method :a in a body -> undef a */ lower_run(c, &lc, LW_EXPLICIT_INITIALIZE_CALLS); /* initialize(x) in a method -> a copy of it as a method */ lower_run(c, &lc, LW_NOARG_DISPLAY_WRAPPERS); /* def inspect(i = 0) -> + `__noarg inspect` = inspect */ /* a bare constant CRuby's lookup cannot reach, bound by its leaf name to a nested definition: refused while the source still says where each reference is written */ AN_STEP_V("refuse_unreachable_bare_constants", refuse_unreachable_bare_constants(c)); /* the parentheses around a block argument, a receiver or a bound value hide the expression from every shape check below */ lower_run(c, &lc, LW_PAREN_BLOCK_ARG); lower_run(c, &lc, LW_PAREN_OPERANDS); lower_run(c, &lc, LW_SEND_CLASS_MAKERS); lower_run(c, &lc, LW_SELF_SUPERCLASS); lower_run(c, &lc, LW_STRUCT_SUBCLASS_NEW_WRITE); lower_run(c, &lc, LW_REFUSE_SUPERCLASS_EXPRS); lower_run(c, &lc, LW_RESCUE_BLOCK_BODIES); AN_STEP_V("desugar_cext_decls", desugar_cext_decls(c)); lower_run(c, &lc, LW_MARK_SYM_PROC_BLOCKS); /* { |_spx| _spx.m } is `&:m` */ lower_run(c, &lc, LW_BODY_LOCAL_SCOPES); /* a class body local -> a name of its own */ desugar_root_scoped_constants(c); /* ::Name -> Name (#4801) */ lower_run(c, &lc, LW_ERRNO_ALIASES); /* Errno::EWOULDBLOCK -> Errno::EAGAIN where they share a number */ desugar_builtin_reopen_named_superclass(c); /* class Rational < Numeric -> class Rational */ desugar_builtin_reopen_self_class(c); /* self.class in a reopened Hash -> Hash */ AN_STEP_V("desugar_time_singleton_bare_ctor", desugar_time_singleton_bare_ctor(c)); /* bare `at` in a Time class method -> Time.at */ desugar_builtin_method_defined_conds(c); /* unless String.method_defined?(:m) -> unless true */ AN_STEP_V("desugar_builtin_reopen_self_cmethods", desugar_builtin_reopen_self_cmethods(c)); /* self.sqrt in an Integer class method -> Integer.sqrt */ AN_STEP_V("desugar_engine_branches", desugar_engine_branches(c)); lower_run(c, &lc, LW_PAREN_DEF_BODY); /* def m = (a; b) -> def m; a; b; end */ desugar_def_unless_method_defined(c); /* def m .. end unless method_defined?(:m), answered in program order -- ahead of the runtime-condition defs, which it answers statically */ AN_STEP_V("desugar_class_body_self_calls", desugar_class_body_self_calls(c)); /* self.v = x / v in a class body -> Cls.v = x / Cls.v */ AN_STEP_V("desugar_conditional_defs", desugar_conditional_defs(c)); AN_STEP_V("desugar_concat_builtin_version", desugar_concat_builtin_version(c)); lower_run(c, &lc, LW_MARK_MATCH_RANGES); /* when 0..0.05 matches as 0.0..0.05 */ lower_run(c, &lc, LW_MARK_RANGE_LOCALS); lower_run(c, &lc, LW_DUPLICATE_UNDERSCORE_PARAMS); /* |_, _| -> |_, _dup1| */ lower_run(c, &lc, LW_ENCODING_QUERIES); /* Encoding.default_internal -> nil, find("x") -> a constant */ lower_run(c, &lc, LW_ALIAS_METHOD_STRING_NAMES); /* alias_method "k", "y" -> alias_method :k, :y */ lower_run(c, &lc, LW_ALIAS_METHOD_VALUES); /* r = alias_method :a, :b -> alias_method :a, :b; r = :a */ AN_STEP_V("desugar_inherited_aliases", desugar_inherited_aliases(c)); /* alias_method :next, :inherited_m -> a forwarding def */ AN_STEP_V("desugar_reader_aliases_before_redef", desugar_reader_aliases_before_redef(c)); /* attr_reader :v; alias w v; def v -> def w = @v */ AN_STEP_V("desugar_readers_over_inherited_defs", desugar_readers_over_inherited_defs(c)); /* def r in Base; attr_reader :r in Sub < Base -> Sub: def r = @r */ lower_run(c, &lc, LW_REASSIGNED_BLOCK_PARAMS); /* |p| p = other -> |p__bpin| p = p__bpin; p = other */ AN_STEP_V("desugar_instance_eval_defs", desugar_instance_eval_defs(c)); /* K.instance_eval { def m } -> K.class_eval { def K.m } */ desugar_class_reopen(c); /* class Class / Class.class_eval -> a module */ AN_STEP_V("desugar_inline_struct_blocks", desugar_inline_struct_blocks(c)); /* Struct.new(:x) { .. } -> (__spinel_struct_1 = Struct.new(:x) { .. }) */ AN_STEP_V("desugar_singleton_class_meta", desugar_singleton_class_meta(c)); /* Foo.singleton_class.attr_accessor :a -> class << Foo; attr_accessor :a; end */ /* X = Class.new(B) do..end -> class X < B; ..; end, a body the host grafts (its class_eval "") and lowers as a class body */ host_begin(&hs, &lc); AN_STEP_V("desugar_class_new_blocks", desugar_class_new_blocks(c, &hs)); host_end(c, &hs); desugar_struct_block_anonymous(c); /* Point = Struct.new do p self end: anonymous until written */ AN_STEP_V("desugar_singleton_body_self", desugar_singleton_body_self(c)); /* class << X; p self; end -> refused */ /* include M / extend M -> M.included/extended(base)'s body, a splice the host grafts (its class_eval "") and lowers with the rows run so far */ host_begin(&hs, &lc); AN_STEP_V("desugar_included_hooks", desugar_included_hooks(c, &hs)); host_end(c, &hs); AN_STEP_V("desugar_eval_method_block", desugar_eval_method_block(c)); /* k.class_eval(&o.method(:m)) -> o.m(k) */ AN_STEP_V("desugar_inherited_hooks", desugar_inherited_hooks(c)); /* class A < B -> B.inherited(A) first in A's body */ AN_STEP_V("desugar_class_body_object_acts", desugar_class_body_object_acts(c)); /* class B; tap { } -> self.tap { } */ lower_run(c, &lc, LW_NIL_BLOCK_ARG); AN_STEP_V("desugar_literal_const_get", desugar_literal_const_get(c)); /* Object.const_get("A::B") -> A::B */ lower_run(c, &lc, LW_ITSELF_RECEIVER); /* x.itself -> x (--share-strings) */ lower_run(c, &lc, LW_TAP_VALUE_WRITE); /* x = E.tap { } -> x = (x = E).tap { } (--share-strings) */ AN_STEP_V("desugar_self_const_get", desugar_self_const_get(c)); /* const_get(:X) / self::X in a class method -> per subclass */ AN_STEP_V("desugar_holder_block_self_const", desugar_holder_block_self_const(c)); /* self.class::X in a Data.define block -> Holder::X */ AN_STEP_V("desugar_dynamic_const_get", desugar_dynamic_const_get(c)); /* M.const_get(expr) -> a table of M's constants */ AN_STEP_V("desugar_builtin_reopen_self_calls", desugar_builtin_reopen_self_calls(c)); /* class Hash; def m = each {..} -> self.each */ AN_STEP_V("desugar_builtin_ivars", desugar_builtin_ivars(c)); /* class Array; def m = @x -> self.__bivar_get(:@x) */ AN_STEP_V("desugar_object_method_builtin_overrides", desugar_object_method_builtin_overrides(c)); /* Hash#m + Object#m -> Object#m branching on self */ AN_STEP_V("desugar_builtin_reopen_methods", desugar_builtin_reopen_methods(c)); /* class Hash; def m -> Object#m guarded by is_a?(Hash) */ AN_STEP_V("desugar_const_ivar_access", desugar_const_ivar_access(c)); /* C.instance_variable_set(:@x, v) -> C.__spinel_civset_x(v) */ AN_STEP_V("desugar_class_eval_ivars", desugar_class_eval_ivars(c)); /* C.class_eval { @x = v } -> C.class_eval { C.__spinel_civset_x(v) } */ AN_STEP_V("desugar_body_ivars", desugar_body_ivars(c)); /* module-body @x read / in a block -> Mod.__spinel_civget_x */ AN_STEP_V("desugar_extended_module_attrs", desugar_extended_module_attrs(c)); lower_run(c, &lc, LW_BLK_PARAM_WRITES); /* `blk = proc {}` on a &blk param -> a fresh local */ lower_run(c, &lc, LW_REST_PARAM_WRITES); /* `args = args.first` on a *args param -> a fresh local */ lower_run(c, &lc, LW_YIELD_IN_CLOSURE); /* `yield` inside proc { } -> blk.call(...) */ lower_run(c, &lc, LW_SAFE_NAV_ATTR_WRITE); /* r&.a op= v -> (t = r; t.nil? ? nil : t.a op= v) */ lower_run(c, &lc, LW_CONST_ATTR_OP_ASSIGN); /* Klass.a op= v -> Klass.a = Klass.a op v (a class-level accessor) */ desugar_body_module_eval(c); /* self.module_eval do S end in a body -> S */ AN_STEP_V("desugar_extend_self", desugar_extend_self(c)); /* `extend self` -> a def self.m twin per def */ AN_STEP_V("desugar_kernel_reopen", desugar_kernel_reopen(c)); /* module Kernel; def m -> a top-level def m */ AN_STEP_V("desugar_ffi_library_functions", desugar_ffi_library_functions(c)); /* attach_function :f -> def self.f (the ffi gem) */ AN_STEP_V("desugar_native_func_defs", desugar_native_func_defs(c)); /* native_func :f -> def self.f = Mod.f (a module value finds it) */ AN_STEP_V("desugar_explicit_receiver_mixin", desugar_explicit_receiver_mixin(c)); /* K.prepend(M) as a statement -> class K; prepend M; end */ desugar_singleton_class_mixin(c); /* singleton_class.prepend(M), class << self; include M -> extend M */ /* extend Enumerable -> an instance-side twin each Enumerable call on the class goes to */ host_begin(&hs, &lc); AN_STEP_V("desugar_extend_enumerable", desugar_extend_enumerable(c, &hs)); host_end(c, &hs); AN_STEP_V("desugar_attr_const_splat", desugar_attr_const_splat(c)); /* attr_accessor(*FLAGS), FLAGS = %i[a b] -> attr_accessor(:a, :b) */ AN_STEP_V("desugar_singleton_attr", desugar_singleton_attr(c)); /* singleton_class.attr_accessor :x -> def self.x / def self.x= */ AN_STEP_V("desugar_sclass_readers_through_class", desugar_sclass_readers_through_class(c)); /* class << self; attr_reader :x read as obj.class.x -> def x = @x */ AN_STEP_V("desugar_handle_attr_accessor", desugar_handle_attr_accessor(c)); /* Thread.attr_accessor :x -> a Thread reopening on its store */ AN_STEP_V("desugar_fiber_prepended_init", desugar_fiber_prepended_init(c)); /* a prepended Fiber#initialize -> __spinel_init run on the new fiber */ AN_STEP_V("desugar_handle_ivars", desugar_handle_ivars(c)); /* @x in a Fiber / Thread reopening or its mixin -> the same store */ desugar_constant_path_self_alias(c); /* A::B::C = remove_const(:C) (C a class) -> dropped */ /* builtins/enumerable.rb, spliced by the parser: its definitions become the receiver-taking top-level functions before any scope is built */ AN_STEP_V("desugar_builtins", desugar_builtins(c)); lower_run(c, &lc, LW_STORED_ENUM_EACH); /* ahead of the per-call-site copies below, which its calls take as a written `v + x` does */ AN_STEP_V("desugar_reopened_op_write", desugar_reopened_op_write(c)); /* v += x with Integer#+ reopened -> v = v + x */ /* builtins/integer.rb, float.rb, comparable.rb: the same idea, one more container per file (analyze_desugar.c's sp_bx_* table) */ AN_STEP_V("desugar_builtin_scalar_defs", desugar_builtin_scalar_defs(c)); prog_settle_files(c); /* (--load-model=graph: a file no live require reaches is dropped; one a nested require places is placed) */ prog_refuse_order(c); /* (--load-model=graph: what these read across files) */ lower_run(c, &lc, LW_SCOPE_NUMBERED_BLOCK_PARAMS); lower_run(c, &lc, LW_SINGLETON_CLASS_DEFINE_METHOD); /* singleton_class.define_method -> define_singleton_method */ lower_run(c, &lc, LW_EMPTY_HASH_COMPARE_BY_IDENTITY); /* {}.compare_by_identity -> Hash.new.compare_by_identity */ lower_run(c, &lc, LW_DEFINE_METHOD_PROC_ARG); /* define_method(:m, -> { }) -> define_method(:m) { } */ AN_STEP_V("desugar_module_function_aliases", desugar_module_function_aliases(c)); /* alias_method :n, :x + module_function :n -> def n (x's body) */ lower_run(c, &lc, LW_DEFINE_METHOD_CAPTURES); /* class body local a define_method reads -> a global */ lower_run(c, &lc, LW_DEFINE_METHOD_KEYWORDS); /* define_method(:m) { |k: 1| } -> def m(k: 1) */ lower_run(c, &lc, LW_NAME_ANON_BLOCK_KWREST); lower_run(c, &lc, LW_RENAME_SHADOWING_BLOCK_PARAMS); /* `:m.to_proc.call(r, a)` -> `r.m(a)`, before the to_proc rewrite below turns the receiver into a fixed-arity lambda (#3097). */ lower_run(c, &lc, LW_SYM_TO_PROC_CALL); lower_run(c, &lc, LW_ENUM_CHAIN_SHAPES); /* each_char.with_index / each.with_object; twice: the second sees the shapes the first made */ lower_run(c, &lc, LW_BUILTIN_KWSPLAT); lower_run(c, &lc, LW_RIGHTWARD_PATTERN); /* `x => pat` -> one-arm case/in */ lower_run(c, &lc, LW_MATCH_PREDICATE); /* `x in pat` -> case/in true/else false */ lower_run(c, &lc, LW_SORT_BY_WITH_INDEX); /* sort_by.with_index -> each_with_index.sort_by */ lower_run(c, &lc, LW_FORWARDING_TO_REST_CALLEE); /* def m(...) = f(*a, **k) -> anon *, ** */ lower_run(c, &lc, LW_ANON_BLOCK_PARAM); /* def m(&) = f(&) -> &__anon_block */ lower_run(c, &lc, LW_BLOCK_IMPLICIT_REST); /* |x,| -> |x, __implicit_rest| (destructures) */ lower_run(c, &lc, LW_MULTI_VALUE_JUMP); /* return *a, b -> return [*a, b] */ lower_run(c, &lc, LW_BLOCK_DESTRUCTURE_PARAMS); /* |a,(b,c),d| -> flat param + `b,c = __destr` */ lower_run(c, &lc, LW_FOR_NONLOCAL_INDEX); /* for $g in xs -> for __for in xs; $g = __for */ lower_run(c, &lc, LW_ENUM_FOR_WITH_ARGS); /* return enum_for(__method__, a) unless block_given? -> Enumerator.new { m(a) { } } */ lower_run(c, &lc, LW_ENUMERATOR_PRODUCE); /* Enumerator.produce -> fiber generator */ lower_run(c, &lc, LW_PARAM_DEFAULT_ASSIGNS_LOCAL); /* def m(p = (x = e)) reading x -> sentinel + callee guard */ lower_run(c, &lc, LW_RECURSIVE_PARAM_DEFAULTS); /* def m(x, y = m(..)) -> default helper method */ qualify_colliding_consts(c); qualify_colliding_classes(c); /* the walk is a step of its own: a form it runs splices through the host, which lowers each splice as it is made; the rows a splice leaves to the program run once, at the walk's end */ host_begin(&hs, &lc); AN_STEP_V("elab_program", elab_program(c, &hs)); /* walk_scope of each file's top level, and the load record */ host_end(c, &hs); lower_finish(c, &lc); /* SPINEL_LOWER_CHECK: the report, and no C */ prog_refuse_loads(c); if (g_dump_load_record) { elab_dump(c, stdout); exit(0); } /* the load phase's points, as the walk leaves them */ AN_STEP_V("expand_static_splat_args", expand_static_splat_args(c, 0, c->nt->count)); AN_STEP_V("register_singleton_defs", register_singleton_defs(c)); /* def CONST.m / def x.m -> synthesized subclass */ register_structs(c); elab_record_builders(c); desugar_struct_index_ctor(c); fix_struct_block_scopes(c); register_module_functions(c); register_locals(c); AN_STEP_V("desugar_block_arg_order", desugar_block_arg_order(c)); /* recv.m(a, &expr): receiver, args, then the block (#4992) */ register_attrs(c); register_method_visibility(c); register_aliases(c); AN_STEP_V("rewrite_builtin_alias_self_calls", rewrite_builtin_alias_self_calls(c, 0)); /* plus_without(o) -> self.+(o), the builtin's */ register_undefs(c); if (elab_check_on(c)) elab_check_surface(c); register_globals_consts(c); AN_STEP_V("rewrite_const_alias_receivers", rewrite_const_alias_receivers(c)); AN_STEP_V("rewrite_builtin_cm_alias_calls", rewrite_builtin_cm_alias_calls(c)); /* Time.stamp -> Time.now, the builtin's */ AN_STEP_V("route_self_builtin_calls", route_self_builtin_calls(c)); /* U.rand(2) before U's def self.rand -> rand(2), Kernel's */ desugar_env_values(c); AN_STEP_V("register_ffi_decls", register_ffi_decls(c)); AN_STEP_V("register_cext_layouts", register_cext_layouts(c)); an_const_presence(c); AN_STEP_V("rewrite_const_alias_raise_targets", rewrite_const_alias_raise_targets(c)); } /* Rescue-variable typing: each `rescue X => e` binding's slot. */ static void type_rescue_bindings(Compiler *c) { /* rescue variables (`rescue => e`) are typed as exception objects. When the arm names exactly one user exception subclass that carries ivars, type the binding as that object instead so `e.` reads resolve and the carried object's fields are reachable (#1415); otherwise plain TY_EXCEPTION. A name reused across rescue arms (`rescue A => e` ... `rescue B => e`) interns to one LocalVar, so it may only specialize when every arm binding it agrees on the same class -- otherwise the slot would collapse onto one of the types and mis-read the others. */ /* Collect the rescue arms that bind a local (`rescue X => e`) once; the unanimity check then compares arms against this small list instead of rescanning the whole node table per arm (was O(rescues * nodes)). */ { int cap = 0, rn = 0; struct { int id; const char *nm; Scope *vsc; int spec; } *arms = NULL; for (int id = 0; id < c->nt->count; id++) { NodeKind k = nt_kind(c->nt, id); if (k != NK_RescueNode) continue; int ref = nt_ref(c->nt, id, "reference"); if (ref < 0 || nt_kind(c->nt, ref) != NK_LocalVariableTargetNode) continue; const char *nm = nt_str(c->nt, ref, "name"); if (!nm) continue; Scope *vsc = comp_scope_of(c, ref); scope_local_intern(vsc, nm); /* ensure the LocalVar exists for every arm first */ if (rn >= cap) { cap = cap ? cap * 2 : 16; arms = realloc(arms, sizeof(*arms) * (size_t)cap); } arms[rn].id = id; arms[rn].nm = nm; arms[rn].vsc = vsc; arms[rn].spec = rescue_arm_spec_cid(c, id); if (arms[rn].spec < 0) arms[rn].spec = bare_rescue_spec_cid(c, id); rn++; } int *rescue_ids = malloc(sizeof(int) * (size_t)(rn ? rn : 1)); for (int i = 0; i < rn; i++) rescue_ids[i] = arms[i].id; for (int i = 0; i < rn; i++) { /* unanimity across every same-name rescue arm in the same scope */ int unanimous = arms[i].spec; for (int j = 0; j < rn && unanimous >= 0; j++) { if (j == i || arms[j].vsc != arms[i].vsc || !sp_streq(arms[j].nm, arms[i].nm)) continue; if (arms[j].spec != arms[i].spec) unanimous = -1; } LocalVar *lv = scope_local_intern(arms[i].vsc, arms[i].nm); lv_set_type(lv, unanimous >= 0 ? ty_object(unanimous) : TY_EXCEPTION); /* the name also holds what an ordinary write put there (`e = Foo.new` before `rescue => e`): the pin would retype those reads as the exception, so the slot holds either, boxed (#4923) */ if (rescue_name_written_elsewhere(c, arms[i].vsc, arms[i].nm, rescue_ids, rn)) lv_set_type(lv, TY_POLY); lv->is_block_param = 1; /* set externally; don't reset in the fixpoint */ } free(rescue_ids); free(arms); } } /* Rescue-variable typing, class parents, inheritance and mixins, the block-aware marks, the synthesized enumeration helpers and the empty-literal marks (analyze_program's steps, in their order) */ static void an_phase_class_structure(Compiler *c) { type_rescue_bindings(c); AN_STEP_V("resolve_parents", resolve_parents(c)); AN_STEP_V("desugar_data_positional_new", desugar_data_positional_new(c)); topup_forwarding_arity(c); AN_STEP_V("expand_struct_forwarding_super", expand_struct_forwarding_super(c)); refuse_super_init_value(c); refuse_readonly_global_write(c); inherit_members(c); reject_dynamic_mixin_args(c); AN_STEP_V("desugar_module_prepends", desugar_module_prepends(c)); AN_STEP_V("register_includes", register_includes(c)); register_include_attrs(c); AN_STEP_V("register_extends", register_extends(c)); AN_STEP_V("register_prepends", register_prepends(c)); /* again, for the bodies the mixins cloned into their includers: a clone's binding is a slot of the copy's own, which the first pass never saw */ type_rescue_bindings(c); AN_STEP_V("rewrite_prepended_builtin_supers", rewrite_prepended_builtin_supers(c)); /* super in a module prepended into Range -> Range's own */ AN_STEP_V("rewrite_ivar_reflection_supers", rewrite_ivar_reflection_supers(c)); /* super in the program's own ivar reflection -> Object's */ AN_STEP_V("desugar_method_added_hook", desugar_method_added_hook(c)); /* def in a class body -> def; method_added(:name): needs the parents and extends, and runs before class methods are specialized per class */ AN_STEP_V("desugar_reader_index_write", desugar_reader_index_write(c)); /* all[k] = v through a reader of @all -> (all; @all[k] = v) */ /* again, now that modules have put their methods in: an alias can take an inherited method an ancestor got from one */ resolve_inherited_aliases(c); settle_object_builtin_aliases(c); /* alias original_dup dup: the builtin's, or an ancestor's */ AN_STEP_V("settle_mixin_aliases", settle_mixin_aliases(c)); /* include A; alias_method :b, :a; include B: A's a */ AN_STEP_V("rewrite_builtin_alias_self_calls", rewrite_builtin_alias_self_calls(c, 1)); /* super into Array, X.new / X[], implicit self, in the class's methods and the copies its modules put in (#7449) */ AN_STEP_V("rewrite_array_subclass_calls", rewrite_array_subclass_calls(c)); AN_STEP_V("rewrite_attr_supers", rewrite_attr_supers(c)); rewrite_self_attr_empty_writes(c); AN_STEP_V("specialize_inherited_cls_new", specialize_inherited_cls_new(c)); /* the modules main includes or extends at a file's top level, in the order the load phase runs them (the load record's), so bare method calls can resolve to those modules' methods. At the top level the two differ in who gets the methods: an include gives them to Object, an extend to main alone (#3787). One under a condition may not run, and is left out, as one main prepends (CRuby has no main.prepend). */ { int nmx = 0; const LrMixin *mx = elab_class_mixins(c, -1, &nmx); for (int i = 0; i < nmx; i++) { if (mx[i].mod < 0 || mx[i].kind == LR_PREPEND || mx[i].kind == LR_SG_PREPEND || elab_point_may(c, mx[i].node)) continue; c->toplevel_includes = realloc(c->toplevel_includes, sizeof(int) * (size_t)(c->ntoplevel_includes + 1)); c->toplevel_extends = realloc(c->toplevel_extends, (size_t)(c->ntoplevel_includes + 1)); if (!c->toplevel_includes || !c->toplevel_extends) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } c->toplevel_extends[c->ntoplevel_includes] = mx[i].kind == LR_EXTEND; c->toplevel_includes[c->ntoplevel_includes++] = mx[i].mod; } } AN_STEP_V("rewrite_ffi_dynamic_calls", rewrite_ffi_dynamic_calls(c)); /* Mod.Name / bare Name with no def -> the attached-function table */ /* `iterator?` is the deprecated alias of `block_given?`; rename it up front (implicit/self receiver) so the block-aware marking and codegen below serve it identically (#3251). */ for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, "iterator?")) continue; int r = nt_ref(c->nt, id, "receiver"); NodeKind rk = nt_kind(c->nt, r); if (r < 0 || (rk == NK_SelfNode)) nt_node_set_str((NodeTable *)c->nt, id, "name", "block_given?"); } /* mark block-aware methods (contain yield or block_given?) -- these are inlined at every call site so block_given? reflects the actual site */ for (int id = 0; id < c->nt->count; id++) { const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) continue; if (ty_k == NK_YieldNode) comp_scope_of(c, id)->yields = 1; else if (ty_k == NK_CallNode) { int r = nt_ref(c->nt, id, "receiver"); NodeKind rk = nt_kind(c->nt, r); int self_or_none = r < 0 || (rk == NK_SelfNode); const char *nm = nt_str(c->nt, id, "name"); /* ...unless the method keeps its named &blk as a real parameter: it stores or hands it on (blk_param_escapes), or calls itself, which no inlining can terminate while the function form, the proc forwarded down the recursion, is plain C recursion (#5377). Its block_given? then asks whether that parameter is NULL. */ Scope *bgs = comp_scope_of(c, id); if (self_or_none && nm && sp_streq(nm, "block_given?") && !blk_param_escapes(c, bgs) && !(bgs && bgs->blk_param && bgs->blk_param[0] && !scope_has_yield_node(c, bgs) && scope_calls_itself(c, (int)(bgs - c->scopes)))) bgs->yields = 1; } } /* A method whose `super` lands on a yielding parent yields too. The parent has no real function -- it is inlined at every call site -- so the child must be inlined as well, or the caller's block never reaches the parent and the call is left referencing a function nobody emitted. Iterate: a chain of supers settles in a few rounds. */ { int ns = c->nscopes; char *has_super = (char *)calloc(ns > 0 ? (size_t)ns : 1, 1); if (has_super) { for (int id = 0; id < c->nt->count; id++) { if (!super_forwards_caller_block(c, id)) continue; Scope *sc = comp_scope_of(c, id); if (!sc) continue; int idx = (int)(sc - c->scopes); if (idx >= 0 && idx < ns) has_super[idx] = 1; } for (int round = 0; round < 8; round++) { int changed = 0; for (int i = 0; i < ns; i++) { Scope *s = &c->scopes[i]; if (s->yields || !has_super[i] || s->class_id < 0 || !s->name) continue; int mi = super_reach(c, s); if (mi >= 0 && mi < ns && c->scopes[mi].yields) { s->yields = 1; changed = 1; } } if (!changed) break; } /* The same for a parent that keeps a named `&blk`: a super that brings no block of its own hands the parent the block this method was called with, declared or not. Without a block parameter to hold it that block was dropped, and the parent answered as if called without one (#4852). The method takes a synthetic one, which its callers fill as they fill any `&blk`. */ char *bare_super = (char *)calloc((size_t)ns, 1); if (bare_super) { for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_SuperNode && ty_k != NK_ForwardingSuperNode) continue; if (nt_ref(c->nt, id, "block") >= 0) continue; Scope *sc = comp_scope_of(c, id); int idx = sc ? (int)(sc - c->scopes) : -1; if (idx >= 0 && idx < ns) bare_super[idx] = 1; } for (int round = 0; round < 8; round++) { int changed = 0; for (int i = 0; i < ns; i++) { Scope *s = &c->scopes[i]; if (s->yields || s->blk_param || !bare_super[i] || s->class_id < 0 || !s->name) continue; int mi = super_reach(c, s); Scope *pm = mi >= 0 && mi < ns ? &c->scopes[mi] : NULL; if (!(pm ? pm->blk_param && pm->blk_param[0] && !pm->yields : super_new_init_keeps_block(c, s))) continue; s->blk_param = strdup("__sblk__"); LocalVar *sblk = scope_local_intern(s, s->blk_param); if (sblk) { lv_set_type(sblk, TY_PROC); sblk->is_param = 1; } changed = 1; } if (!changed) break; } free(bare_super); } free(has_super); } } /* Bare-Enumerable-via-#each: synthesize a per-class `__enum_to_a` helper that materializes #each into an array, used by desugar_enum_method_recv for bare Enumerable calls (`obj.map{}`). Done before the fixpoint so the helper body is typed, and before reachability so it is kept only when actually called. Explicit `to_enum`/`enum_for` no longer route here -- synth_to_enum_generators builds a real lazy Enumerator helper, retargeted by desugar_to_enum. */ /* Reject the legacy `Struct::Name` constant path (the string-named Struct form), deliberately dropped -- see limitations.md and the matching check in register_struct_members (#3080). A bare `Struct.new("Foo", ...)` definition is compile-time only, so the `Struct::Foo` access is where the dropped form surfaces; reject it with a pointer to the modern `Name = Struct.new(...)`. */ { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_ConstantPathNode) continue; int par = nt_ref(nt, id, "parent"); const char *pn = (par >= 0 && nt_kind(nt, par) == NK_ConstantReadNode) ? nt_str(nt, par, "name") : NULL; if (pn && sp_streq(pn, "Struct")) { int ln = (int)nt_int(nt, id, "node_line", 0); const char *file = nt->source_file ? nt->source_file : "source.rb"; fprintf(stderr, "spinel: %s:%d: the Struct::Name constant path is not supported " "(legacy string-named Struct); use `Name = Struct.new(...)`\n", file, ln); exit(1); } } } AN_STEP_V("synth_struct_each", synth_struct_each(c)); AN_STEP_V("synth_enum_to_a", synth_enum_to_a(c)); AN_STEP_V("synth_to_enum_generators", synth_to_enum_generators(c)); /* pre-fixpoint: an empty `{}` block-method receiver types as the STR_POLY hash so infer_block_params declares its |k, v| params (#2336). */ mark_empty_hash_receivers(c); mark_empty_hash_enum_locals(c); mark_empty_literal_tails(c); AN_STEP_V("mark_empty_literal_args", mark_empty_literal_args(c)); /* after the arg/receiver marks: a bare `{}` with no other context takes its variant from the peer it is compared against (#3040) or from the key it is indexed with (#3028, #3029) */ /* Same argument, for the four passes mark_empty_literal_args is called alongside: none of them creates or renames a scope either, and each crosses the node table with method lookups underneath. */ comp_scope_index_set_frozen(1); AN_STEP_V("mark_mixed_key_hash_locals", mark_mixed_key_hash_locals(c)); AN_STEP_V("mark_empty_hash_cmp_peers", mark_empty_hash_cmp_peers(c)); g_eh_pre_infer = 1; comp_touch_global(); /* (a mode inference reads) */ (void)mark_empty_hash_key_ctx(c); g_eh_pre_infer = 0; comp_touch_global(); /* (a mode inference reads) */ AN_STEP_V("mark_empty_hash_const_writes", mark_empty_hash_const_writes(c)); comp_scope_index_set_frozen(0); } /* Explicit self receivers for implicit-self reads, then the block-forward inlining: a callee whose block is only called or forwarded is inlined at its call sites (analyze_program's steps, in their order) */ /* A bare identifier inside a class method that names a `class << self` attr reader is an implicit-self read of that singleton attribute; give it an explicit self receiver so it resolves like `self.reader` instead of failing as an undefined local/method (#3108). */ static void desugar_sclass_reader_self_recv(Compiler *c) { NodeTable *ntm = (NodeTable *)c->nt; int nend = ntm->count; for (int id = 0; id < nend; id++) { if (nt_kind(ntm, id) != NK_CallNode) continue; if (nt_ref(ntm, id, "receiver") >= 0) continue; int args = nt_ref(ntm, id, "arguments"); int ac = 0; if (args >= 0) nt_arr(ntm, args, "arguments", &ac); if (ac != 0 || nt_ref(ntm, id, "block") >= 0) continue; const char *nm = nt_str(ntm, id, "name"); if (!nm) continue; Scope *s = comp_scope_of(c, id); if (!s || !s->is_cmethod || s->class_id < 0 || s->class_id >= c->nclasses) continue; if (!comp_is_sg_reader(&c->classes[s->class_id], nm)) continue; nt_set_self_receiver(ntm, id, nt_new_node(ntm, "SelfNode")); } if (ntm->count != nend) comp_grow_node_arrays(c); } /* A receiverless `instance_variable_get`/`_set`/`_defined?` inside an instance method operates on self; give it an explicit self receiver so it lowers through the object-receiver handler instead of failing as an unsupported bare call (#3059). The synthesized SelfNode must inherit the call's scope so infer_type resolves self to the instance (not top-level main), which the return-type fixpoint depends on. */ static void desugar_ivar_reflection_self_recv(Compiler *c) { NodeTable *ntm = (NodeTable *)c->nt; int nend = ntm->count; int *self_ids = NULL, *self_scopes = NULL, npend = 0, cap = 0; for (int id = 0; id < nend; id++) { if (nt_kind(ntm, id) != NK_CallNode) continue; if (nt_ref(ntm, id, "receiver") >= 0) continue; const char *nm = nt_str(ntm, id, "name"); if (!nm || (!sp_streq(nm, "instance_variable_get") && !sp_streq(nm, "instance_variable_set") && !sp_streq(nm, "instance_variable_defined?") && !sp_streq(nm, "remove_instance_variable"))) continue; Scope *s = comp_scope_of(c, id); if (!s || s->is_cmethod || s->class_id < 0 || s->class_id >= c->nclasses) continue; int sid = nt_new_node(ntm, "SelfNode"); nt_set_self_receiver(ntm, id, sid); if (npend == cap) { cap = cap ? cap * 2 : 8; self_ids = realloc(self_ids, (size_t)cap * sizeof(int)); self_scopes = realloc(self_scopes, (size_t)cap * sizeof(int)); } self_ids[npend] = sid; self_scopes[npend] = c->nscope[id]; npend++; } if (ntm->count != nend) comp_grow_node_arrays(c); for (int k = 0; k < npend; k++) c->nscope[self_ids[k]] = self_scopes[k]; free(self_ids); free(self_scopes); } /* A receiverless call to a Struct/Data-inherited builtin (to_h, with, to_a, members, deconstruct, ...) inside an instance method of that Struct/Data class operates on self; give it an explicit self receiver so it lowers through the object-receiver struct emit instead of failing as an undefined method (to_h) or an unsupported bare call (with) (#3226). Skip names the class shadows with a member accessor or a user method -- those already resolve. The SelfNode inherits the call's scope so self infers as the instance (mirrors the instance_variable_get rewrite above). */ static void desugar_struct_builtin_self_recv(Compiler *c) { /* Data and Struct expose different inherited surfaces: `with` is Data-only, while to_a/values/size/length/dig/each_pair are Struct-only. Rewriting a name the class does not actually inherit would turn a correct NameError into a bad self-dispatch, so gate each list by the class kind. */ static const char *const data_native[] = { "to_h", "with", "members", "deconstruct", "deconstruct_keys", NULL }; static const char *const struct_native[] = { "to_h", "to_a", "values", "members", "size", "length", "dig", "deconstruct", "deconstruct_keys", "each_pair", NULL }; NodeTable *ntm = (NodeTable *)c->nt; int nend = ntm->count; int *self_ids = NULL, *self_scopes = NULL, npend = 0, cap = 0; for (int id = 0; id < nend; id++) { if (nt_kind(ntm, id) != NK_CallNode) continue; if (nt_ref(ntm, id, "receiver") >= 0) continue; const char *nm = nt_str(ntm, id, "name"); if (!nm) continue; Scope *s = comp_scope_of(c, id); if (!s || s->is_cmethod || s->class_id < 0 || s->class_id >= c->nclasses) continue; if (!c->classes[s->class_id].is_struct) continue; const char *const *native = c->classes[s->class_id].is_data ? data_native : struct_native; int is_native = 0; for (int t = 0; native[t]; t++) if (sp_streq(nm, native[t])) { is_native = 1; break; } if (!is_native) continue; /* a member accessor of that name beats the inherited builtin */ char mn[272]; snprintf(mn, sizeof mn, "@%s", nm); if (comp_ivar_index(&c->classes[s->class_id], mn) >= 0) continue; /* a user method of that name is dispatched directly, not as self.builtin */ if (call_target(c, id, s->class_id, nm, 0, NULL) >= 0) continue; int sid = nt_new_node(ntm, "SelfNode"); nt_set_self_receiver(ntm, id, sid); if (npend == cap) { cap = cap ? cap * 2 : 8; self_ids = realloc(self_ids, (size_t)cap * sizeof(int)); self_scopes = realloc(self_scopes, (size_t)cap * sizeof(int)); } self_ids[npend] = sid; self_scopes[npend] = c->nscope[id]; npend++; } if (ntm->count != nend) comp_grow_node_arrays(c); for (int k = 0; k < npend; k++) c->nscope[self_ids[k]] = self_scopes[k]; free(self_ids); free(self_scopes); } static void an_phase_block_inline(Compiler *c) { AN_STEP_V("desugar_begin_args", desugar_begin_args(c)); AN_STEP_V("desugar_diverging_args", desugar_diverging_args(c)); AN_STEP_V("desugar_sclass_reader_self_recv", desugar_sclass_reader_self_recv(c)); AN_STEP_V("desugar_ivar_reflection_self_recv", desugar_ivar_reflection_self_recv(c)); AN_STEP_V("desugar_struct_builtin_self_recv", desugar_struct_builtin_self_recv(c)); /* `&block` + block.call: a method whose block parameter never escapes (every read is a `.call` receiver or a `&block` forward) is inlined at its call sites exactly like a yielding method. The block-param slot is then virtual -- the literal block flows in like an implicit yield. */ /* Reverse flags built once: blk_call_recv[id] -- a `.call` CallNode has receiver == id; blk_arg_expr[id] -- a `&arg` forwards id. They answer the per-read "approved use?" test below in O(1) instead of an inner whole-table scan (which made the escape analysis O(methods * reads * nodes)). */ char *blk_call_recv = (char *)calloc((size_t)c->nt->count, 1); char *blk_arg_expr = (char *)calloc((size_t)c->nt->count, 1); /* blk_cond_pred[id] -- id is the condition of an if/unless/while/until. A bare `blk` there asks only "was a block given?", which emit_cond answers at the inline site without naming the slot. It is not an approved use (the method still cannot be spliced through it) but it is not a VALUE use either, so it must not drag the method into the lowered form -- that turns the folded `yield if block` into a real proc call. The four are named rather than taking every `predicate` ref: CaseNode and CaseMatchNode spell their SUBJECT `predicate` too, and `case blk` wants the block's value like any other read. Naming the boolean contexts also makes the failure direction the safe one -- a node type left out is read as a value use, which lowers a method that need not have been. */ char *blk_cond_pred = (char *)calloc((size_t)c->nt->count, 1); for (int p = 0; blk_cond_pred && p < c->nt->count; p++) { const char *cty = nt_type(c->nt, p); NodeKind ck = nt_kind(c->nt, p); if (!cty || !(ck == NK_IfNode || ck == NK_UnlessNode || ck == NK_WhileNode || ck == NK_UntilNode)) continue; int pr = nt_ref(c->nt, p, "predicate"); if (pr >= 0 && pr < c->nt->count) blk_cond_pred[pr] = 1; } for (int p = 0; (blk_call_recv && blk_arg_expr) && p < c->nt->count; p++) { const char *pty = nt_type(c->nt, p); NodeKind pk = nt_kind(c->nt, p); if (!pty) continue; if (pk == NK_CallNode) { const char *cn = nt_str(c->nt, p, "name"); int r = nt_ref(c->nt, p, "receiver"); if (cn && sp_streq(cn, "call")) { if (r >= 0 && r < c->nt->count) blk_call_recv[r] = 1; } /* `blk.nil?` / `!blk` only ASK about the block; the block goes nowhere. `__blk.call unless __blk.nil?` is how an ingested Rails helper forwards an anonymous block, and reading the nil? test as an escape kept the helper out of line, lifted its caller's block into a proc, and celled the caller's String buffer -- which took the by-reference ABI away from the buffer, so every append the caller made vanished (#4477). A bare `if blk` is NOT approved here: the methods it would newly inline include forwarders called without a block, whose `inner(&blk)` then names a block that no inline site declares (block_forward_nilcheck, toplevel_extend_block_param). */ else if (cn && (sp_streq(cn, "nil?") || sp_streq(cn, "!"))) { int an = 0; int aa = nt_ref(c->nt, p, "arguments"); if (aa >= 0) nt_arr(c->nt, aa, "arguments", &an); if (an == 0 && r >= 0 && r < c->nt->count) blk_call_recv[r] = 1; } } else if (pk == NK_BlockArgumentNode) { int e = nt_ref(c->nt, p, "expression"); if (e >= 0 && e < c->nt->count) blk_arg_expr[e] = 1; } } /* `Fiber.new(&blk)` / `Thread.new(&blk)` keep the block in the fiber (sp_Fiber_new_callable), which reads it as a value: that forward is an escape, so the method keeps a real &blk rather than being yield-inlined with no slot for it. */ for (int p = 0; p < c->nt->count; p++) { if (nt_kind(c->nt, p) != NK_CallNode) continue; const char *cn = nt_str(c->nt, p, "name"); int r = nt_ref(c->nt, p, "receiver"), bk = nt_ref(c->nt, p, "block"); if (!cn || !sp_streq(cn, "new") || r < 0 || bk < 0) continue; if (nt_kind(c->nt, r) != NK_ConstantReadNode || nt_kind(c->nt, bk) != NK_BlockArgumentNode) continue; const char *rn = nt_str(c->nt, r, "name"); if (!rn || (!sp_streq(rn, "Fiber") && !sp_streq(rn, "Thread"))) continue; int e = nt_ref(c->nt, bk, "expression"); if (e >= 0 && e < c->nt->count) blk_arg_expr[e] = 0; } char *scope_keeps_blk = a_scopes_keeping_block(c, blk_call_recv, blk_arg_expr); /* For each forwarded `&blk` argument, the user method it is handed to: a forward is only harmless when the CALLEE lets the block go no further. One that stores it (`$p = b`) keeps it past the call, so the forwarder cannot be inlined either -- its captures need cells (#3772). */ int *blk_fwd_callee = (int *)malloc(sizeof(int) * (size_t)(c->nt->count > 0 ? c->nt->count : 1)); if (blk_fwd_callee) { for (int p = 0; p < c->nt->count; p++) blk_fwd_callee[p] = -1; for (int p = 0; p < c->nt->count; p++) { if (nt_kind(c->nt, p) != NK_CallNode) continue; int fb = nt_call_block_arg(c->nt, p); if (fb < 0) continue; int fe = nt_ref(c->nt, fb, "expression"); if (fe < 0 || fe >= c->nt->count) continue; const char *fn = nt_str(c->nt, p, "name"); if (!fn) continue; int fmi = -1; int frecv = nt_ref(c->nt, p, "receiver"); if (frecv < 0) { fmi = call_target(c, p, -1, fn, CT_TOP, NULL); Scope *fs = comp_scope_of(c, p); if (fmi < 0 && fs && fs->class_id >= 0) fmi = call_target(c, p, fs->class_id, fn, fs->is_cmethod ? CT_CMETHOD : 0, NULL); } else { /* `h.store(&b)`: an object receiver resolves through its class, and a constant one through its class methods. Without this the callee was unknown and a forward into a method that STORES the block counted as harmless -- the block was inlined and its captures never celled (#3783). */ TyKind frt = infer_type(c, frecv); if (ty_is_object(frt)) fmi = call_target(c, p, ty_object_class(frt), fn, 0, NULL); else if (nt_kind(c->nt, frecv) == NK_ConstantReadNode || nt_kind(c->nt, frecv) == NK_ConstantPathNode) { int fci = const_node_class_index(c, frecv); if (fci >= 0) fmi = call_target(c, p, fci, fn, CT_CMETHOD, NULL); /* `Klass.new(&b)` with no `def self.new` hands the block to initialize, which may store it */ if (fci >= 0 && fmi < 0 && sp_streq(fn, "new")) fmi = comp_method_in_chain(c, fci, "initialize", NULL); } /* This pass runs before the receiver's type settles, so an unresolved receiver falls back to the name: any block-taking method that could be the callee decides, which at worst leaves a forwarder uninlined. */ if (fmi < 0) { /* Several methods can share the name, and the first one found is not necessarily the callee. Take the one that KEEPS the block if any does: assuming the harmless candidate compiled the same program right or wrong depending on the order the classes were defined in (#3786). */ int first = -1; int keeper = a_name_keeps_block(c, fn, scope_keeps_blk, &first); fmi = keeper >= 0 ? keeper : first; } } blk_fwd_callee[fe] = fmi; } } /* The yield-inlining analysis below reads scope shape but never creates or renames a scope, so the shape index is stable across it -- the same argument mark_empty_literal_args makes for its own body. Frozen, the method lookups underneath it (per scope * per node, through a_block_is_lifted -> infer_type -> infer_call) take the O(1) hash index instead of the unfrozen O(nscopes) reverse scan, and the name-keyed memo in an_user_defines_or_reads -- which stands down while the index is unfrozen -- is live for them. */ comp_scope_index_set_frozen(1); char *inline_cand = (char *)calloc((size_t)(c->nscopes > 0 ? c->nscopes : 1), 1); int cfwd = 64, nfwd = 0; int *fwd_from = (int *)malloc(sizeof(int) * (size_t)cfwd); int *fwd_to = (int *)malloc(sizeof(int) * (size_t)cfwd); for (int mi = 0; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; if (!m->blk_param) continue; /* instance_eval/exec trampolines are inlined at call sites by their own dedicated splice; don't treat the &block forward as a yield here. */ if (m->class_id >= 0 && !m->is_cmethod && m->name && comp_trampoline_kind(c, m->class_id, m->name, NULL)) continue; /* Anonymous `&`: nameless, so it can only be forwarded -- always safe to inline (there is no escaping read to worry about). */ if (!m->blk_param[0]) { m->yields = 1; continue; } /* Mark nodes inside proc/lambda bodies nested within this method. A blk_param read there is a real capture-escape: the proc runs independently and needs blk to live in a heap cell. */ char *inproc_m = (char *)calloc((size_t)c->nt->count, 1); bi_scope_nodes(c); if (inproc_m) { for (int id = bi_first[mi]; id >= 0; id = bi_next[id]) { /* A Fiber/Enumerator/Thread `.new { }` block runs as an independent closure on its own fiber stack, and a `{ }` block lifted to a standalone proc (passed to a method that keeps a real &block) captures like a proc literal: a blk_param read inside either is a real capture-escape, so the method must keep a heap-materialized &blk (not be yield-inlined). */ /* ... and a literal block handed to a method on a receiver whose type has not settled yet (`@reg.set { handler.call(v) }`, `@reg` typed later): the callee is judged by name, as the forward above is, and one that keeps its block lifts this one. Asking a_block_is_lifted alone answered "spliced", the method was inlined with no storage for `handler`, and codegen -- on settled types -- lifted the block and named a `_cell_handler` nothing declared. */ if (!a_proc_create_or_lifted(c, id) && !a_block_lifted_by_callee_name(c, id, scope_keeps_blk)) continue; int body = a_proc_body(c, id); if (body >= 0) a_mark_subtree(c, body, inproc_m); } } int escapes = 0, uses = 0, value_use = 0; for (int id = bi_first[mi]; id >= 0 && !(escapes && value_use); id = bi_next[id]) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_LocalVariableReadNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, m->blk_param)) continue; /* A read inside a nested proc body is a capture-escape: the proc holds a reference to blk independently of the call site. */ if (inproc_m && inproc_m[id]) { escapes = 1; value_use = 1; continue; } uses++; /* approved: receiver of a `.call`, or expression of a `&block` arg */ int ok = (blk_call_recv && blk_call_recv[id]) || (blk_arg_expr && blk_arg_expr[id]); if (!ok) { escapes = 1; /* Everything the inline path can still answer without the slot -- `blk.nil?`, `!blk`, and a bare `blk` in a condition -- is approved above or folded by emit_cond. What is left genuinely wants the block's VALUE, and no call site can supply one. */ if (!(blk_cond_pred && blk_cond_pred[id])) value_use = 1; } /* ... but a forward into a user method that keeps the block is an escape all the same, one call deeper (#3772). Only while nothing has escaped yet: the scan used to stop at the first escape, and an already disqualified method adding edges here would crowd real ones out of the fixed-size list. */ else if (!escapes && blk_arg_expr && blk_arg_expr[id] && blk_fwd_callee) { int callee = blk_fwd_callee[id]; if (callee >= 0 && callee < c->nscopes) { Scope *cs2 = &c->scopes[callee]; if (cs2->blk_param && cs2->blk_param[0] && nfwd < cfwd) { fwd_from[nfwd] = mi; fwd_to[nfwd] = callee; nfwd++; } } } } free(inproc_m); /* `super`, bare or with `&blk`, hands this method's block to the parent method, which may keep it: an edge like a forward into a user method. */ if (!escapes && uses > 0 && m->class_id >= 0 && m->name && nfwd < cfwd) { int sm = a_super_target(c, m); Scope *ss = sm >= 0 ? &c->scopes[sm] : NULL; if (ss && ss->blk_param && ss->blk_param[0] && a_scope_super_passes_block(c, m)) { fwd_from[nfwd] = mi; fwd_to[nfwd] = sm; nfwd++; } } /* Recorded for the lowering pass further down: a method that wants its block's VALUE cannot be spliced, and if it also contains a literal `yield` the inline path is the only one it has -- which emits no storage for the block while the value use still names it. */ m->blk_param_value_use = value_use; if (!escapes && uses > 0) { /* Don't mark yields=1 if the method has an explicit return: emit_inlined_call would reject inlining anyway (scope_has_return), but the method would then be skipped in emission because yields=1 -- causing undefined references. */ int has_ret = 0; for (int id2 = bi_first[mi]; id2 >= 0 && !has_ret; id2 = bi_next[id2]) { NodeKind ty2_k = nt_kind(c->nt, id2); if (ty2_k == NK_ReturnNode) has_ret = 1; } /* Nor if the method calls itself: inlining a self-recursive block-driving method cannot terminate (the depth-cap diagnostic), while the real (yields=0) function form -- &blk as a materialized sp_Proc * param, the self-call forwarding it -- compiles to plain C recursion and already works for the explicit-return shape above. */ if (!has_ret && !scope_calls_itself(c, mi)) inline_cand[mi] = 1; } } /* A forwarder may only be inlined when every method it hands the block to is inlined too: an un-inlined callee keeps the block as a real proc past the call, so the forwarder's captures must live in cells (#3772). */ for (int round = 0; round < 8; round++) { int changed2 = 0; for (int e = 0; e < nfwd; e++) if (inline_cand[fwd_from[e]] && !inline_cand[fwd_to[e]] && !c->scopes[fwd_to[e]].yields) { inline_cand[fwd_from[e]] = 0; changed2 = 1; } if (!changed2) break; } for (int mi = 0; mi < c->nscopes; mi++) if (inline_cand[mi]) c->scopes[mi].yields = 1; comp_scope_index_set_frozen(0); free(inline_cand); free(fwd_from); free(fwd_to); free(blk_call_recv); free(blk_arg_expr); free(scope_keeps_blk); free(blk_cond_pred); free(blk_fwd_callee); } /* Is the value of the def `d` used, so that it is the Symbol it names? A statement before its list's last, the program's last statement and a class or module body's last when the class's value is not used throw it away; anything else may hand it on. The climb goes through statement lists (vp_climb over the wrappers), and on from a class whose body's value the def is. Parentheses take the value: the emitter renders a parenthesized def as its Symbol (`rv = (def o.m = 1)`, whose write a rewrite holds aside, so the parentheses have no parent node), so the table holds the name. */ int an_def_value_used(const NodeTable *nt, const int *par, int v) { for (long guard = nt->count; guard >= 0; guard--) { VpUp up; VpUpKind ended = vp_climb(nt, par, v, VPS_NONE, &up); for (int n = v; n != up.node && n >= 0; n = par[n]) if (nt_kind(nt, par[n]) == NK_ParenthesesNode) return 1; switch (ended) { case VPU_ROOT: case VPU_DROPPED: return 0; case VPU_ENDED: { NodeKind k = nt_kind(nt, up.parent); if ((k == NK_ClassNode || k == NK_ModuleNode || k == NK_SingletonClassNode) && nt_ref(nt, up.parent, "body") == up.node) { v = up.parent; continue; } /* a required file's top level (--load-model=graph): nothing reads its value, since its require answers true or false */ if (k == NK_ProgramNode && nt_int(nt, up.parent, "load_file", 0) > 0) return 0; return 1; } default: return 1; } } return 1; } /* ---- String methods whose value no call reads (Scope.ret_unread) ---- `def write(n) ... @out << ")" end` answers @out, and a String held in a shared handle answers a copy of its bytes. Every call then copied the whole buffer, and a writer called once per node of a tree took time quadratic in its output, though every caller dropped the value. Where no call that can reach the method reads the value, its body runs as statements instead. A call of the method's name can reach it unless its receiver's class is known and is neither the method's class, an ancestor, nor a descendant: self in an instance method of a class, or a receiver typed as one. A method of a module, of a singleton or at the top level is reached by every call of its name, and so is a class method. Each `super` in a def of the name counts as a call of it. A call's value is dropped when it stands as a statement before its list's last, in a loop body, in an ensure, or in a builtin iterator's block (each on an Array), reaching there through the tails of statement lists, the arms of conditionals and the right of && / ||. The tail of a def hands it on as that def's value, read where that def's value is. Anything else reads it. A name written as a Symbol or a String where a method name is taken (send, method, alias, respond_to?, &:name) is read, every name is when a call computes the name it sends, and the names the runtime calls on its own are never candidates. */ /* the walk's view of the program: each node's parent and how many places hold it (ur_walk), which iterators a program class overrides, and whether a block can be handed to instance_exec and the like as a value */ typedef struct { const int *par; const unsigned char *nref; unsigned char iter_over[16]; int rebind_any; } UrCtx; /* a name the runtime or the compiler may call with no call of its own in the program, or one that is not a plain method name */ static int ur_name_excluded(const char *nm) { static const char *const names[] = { "initialize", "method_missing", "respond_to_missing?", "respond_to?", "message", "full_message", "detailed_message", "exception", "to_json", "as_json", "name", "succ", "pred", "marshal_dump", "_dump", "encode_with", "pretty_print", "pretty_inspect", "deconstruct", "deconstruct_keys", "each_pair", "dup", "clone", "with", NULL }; if (!nm || !nm[0] || !strncmp(nm, "to_", 3)) return 1; /* the conversions the runtime asks for */ if (!(nm[0] == '_' || (nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z'))) return 1; if (nm[strlen(nm) - 1] == '=') return 1; return method_name_implicitly_invoked(nm) || is_definition_hook_name(nm) || str_in(nm, names); } /* a plain String method, emitted as a function of its own under the name its def gives it (not a prepend's shadow, which a `super` reaches). An --ext-entry is no candidate: the host reads its value. Neither is a version entry: the calls it answers carry the method's own name, not the entry's. */ static int ur_candidate(Compiler *c, const Scope *s) { return s->ret == TY_STRING && s->name && s->def_node >= 0 && nt_kind(c->nt, s->def_node) == NK_DefNode && nt_str(c->nt, s->def_node, "name") && sp_streq(nt_str(c->nt, s->def_node, "name"), s->name) && s->body >= 0 && !s->yields && !s->is_proc_form && !s->cs_synth && !s->is_ext_entry && !s->entry && !ur_name_excluded(s->name); } /* the builtin iterators that drop their block's value */ static const char *const ur_dropping_iters[] = { "each", "each_with_index", "reverse_each", "each_index", "each_key", "each_value", "times", "upto", "downto", NULL }; /* Does a program class that is a builtin container (or reopens one) define iterator `it`, so that its block's value may be read after all? */ static int ur_iter_overridden(Compiler *c, const char *it) { static const char *const builtins[] = { "Array", "Hash", "Range", "Integer", NULL }; for (int k = 0; k < c->nclasses; k++) { int contained = 0; for (int a = k, d = 0; a >= 0 && d < 64 && !contained; a = c->classes[a].parent, d++) contained = c->classes[a].name && str_in(c->classes[a].name, builtins); if (contained && comp_method_in_chain(c, k, it, NULL) >= 0) return 1; } return 0; } /* Does the block of call `call` drop its value: a builtin iterator over a builtin Array, Hash, Range or Integer? */ static int ur_block_dropped(Compiler *c, const UrCtx *u, int call) { const NodeTable *nt = c->nt; const char *nm = nt_str(nt, call, "name"); int recv = nt_ref(nt, call, "receiver"); if (!nm || recv < 0) return 0; int k = -1; for (int i = 0; ur_dropping_iters[i]; i++) if (sp_streq(nm, ur_dropping_iters[i])) k = i; if (k < 0 || u->iter_over[k]) return 0; TyKind rt = comp_ntype(c, recv); if (sp_streq(nm, "times") || sp_streq(nm, "upto") || sp_streq(nm, "downto")) return rt == TY_INT; return ty_is_array(rt) || ty_is_hash(rt) || rt == TY_RANGE; } /* The edges ur_value_use climbs: the arms of an if, unless and case (and so their else and when clauses), the right of `&&` and both sides of `||` (its left is tested, so a node there is read: VpUp.reach), and a begin with no rescue clause, else or ensure. A begin with an ensure alone also answers its body; the climb goes on from it by hand. */ enum { UR_CLIMB = VPS_IF | VPS_UNLESS | VPS_CASE | VPS_LOGIC | VPS_BEGIN_PLAIN }; /* How the value of node v is taken: 0 dropped, 1 read, 2 + d handed on as the value of def d (its body's tail). nref counts each node's parents: a node the desugaring shares between two places is read. The climb (vp_climb, over UR_CLIMB) ends at a statement before its list's last (dropped), a loop's body (dropped) or its condition, an ensure clause (dropped), a def's body, the block of a builtin iterator that drops its value, or anything else, which reads it. */ static int ur_value_use(Compiler *c, const UrCtx *u, int v) { const NodeTable *nt = c->nt; const int *par = u->par; for (long guard = nt->count; guard >= 0; guard--) { VpUp up; VpUpKind ended = vp_climb(nt, par, v, UR_CLIMB, &up); for (int n = v;; n = par[n]) { if (u->nref[n] > 1) return 1; if (n == up.node) break; } if (up.reach) return 1; if (ended == VPU_DROPPED) return 0; if (ended != VPU_ENDED) return 1; int p = up.parent; switch (nt_kind(nt, p)) { case NK_WhileNode: case NK_UntilNode: return nt_ref(nt, p, "predicate") == up.node; case NK_BeginNode: if (nt_ref(nt, p, "statements") != up.node || nt_ref(nt, p, "rescue_clause") >= 0 || nt_ref(nt, p, "else_clause") >= 0) return 1; v = p; continue; case NK_DefNode: return nt_ref(nt, p, "body") == up.node ? 2 + p : 1; case NK_BlockNode: { int call = vp_up_block_call(nt, par, &up); return !(call >= 0 && nt_ref(nt, call, "block") == p && ur_block_dropped(c, u, call)); } case NK_EnsureNode: return 0; default: return 1; } } return 1; } /* the def a node sits in, -1 outside one */ static int ur_enclosing_def(const NodeTable *nt, const int *par, int v) { for (int d = 0; v >= 0 && d < 4096; v = par[v], d++) if (nt_kind(nt, v) == NK_DefNode) return v; return -1; } /* Is call n one that sends a name it computes (`send(x)`, `method(name)`, `x.to_proc`), or an `alias_method` either of whose names it computes (a call by the new name may read the old one's value)? */ static int ur_computed_send(const NodeTable *nt, int n) { static const char *const senders[] = { "send", "__send__", "public_send", "method", "public_method", "instance_method", "public_instance_method", "singleton_method", "alias_method", NULL }; const char *nm = nt_str(nt, n, "name"); if (!nm) return 0; if (sp_streq(nm, "to_proc")) { int r = nt_ref(nt, n, "receiver"); return r >= 0 && nt_kind(nt, r) != NK_SymbolNode; } if (!str_in(nm, senders)) return 0; int args = nt_ref(nt, n, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an == 0) return 0; for (int i = 0; i < (sp_streq(nm, "alias_method") ? an : 1); i++) { NodeKind k = nt_kind(nt, av[i]); if (k != NK_SymbolNode && k != NK_StringNode) return 1; } return 0; } /* Is `&e` (BlockArgumentNode b) a block the program may have computed from a name? Forwarding the method's own block parameter is not: what it holds came in through a `&` of some call, which is asked where it stands. */ static int ur_computed_block_arg(Compiler *c, const int *par, int b) { const NodeTable *nt = c->nt; int e = nt_ref(nt, b, "expression"); if (e < 0 || nt_kind(nt, e) == NK_SymbolNode) return 0; if (nt_kind(nt, e) == NK_LocalVariableReadNode) { int d = ur_enclosing_def(nt, par, b); int ps = d >= 0 ? nt_ref(nt, d, "parameters") : -1; int bp = ps >= 0 ? nt_ref(nt, ps, "block") : -1; const char *bn = bp >= 0 ? nt_str(nt, bp, "name") : NULL; const char *en = nt_str(nt, e, "name"); if (bn && en && sp_streq(bn, en)) return 0; } TyKind et = comp_ntype(c, e); return et == TY_SYMBOL || et == TY_POLY || et == TY_UNKNOWN; } /* Does the Symbol or String literal v name a method where it stands: an argument of a call that takes a method name (send, method, alias_method, define_method, respond_to? ...), an alias, a `&:name`, or a `:name.to_proc`? A literal anywhere else reaches such a call only as a computed name, which ur_computed_send answers for the whole program. */ static int ur_names_method(const NodeTable *nt, const int *par, int v) { static const char *const takers[] = { "send", "__send__", "public_send", "method", "public_method", "instance_method", "public_instance_method", "singleton_method", "define_method", "alias_method", "respond_to?", "method_defined?", "public_method_defined?", "private_method_defined?", "protected_method_defined?", "try", "inject", "reduce", "def_delegator", "def_delegators", "def_instance_delegator", "def_instance_delegators", "def_single_delegator", "def_single_delegators", "delegate", "instance_delegate", "single_delegate", NULL }; int p = par[v]; if (p < 0) return 1; NodeKind pk = nt_kind(nt, p); if (pk == NK_BlockArgumentNode || pk == NK_AliasMethodNode) return 1; if (pk == NK_CallNode) { /* `:name.to_proc` */ const char *nm = nt_str(nt, p, "name"); return nt_ref(nt, p, "receiver") == v && nm && sp_streq(nm, "to_proc"); } if (pk != NK_ArgumentsNode) return 0; int call = par[p]; const char *nm = call >= 0 && nt_kind(nt, call) == NK_CallNode ? nt_str(nt, call, "name") : NULL; return !nm || str_in(nm, takers); } /* the calls that run a block with a self of their own */ static const char *const ur_rebinders[] = { "instance_eval", "instance_exec", "class_eval", "class_exec", "module_eval", "module_exec", "define_method", "define_singleton_method", NULL }; /* Can a block be run with another self as a value: is an instance_exec (or the like) handed one with `&`? Then no block's self is known. */ static int ur_block_rebound(Compiler *c, const unsigned char *nref) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { if (!nref[id] || nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); int blk = nt_ref(nt, id, "block"); if (nm && blk >= 0 && nt_kind(nt, blk) == NK_BlockArgumentNode && str_in(nm, ur_rebinders)) return 1; } return 0; } /* Is class a among b's ancestors (b itself included)? */ static int ur_ancestor(Compiler *c, int a, int b) { for (int d = 0; b >= 0 && d < 256; b = c->classes[b].parent, d++) if (b == a) return 1; return 0; } /* Is local v written anywhere under node root? Asked of a short expression; a long one (over 256 nodes) answers yes. */ static int ur_local_written_under(const NodeTable *nt, int root, const char *v) { int stack[256], sp = 0, seen = 0; stack[sp++] = root; while (sp > 0) { int id = stack[--sp]; if (++seen > 256) return 1; const char *ty = nt_type(nt, id); NodeKind k = nt_kind(nt, id); const char *nm = nt_str(nt, id, "name"); if (ty && nm && sp_streq(nm, v) && strstr(ty, "LocalVariable") && k != NK_LocalVariableReadNode) return 1; int nr = nt_num_refs(nt, id), na = nt_num_arrs(nt, id); for (int i = 0; i < nr + na; i++) { int an = 1, one = i < nr ? nt_ref_at(nt, id, i) : -1; const int *av = i < nr ? &one : nt_arr_at(nt, id, i - nr, &an); for (int j = 0; av && j < an; j++) { if (av[j] < 0) continue; if (sp == 256) return 1; stack[sp++] = av[j]; } } } return 0; } /* The class a conjunct `v.is_a?(K)` (kind_of?, instance_of?) under the && chain x tests local v for, -1 for none */ static int ur_isa_conjunct(Compiler *c, int x, const char *v) { const NodeTable *nt = c->nt; for (int d = 0; x >= 0 && d < 64; d++) { NodeKind k = nt_kind(nt, x); if (k == NK_ParenthesesNode) { int body = nt_ref(nt, x, "body"), n = 0; const int *st = body >= 0 && nt_kind(nt, body) == NK_StatementsNode ? nt_arr(nt, body, "body", &n) : NULL; x = n == 1 ? st[0] : -1; continue; } if (k == NK_AndNode) { int r = ur_isa_conjunct(c, nt_ref(nt, x, "right"), v); if (r >= 0) return r; x = nt_ref(nt, x, "left"); continue; } if (k != NK_CallNode) return -1; const char *nm = nt_str(nt, x, "name"); int r = nt_ref(nt, x, "receiver"), args = nt_ref(nt, x, "arguments"), an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (!nm || (!sp_streq(nm, "is_a?") && !sp_streq(nm, "kind_of?") && !sp_streq(nm, "instance_of?")) || an != 1 || r < 0 || nt_kind(nt, r) != NK_LocalVariableReadNode || !nt_str(nt, r, "name") || !sp_streq(nt_str(nt, r, "name"), v)) return -1; NodeKind ak = nt_kind(nt, av[0]); if (ak != NK_ConstantReadNode && ak != NK_ConstantPathNode) return -1; /* by its last segment, as the emitted test names it */ int cls = const_node_class_index(c, av[0]); return cls >= 0 && !comp_class_is_module(c, &c->classes[cls]) ? cls : -1; } return -1; } /* Is node n in a body typed once per place it runs: a yielding method's, or a block that runs with a self of its own? */ static int ur_retyped(Compiler *c, const UrCtx *u, int n) { const Scope *s = comp_scope_of(c, n); if (!s || s->yields || s->is_proc_form) return 1; for (int a = n, d = 0; a >= 0 && d < 4096; a = u->par[a], d++) { NodeKind ak = nt_kind(c->nt, a); if (ak == NK_DefNode) break; if (ak == NK_BlockNode) { int call = u->par[a]; const char *cn = call >= 0 && nt_kind(c->nt, call) == NK_CallNode ? nt_str(c->nt, call, "name") : NULL; if (cn && str_in(cn, ur_rebinders)) return 1; } } return 0; } /* the class call n's receiver is known to be: self in an instance method of a class, a receiver typed as an instance of one, or a local the left of an enclosing && tested with is_a? (`o.is_a?(K) && o.m`); -1 otherwise */ static int ur_recv_class(Compiler *c, const UrCtx *u, int n) { const NodeTable *nt = c->nt; const int *par = u->par; int r = nt_kind(nt, n) == NK_CallNode ? nt_ref(nt, n, "receiver") : -1; if (r < 0 || nt_kind(nt, r) == NK_SelfNode) { /* a block that may run with another self (instance_eval, define_method ...) leaves self unknown */ for (int a = n, d = 0; a >= 0 && d < 4096; a = par[a], d++) { NodeKind ak = nt_kind(nt, a); if (ak == NK_DefNode) break; if (ak == NK_BlockNode || ak == NK_LambdaNode) { int call = par[a]; const char *cn = call >= 0 && nt_kind(nt, call) == NK_CallNode ? nt_str(nt, call, "name") : NULL; if (!cn || u->rebind_any || str_in(cn, ur_rebinders)) return -1; } } const Scope *s = comp_scope_of(c, n); return s && s->class_id >= 0 && !s->is_cmethod ? s->class_id : -1; } /* a type read where the body is typed once per place it runs (a yielding method's, spliced into each caller, or an instance_eval block's) is only one of its types */ TyKind rt = ur_retyped(c, u, n) ? TY_UNKNOWN : comp_ntype(c, r); if (ty_is_object(rt)) return ty_object_class(rt); const char *v = nt_kind(nt, r) == NK_LocalVariableReadNode ? nt_str(nt, r, "name") : NULL; for (int a = n, d = 0; v && a >= 0 && d < 64; a = par[a], d++) { int p = par[a]; NodeKind pk = p >= 0 ? nt_kind(nt, p) : NK_NONE; /* within the one expression: a parenthesized one's statements count */ if (p < 0 || pk == NK_DefNode || pk == NK_BlockNode || (pk == NK_StatementsNode && (par[p] < 0 || nt_kind(nt, par[p]) != NK_ParenthesesNode))) break; if (pk != NK_AndNode || nt_ref(nt, p, "right") != a) continue; int cls = ur_isa_conjunct(c, nt_ref(nt, p, "left"), v); if (cls >= 0 && !ur_local_written_under(nt, p, v)) return cls; } return -1; } /* Can call (or super) n reach method t? Not when its receiver's class cls is known (ur_recv_class) and is neither t's class, an ancestor nor a descendant, nor when every class its receiver can hold (cls_fact_node) is one that does not inherit t. */ static int ur_may_reach(Compiler *c, const UrCtx *u, const Scope *t, int n, int cls) { int k = t->class_id; /* a class the program follows from its construction: not a module, a reopened builtin (Object, whose methods every class inherits), a native class or a singleton */ if (k < 0 || t->is_cmethod || !cls_fact_bounded(c, k) || c->classes[k].is_singleton_of) return 1; if (cls >= 0) /* self in a method of Object (or a module) can be anything */ return !cls_fact_bounded(c, cls) || ur_ancestor(c, k, cls) || ur_ancestor(c, cls, k); int r = nt_kind(c->nt, n) == NK_CallNode ? nt_ref(c->nt, n, "receiver") : -1; const ClsSet *f = r >= 0 && !ur_retyped(c, u, n) ? cls_fact_node(c, r) : NULL; if (!f || f->all) return 1; for (int fk = cls_set_next(f, -1); fk >= 0; fk = cls_set_next(f, fk)) if (!cls_fact_bounded(c, fk) || ur_ancestor(c, k, fk)) return 1; return 0; } typedef struct { const char *name; int mi; } UrCand; static int ur_cand_cmp(const void *a, const void *b) { int r = strcmp(((const UrCand *)a)->name, ((const UrCand *)b)->name); return r ? r : ((const UrCand *)a)->mi - ((const UrCand *)b)->mi; } /* the first candidate named nm, -1 for none */ static int ur_first(const UrCand *tab, int n, const char *nm) { if (!nm) return -1; int lo = 0, hi = n; while (lo < hi) { int m = (lo + hi) / 2; if (strcmp(tab[m].name, nm) < 0) lo = m + 1; else hi = m; } return lo < n && strcmp(tab[lo].name, nm) == 0 ? lo : -1; } /* Walk the tree under root, filling par (each node's parent) and nref (1 for a node reached once, 2 for one reached again); stack has room for every node. */ static void ur_walk(const NodeTable *nt, int root, int *par, unsigned char *nref, int *stack) { int nn = nt->count, sp = 0; nref[root] = 1; stack[sp++] = root; while (sp > 0) { int id = stack[--sp]; int nr = nt_num_refs(nt, id), na = nt_num_arrs(nt, id); for (int i = 0; i < nr + na; i++) { int an = 1, one = i < nr ? nt_ref_at(nt, id, i) : -1; const int *av = i < nr ? &one : nt_arr_at(nt, id, i - nr, &an); for (int j = 0; av && j < an; j++) { int ch = av[j]; if (ch < 0 || ch >= nn) continue; if (nref[ch]) { nref[ch] = 2; continue; } nref[ch] = 1; par[ch] = id; stack[sp++] = ch; } } } } static void an_mark_unread_returns(Compiler *c) { const NodeTable *nt = c->nt; int nn = nt->count, ns = c->nscopes; UrCand *tab = calloc((size_t)ns + 1, sizeof *tab); unsigned char *read = calloc((size_t)ns + 1, 1); if (!tab || !read) { perror("calloc"); exit(1); } int nc = 0; for (int mi = 0; mi < ns; mi++) if (ur_candidate(c, &c->scopes[mi])) { tab[nc].name = c->scopes[mi].name; tab[nc].mi = mi; nc++; } if (nc == 0) { free(tab); free(read); return; } qsort(tab, (size_t)nc, sizeof *tab, ur_cand_cmp); /* the parents in the program as it stands, from its root: a node the desugaring left behind (an orphan) is not visited, and one it shares between two places is counted twice (nref) */ int *par = malloc(sizeof(int) * ((size_t)nn + 1)); unsigned char *nref = calloc((size_t)nn + 1, 1); int *stack = malloc(sizeof(int) * ((size_t)nn + 1)); /* per def node: its one candidate scope, -2 when a scope of it is not one */ int *defscope = malloc(sizeof(int) * ((size_t)nn + 1)); if (!par || !nref || !stack || !defscope) { perror("malloc"); exit(1); } for (int id = 0; id < nn; id++) { par[id] = -1; defscope[id] = -1; } if (nt->root_id >= 0 && nt->root_id < nn) ur_walk(nt, nt->root_id, par, nref, stack); /* then every method's own def and body that the tree does not hold (a synthesized method's) */ for (int mi = 0; mi < ns; mi++) { int r[2] = { c->scopes[mi].def_node, c->scopes[mi].body }; for (int i = 0; i < 2; i++) if (r[i] >= 0 && r[i] < nn && !nref[r[i]]) ur_walk(nt, r[i], par, nref, stack); } free(stack); for (int mi = 0; mi < ns; mi++) { int d = c->scopes[mi].def_node; if (d < 0 || d >= nn) continue; defscope[d] = !ur_candidate(c, &c->scopes[mi]) || defscope[d] != -1 ? -2 : mi; } UrCtx ux; memset(&ux, 0, sizeof ux); ux.par = par; ux.nref = nref; for (int i = 0; ur_dropping_iters[i]; i++) ux.iter_over[i] = (unsigned char)ur_iter_overridden(c, ur_dropping_iters[i]); ux.rebind_any = ur_block_rebound(c, nref); /* edges: method eto[e] is read when method efrom[e] is */ int ne = 0, ce = 0, computed = 0; int *efrom = NULL, *eto = NULL; for (int id = 0; id < nn && !computed; id++) { if (!nref[id]) continue; /* not in the program */ NodeKind k = nt_kind(nt, id); if (k == NK_SymbolNode || k == NK_StringNode) { int f = ur_first(tab, nc, nt_str(nt, id, k == NK_SymbolNode ? "value" : "content")); if (f >= 0 && ur_names_method(nt, par, id)) for (int j = f; j < nc && sp_streq(tab[j].name, tab[f].name); j++) read[tab[j].mi] = 1; continue; } if (k == NK_BlockArgumentNode) { computed = ur_computed_block_arg(c, par, id); continue; } const char *nm = NULL; if (k == NK_CallNode) { if (ur_computed_send(nt, id)) { computed = 1; continue; } nm = nt_str(nt, id, "name"); } else if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { int d = ur_enclosing_def(nt, par, id); nm = d >= 0 ? nt_str(nt, d, "name") : NULL; } int f = ur_first(tab, nc, nm); if (f < 0) continue; int use = -1, cls = ur_recv_class(c, &ux, id); for (int j = f; j < nc && sp_streq(tab[j].name, nm); j++) { int t = tab[j].mi; if (read[t] || !ur_may_reach(c, &ux, &c->scopes[t], id, cls)) continue; if (use < 0) use = ur_value_use(c, &ux, id); if (use == 0) continue; int from = use >= 2 ? defscope[use - 2] : -2; if (from < 0) { read[t] = 1; continue; } if (ne == ce) { ce = ce ? ce * 2 : 64; efrom = realloc(efrom, sizeof(int) * (size_t)ce); eto = realloc(eto, sizeof(int) * (size_t)ce); if (!efrom || !eto) { perror("realloc"); exit(1); } } efrom[ne] = from; eto[ne] = t; ne++; } } if (!computed) { /* hand each read on along the tails: the edges grouped by the method they leave (a counting sort), then a worklist from every read one */ int *first = calloc((size_t)ns + 2, sizeof(int)), *to = malloc(sizeof(int) * ((size_t)ne + 1)); int *work = malloc(sizeof(int) * ((size_t)ns + 1)), wn = 0; if (!first || !to || !work) { perror("malloc"); exit(1); } for (int e = 0; e < ne; e++) first[efrom[e] + 1]++; for (int m = 0; m < ns; m++) first[m + 1] += first[m]; for (int e = 0; e < ne; e++) to[first[efrom[e]]++] = eto[e]; for (int m = ns; m > 0; m--) first[m] = first[m - 1]; first[0] = 0; for (int m = 0; m < ns; m++) if (read[m]) work[wn++] = m; while (wn > 0) { int m = work[--wn]; for (int e = first[m]; e < first[m + 1]; e++) if (!read[to[e]]) { read[to[e]] = 1; work[wn++] = to[e]; } } free(first); free(to); free(work); for (int j = 0; j < nc; j++) if (!read[tab[j].mi]) c->scopes[tab[j].mi].ret_unread = 1; } free(par); free(nref); free(defscope); free(efrom); free(eto); free(tab); free(read); } /* Ahead of the fixpoint: intern the symbol literals and Proc#parameters names, apply the --rbs seeds, freeze the method-lookup index, compute the reachable and instantiated classes (analyze_program's steps, in their order) */ static void an_phase_pre_fixpoint(Compiler *c) { int *par = an_parent_map(c->nt); /* intern every symbol literal so codegen can emit the id table */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k == NK_SymbolNode) { const char *v = nt_str(c->nt, id, "value"); /* the node's own byte length: a symbol literal may hold a NUL, and interning it by strlen registers the short name codegen then emits */ if (v) comp_sym_intern_n(c, v, nt_str_len(c->nt, id, "value")); } /* a def in value position evaluates to :name. A builtin's definition (`__enum_`, `__int_`, `__flt_`, `__cmp_`, and each of their per-call-site copies, and enumerator.rb's `__enumw_`) is never in value position: its name would only add a string per call site to the symbol table. The Integer/Float/Comparable containers (analyze_desugar.c's sp_bx_* tables) share this same "generic def cloned per call site" shape enumerable.rb pioneered, but this exclusion was never extended to them -- invisible for Integer/Float's narrower methods, but a Comparable#clamp/#between? and its own private helper, reached from nearly everywhere, added one string per call site of any of them (found in this migration's own corpus sweep). */ else if (ty_k == NK_DefNode) { const char *dn = nt_str(c->nt, id, "name"); if (dn && strncmp(dn, "__enum_", 7) != 0 && strncmp(dn, "__int_", 6) != 0 && strncmp(dn, "__flt_", 6) != 0 && strncmp(dn, "__cmp_", 6) != 0 && strncmp(dn, "__enumw_", 8) != 0 && /* only where its value is used: a statement def's name alone made every program with a method carry the symbol runtime and with it the inspect and Bignum code it reaches (#8401) */ (!par || an_def_value_used(c->nt, par, id))) comp_sym_intern(c, dn); } /* __method__ / __callee__ yield the enclosing method's name as a symbol; intern it now so the id table is sized before the codegen prologue */ else if (ty_k == NK_CallNode && nt_ref(c->nt, id, "receiver") < 0) { const char *nm = nt_str(c->nt, id, "name"); if (nm && (is_current_method(nm))) { Scope *s = comp_scope_of(c, id); if (s && s->name && s->name[0]) comp_sym_intern(c, s->name); } } } free(par); /* Proc#parameters reports param kinds (:req/:opt/:rest/:keyreq/:key/ :keyrest/:block) and names as symbols; intern them now so they land in the table before the codegen prologue. Anonymous rest/kwrest/block use the CRuby placeholder names (:*, :**, :&); numbered params report :_1.._9. */ for (int id = 0; id < c->nt->count; id++) { if (!is_proc_create(c, id)) continue; comp_sym_intern(c, "req"); comp_sym_intern(c, "opt"); int pn = a_proc_params_node(c, id); if (pn < 0) { for (int k = 1; k <= 9; k++) { char nbuf[4]; snprintf(nbuf, sizeof nbuf, "_%d", k); comp_sym_intern(c, nbuf); } continue; } int rn = 0; const int *reqs = nt_arr(c->nt, pn, "requireds", &rn); for (int k = 0; k < rn; k++) { const char *nm = nt_str(c->nt, reqs[k], "name"); if (nm) comp_sym_intern_n(c, nm, reassigned_param_written_len(c, nm)); } int on = 0; const int *opts = nt_arr(c->nt, pn, "optionals", &on); for (int k = 0; k < on; k++) { const char *nm = nt_str(c->nt, opts[k], "name"); if (nm) comp_sym_intern(c, nm); } int psn = 0; const int *posts = nt_arr(c->nt, pn, "posts", &psn); for (int k = 0; k < psn; k++) { const char *nm = nt_str(c->nt, posts[k], "name"); if (nm) comp_sym_intern(c, nm); } int rest = nt_ref(c->nt, pn, "rest"); if (rest >= 0) { comp_sym_intern(c, "rest"); const char *nm = nt_str(c->nt, rest, "name"); comp_sym_intern(c, nm ? nm : "*"); } int kn = 0; const int *kws = nt_arr(c->nt, pn, "keywords", &kn); for (int k = 0; k < kn; k++) { NodeKind kk = nt_kind(c->nt, kws[k]); comp_sym_intern(c, (kk == NK_OptionalKeywordParameterNode) ? "key" : "keyreq"); const char *nm = nt_str(c->nt, kws[k], "name"); if (nm) comp_sym_intern(c, nm); } int kwrest = nt_ref(c->nt, pn, "keyword_rest"); if (kwrest >= 0) { comp_sym_intern(c, "keyrest"); const char *nm = nt_str(c->nt, kwrest, "name"); comp_sym_intern(c, nm && strncmp(nm, "__blk_kwrest", 12) ? nm : "**"); } int bpar = nt_ref(c->nt, pn, "block"); if (bpar >= 0) { comp_sym_intern(c, "block"); const char *nm = nt_str(c->nt, bpar, "name"); comp_sym_intern(c, nm ? nm : "&"); } } /* Apply --rbs advisory seeds (pin param/return/ivar types) before the fixpoint so the inference passes observe the pinned types from round one. No-op unless --rbs made a seed file (g_opt_rbs_seed). */ { const char *seed = g_opt_rbs_seed; apply_inline_rbs(c); if (seed && *seed) apply_rbs_seeds(c, seed); if (g_inline_rbs_bad) exit(1); } /* Scope shape (count, class_id, name, is_cmethod) is fixed from here on, so the method-lookup index can be used; it is rebuilt if the scope count ever grows. This is where comp_method_in_class / comp_cmethod_in_class are hottest (called per node, every fixpoint iteration). */ elab_mark_versions(c); ver_entries_build(c); comp_scope_index_set_frozen(1); /* The classes that can exist, before any type is derived from them (see compute_instantiated): reachability is a matter of names and a construction site is a call node, so both are known now. Recomputed after the fixpoint, when a dynamic `.new` can be told from a static one. */ AN_STEP_V("compute_reachable", compute_reachable(c, 0)); ver_cells_write(c, 0); compute_instantiated(c, 1); } /* A digest of every type the inference holds: each node's, each scope's return and locals (its parameters among them), each class's instance variables, the globals and the constants. */ static uint64_t an_type_state(Compiler *c) { uint64_t h = 0xCBF29CE484222325ull; #define AN_TS_MIX(t) (h = (h ^ (uint64_t)(uint32_t)(t)) * 0x100000001B3ull) for (int id = 0; id < c->nt->count && id < c->node_cap; id++) AN_TS_MIX(c->ntype[id]); for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; AN_TS_MIX(sc->ret); for (int i = 0; i < sc->nlocals; i++) AN_TS_MIX(sc->locals[i].type); } for (int k = 0; k < c->nclasses; k++) for (int iv = 0; iv < c->classes[k].nivars; iv++) AN_TS_MIX(c->classes[k].ivar_types[iv]); for (int i = 0; i < c->ngvars; i++) AN_TS_MIX(c->gvars[i].type); for (int i = 0; i < c->nconsts; i++) AN_TS_MIX(c->consts[i].type); #undef AN_TS_MIX return h; } /* FNV-1a over n bytes, continuing h. */ static uint64_t an_ts_mix_bytes(uint64_t h, const void *p, size_t n) { const unsigned char *b = p; for (size_t i = 0; i < n; i++) h = (h ^ b[i]) * 0x100000001B3ull; return h; } /* A digest of the whole state the inference passes read, for the re-narrow's periodic exit, which takes two equal digests for two equal states: the types an_type_state digests, and with them the notes the passes keep beside a type -- every per-node table but the memos (a memo computed or not is no difference of state) and the structural ones, each local's whole record but its name and its trace (why: where its type came from, with the round it happened on), each scope's return block and flags, and each class's ivar and cvar tables and flags. The records are zeroed when made, so their padding is no noise. */ static uint64_t an_loop_state(Compiler *c) { uint64_t h = an_type_state(c); int n = c->nt->count < c->node_cap ? c->nt->count : c->node_cap; #define AN_LS_ARR(a) do { if (c->a) h = an_ts_mix_bytes(h, c->a, (size_t)n * sizeof *c->a); } while (0) AN_LS_ARR(norigin); AN_LS_ARR(strbuf_box); AN_LS_ARR(strbuf_handle_demand); AN_LS_ARR(strbuf_read_raw); AN_LS_ARR(poly_strbuf_lift); AN_LS_ARR(nil_tested); AN_LS_ARR(head_held); AN_LS_ARR(nilnarrow); AN_LS_ARR(empty_arr_recv); AN_LS_ARR(empty_hash_recv); AN_LS_ARR(empty_hash_arg); AN_LS_ARR(store_misfit_arg); AN_LS_ARR(ivar_widen_src); AN_LS_ARR(hash_want); AN_LS_ARR(arr_want); AN_LS_ARR(lw_joined); AN_LS_ARR(poly_builtin_ty); AN_LS_ARR(bop_inf); #undef AN_LS_ARR if (c->ucall_inf) for (int id = 0; id < n; id++) { UCallInf *u = &c->ucall_inf[id]; h = (h ^ (uint64_t)(uint32_t)u->mi) * 0x100000001B3ull; h = (h ^ (uint64_t)(uint16_t)u->owner_ci) * 0x100000001B3ull; h = (h ^ u->via) * 0x100000001B3ull; } #define AN_LS_RANGE(p, T, from, to) h = an_ts_mix_bytes(h, (const char *)(p) + offsetof(T, from), offsetof(T, to) - offsetof(T, from)) #define AN_LS_LV(lv) h = an_ts_mix_bytes(h, (const char *)(lv) + offsetof(LocalVar, last_src), sizeof(LocalVar) - offsetof(LocalVar, last_src)) for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; AN_LS_RANGE(sc, Scope, def_node, blk_param); AN_LS_RANGE(sc, Scope, ret_poly_ctr, locals); for (int i = 0; i < sc->nlocals; i++) AN_LS_LV(&sc->locals[i]); } for (int i = 0; i < c->ngvars; i++) AN_LS_LV(&c->gvars[i]); for (int i = 0; i < c->nconsts; i++) AN_LS_LV(&c->consts[i]); #undef AN_LS_LV for (int k = 0; k < c->nclasses; k++) { ClassInfo *ci = &c->classes[k]; #define AN_LS_TAB(a, cnt) do { if (ci->a) h = an_ts_mix_bytes(h, ci->a, (size_t)ci->cnt * sizeof *ci->a); } while (0) AN_LS_TAB(ivar_str_shared, nivars); AN_LS_TAB(ivar_elems_shared, nivars); AN_LS_TAB(ivar_nullable_int, nivars); AN_LS_TAB(ivar_arr_elem_arr_or_nil, nivars); AN_LS_TAB(ivar_nullable_int_elem, nivars); AN_LS_TAB(ivar_oa_type, nivars); AN_LS_TAB(ivar_oa_seed, nivars); AN_LS_TAB(ivar_oa_conflict, nivars); AN_LS_TAB(ivar_int_table, nivars); /* the two nil tables are sized when they are built, by the ivars then */ AN_LS_TAB(ivar_obj_may_nil, n_ivar_obj_may_nil); AN_LS_TAB(ivar_elem_may_nil, n_ivar_obj_may_nil); AN_LS_TAB(cvar_types, ncvars); AN_LS_TAB(cvar_nullable_int, ncvars); AN_LS_TAB(cvar_str_shared, ncvars); AN_LS_TAB(cvar_elems_shared, ncvars); #undef AN_LS_TAB AN_LS_RANGE(ci, ClassInfo, enum_yield_arity, c_struct); AN_LS_RANGE(ci, ClassInfo, nsub_root, prep_from); } #undef AN_LS_RANGE return h; } /* The slots a type waits a round to cross: each scope's return and locals, each class's instance variables, the globals and the constants. */ static int an_type_slots(Compiler *c) { int n = c->ngvars + c->nconsts; for (int s = 0; s < c->nscopes; s++) n += 1 + c->scopes[s].nlocals; for (int k = 0; k < c->nclasses; k++) n += c->classes[k].nivars; return n; } /* The round cap of an inference loop, the type states its rounds have ended in, and the node count as the last of them ended. */ typedef struct { int cap; int nseen, cseen; uint64_t *seen; int nodes; } AnRoundCap; /* The round cap is for an oscillation: slots that trade types round after round never settle, and where the loop stops decides what is emitted. A chain is not one. A type travels one call a round -- a return up to its caller where the caller is defined first, a parameter down to its callee where the callee is -- so 200 methods each answering the next one's value take 200 rounds, and stopped at 128 the first 72 had no return type: `p f1(1)` printed nil. The two are told apart by where a round ends. An oscillation comes back to a state it has been in; a chain ends each round in one it has not. So the last round the cap allows earns one more when its state is new and the round added no node: a rewrite that is undone and made again grows the program every round and is never where it was, and the cap is what ends that. The states are kept from the cap's halfway point on (a program that settles before then is not measured). A program that has stopped growing has finitely many, so a loop that never settles still ends; and none is given more than four rounds for each slot a type waits a round to cross (an_type_slots), a slot widening a few times at most. Called as a round ends. */ static void an_round_cap_step(Compiler *c, AnRoundCap *rc, int iter) { if (iter < 64) return; int grew = c->nt->count != rc->nodes; rc->nodes = c->nt->count; uint64_t st = an_type_state(c); for (int i = 0; i < rc->nseen; i++) if (rc->seen[i] == st) return; if (rc->nseen == rc->cseen) { rc->cseen = rc->cseen ? rc->cseen * 2 : 64; rc->seen = realloc(rc->seen, (size_t)rc->cseen * sizeof *rc->seen); } rc->seen[rc->nseen++] = st; if (!grew && iter + 1 == rc->cap && rc->cap < 128 + 4 * an_type_slots(c)) rc->cap++; } /* Parameters a reverse binding widened to the general Array for a callee's parameter that is no longer one (#7948): reset them, and bar the reverse binding from them. 1 when any moved. */ static int an_retract_stale_reverse_widening(Compiler *c) { int any = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nparams; i++) { LocalVar *p = scope_local(sc, sc->pnames[i]); if (!p || p->type != TY_POLY_ARRAY || !p->push_widened || !p->widen_from_name || p->rbs_seeded || p->widen_from_scope < 0 || p->widen_from_scope >= c->nscopes) continue; LocalVar *cause = scope_local(&c->scopes[p->widen_from_scope], p->widen_from_name); if (cause && cause->type == TY_POLY_ARRAY && cause->push_widened) continue; lv_set_type(p, TY_UNKNOWN); p->push_widened = 0; p->widen_from_name = NULL; p->widen_blocked = 1; why_reset(&p->why); any = 1; } } return any; } /* A container seed held against a box while the types settled (a parameter's, a return's, an ivar's pin: param_seed_holds, ret_seed_holds, class_ivar_pin_holds): derive their bindings, returns and ivar writes once more on the settled types, where a box which may still be another form drops the seed. 1 when one did: the program then derives again without it. Only a parameter's drop clears local pins (below): a return's or an ivar's leaves them, as narrowing under one is not a case we have seen. */ static int an_settle_seed_boxes(Compiler *c) { int held = 0; for (int s = 0; s < c->nscopes; s++) { held |= c->scopes[s].ret_rbs_box_held; for (int i = 0; i < c->scopes[s].nlocals; i++) held |= c->scopes[s].locals[i].rbs_box_held; } for (int k = 0; k < c->nclasses; k++) held |= c->classes[k].ivar_box_held; if (!held) return 0; int *pins = (int *)malloc(sizeof(int) * (size_t)(c->nclasses > 0 ? c->nclasses : 1)); for (int k = 0; k < c->nclasses; k++) pins[k] = c->classes[k].n_rbs_pin_ivars; int nclasses = c->nclasses; g_seed_box_settled = 1; infer_param_types(c); infer_ivar_types(c); infer_return_types(c); g_seed_box_settled = 0; int dropped = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->ret_rbs_box_held && !sc->ret_rbs_seeded) dropped = 1; sc->ret_rbs_box_held = 0; int here = 0; for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if (lv->rbs_box_held && !lv->rbs_seeded) here = 1; lv->rbs_box_held = 0; } if (!here) continue; dropped = 1; /* A local of the method narrowed while the seed held (`row = ci[i]` pinned to the element of a typed `ci`, narrow_locals_from_arrays) would keep that pin through the re-derivation (lv_keep_decisions), though `ci` is a box now; the narrowing passes give back the pins the settled types support. Only this method's: a pin elsewhere was not narrowed under the seed, and one the re-derivation cannot give back leaves its slot boxed for good. */ for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if (lv->is_param || lv->is_block_param || lv->rbs_seeded) continue; lv->oa_pin = TY_UNKNOWN; lv->oa_grace = 0; } } for (int k = 0; k < c->nclasses; k++) { if (k < nclasses && c->classes[k].ivar_box_held && c->classes[k].n_rbs_pin_ivars < pins[k]) dropped = 1; c->classes[k].ivar_box_held = 0; } free(pins); return dropped; } /* The inference fixpoint: two rounds with the proc-form clones made between them, then the optimistic re-narrow of the slots a transient poly locked (analyze_program's steps, in their order) */ /* --timing (#7236): each pass of the inference fixpoint, its wall time summed over the rounds, how often it ran and how often it reported a change. AN_PASS wraps a pass call: off, a flag test and the call; on, a clock read on each side. Reported by an_pass_report after the analysis. */ #define AN_PASS_MAX 256 static struct { const char *name; double ms; int calls, changed; } g_an_pass[AN_PASS_MAX]; static int g_an_npass; static double g_renarrow_ms; static int g_renarrow_rounds; static void an_pass_note(const char *name, double t0, int changed) { double ms = sp_timing_now() - t0; int i = 0; while (i < g_an_npass && g_an_pass[i].name != name && strcmp(g_an_pass[i].name, name) != 0) i++; if (i == g_an_npass) { if (g_an_npass == AN_PASS_MAX) return; g_an_pass[g_an_npass].name = name; g_an_pass[g_an_npass].ms = 0; g_an_pass[g_an_npass].calls = g_an_pass[g_an_npass].changed = 0; g_an_npass++; } g_an_pass[i].ms += ms; g_an_pass[i].calls++; if (changed) g_an_pass[i].changed++; } /* The shadow schedule of #7237 (SP_SHADOW_GEN, a measurement only): a desugar pass rewrites the tree, so one that ran since the node table last changed (NodeTable.version) can find nothing new -- unless it also reads inferred types. Every pass still runs; a call that would have been skipped is counted with its time, and one that changed anything all the same is reported as a violation. */ /* The desugar passes that read nothing but the tree (#7237): no inferred type, directly or through what they call, and none of them ever changed anything in the shadow schedule over the corpus. One that has run since the node table last changed is not run again: it would find what it found then. A pass that comes to read types leaves this list; `make shadow-check` runs the shadow schedule and fails on a violation by one of these. */ static const char *const an_tree_only_passes[] = { "desugar_bare_class_self_calls", "desugar_bare_object_reopen_calls", "desugar_binding_lvget", "desugar_block_capture_wrap", "desugar_body_self_call", "desugar_builtin_class_var_recv", "desugar_call_op_write", "desugar_class_body_bare_new", "desugar_class_literal_ctors", "desugar_cmethod_cvar_reflection", "desugar_defined_method_call", "desugar_descendant_reader_calls", "desugar_dynamic_const_get_arms", "desugar_dynamic_respond_to", "desugar_empty_block_body", "desugar_file_stat_new", "desugar_handle_reopen_self_recv", "desugar_implicit_send", "desugar_include_math", "desugar_kernel_recv", "desugar_lazy_method_call", "desugar_lazy_terminal", "desugar_main_self_call", "desugar_method_call_runtime_name", "desugar_module_function_call", "desugar_reduce_method_symbol", "desugar_step_kwargs", "desugar_sym_to_proc_call", "desugar_symbol_to_proc_call", "desugar_toplevel_instance_exec", NULL }; static int an_shadow_on(void) { static int on = -1; if (on < 0) on = getenv("SP_SHADOW_GEN") != NULL; return on; } #define AN_SHADOW_MAX 256 static struct { const char *name; unsigned after; int ran, skippable, violations; double skip_ms; } g_an_sh[AN_SHADOW_MAX]; static int g_an_nsh; static int an_shadow_slot(const char *name) { int i = 0; while (i < g_an_nsh && g_an_sh[i].name != name && strcmp(g_an_sh[i].name, name) != 0) i++; if (i == g_an_nsh) { if (g_an_nsh == AN_SHADOW_MAX) return -1; memset(&g_an_sh[i], 0, sizeof g_an_sh[i]); g_an_sh[i].name = name; g_an_nsh++; } return i; } /* around one pass call (AN_PASS calls do not nest): -1 for a pass not shadowed */ static unsigned g_an_sh_v0; static int g_an_sh_stale; static struct timespec g_an_sh_t0; static int an_shadow_begin(Compiler *c, const char *name) { int i = strncmp(name, "desugar_", 8) == 0 ? an_shadow_slot(name) : -1; if (i < 0) return -1; g_an_sh_v0 = c->nt->version; g_an_sh_stale = g_an_sh[i].ran && g_an_sh[i].after == g_an_sh_v0; clock_gettime(CLOCK_MONOTONIC, &g_an_sh_t0); return i; } static void an_shadow_end(Compiler *c, int i, int r) { if (i < 0) return; struct timespec ts1; clock_gettime(CLOCK_MONOTONIC, &ts1); if (g_an_sh_stale) { g_an_sh[i].skippable++; g_an_sh[i].skip_ms += (double)(ts1.tv_sec - g_an_sh_t0.tv_sec) * 1000.0 + (double)(ts1.tv_nsec - g_an_sh_t0.tv_nsec) / 1e6; if (r || c->nt->version != g_an_sh_v0) { g_an_sh[i].violations++; int listed = 0; for (int k = 0; an_tree_only_passes[k]; k++) if (strcmp(an_tree_only_passes[k], g_an_sh[i].name) == 0) listed = 1; fprintf(stderr, "spinel-shadow: violation pass=%s round=%d%s%s\n", g_an_sh[i].name, g_infer_round, c->nt->version != g_an_sh_v0 ? " tree" : " result", listed ? " LISTED" : ""); } } g_an_sh[i].ran = 1; g_an_sh[i].after = c->nt->version; } static void an_shadow_report(void) { if (!an_shadow_on()) return; int calls = 0, viol = 0; double ms = 0; for (int i = 0; i < g_an_nsh; i++) { calls += g_an_sh[i].skippable; viol += g_an_sh[i].violations; ms += g_an_sh[i].skip_ms; } fprintf(stderr, "spinel-shadow: passes=%d skippable_calls=%d skippable_ms=%.1f violations=%d\n", g_an_nsh, calls, ms, viol); for (int i = 0; i < g_an_nsh; i++) fprintf(stderr, "spinel-shadow: pass=%s skippable=%d skippable_ms=%.2f violations=%d\n", g_an_sh[i].name, g_an_sh[i].skippable, g_an_sh[i].skip_ms, g_an_sh[i].violations); } /* the shadow schedule (SP_SHADOW_GEN) runs every pass */ static int an_tree_skip_off(void) { return an_shadow_on(); } #define AN_TREE_MAX 64 static struct { const char *name; unsigned after; int ran; } g_an_tree[AN_TREE_MAX]; static int g_an_ntree = -1; /* the slot of a tree-only pass, -1 for any other */ static int an_tree_slot(const char *name) { if (g_an_ntree < 0) { g_an_ntree = 0; for (int k = 0; an_tree_only_passes[k] && g_an_ntree < AN_TREE_MAX; k++) { g_an_tree[g_an_ntree].name = an_tree_only_passes[k]; g_an_tree[g_an_ntree].ran = 0; g_an_ntree++; } } for (int i = 0; i < g_an_ntree; i++) if (g_an_tree[i].name == name || strcmp(g_an_tree[i].name, name) == 0) return i; return -1; } /* a tree-only pass that ran with the tree as it is now: skip it */ static int an_tree_skip(Compiler *c, const char *name, int *slot) { *slot = an_tree_skip_off() ? -1 : an_tree_slot(name); return *slot >= 0 && g_an_tree[*slot].ran && g_an_tree[*slot].after == c->nt->version; } static void an_tree_ran(Compiler *c, int slot) { if (slot < 0) return; g_an_tree[slot].ran = 1; g_an_tree[slot].after = c->nt->version; } /* a named pass begins and ends with infer_type's memo emptied (#8852) */ #define AN_MEMO_REGION(call) ({ infer_memo_boundary(); int _mr = (call); infer_memo_boundary(); _mr; }) #define AN_SHADOWED(nm, call) (!an_shadow_on() ? AN_TREE_SKIPPED(nm, call) : \ ({ int _si = an_shadow_begin(c, (nm)); int _sr = AN_MEMO_REGION(call); an_shadow_end(c, _si, _sr); _sr; })) #define AN_TREE_SKIPPED(nm, call) \ ({ int _ts; int _tr = an_tree_skip(c, (nm), &_ts) ? 0 : AN_MEMO_REGION(call); if (!_tr || _ts >= 0) an_tree_ran(c, _ts); _tr; }) #define AN_PASS(nm, call) (!sp_timing_on() ? AN_SHADOWED(nm, AN_STEP(nm, call)) : \ ({ double _pt0 = sp_timing_now(); int _pr = AN_SHADOWED(nm, AN_STEP(nm, call)); an_pass_note((nm), _pt0, _pr != 0); _pr; })) #define AN_PASS_V(nm, call) do { infer_memo_boundary(); \ if (!sp_timing_on()) { AN_STEP_V(nm, call); } \ else { \ double _pt0 = sp_timing_now(); AN_STEP_V(nm, call); an_pass_note((nm), _pt0, 0); } infer_memo_boundary(); } while (0) /* A pass timed but not framed: the re-narrow runs its inner passes without the memo boundary and the tree skip a round's passes get (they read the slots the loop re-clears), and the pass table missed them. */ #define AN_PASS_T(nm, call) (!sp_timing_on() ? AN_STEP(nm, call) : \ ({ double _pt0 = sp_timing_now(); int _pr = AN_STEP(nm, call); an_pass_note((nm), _pt0, _pr != 0); _pr; })) static int an_pass_cmp(const void *a, const void *b) { double x = ((const typeof(g_an_pass[0]) *)a)->ms, y = ((const typeof(g_an_pass[0]) *)b)->ms; return x < y ? 1 : x > y ? -1 : 0; } static void an_pass_report(void) { if (!sp_timing_on()) return; if (g_renarrow_rounds) fprintf(stderr, "spinel-timing: phase=analysis_renarrow ms=%.1f rounds=%d\n", g_renarrow_ms, g_renarrow_rounds); qsort(g_an_pass, (size_t)g_an_npass, sizeof g_an_pass[0], an_pass_cmp); for (int i = 0; i < g_an_npass; i++) fprintf(stderr, "spinel-timing: pass=%s ms=%.1f calls=%d changed=%d\n", g_an_pass[i].name, g_an_pass[i].ms, g_an_pass[i].calls, g_an_pass[i].changed); } static void an_phase_infer_fixpoint(Compiler *c) { g_fixpoint_rounds = 0; g_fixpoint_capped = 0; AN_STEP_V("mark_ie_rebindable_literals", mark_ie_rebindable_literals(c)); /* Two rounds. The proc-form clones are made between them: knowing which methods a poly dispatch will name needs settled receiver types, and the clones' own bodies then need inferring like any other. The second round is a no-op when nothing was cloned. (#3399) */ for (int pf_round = 0; pf_round < 2; pf_round++) { if (pf_round == 1 && !AN_STEP("make_yield_proc_forms", make_yield_proc_forms(c))) break; g_infer_optimistic = 1; comp_touch_global(); /* (a mode inference reads) */ AnRoundCap rc = { 128, 0, 0, NULL, 0 }; for (int iter = 0; iter < rc.cap; iter++) { if (iter + 1 > g_fixpoint_rounds) g_fixpoint_rounds = iter + 1; g_infer_round = iter + 1; int ch = 0; AN_PASS_V("seed_unsupplied_nil_defaults", seed_unsupplied_nil_defaults(c)); /* ahead of the round's binding, every round: the round's reset clears it (#4583) */ sp_narrow_memo_bump(); /* invalidate per-iteration narrow-helper memo */ AN_PASS_V("build_ie_map", build_ie_map(c)); /* refresh instance_exec receiver-class map each pass */ ch |= AN_PASS("register_ie_block_ivars", register_ie_block_ivars(c)); /* slot ivars first assigned in iexec blocks */ ch |= AN_PASS("infer_write_types", infer_write_types(c)); /* The table type has to be visible HERE, not after the fixpoint: a parameter bound from `@t[k][j]` widens to poly on the first iteration and never comes back (parameters only widen). The narrowing needs infer_write_types to have given the ivar its poly-array type first, and the locals read out of it (`row = @t[r]`) need one more write pass to re-derive from the narrowed type before the binding below sees them. */ if (AN_PASS("narrow_int_table_ivars", narrow_int_table_ivars(c, 1))) ch |= AN_PASS("infer_write_types", infer_write_types(c)); /* The same timing argument for a table held in a LOCAL, or one that crosses a call: the helper reading `row = t[i]` binds its parameter on the round the call is first seen, and a parameter only ever widens. Run after the ivar narrowing so a table read out of an ivar is already typed when the local reading it is derived. */ if (AN_PASS("narrow_object_arrays", narrow_object_arrays(c))) ch |= AN_PASS("infer_write_types", infer_write_types(c)); /* Enumerable over a Hash/Range with only an Array arm: route through to_a (needs the receiver kind, so it runs inside the fixpoint). */ if (AN_PASS("desugar_enumerable_via_to_a", desugar_enumerable_via_to_a(c))) ch |= AN_PASS("infer_write_types", infer_write_types(c)); AN_PASS_V("narrow_locals_from_arrays", narrow_locals_from_arrays(c)); /* the binding settles a forwarding chain within the round (infer_param_types_settle); the re-runs after the fixpoint, which reset slots on purpose, bind once as before */ ch |= AN_PASS("infer_param_types_settle", infer_param_types_settle(c)); AN_PASS_V("reassert_rbs_param_seeds", reassert_rbs_param_seeds(c)); /* a seed outranks a narrowing derived from one call site */ ch |= AN_PASS("bind_coerce_operator_params", bind_coerce_operator_params(c)); /* 3 + obj calls obj's op WITH obj */ ch |= AN_PASS("infer_param_hash_value", infer_param_hash_value(c)); ch |= AN_PASS("propagate_prep_params", propagate_prep_params(c)); ch |= AN_PASS("infer_string_params", infer_string_params(c)); ch |= AN_PASS("infer_default_param_types", infer_default_param_types(c)); ch |= AN_PASS("expand_literal_splat_args", expand_literal_splat_args(c)); /* builtin/proc m(*[a,b]) -> m(a, b) */ ch |= AN_PASS("desugar_hash_new_capacity", desugar_hash_new_capacity(c)); /* Hash.new(capacity: n) -> Hash.new */ ch |= AN_PASS("pin_arg_position_hash_new", pin_arg_position_hash_new(c)); /* f(Hash.new(d)) -> PolyPoly variant */ ch |= AN_PASS("pad_unsupplied_params", pad_unsupplied_params(c)); /* under-supplied call: placeholder param type */ ch |= AN_PASS("desugar_builtin_method_obj", desugar_builtin_method_obj(c)); /* builtin recv.method(:sym) -> wrapper def */ ch |= AN_PASS("desugar_class_body_bare_new", desugar_class_body_bare_new(c)); /* class body `new(x)` -> `Klass.new(x)` */ ch |= AN_PASS("desugar_bare_class_self_calls", desugar_bare_class_self_calls(c)); /* cmethod `const_get(:K)` -> `self.const_get(:K)` */ ch |= AN_PASS("desugar_ie_bare_object_calls", desugar_ie_bare_object_calls(c)); /* instance_eval { is_a?(K) } -> self.is_a?(K) */ ch |= AN_PASS("desugar_bare_object_reopen_calls", desugar_bare_object_reopen_calls(c)); /* Object reopened: `helper` -> `self.helper` */ ch |= AN_PASS("desugar_descendant_reader_calls", desugar_descendant_reader_calls(c)); ch |= AN_PASS("desugar_masgn_store_evidence", desugar_masgn_store_evidence(c)); /* h[k], o.x = v, w -> detached h[k] = v, o.x = w as type evidence */ ch |= AN_PASS("desugar_index_assign_user_recv", desugar_index_assign_user_recv(c)); /* r[k] ||= v on a user [] / []= receiver -> r[k] || (r[k] = v) */ ch |= AN_PASS("desugar_dynamic_const_get_arms", desugar_dynamic_const_get_arms(c)); /* recv.const_get(var) on any other receiver -> static name dispatch */ ch |= AN_PASS("desugar_reopen_implicit_self", desugar_reopen_implicit_self(c)); /* `last` in `class Range; def m` -> self.last */ ch |= AN_PASS("desugar_include_math", desugar_include_math(c)); /* include Math: sqrt(x) -> Math.sqrt(x) */ ch |= AN_PASS("desugar_bare_spawn", desugar_bare_spawn(c)); /* spawn(...) -> Process.spawn(...) */ ch |= AN_PASS("desugar_kernel_recv", desugar_kernel_recv(c)); /* Kernel.puts x -> puts x */ ch |= AN_PASS("desugar_builtin_alias_calls", desugar_builtin_alias_calls(c)); /* Params#to_params_hash -> to_h */ ch |= AN_PASS("desugar_std_stream_bare_output", desugar_std_stream_bare_output(c)); /* puts x -> $stdout.puts x, once $stdout can hold a StringIO */ ch |= AN_PASS("desugar_std_stream_bare_warn", desugar_std_stream_bare_warn(c)); /* warn x -> $stderr.write(), once $stderr can hold a program object with its own write */ ch |= AN_PASS("desugar_class_literal_ctors", desugar_class_literal_ctors(c)); /* Array[a,b] -> [a,b]; Range.new -> (a..b) */ ch |= AN_PASS("desugar_enum_iter_splat_args", desugar_enum_iter_splat_args(c)); /* enum.map(*a, &b) -> enum.map(&b) */ ch |= AN_PASS("desugar_index_value_block_pass", desugar_index_value_block_pass(c)); /* a.index(v, &mk) -> (b = mk; a.index(v, &b)) */ ch |= AN_PASS("desugar_value_form_drop_block", desugar_value_form_drop_block(c)); /* a.count(v) { } -> a.count(v) */ ch |= AN_PASS("desugar_iter_splat_args", desugar_iter_splat_args(c)); /* ary.each(*a) { } -> raise unless a.empty?; ary.each { } */ ch |= AN_PASS("desugar_enum_pair_lone_param", desugar_enum_pair_lone_param(c)); /* a.each_with_index.map { |x| } -> { |x, i| } */ ch |= AN_PASS("desugar_builtin_iter_block_shapes", desugar_builtin_iter_block_shapes(c)); /* [1].each { |c, a = 10| } -> { |v| c = v; a = 10 } */ ch |= AN_PASS("desugar_fresh_array_each_writeback", desugar_fresh_array_each_writeback(c)); /* s.scan(re).each { |x| x.gsub!(..) }.join -> map! { |x| ..; x } */ ch |= AN_PASS("desugar_fresh_array_map_bang", desugar_fresh_array_map_bang(c)); /* "1 2".split.map! { |s| s.to_i } -> map */ ch |= AN_PASS("desugar_multi_yield_map_param", desugar_multi_yield_map_param(c)); /* multi-yield each: map's |x| takes the 1st */ ch |= AN_PASS("desugar_ewi_pack_values", desugar_ewi_pack_values(c)); /* multi-yield each: each_with_index packs */ ch |= AN_PASS("desugar_enum_walk_calls", desugar_enum_walk_calls(c)); /* enum.map { break } -> __enumw_map(enum) { } */ ch |= AN_PASS("desugar_enum_method_recv", desugar_enum_method_recv(c)); /* obj.map{} -> obj.__enum_to_a.map{} */ ch |= AN_PASS("give_native_self_calls_a_receiver", give_native_self_calls_a_receiver(c)); /* native class: implicit self -> self.m */ ch |= AN_PASS("give_exception_self_calls_a_receiver", give_exception_self_calls_a_receiver(c)); /* exception class: message on implicit self -> self.message */ ch |= AN_PASS("give_object_methods_a_class_self", give_object_methods_a_class_self(c)); /* class method: Object#m on implicit self -> self.m */ ch |= AN_PASS("give_self_predicates_a_receiver", give_self_predicates_a_receiver(c)); /* is_a?(X) on implicit self -> self.is_a?(X) */ ch |= AN_PASS("desugar_for_enumerable", desugar_for_enumerable(c)); /* for x in obj -> for x in obj.__enum_to_a */ ch |= AN_PASS("desugar_lazy_terminal", desugar_lazy_terminal(c)); /* lz.sum -> lz.to_a.sum */ ch |= AN_PASS("desugar_string_upto", desugar_string_upto(c)); /* "a".upto("c") -> ("a".."c").each */ ch |= AN_PASS("desugar_file_stat_new", desugar_file_stat_new(c)); /* File::Stat.new(p) -> File.stat(p) */ ch |= AN_PASS("desugar_module_function_call", desugar_module_function_call(c)); /* helper(x) in an includer -> M.helper(x) */ ch |= AN_PASS("desugar_method_block_arg", desugar_method_block_arg(c)); /* m(&method(:x)) -> m(&method(:x).to_proc) */ ch |= AN_PASS("desugar_curry_block_arg", desugar_curry_block_arg(c)); /* iter(&curried) -> iter { |e| curried[e] } */ ch |= AN_PASS("desugar_yielder_block_arg", desugar_yielder_block_arg(c)); /* src.each(&y) -> src.each { |e| y << e } */ ch |= AN_PASS("desugar_to_enum", desugar_to_enum(c)); /* recv.to_enum(:m) -> generator/blockless */ ch |= AN_PASS("type_block_rest_params", type_block_rest_params(c)); /* |*rest| locals are poly arrays */ ch |= AN_PASS("desugar_public_method", desugar_public_method(c)); /* recv.public_method(:m) -> recv.method(:m) */ ch |= AN_PASS("desugar_class_eval_value", desugar_class_eval_value(c)); ch |= AN_PASS("desugar_instance_eval_builtin", desugar_instance_eval_builtin(c)); /* "s".instance_eval { m } -> splice on a temp */ ch |= AN_PASS("desugar_builtin_class_var_recv", desugar_builtin_class_var_recv(c)); /* k = Array; k.new(..) -> Array.new(..) */ ch |= AN_PASS("desugar_compose_method_operand", desugar_compose_method_operand(c)); /* proc >> meth -> proc >> meth.to_proc */ ch |= AN_PASS("desugar_array_op_to_ary", desugar_array_op_to_ary(c)); /* [1] + w -> [1] + w.to_ary (w answers to_ary) */ ch |= AN_PASS("desugar_mutator_receiver_value", desugar_mutator_receiver_value(c)); /* (c ? s : t) << x -> (c ? s << x : t << x) */ ch |= AN_PASS("desugar_method_curry", desugar_method_curry(c)); /* meth.curry -> meth.to_proc.curry */ ch |= AN_PASS("desugar_curry_arity_to_int", desugar_curry_arity_to_int(c)); /* proc.curry(obj) -> proc.curry(obj.to_int) */ ch |= AN_PASS("desugar_int_enum_with_index", desugar_int_enum_with_index(c)); /* n.times.with_index -> n.times.each.with_index */ ch |= AN_PASS("desugar_hash_iter_with_index", desugar_hash_iter_with_index(c)); /* h.select.with_index { } -> h.select { } with a counter */ ch |= AN_PASS("widen_shared_cmp_params", widen_shared_cmp_params(c)); /* multi-class <=> takes its operand boxed */ ch |= AN_PASS("desugar_reduce_splat_args", desugar_reduce_splat_args(c)); /* inject(*a) -> by a.length */ ch |= AN_PASS("desugar_splat_view_scalars", desugar_splat_view_scalars(c)); /* h.fetch(*k), k an Integer -> the arms read [k] */ ch |= AN_PASS("desugar_reduce_proc_arg", desugar_reduce_proc_arg(c)); /* reduce(&pr) -> reduce { |a,b| pr.call(a,b) } */ ch |= AN_PASS("desugar_block_capture_wrap", desugar_block_capture_wrap(c)); /* { |i| ->{i} } -> { |i| (->(i){ ->{i} }).call(i) } */ ch |= AN_PASS("desugar_user_not_match", desugar_user_not_match(c)); /* a !~ b -> !(a =~ b) for a user =~ (#3019) */ ch |= AN_PASS("desugar_env_enum", desugar_env_enum(c)); /* ENV.keys/... -> __env_to_h.keys/... */ ch |= AN_PASS("desugar_io_console_modes", desugar_io_console_modes(c)); /* io.raw { } / io.noecho { } -> mode, begin, ensure restore */ ch |= AN_PASS("desugar_dir_surface", desugar_dir_surface(c)); /* Dir.foreach{} -> entries.each{}, chdir{} splice, ... */ /* C.class_eval { v } -> (->(){v}).call */ ch |= AN_PASS("desugar_enumerable_chain", desugar_enumerable_chain(c)); /* x.chain(y) / enum+enum -> __enum_chain(x.to_a + y.to_a) */ ch |= AN_PASS("desugar_implicit_send", desugar_implicit_send(c)); /* send(:m, a) -> m(a) on self */ ch |= AN_PASS("desugar_public_send_recv", desugar_public_send_recv(c)); /* r.public_send(:m, a) -> r.m(a), visibility-stamped */ ch |= AN_PASS("desugar_symbol_string_methods", desugar_symbol_string_methods(c)); /* :sym.match(re) -> :sym.to_s.match(re) */ ch |= AN_PASS("desugar_interp_reopened_to_s", desugar_interp_reopened_to_s(c)); /* "#{5}" with Integer#to_s reopened -> "#{5.to_s}" */ ch |= AN_PASS("desugar_interp_elided_to_s", desugar_interp_elided_to_s(c)); /* "#{s}\" -> s.to_s (--share-strings) */ /* re-run inside the fixpoint: a key whose type comes from a PARAMETER is still UNKNOWN on the pre-fixpoint pass, so `h[k] ||= []` fell back to the StrPolyHash default and handed an Integer key to a const char * (#3353). The mark is monotone, so repeating it only ever fills in. */ ch |= AN_PASS("mark_empty_hash_key_ctx", mark_empty_hash_key_ctx(c)); ch |= AN_PASS("widen_mixed_key_hash_slots", widen_mixed_key_hash_slots(c)); ch |= AN_PASS("desugar_lazy_stateful_stage", desugar_lazy_stateful_stage(c)); /* arr.lazy.uniq -> arr.uniq (finite source) */ ch |= AN_PASS("desugar_lazy_method_call", desugar_lazy_method_call(c)); /* lz.first where `def lz; ...lazy...; end` */ ch |= AN_PASS("desugar_str_range_methods", desugar_str_range_methods(c)); /* ("a".."e").map -> .to_a.map */ ch |= AN_PASS("desugar_sym_to_proc_call", lower_row_once(c, LW_SYM_TO_PROC_CALL)); /* :m.to_proc.call(r, a) -> r.m(a) */ ch |= AN_PASS("desugar_reduce_method_symbol", desugar_reduce_method_symbol(c)); /* reduce(:gcd) -> reduce { |a,x| a.gcd(x) } */ ch |= AN_PASS("desugar_unused_block_arg", desugar_unused_block_arg(c)); /* a.size(&b) -> a.size */ ch |= AN_PASS("desugar_symbol_var_block_arg", desugar_symbol_var_block_arg(c)); /* m(&sym_var) -> m { |x| x.send(sym_var) } */ ch |= AN_PASS("desugar_poly_symbol_block_arg", desugar_poly_symbol_block_arg(c)); /* m(&poly) -> each literal Symbol its own proc */ ch |= AN_PASS("desugar_kernel_method_block_arg", desugar_kernel_method_block_arg(c)); /* m(&method(:Integer)) -> m { |x| Integer(x) } */ ch |= AN_PASS("desugar_empty_block_body", desugar_empty_block_body(c)); /* m { } -> m { nil } */ ch |= AN_PASS("desugar_hash_block_arg", desugar_hash_block_arg(c)); /* m(&hash) -> m { |x| hash[x] } */ ch |= AN_PASS("desugar_method_call_runtime_name", desugar_method_call_runtime_name(c)); /* method(var).call(a) -> send(var, a) */ ch |= AN_PASS("desugar_dynamic_send", desugar_dynamic_send(c)); /* recv.send(var, a) -> static name dispatch */ ch |= AN_PASS("desugar_dynamic_method", desugar_dynamic_method(c)); ch |= AN_PASS("ver_entries_demand", ver_entries_demand(c)); /* a deferred history a call names now */ ch |= AN_PASS("desugar_dynamic_respond_to", desugar_dynamic_respond_to(c)); /* recv.respond_to?(var) -> static name dispatch */ ch |= AN_PASS("desugar_toplevel_instance_exec", desugar_toplevel_instance_exec(c)); /* top-level instance_exec(&b) -> b.call */ ch |= AN_PASS("desugar_binding_lvget", desugar_binding_lvget(c)); /* binding.local_variable_get(:x) -> x.itself */ ch |= AN_PASS("desugar_step_kwargs", desugar_step_kwargs(c)); /* n.step(to: X, by: Y) -> n.step(X, Y) */ ch |= AN_PASS("desugar_defined_method_call", desugar_defined_method_call(c)); /* defined?(r.m) -> r.respond_to?(:m) */ ch |= AN_PASS("desugar_respond_to_probe", desugar_respond_to_probe(c)); /* recv.respond_to?(:m) -> probe recv.m type */ ch |= AN_PASS("desugar_body_self_call", desugar_body_self_call(c)); /* self.m(..) in a class body -> Klass.m(..) */ ch |= AN_PASS("desugar_handle_reopen_self_recv", desugar_handle_reopen_self_recv(c)); /* bare m(..) in a Thread/Fiber reopen -> self.m(..) */ ch |= AN_PASS("desugar_symbol_to_proc_call", desugar_symbol_to_proc_call(c)); /* :sym.to_proc.call(x) -> x.sym */ ch |= AN_PASS("desugar_call_op_write", desugar_call_op_write(c)); /* r.x += 1 with a def writer -> r.x = r.x + 1 */ ch |= AN_PASS("desugar_call_or_write_reopen", desugar_call_or_write_reopen(c)); /* r.x ||= v on a reopened builtin with a def writer, or on a poly r -> the two calls */ ch |= AN_PASS("desugar_index_op_write_user", desugar_index_op_write_user(c)); /* obj[k] ||= v on a user [] / []= -> the calls */ ch |= AN_PASS("desugar_index_assign_value", desugar_index_assign_value(c)); /* x = (obj[k] = v) on a program []= -> (v1 = v; obj[k] = v1; v1) */ ch |= AN_PASS("desugar_main_self_call", desugar_main_self_call(c)); /* self.m on main with a top-level def m -> m */ ch |= AN_PASS("desugar_cmethod_cvar_reflection", desugar_cmethod_cvar_reflection(c)); /* class_variable_get(:@@x) in def self.m -> K.class_variable_get */ ch |= AN_PASS("desugar_array_at", desugar_array_at(c)); /* a.at(i) -> a[i] */ ch |= AN_PASS("desugar_array_first_last", desugar_array_first_last(c)); /* arr.first -> arr[0], arr.last -> arr[-1] */ ch |= AN_PASS("desugar_to_h_block", desugar_to_h_block(c)); /* recv.to_h{|e|[k,v]} -> recv.map{...}.to_h */ ch |= AN_PASS("desugar_unpack_block", desugar_unpack_block(c)); /* s.unpack(f){|v|..} -> (s.unpack(f).each{|v|..}; nil) */ ch |= AN_PASS("desugar_to_proc_block_arg", desugar_to_proc_block_arg(c)); /* &obj (user to_proc) -> &(obj.to_proc) hoisted once */ ch |= AN_PASS("desugar_proc_expr_block_arg", desugar_proc_expr_block_arg(c)); /* &(a >> b) -> hoisted temp */ ch |= AN_PASS("desugar_to_hash_splat", desugar_to_hash_splat(c)); /* f(**obj) -> f(**obj.to_hash) */ ch |= AN_PASS("desugar_hash_merge_fold", desugar_hash_merge_fold(c)); /* h.merge(a, b) -> h.merge(a).merge(b) (a program merge elsewhere) */ ch |= AN_PASS("desugar_kernel_array_conv", desugar_kernel_array_conv(c)); /* Array(h) -> Array(h.to_ary) (Hash/String subclass's own) */ ch |= AN_PASS("desugar_splat_to_a", desugar_splat_to_a(c)); /* [*h] -> [*h.to_a] (Hash, user #to_a) */ ch |= AN_PASS("desugar_builtin_splat_forward", desugar_builtin_splat_forward(c)); /* @a.m(*args, &blk) on a builtin -> one fixed arity per args.size */ ch |= AN_PASS("desugar_value_callable_forwards", desugar_value_callable_forwards(c)); /* &proc -> { |x| proc.call(x) } */ if (AN_PASS("desugar_builtin_enum_calls", desugar_builtin_enum_calls(c))) { /* recv.m(a) { } -> __enum_m(recv, a) { } */ ch = 1; /* the call is a user method's now: its empty `{}` argument takes the widest hash the way any yielding method's does */ AN_PASS_V("mark_empty_literal_args", mark_empty_literal_args(c)); } ch |= AN_PASS("desugar_builtin_scalar_calls", desugar_builtin_scalar_calls(c)); /* recv.m(a) -> __int_m(recv, a) etc */ ch |= AN_PASS("narrow_empty_array_args_by_yield", narrow_empty_array_args_by_yield(c)); /* f([]) { |m| m << 1 }: the [] is an int array */ ch |= AN_PASS("fold_static_is_a", fold_static_is_a(c)); /* if v.is_a?(Array) on a typed local: one arm */ ch |= AN_PASS("infer_block_params", infer_block_params(c)); ch |= AN_PASS("infer_for_index", infer_for_index(c)); ch |= AN_PASS("infer_catch_block_params", infer_catch_block_params(c)); /* Resolve constant types before ivar inference: a destructured constant (`CLK_1,.. = (1..8).map{...}`) is transiently poly in infer_write_types before its array element type settles, and a monotonic ivar that reads it (`@clk += CLK_1`) would lock onto that poly. Resolving constants first feeds the settled type into ivar inference. */ ch |= AN_PASS("infer_global_const_types", infer_global_const_types(c)); ch |= AN_PASS("infer_multiwrite_const_types", infer_multiwrite_const_types(c)); ch |= AN_PASS("promote_forwarded_rest_args", promote_forwarded_rest_args(c)); ch |= AN_PASS("infer_ivar_types", infer_ivar_types(c)); ch |= AN_PASS("all_handle_static_slots", all_handle_static_slots(c)); ch |= AN_PASS("infer_cvar_types", infer_cvar_types(c, 0)); ch |= AN_PASS("infer_inherited_ivars", infer_inherited_ivars(c)); ch |= AN_PASS("infer_return_types", infer_return_types(c)); ch |= AN_PASS("backprop_hash_return_types", backprop_hash_return_types(c)); if (!ch) { /* Converged with the ambiguous guesses held at bottom. Clear the flag and keep going: a slot whose evidence never arrived is genuinely untypable, so it now takes the pessimistic type and the slots it feeds settle on that. Everything resolvable has already settled concretely, so this second stage widens only what really is open. */ if (g_infer_optimistic) { g_infer_optimistic = 0; comp_touch_global(); /* (a mode inference reads) */ /* settled, and on the last round the cap allows: the second stage still has its round to run */ if (iter + 1 == rc.cap) rc.cap++; continue; } /* Converged: one backstop bind pass lets an empty array-literal arg fill a still-UNKNOWN parameter as an (empty) poly array. If it fills anything, keep iterating so dependent return types resolve. */ static int backstop_ran; if (!backstop_ran) { backstop_ran = 1; g_final_bind_pass = 1; ch = infer_param_types(c); g_final_bind_pass = 0; } if (!ch && !AN_STEP("desugar_mutator_recv_rebind", desugar_mutator_recv_rebind(c))) break; } an_round_cap_step(c, &rc, iter); if (iter + 1 == rc.cap) g_fixpoint_capped = 1; } free(rc.seen); g_infer_optimistic = 0; comp_touch_global(); /* (a mode inference reads) */ } /* Optimistic re-narrow: the monotonic fixpoint locks a slot to poly the first time it sees a transient poly (a value read before its type settled, or an index/hash promotion driven by a then-poly key). Reset every poly ivar and poly (non-block) param to UNKNOWN once, then re-run the monotonic passes. With the now-stable concrete slots feeding them, a slot whose entire contribution closure is concrete re-narrows; one with a genuine heterogeneous contribution re-widens to poly. Sound (monotonic re-derive over fixed inputs); dissolves the cleared transient-poly cycles. A Hash store through a box decides here, from the settled types, which original Hashes it widens (infer_hash_aset_call): during the fixpoint its key or value can still read poly (a parameter not yet re-narrowed), and a widened Hash never narrows back. Its first decision forces the re-run. A slot that joined the Hash's old type cannot follow its widening, so a widening sends the re-narrow round once more. A round can widen a further Hash (a store whose value read the one widened before), so the rounds repeat until one widens none; widening is monotone, so they end. Within a round a widening holds off the exits below until what reads the Hash (a parameter bound from it, a return reading that) has stopped moving, so a store that depends on it decides in the same round. A round that stopped short of a fixpoint (on the stability test) has the next one run even with no poly slot to reset. */ int unsettled = 0; double tm_rn = sp_timing_now(); for (int pass = 0;; pass++) { g_renarrow_rounds++; int widened0 = pivs_hash_stores_widened(c), widened = widened0; int any = pass ? unsettled : pivs_settle_hash_stores(c); /* Record the reset poly ivars so the re-run can re-clear them FRESH each iteration (a narrowing recompute), not just once. infer_ivar_types is monotonic (ty_unify only widens), so a one-shot reset still re-locks an ivar to poly the first time the re-run observes a transient poly from a not-yet-settled slot -- which is exactly what happens in a groundless self-referential poly cycle (clk_irq <- next_interrupt_clock(clk) <- clk <- clk_irq, whose real writes are all int). Re-clearing each iteration makes the ivar = unify-of-its-writes-this-iteration, so once the cycle's int anchors dominate it settles to int; a genuinely heterogeneous ivar re-widens and stays poly. Bounded narrowing over the lattice. */ int rcap = 16, nrec = 0; int *recCi = (int *)malloc(sizeof(int) * rcap), *recIv = (int *)malloc(sizeof(int) * rcap); int *nsoff = (int *)malloc(sizeof(int) * (size_t)(c->nclasses > 0 ? c->nclasses : 1)); char *nsbad = nsoff ? an_ivar_nonscalar_table(c, nsoff) : NULL; for (int ci = 0; ci < c->nclasses; ci++) for (int iv = 0; iv < c->classes[ci].nivars; iv++) /* TY_POLY_ARRAY joins TY_POLY: a container slot locks to the boxed ARRAY the same way a scalar locks to the boxed value, and for the same reason -- `@f << x` folded the push while `x` was still untyped, so the slot took poly_array on evidence that had not settled, and `@f = Array.new(0, 0.0)` unified with it to poly_array rather than the float array it names. Resetting only TY_POLY left exactly the container case the re-narrow exists for out of it. A slot whose pushes really do disagree re-widens on the re-run, as a heterogeneous scalar does. */ if ((c->classes[ci].ivar_types[iv] == TY_POLY || (c->classes[ci].ivar_types[iv] == TY_POLY_ARRAY && nsbad && !nsbad[nsoff[ci] + iv])) && (!c->classes[ci].ivars[iv] || !class_ivar_pinned(&c->classes[ci], c->classes[ci].ivars[iv]))) { ivar_set_type(&c->classes[ci], iv, TY_UNKNOWN); any = 1; if (nrec >= rcap) { rcap *= 2; recCi = realloc(recCi, sizeof(int) * rcap); recIv = realloc(recIv, sizeof(int) * rcap); } recCi[nrec] = ci; recIv[nrec] = iv; nrec++; } for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (cext_scope(c, s)) continue; /* the external ABI is permanently boxed */ for (int i = 0; i < sc->nparams; i++) { LocalVar *p = scope_local(sc, sc->pnames[i]); if (p && p->type == TY_POLY && !p->is_block_param && !p->poly_dispatch_widened) { lv_set_type(p, TY_UNKNOWN); why_reset(&p->why); any = 1; } } } /* Plain locals ratchet through the same monotonic unify, so a local read before its feeding ivar settled (Pulse#sample's `sum` <- @timer) locks poly and re-poisons the ivars this loop just re-narrowed. Reset and re-clear them per iteration exactly like the ivars. */ int lcap = 16, nlrec = 0; int *recLs = (int *)malloc(sizeof(int) * lcap), *recLi = (int *)malloc(sizeof(int) * lcap); for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if (lv->type != TY_POLY || lv->is_param || lv->is_block_param) continue; lv_set_type(lv, TY_UNKNOWN); why_reset(&lv->why); any = 1; if (nlrec >= lcap) { lcap *= 2; recLs = realloc(recLs, sizeof(int) * lcap); recLi = realloc(recLi, sizeof(int) * lcap); } recLs[nlrec] = s; recLi[nlrec] = i; nlrec++; } } /* Returns ratchet through the same monotonic unify, and a poly return is the one link the re-run could not break. An ivar assigned from a call that TAKES THAT IVAR -- `@s3 = rotl32(@s3, 11)` -- re-locks to poly from the call's return every iteration, however often the ivar itself is re-cleared, because nothing re-derives the return. A whole xoshiro PRNG stayed boxed on that: the lanes, the rotate, the draw and every Float built from it. ONLY those returns are reset, not every poly one. A return is boxed for reasons its re-derivation cannot always see -- `@banks[0] = pattern` hands back a row read out of a boxed table, and re-deriving it as the typed array its pushes suggest makes the table store a boxed row as a bare one and read it back as garbage (test/ivar_table_boxed_row_store). The cycle above is the one shape where the poly is groundless, so it is the only one released. An --rbs seed and an --ext-entry are DECLARED, not inferred, and stay put either way. */ int rrcap = 16, nrrec = 0; ACallTargets rtg = { NULL, 0, 0, -1, 0 }; int *recRs = (int *)malloc(sizeof(int) * rrcap); const NodeTable *rnt = c->nt; for (int w = 0; w < rnt->count; w++) { if (nt_kind(rnt, w) != NK_InstanceVariableWriteNode) continue; int wv = nt_ref(rnt, w, "value"); if (wv < 0 || nt_kind(rnt, wv) != NK_CallNode) continue; const char *ivn = nt_str(rnt, w, "name"); int icid = comp_ivar_owner(c, w); if (!ivn || icid < 0) continue; /* the call has to READ the same slot it is assigned to */ int aN = nt_ref(rnt, wv, "arguments"); int aC = 0; const int *aV = aN >= 0 ? nt_arr(rnt, aN, "arguments", &aC) : NULL; int selfref = 0; for (int k = 0; k < aC && aV && !selfref; k++) { if (nt_kind(rnt, aV[k]) != NK_InstanceVariableReadNode) continue; const char *an2 = nt_str(rnt, aV[k], "name"); if (an2 && sp_streq(an2, ivn) && comp_ivar_owner(c, aV[k]) == icid) selfref = 1; } if (!selfref) continue; AN_STEP_V("an_call_targets_of", an_call_targets_of(c, wv, &rtg)); for (int k = 0; k < rtg.n; k++) { int mi = rtg.v[k]; if (mi < 0 || mi >= c->nscopes) continue; Scope *sc = &c->scopes[mi]; if (sc->ret != TY_POLY || sc->is_ext_entry || sc->ret_rbs_seeded) continue; int seen = 0; for (int q = 0; q < nrrec && !seen; q++) if (recRs[q] == mi) seen = 1; if (seen) continue; scope_set_ret(sc, TY_UNKNOWN); any = 1; if (nrrec >= rrcap) { rrcap *= 2; recRs = (int *)realloc(recRs, sizeof(int) * rrcap); } recRs[nrrec++] = mi; } } free(rtg.v); if (AN_PASS("reset_locked_iter_block_params", reset_locked_iter_block_params(c))) any = 1; if (any) { TyKind *prev = (TyKind *)malloc(sizeof(TyKind) * (nrec > 0 ? nrec : 1)); TyKind *lprev = (TyKind *)malloc(sizeof(TyKind) * (nlrec > 0 ? nlrec : 1)); /* post-derive snapshots of the previous iteration, for the fixed-cycle exit below */ TyKind *prevd = (TyKind *)malloc(sizeof(TyKind) * (nrec > 0 ? nrec : 1)); TyKind *lprevd = (TyKind *)malloc(sizeof(TyKind) * (nlrec > 0 ? nlrec : 1)); int have_prevd = 0; /* Every class's ivar slots, flagged where this loop re-clears them, and a snapshot of their types: infer_inherited_ivars below re-fills an inherited slot the re-clear zeroed (a child's copy of a reset parent ivar) every round, the way the re-derive pair does, and only a change to a slot NOT re-cleared counts toward the fixed-cycle exit. */ int ivncls = c->nclasses; int *ivoff = (int *)malloc(sizeof(int) * (size_t)(ivncls + 1)); ivoff[0] = 0; for (int ci = 0; ci < ivncls; ci++) ivoff[ci + 1] = ivoff[ci] + c->classes[ci].nivars; char *ivrec = (char *)calloc((size_t)ivoff[ivncls] + 1, 1); TyKind *ivsnap = (TyKind *)malloc(sizeof(TyKind) * (size_t)(ivoff[ivncls] + 1)); for (int k = 0; k < nrec; k++) ivrec[ivoff[recCi[k]] + recIv[k]] = 1; /* The same for returns: infer_return_types re-fills a return the loop reset every round, so only a change to one it did not reset counts, and the reset ones join the fixed-cycle test. */ int rsn = c->nscopes; char *rsrec = (char *)calloc((size_t)rsn + 1, 1); TyKind *rssnap = (TyKind *)malloc(sizeof(TyKind) * (size_t)(rsn + 1)); TyKind *rprevd = (TyKind *)malloc(sizeof(TyKind) * (nrrec > 0 ? nrrec : 1)); int have_rprevd = 0; for (int k = 0; k < nrrec; k++) rsrec[recRs[k]] = 1; AnRoundCap rc = { 128, 0, 0, NULL, 0 }; unsettled = 1; /* the type states the last iterations ended in, newest first, for the periodic exit below; cyc_left counts the iterations still to run once a period is found */ enum { CYC_KEEP = 8 }; uint64_t cyc_st[CYC_KEEP]; int cyc_wid[CYC_KEEP], cyc_nodes[CYC_KEEP], cyc_n = 0, cyc_p = 0, cyc_left = -1; for (int iter = 0; iter < rc.cap; iter++) { /* Parameters bind from the SETTLED state of the previous iteration, before this one's re-clear. Bound after it, a parameter sampled whichever of an ivar's writes had been merged by then: a poly ivar holding an Array on one path and a Relation on another was merged from the Relation write alone when the Array-answering call had not been typed yet that round, the parameter was pinned to Relation, and the later widening never reached it because the next iteration's bind sampled the same partial state. The call site then read the Array's header as a Relation (#4437). One iteration of lag, and the loop's stability test already waits for the ivars to stop moving. */ AN_PASS_V("seed_unsupplied_nil_defaults", seed_unsupplied_nil_defaults(c)); /* a re-cleared nil-default parameter is poly again before anything binds on it (#4583) */ AN_PASS_V("infer_param_types", infer_param_types(c)); /* A default is a binding like any call site's argument, and reads the same settled state: after the re-clear below, a default reading a reset ivar (`romh: @rom`, @rom poly) saw UNKNOWN every iteration, so it never widened the parameter and the one call site's explicit argument type stood alone. The pass further down still runs for defaults over state this iteration derives; types only widen. */ int pre_def = AN_PASS_T("infer_default_param_types", infer_default_param_types(c)); /* stash last-settled values, then re-clear the reset ivars so they recompute fresh (narrowing) this iteration. */ for (int k = 0; k < nrec; k++) prev[k] = c->classes[recCi[k]].ivar_types[recIv[k]]; for (int k = 0; k < nrec; k++) ivar_set_type(&c->classes[recCi[k]], recIv[k], TY_UNKNOWN); for (int k = 0; k < nlrec; k++) lprev[k] = c->scopes[recLs[k]].locals[recLi[k]].type; for (int k = 0; k < nlrec; k++) lv_set_type(&c->scopes[recLs[k]].locals[recLi[k]], TY_UNKNOWN); for (int k = 0; k < nrrec; k++) scope_set_ret(&c->scopes[recRs[k]], TY_UNKNOWN); sp_narrow_memo_bump(); /* invalidate per-iteration narrow-helper memo */ int ch = pre_def, ch_other = pre_def; ch |= AN_PASS("infer_write_types", infer_write_types(c)); { int _w = AN_PASS_T("bind_coerce_operator_params", bind_coerce_operator_params(c)); ch |= _w; ch_other |= _w; } /* 3 + obj calls obj's op WITH obj */ { int _w = AN_PASS_T("infer_param_hash_value", infer_param_hash_value(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("propagate_prep_params", propagate_prep_params(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_string_params", infer_string_params(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_default_param_types", infer_default_param_types(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_block_params", infer_block_params(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_for_index", infer_for_index(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_catch_block_params", infer_catch_block_params(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_global_const_types", infer_global_const_types(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_multiwrite_const_types", infer_multiwrite_const_types(c)); ch |= _w; ch_other |= _w; } ch |= AN_PASS("infer_ivar_types", infer_ivar_types(c)); ch |= AN_PASS("all_handle_static_slots", all_handle_static_slots(c)); /* AFTER the ivar write-merge: the re-clear zeroes recorded ivars at each iteration's top, so a promote gated on STRING must see the freshly re-derived type, not the cleared UNKNOWN (#3227 P4) */ { int _w = AN_PASS_T("promote_forwarded_rest_args", promote_forwarded_rest_args(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("widen_shared_cmp_params", widen_shared_cmp_params(c)); ch |= _w; ch_other |= _w; } { int _w = AN_PASS_T("infer_cvar_types", infer_cvar_types(c, 0)); ch |= _w; ch_other |= _w; } int ivsame = c->nclasses == ivncls; for (int ci = 0; ivsame && ci < ivncls; ci++) if (c->classes[ci].nivars != ivoff[ci + 1] - ivoff[ci]) ivsame = 0; for (int ci = 0; ivsame && ci < ivncls; ci++) for (int iv = 0; iv < c->classes[ci].nivars; iv++) ivsnap[ivoff[ci] + iv] = c->classes[ci].ivar_types[iv]; if (AN_PASS("infer_inherited_ivars", infer_inherited_ivars(c))) { ch = 1; if (!ivsame) ch_other = 1; /* the layout moved: count it, as before */ for (int ci = 0; ci < ivncls && !ch_other; ci++) { for (int iv = 0; iv < c->classes[ci].nivars; iv++) if (!ivrec[ivoff[ci] + iv] && c->classes[ci].ivar_types[iv] != ivsnap[ivoff[ci] + iv]) { ch_other = 1; break; } } } int rssame = c->nscopes == rsn; for (int q = 0; rssame && q < rsn; q++) rssnap[q] = c->scopes[q].ret; if (infer_return_types(c)) { if (!rssame || c->nscopes != rsn) ch = ch_other = 1; for (int q = 0; q < rsn && !ch_other; q++) if (!rsrec[q] && c->scopes[q].ret != rssnap[q]) ch = ch_other = 1; if (!have_rprevd) ch = 1; for (int k = 0; k < nrrec && !ch; k++) if (c->scopes[recRs[k]].ret != rprevd[k]) ch = 1; } /* a Hash store widened this iteration, or one widened this round and its readers still moved */ int spreading = pivs_hash_stores_widened(c) != widened || (widened != widened0 && ch_other); widened = pivs_hash_stores_widened(c); /* Periodic exit. The whole state the passes read (an_loop_state: every node's type and the per-node notes, each scope's return and locals with the notes kept on them, the ivars, globals and constants) is digested as each iteration ends, with the node count and the count of widened Hash stores. The passes are deterministic over that state: once an iteration ends in the state one p iterations back ended in, and the one before it in that one's predecessor, every further iteration repeats the cycle of p states, and the cap ends in the one of them whose distance to the cap's last iteration is a multiple of p. The loop runs on to that one and stops there, so what it leaves is what the cap would have left. The two exits below cannot always see a settled loop. The stability test compares each reset slot with a snapshot taken after the bind at the top, and a slot the bind re-widens re-derives narrower every iteration, forever. The fixed-cycle exit, and the `spreading` term the stability test shares with it, wait for every pass but the re-derive pair to report no change, and the share pass re-promotes the slots the re-clear zeroed every iteration -- the value it gave them last time -- and so reports one every iteration. A Rails application was down to one ivar and one return trading two states by iteration 5 and ran the remaining 123 (19 seconds each) to the cap on exactly that: a third of its analysis. */ { uint64_t st = an_loop_state(c); int nodes = c->nt->count, stop = 0; for (int q = CYC_KEEP - 1; q > 0; q--) { cyc_st[q] = cyc_st[q - 1]; cyc_wid[q] = cyc_wid[q - 1]; cyc_nodes[q] = cyc_nodes[q - 1]; } cyc_st[0] = st; cyc_wid[0] = widened; cyc_nodes[0] = nodes; if (cyc_n < CYC_KEEP) cyc_n++; #define CYC_SAME(a, b) (cyc_st[a] == cyc_st[b] && cyc_wid[a] == cyc_wid[b] && cyc_nodes[a] == cyc_nodes[b]) if (cyc_left > 0) { /* on the way to the cap's state: still in the cycle? */ if (cyc_p < cyc_n && CYC_SAME(0, cyc_p)) { if (--cyc_left == 0) stop = 1; } else cyc_left = -1; } if (cyc_left < 0) for (int p = 1; p + 1 < cyc_n; p++) if (CYC_SAME(0, p) && CYC_SAME(1, p + 1)) { int r = (rc.cap - 1 - iter) % p; if (r == 0) stop = 1; else { cyc_p = p; cyc_left = r; } break; } #undef CYC_SAME if (stop) break; } /* With reset ivars, the re-clear makes infer_ivar_types report change every iteration, so converge on ivar value-stability instead. With none (only poly params/returns reset), fall back to the normal no-change fixpoint so the param/return passes fully settle. */ if (nrec > 0 || nlrec > 0) { int stable = 1; for (int k = 0; k < nrec; k++) if (c->classes[recCi[k]].ivar_types[recIv[k]] != prev[k]) { stable = 0; break; } for (int k = 0; stable && k < nlrec; k++) if (c->scopes[recLs[k]].locals[recLi[k]].type != lprev[k]) stable = 0; if (stable && !spreading) break; /* Fixed-cycle exit: when no pass beyond the re-derive pair (infer_write_types / infer_ivar_types, which re-fill the slots the re-clear zeroes and so report "change" every round by construction) changed anything this iteration, AND every recorded slot re-derived to the value it re-derived to last iteration, the loop is a period-1 cycle: the re-derive pair is deterministic over frozen inputs, so each further iteration recomputes exactly this state, and running to the 128-round cap ends HERE anyway. The stability test above cannot see it: it compares against the pre-clear snapshot, and a slot the top-of-loop param bind re-widens (poly in, nothing derives it back) differs from its own recompute every round, forever. One 53k-line machine-generated program burned the whole cap -- 123 no-op iterations, each a dozen whole-program passes -- on exactly that shape. */ if (!ch_other && !spreading && have_prevd) { int cyc = 1; for (int k = 0; k < nrec && cyc; k++) if (c->classes[recCi[k]].ivar_types[recIv[k]] != prevd[k]) cyc = 0; for (int k = 0; k < nlrec && cyc; k++) if (c->scopes[recLs[k]].locals[recLi[k]].type != lprevd[k]) cyc = 0; for (int k = 0; k < nrrec && cyc; k++) if (c->scopes[recRs[k]].ret != rprevd[k]) cyc = 0; if (cyc) break; } for (int k = 0; k < nrec; k++) prevd[k] = c->classes[recCi[k]].ivar_types[recIv[k]]; for (int k = 0; k < nlrec; k++) lprevd[k] = c->scopes[recLs[k]].locals[recLi[k]].type; have_prevd = 1; } else if (!ch) { unsettled = 0; break; } for (int k = 0; k < nrrec; k++) rprevd[k] = c->scopes[recRs[k]].ret; have_rprevd = 1; /* a return whose parameter was reset loses its type for a round, and so in turn does each caller's up a chain */ an_round_cap_step(c, &rc, iter); } free(rc.seen); /* the bind lags one iteration; take the settled state once more */ AN_PASS_V("infer_param_types", infer_param_types(c)); free(prev); free(lprev); free(prevd); free(lprevd); free(ivoff); free(ivrec); free(ivsnap); free(rsrec); free(rssnap); free(rprevd); } free(recCi); free(recIv); free(recLs); free(recLi); free(recRs); free(nsoff); free(nsbad); if (AN_PASS("pivs_hash_stores_widened", pivs_hash_stores_widened(c)) == widened0) break; } if (sp_timing_on()) g_renarrow_ms += sp_timing_now() - tm_rn; /* A parameter the reverse binding widened for a callee's parameter that settled boxed or untyped keeps its general Array, though the callee takes the array by reference (#7948). Now that the callee's type is final, give the parameter back to its callers' arguments and derive once more, the reverse binding held off it. */ if (AN_PASS("an_retract_stale_reverse_widening", an_retract_stale_reverse_widening(c))) an_phase_infer_fixpoint(c); if (AN_PASS("an_settle_seed_boxes", an_settle_seed_boxes(c))) an_phase_infer_fixpoint(c); } /* After the fixpoint: the backstops for slots left without a type (empty literals, nullable params, unknown ivars and hashes), the nil-guard and is_a? narrowing, the return-type re-run, the param and hash-shape reconciliation, the bigint loop variables (analyze_program's steps, in their order) */ static void an_phase_post_fixpoint(Compiler *c) { /* Backstop: a constant bound to an EMPTY array literal has no element type to read off the literal, and nothing else in the program need ever narrow it (`DISPATCH = []` filled only through `DISPATCH[op] = args`, whose value type may itself still be settling). Leaving it UNKNOWN does not read as "unknown" downstream, it reads as UNDEFINED: every reference reports the constant as defined nowhere and raises NameError, where CRuby answers the empty array (#4051). Default it the way an empty literal bound to a local defaults. This runs after the fixpoint, so any real element evidence still wins. */ for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_ConstantWriteNode) continue; const char *cwn = nt_str(c->nt, id, "name"); LocalVar *cwv = cwn ? comp_const(c, cwn) : NULL; if (!cwv || cwv->type != TY_UNKNOWN) continue; int cwval = nt_ref(c->nt, id, "value"); if (cwval < 0 || nt_kind(c->nt, cwval) != NK_ArrayNode) continue; int cwn_els = 0; nt_arr(c->nt, cwval, "elements", &cwn_els); if (cwn_els == 0) lv_set_type(cwv, TY_POLY_ARRAY); } /* Backstop: a parameter still unknown but with a `= nil` default is a nullable param -- represent it as poly so it can hold nil or a value. Also widen TY_SYMBOL/TY_BOOL params: those types have no nil sentinel and must be boxed into poly when the nil default is reachable. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nparams; i++) { if (sc->pdefault[i] < 0) continue; NodeKind dk = nt_kind(c->nt, sc->pdefault[i]); if (dk != NK_NilNode) continue; LocalVar *p = scope_local(sc, sc->pnames[i]); if (!p || p->rbs_seeded) continue; if (p->type == TY_UNKNOWN) slot_rule(c, p, TY_POLY, sc->pdefault[i], "a `= nil` default and no call site typing it: the parameter holds nil or a value, untyped"); else if (p->type == TY_SYMBOL || p->type == TY_BOOL || p->type == TY_RANGE || p->type == TY_FLOAT_RANGE || p->type == TY_STR_RANGE) slot_rule(c, p, TY_POLY, sc->pdefault[i], "a `= nil` default on a Symbol, Bool or Range parameter, which has no nil of its own: boxed, untyped"); } } /* Backstop: transplanted module scopes share the same def_node. If one copy has known param types (from call sites) but another copy lacks callers and has TY_UNKNOWN params, propagate the known types across. */ for (int s1 = 0; s1 < c->nscopes; s1++) { Scope *sc1 = &c->scopes[s1]; if (sc1->nparams == 0 || sc1->def_node < 0 || !sc1->name) continue; for (int pi = 0; pi < sc1->nparams; pi++) { if (!sc1->pnames[pi]) continue; LocalVar *p1 = scope_local(sc1, sc1->pnames[pi]); if (!p1 || p1->type != TY_UNKNOWN) continue; for (int s2 = 0; s2 < c->nscopes; s2++) { if (s2 == s1) continue; Scope *sc2 = &c->scopes[s2]; if (sc2->def_node != sc1->def_node || sc2->nparams != sc1->nparams) continue; if (pi >= sc2->nparams || !sc2->pnames[pi]) continue; LocalVar *p2 = scope_local(sc2, sc2->pnames[pi]); if (!p2 || p2->type == TY_UNKNOWN) continue; lv_set_type(p1, p2->type); break; } } } /* Backstop: an ivar assigned only an empty array literal (no element evidence from usage) is left UNKNOWN, which falls back to int and a scalar struct field. Default such a slot to an (empty) Array, boxed as a local's is below: it may still be filled through another name -- the block a method yields it to, or a method it is passed to -- and an Integer Array raised TypeError on the String such a block pushed. */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_InstanceVariableWriteNode) continue; int v = nt_ref(c->nt, id, "value"); NodeKind vty_k = nt_kind(c->nt, v); if (vty_k != NK_ArrayNode) continue; int en = 0; nt_arr(c->nt, v, "elements", &en); if (en != 0) continue; Scope *s = comp_scope_of(c, id); int cls_id_bs = s->class_id; if (cls_id_bs < 0) cls_id_bs = c->toplevel_class; if (cls_id_bs < 0) continue; ClassInfo *ci = &c->classes[cls_id_bs]; int iv = comp_ivar_index(ci, nt_str(c->nt, id, "name")); if (iv >= 0 && ci->ivar_types[iv] == TY_UNKNOWN) ivar_set_type(ci, iv, TY_POLY_ARRAY); } /* An ivar write in VALUE position evaluates to the slot. When the slot is a strbuf, the value is presented exactly as an ordinary read of the slot is: the const char * face with the live handle in _sp_ret_strbuf, so the write's type, the return of a method whose body it is and every call to that method agree on `const char *`. The write used to be marked to yield the handle itself (#3993), which typed the function `sp_String *` while its callers, typed from the String face, read a `const char *`: `p obj.w("a")` for `def w(v) = (@body = v.to_s)` printed garbage, and the C did not build under -Werror (#4567). */ /* Backstop: a local variable assigned only empty arrays -- a `[]` literal or a bare `Array.new`, the same empty array (#3613) -- with no push evidence stays TY_UNKNOWN. Default it to TY_POLY_ARRAY so array operations (map!, p, etc.) can dispatch. */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_LocalVariableWriteNode) continue; int v = nt_ref(c->nt, id, "value"); NodeKind vk = nt_kind(c->nt, v); if ((vk != NK_ArrayNode && vk != NK_CallNode) || an_empty_container_kind(c, v) != 1) continue; const char *nm = nt_str(c->nt, id, "name"); Scope *s = comp_scope_of(c, id); LocalVar *lv = nm ? scope_local(s, nm) : NULL; /* Also reset any hash type that crept in via premature [] read promotion: a variable whose only write is an empty array literal is definitively an array, not a hash. */ if (!lv || lv->rbs_seeded) continue; if (lv->type == TY_UNKNOWN) { /* Another write of a hash literal makes the slot hold both kinds, and only the boxed one does: the two empty literals otherwise agreed on "no type" and the array backstop claimed the slot for arrays, so the hash literal was built into an array-shaped slot. */ int has_hash_write = 0; for (int w = 0; w < c->nt->count && !has_hash_write; w++) { if (nt_kind(c->nt, w) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(c->nt, w, "name"); if (!wn || !nm || !sp_streq(wn, nm) || comp_scope_of(c, w) != s) continue; int wv = nt_ref(c->nt, w, "value"); if (wv >= 0 && nt_kind(c->nt, wv) == NK_HashNode) has_hash_write = 1; } lv_set_type(lv, has_hash_write ? TY_POLY : TY_POLY_ARRAY); } /* A hash type here is usually one a premature `[]` read promoted, and a local whose every write is an empty array literal is definitively an array. A local that really is written a hash somewhere else holds both, and only the boxed slot does. */ else if (ty_is_hash(lv->type)) lv_set_type(lv, local_all_writes_empty_array(c, s, nm) ? TY_POLY_ARRAY : TY_POLY); /* A slot another write typed as something that cannot hold an array (a String, an Integer, an object) has to widen: the empty literal carries no type of its own, so the union kept the other write's type and the array was emitted into a slot shaped for it (`a = ""; a = []` assigned an sp_IntArray to a const char *). A non-empty literal already widens this way -- the union of the two types is the boxed one (#3990). */ else if (!ty_is_array(lv->type) && lv->type != TY_POLY) lv_set_type(lv, TY_POLY); } /* Backstop: a local assigned only empty hashes -- a `{}` literal or a bare `Hash.new` -- that no key-write ever narrowed stays TY_UNKNOWN, so its hash block methods (select/reject/each/...) can't dispatch. Default it to the bare-{} hash type. Runs post-fixpoint, so a key-write that narrowed it to a concrete variant (SYM_POLY / STR_INT / ...) already set lv->type and is kept (#2336). */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_LocalVariableWriteNode) continue; int v = nt_ref(c->nt, id, "value"); NodeKind vk = nt_kind(c->nt, v); if (vk == NK_HashNode) { int en = 0; nt_arr(c->nt, v, "elements", &en); if (en != 0) continue; } else if (vk != NK_CallNode || an_empty_container_kind(c, v) != 2 || nt_ref(c->nt, v, "arguments") >= 0 || nt_ref(c->nt, v, "block") >= 0) continue; const char *nm = nt_str(c->nt, id, "name"); Scope *s = comp_scope_of(c, id); LocalVar *lv = nm ? scope_local(s, nm) : NULL; if (lv && !lv->rbs_seeded && lv->type == TY_UNKNOWN && local_all_writes_empty_hash_or_new(c, s, nm)) { /* a use context on this write's literal (an indexing key, a compared peer) picks the variant; otherwise the local's own `[]=` keys do. The StrPolyHash default is only safe for a String (or absent) key: codegen reads a StrPolyHash key out of the boxed value as `.v.s`, so defaulting an Integer- or poly-keyed hash to it hands a boxed int to a `const char *` slot and segfaults. The key context pass could not see this key -- it runs during the fixpoint, and a key that is a block's return value has no type yet -- but here, post-fixpoint, it does (#3397). */ TyKind want = (c->hash_want && v < c->node_cap) ? c->hash_want[v] : TY_UNKNOWN; /* The key context marks only a `{}` literal, so a bare `Hash.new` has no variant here. It takes the boxed slot such a local always ended in (a default proc read with other keys still answers), but here, where an empty `{}` settles, so a literal holding it as an element, and a local read out of one, follow as they do for `{}`. */ if (vk == NK_CallNode && !ty_is_hash(want)) want = TY_POLY; else if (!ty_is_hash(want)) { int nkw = 0; TyKind kt = local_aset_key_type(c, s, nm, &nkw); want = (kt == TY_SYMBOL) ? TY_SYM_POLY_HASH : (kt == TY_STRING || nkw == 0) ? TY_STR_POLY_HASH : TY_POLY_POLY_HASH; } lv_set_type(lv, want); } } /* Re-narrow a POLY_ARRAY ivar to IntArray when every element source is now (post-fixpoint) int. The monotonic usage pass locks the slot to POLY_ARRAY the first time it sees a push whose element type is still unknown -- e.g. `@output_pixels << @output_color[i]` evaluated before @output_color settled to IntArray. Once the element types settle, a slot whose only writes are int pushes / int `[]=` / empty-or-int-array assignments can drop the per-element boxing (optcarrot's per-frame pixel buffer is the motivating case). Strictly conservative: any non-int source, or a write shape we don't model, keeps it poly. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { if (cl->ivar_int_table[iv]) continue; /* already narrowed and pinned */ if (cl->ivar_types[iv] != TY_POLY_ARRAY) continue; const char *ivn = cl->ivars[iv]; int saw = 0, ok = 1; for (int id = 0; id < c->nt->count && ok; id++) { const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) continue; if (ty_k == NK_CallNode) { int recv = nt_ref(c->nt, id, "receiver"); if (recv < 0) continue; NodeKind rk = nt_kind(c->nt, recv); if (rk != NK_InstanceVariableReadNode) continue; const char *rivn = nt_str(c->nt, recv, "name"); if (!rivn || !sp_streq(rivn, ivn)) continue; Scope *sc = comp_scope_of(c, id); if (!sc || sc->class_id != ci) { ok = 0; break; } const char *nm = nt_str(c->nt, id, "name"); int args = nt_ref(c->nt, id, "arguments"); int an = 0; const int *argv = args >= 0 ? nt_arr(c->nt, args, "arguments", &an) : NULL; if (nm && is_push_alias(nm)) { for (int a = 0; a < an; a++) { saw = 1; if (infer_type(c, argv[a]) != TY_INT) { ok = 0; break; } } } else if (nm && sp_streq(nm, "[]=") && an == 2) { saw = 1; if (infer_type(c, argv[1]) != TY_INT) ok = 0; } } else if (ty_k == NK_InstanceVariableWriteNode) { const char *wivn = nt_str(c->nt, id, "name"); if (!wivn || !sp_streq(wivn, ivn)) continue; Scope *sc = comp_scope_of(c, id); if (!sc || sc->class_id != ci) { ok = 0; break; } int v = nt_ref(c->nt, id, "value"); TyKind vt = v >= 0 ? infer_type(c, v) : TY_UNKNOWN; if (vt == TY_INT_ARRAY) { /* int array source */ } else if (v >= 0 && nt_kind(c->nt, v) == NK_ArrayNode) { int en = 0; const int *el = nt_arr(c->nt, v, "elements", &en); for (int e = 0; e < en; e++) if (infer_type(c, el[e]) != TY_INT) { ok = 0; break; } } else { ok = 0; } } } if (saw && ok) { ivar_set_type(cl, iv, TY_INT_ARRAY); } } } /* A read-only ivar (referenced but never assigned a typed value) stays TY_UNKNOWN -> it has no C type. Such a slot always reads nil at runtime; give it a boxed-nil poly field so `.nil?`/`.inspect` behave (#712). A never-typed BASE-class slot (only nil inits, e.g. an abstract Oscillator whose subclasses hold the real object) takes its subclasses' unified type first: going straight to poly would re-widen every typed subclass slot on the rerun below. Post-fixpoint, so subclass types are final. */ int ivar_backstop_changed = 0; for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { if (cl->ivar_types[iv] != TY_UNKNOWN) continue; /* Unify across the whole hierarchy this class belongs to (root's subtree), so an abstract base AND a leaf that never writes the ivar (e.g. Triangle's @envelope) both take the type their siblings hold, keeping one C type per field across the hierarchy. */ int root = ci; while (c->classes[root].parent >= 0) root = c->classes[root].parent; TyKind fill = TY_UNKNOWN; for (int cj = 0; cj < c->nclasses; cj++) { int an = cj; while (an >= 0 && an != root) an = c->classes[an].parent; if (an != root) continue; int cidx = comp_ivar_index(&c->classes[cj], cl->ivars[iv]); if (cidx >= 0 && c->classes[cj].ivar_types[cidx] != TY_UNKNOWN) fill = ty_unify(fill, c->classes[cj].ivar_types[cidx]); } ivar_set_type(cl, iv, fill != TY_UNKNOWN ? fill : TY_POLY); ivar_backstop_changed = 1; } } /* A void/nil-typed ivar slot -- only ever assigned a value-less expression (a writer call, a value-less `if`) or a bare nil that never unified with a concrete type -- has no C storage type: emit_class_struct would declare a `void` field. Widen to poly; such a slot always reads nil at runtime. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { TyKind ivt = cl->ivar_types[iv]; if (ivt != TY_VOID && ivt != TY_NIL) continue; ivar_set_type(cl, iv, TY_POLY); ivar_backstop_changed = 1; } } /* A class variable never assigned a typed value -- activesupport's `mattr_accessor :parse_json_times`, declared nil and set by nobody in the program -- has no C storage type either, and fell to an Integer slot whose reader was refused in a condition. Such a slot always reads nil: give it the boxed-nil poly global, as the ivar above. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int cv = 0; cv < cl->ncvars; cv++) { TyKind t = cl->cvar_types[cv]; if (t != TY_UNKNOWN && t != TY_VOID && t != TY_NIL) continue; cvar_set_type(cl, cv, TY_POLY); ivar_backstop_changed = 1; } } /* An attr_reader/attr_accessor ivar typed via a writer call (scalar type), but whose class has no initialize that writes it, starts nil on fresh instances. Only widen when there is NO write inside ANY initialize in the inheritance chain (the read-only case is already TY_POLY via the TY_UNKNOWN pass above). */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; if (cl->is_struct) continue; /* struct members are set by generated ctor */ for (int ri = 0; ri < cl->nreaders; ri++) { const char *rname = cl->readers[ri]; if (!rname) continue; char ivname[300]; snprintf(ivname, sizeof ivname, "@%s", rname); /* skip only when THIS ivar is assigned in a ctor; an existing ctor that does not touch it still leaves it nil (#3136) */ if (ctor_writes_ivar(c, ci, ivname)) continue; int iv = comp_ivar_index(cl, ivname); if (iv < 0) continue; if (class_ivar_pinned(cl, ivname)) continue; /* --rbs seed pins the type */ TyKind t = cl->ivar_types[iv]; /* TY_INT is exempt: the generated constructor already seeds int ivars with their nil bit set (emit_ivar_nil_inits), so a pre-write read is nil through the nullable-int machinery without widening to poly. */ if (t != TY_FLOAT && t != TY_STRING && t != TY_SYMBOL && t != TY_BOOL) continue; ivar_set_type(cl, iv, TY_POLY); ivar_backstop_changed = 1; /* Also patch the node-type cache for all InstanceVariableReadNode and InstanceVariableWriteNode nodes that reference this ivar, so codegen sees TY_POLY for both the struct field and the node type. The nodes of the five kinds are read off the kind index: a walk of every node for each widened reader grew with the program squared (#10546). */ static const NodeKind ivar_kinds[5] = { NK_InstanceVariableReadNode, NK_InstanceVariableWriteNode, NK_InstanceVariableOperatorWriteNode, NK_InstanceVariableOrWriteNode, NK_InstanceVariableAndWriteNode }; for (int q = 0; q < 5; q++) NT_FOREACH_KIND(c->nt, ivar_kinds[q], nid) { if (!nt_type(c->nt, nid)) continue; /* only within methods of this class */ Scope *s = comp_scope_of(c, nid); if (!s || s->class_id != ci) continue; const char *nm = nt_str(c->nt, nid, "name"); if (nm && sp_streq(nm, ivname)) comp_set_ntype(c, nid, TY_POLY); } } } /* `return .. if p.nil?` early-return guards: reads of the guarded poly param after the guard take its non-nil type (#1661). Post-fixpoint so param and argument types are final; before the return recompute below so narrowed tails type the enclosing method's return. */ AN_STEP_V("narrow_nil_guard_params", narrow_nil_guard_params(c)); AN_STEP_V("narrow_nil_guard_locals", narrow_nil_guard_locals(c)); /* `if v.is_a?(K)` occurrence typing: reads of a poly v inside the guard's then-arm narrow to K's concrete type (post-fixpoint, same read-site unbox as the nil guards above). */ narrow_isa_guards(c); /* `rescue K => e` where a second arm binds the same name: reads inside an arm take that arm's class (#4343). */ narrow_rescue_arm_reads(c); /* Post-backstop: re-run write type inference so multi-write locals whose RHS chains through a now-typed ivar (e.g. @h[bank][idx] where @h was just promoted from UNKNOWN to POLY) get their types resolved. */ AN_STEP_V("infer_write_types", infer_write_types(c)); /* recompute returns: a method returning such a param is now poly */ for (int iter = 0; iter < 8; iter++) if (!infer_return_types(c)) break; /* Post-fixpoint body-usage inference: type any param still TY_UNKNOWN from how it is used inside the method body (hash subscript patterns, array-specific calls). Runs after the main fixpoint so caller-side types always win; the mini-loop below propagates the new types. Also re-runs after the ivar backstops above widened an UNKNOWN slot to poly: reads of such an ivar inferred UNKNOWN during the fixpoint, leaving params bound from them untyped (and the method dropped, turning its call sites into undefined references). */ if (infer_hash_params(c) | infer_array_params(c) | infer_params_from_ivar_hash_ops(c) | ivar_backstop_changed) { for (int iter = 0; iter < 16; iter++) { int ch = 0; ch |= infer_param_types(c); ch |= infer_return_types(c); /* Re-run write-type inference so locals whose types derive from function return types (e.g. `x = f([])` after `f`'s param was promoted from UNKNOWN to POLY_ARRAY) get updated. */ ch |= infer_write_types(c); /* ...and a global or constant written from such an ivar: `$g = @v` merged the ivar's UNKNOWN or nil during the fixpoint and kept its scalar slot, which the boxed ivar cannot be assigned to. */ ch |= infer_global_const_types(c); if (!ch) break; } } /* Post-fixpoint: hash-variant back-propagation. A local that unified to TY_POLY_POLY_HASH (e.g. a subscript write with a poly key widened it) while the ivars feeding it stayed Str/Sym-keyed leaves the emitted C with a PolyPolyHash* local pointing at a StrPolyHash object -- every write through it then runs PolyPolyHash_set against the wrong struct layout and corrupts the heap (doom's Animations#update `translation = ... ? @texture_translation : @flat_translation`, which shredded neighboring framebuffer memory into nils). Widen such source ivars to the local's variant and re-run inference. */ { int hb_changed = 0; for (int id = 0; id < c->nt->count; id++) { NodeKind nk = nt_kind(c->nt, id); if (nk != NK_LocalVariableWriteNode) continue; const char *nm = nt_str(c->nt, id, "name"); LocalVar *lv = nm ? scope_local(comp_scope_of(c, id), nm) : NULL; if (!lv || lv->type != TY_POLY_POLY_HASH) continue; int v = nt_ref(c->nt, id, "value"); if (v < 0) continue; /* DFS the value subtree for ivar reads (covers ternary/if arms); stop at nested defs. The worklist grows: a fixed 250 dropped the children past it, and an ivar read there was missed. */ int cap = 256, sp = 0; int *stack = (int *)malloc(sizeof(int) * (size_t)cap); if (!stack) continue; stack[sp++] = v; while (sp > 0) { int nid = stack[--sp]; const char *t2 = nt_type(c->nt, nid); NodeKind t2_k = nt_kind(c->nt, nid); if (!t2 || t2_k == NK_DefNode) continue; if (t2_k == NK_InstanceVariableReadNode) { Scope *sc2 = comp_scope_of(c, nid); int ci2 = sc2 ? sc2->class_id : -1; const char *ivn = nt_str(c->nt, nid, "name"); if (ci2 >= 0 && ivn) { int ix = comp_ivar_index(&c->classes[ci2], ivn); if (ix >= 0 && !class_ivar_pinned(&c->classes[ci2], ivn) && (c->classes[ci2].ivar_types[ix] == TY_STR_POLY_HASH || c->classes[ci2].ivar_types[ix] == TY_SYM_POLY_HASH)) { ivar_set_type(&c->classes[ci2], ix, TY_POLY_POLY_HASH); hb_changed = 1; /* patch the node-type cache for every read/write of this ivar in this class so codegen agrees (mirrors the UNKNOWN backstop above) */ for (int nid2 = 0; nid2 < c->nt->count; nid2++) { const char *nty2 = nt_type(c->nt, nid2); NodeKind nty2_k = nt_kind(c->nt, nid2); if (!nty2) continue; if (nty2_k != NK_InstanceVariableReadNode && nty2_k != NK_InstanceVariableWriteNode && nty2_k != NK_InstanceVariableOrWriteNode) continue; Scope *s2 = comp_scope_of(c, nid2); if (!s2 || s2->class_id != ci2) continue; const char *nm2 = nt_str(c->nt, nid2, "name"); if (nm2 && sp_streq(nm2, ivn)) comp_set_ntype(c, nid2, TY_POLY_POLY_HASH); } } } } int nr2 = nt_num_refs(c->nt, nid); int na2 = nt_num_arrs(c->nt, nid); int more = nr2; for (int i2 = 0; i2 < na2; i2++) { int nn2 = 0; nt_arr_at(c->nt, nid, i2, &nn2); more += nn2; } if (sp + more > cap) { while (sp + more > cap) cap *= 2; int *g = (int *)realloc(stack, sizeof(int) * (size_t)cap); if (!g) break; stack = g; } for (int i2 = 0; i2 < nr2; i2++) { int ch2 = nt_ref_at(c->nt, nid, i2); if (ch2 >= 0) stack[sp++] = ch2; } for (int i2 = 0; i2 < na2; i2++) { int nn2 = 0; const int *ids2 = nt_arr_at(c->nt, nid, i2, &nn2); for (int k2 = 0; k2 < nn2; k2++) if (ids2[k2] >= 0) stack[sp++] = ids2[k2]; } } free(stack); } if (hb_changed) { for (int iter = 0; iter < 16; iter++) { int ch = 0; ch |= infer_param_types(c); ch |= infer_return_types(c); ch |= infer_write_types(c); if (!ch) break; } } } /* Post-fixpoint: unify param types across method override families. When an override widens a param to TY_POLY but the parent (or sibling) keeps it scalar, the generated C signatures disagree and virtual dispatch can't call both with the same arg temps. Walk all scope pairs that are overrides of the same instance method in a parent-child class pair and widen any differing slot to TY_POLY. */ for (int s1 = 0; s1 < c->nscopes; s1++) { Scope *sc1 = &c->scopes[s1]; if (sc1->class_id < 0 || !sc1->name || sc1->is_cmethod || sc1->nparams == 0) continue; /* initialize is never virtually dispatched (always via ClassName.new), so each override may have fully independent param types. */ if (sp_streq(sc1->name, "initialize")) continue; for (int s2 = s1 + 1; s2 < c->nscopes; s2++) { Scope *sc2 = &c->scopes[s2]; if (sc2->class_id < 0 || !sc2->name || sc2->is_cmethod || sc2->nparams == 0) continue; if (!sp_streq(sc1->name, sc2->name)) continue; /* check ancestor relationship: one class must be an ancestor of the other */ int c1 = sc1->class_id, c2 = sc2->class_id; int related = 0; for (int k = c1; k >= 0; k = c->classes[k].parent) if (k == c2) { related = 1; break; } if (!related) for (int k = c2; k >= 0; k = c->classes[k].parent) if (k == c1) { related = 1; break; } if (!related) continue; int np = sc1->nparams < sc2->nparams ? sc1->nparams : sc2->nparams; for (int k = 0; k < np; k++) { /* a rest collects its arguments into an array whatever the other method has in that slot (#4871), and a **kwrest its keywords into a hash */ if (k == sc1->rest_idx || k == sc2->rest_idx || k == sc1->kwrest_idx || k == sc2->kwrest_idx) continue; LocalVar *p1 = scope_local(sc1, sc1->pnames[k]); LocalVar *p2 = scope_local(sc2, sc2->pnames[k]); if (!p1 || !p2) continue; if (p1->type != p2->type && (p1->type == TY_POLY || p2->type == TY_POLY)) { /* a handle widened to a box is still appended to: its callers hand over the handle (convert_byref_handle_params) */ LocalVar *ph = p1->type == TY_POLY ? p2 : p1; if (ph->type == TY_STRBUF && ph->str_shared) { ph->poly_lift |= POLY_LIFT_APPENDED; /* its reads marked to hand out the handle read the box now */ Scope *sh = p1->type == TY_POLY ? sc2 : sc1; const char *hn = sh->pnames[k]; for (int r = comp_kind_first(c, NK_LocalVariableReadNode); r >= 0; r = comp_kind_next(c, r)) { if (nt_kind(c->nt, r) != NK_LocalVariableReadNode || !c->strbuf_box[r] || !sp_streq(nt_str(c->nt, r, "name"), hn) || comp_scope_of(c, r) != sh) continue; comp_set_strbuf_box(c, r, 0); comp_sn_retype(c, r, TY_POLY); } } lv_set_type(p1, TY_POLY); lv_set_type(p2, TY_POLY); } } /* Also unify return types: if one member returns poly and another void/nil, make both return poly so the dispatch statement-expression can capture a scalar result from any arm. */ if (sc1->ret != sc2->ret && (sc1->ret == TY_POLY || sc2->ret == TY_POLY)) { scope_set_ret(sc1, TY_POLY); scope_set_ret(sc2, TY_POLY); } } } /* Promote loop-multiplication variables to bigint */ detect_bigint_loop_vars(c); AN_STEP_V("propagate_bigint_cascade", propagate_bigint_cascade(c)); } /* The lowering of yielding methods into functions taking their block as a proc, out of an_phase_procs so it can run again once the types it reads have settled (an_phase_method_backstops can still widen a receiver to poly, whose dispatch lifts the block). Answers whether it lowered any. */ static int an_lower_yielders(Compiler *c) { int lowered_recursive_yield = 0; /* A method lowered here takes its block as a real proc, so a literal block passed to it is lifted from then on (a_block_is_lifted). A caller defined before it that hands it a block, and whose question asks whether that block is lifted -- a returning proc in the block, a `yield` in it -- is looked at again, so the order of the definitions does not decide. Each pass after the first lowers one more method or ends the loop. */ for (int again_ret = 1; again_ret; ) { again_ret = 0; for (int mi = 1; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; if (!m->name || !m->reachable) continue; /* An anonymous `&` (blk_param == "") has no name to reference from the lowered body's yield sites; it keeps the inline path's diagnostic. */ if (m->blk_param && !m->blk_param[0]) continue; if (!m->yields) continue; if (m->body < 0) continue; int has_yld = 0; for (int id = 0; id < c->nt->count && !has_yld; id++) { if (c->nscope[id] != mi) continue; NodeKind k = nt_kind(c->nt, id); if (k == NK_YieldNode) has_yld = 1; /* `block_given?` marks a method yielding exactly as a `yield` does (the block-aware marking above), so a method whose block use is block_given? PLUS a value use of its &blk -- `x = blk`, a capture in a proc, a `&blk` forward -- has the same problem a literal yield has: the inline path emits no storage for the block while the value use names it (`lv_blk` undeclared). It takes the same lowering. */ else if (k == NK_CallNode && m->blk_param_value_use) { const char *cn = nt_str(c->nt, id, "name"); int r = nt_ref(c->nt, id, "receiver"); NodeKind rk = nt_kind(c->nt, r); if (cn && sp_streq(cn, "block_given?") && (r < 0 || (rk == NK_SelfNode))) has_yld = 1; } } if (!has_yld) continue; /* A yield inside a Thread/Fiber body cannot be spliced: that body is lifted to its own C function, where the caller's block is not in scope, so the splice emitted a LocalJumpError raise and the surrounding expression was then ill-typed. Lowering forwards the block as a proc the lifted body can call (#3355). */ /* A self-recursive yielder's `{ yield }` is itself a lifted block (it is passed to the lowered method), so the recursion form is asked first: it carries the block's value back at its real type, which the lifted form does not. */ int self_rec = scope_calls_itself(c, mi); int thread_yld = !self_rec && scope_yields_inside_lifted_body(c, mi); /* The declared `&blk` is also used as a VALUE -- handed to another method, assigned to a local, captured by a nested proc. The inline path has no storage to name there (it emitted a reference to an `lv_blk` it never declared), and a `yield` next to such a use is otherwise the only thing standing between the method and the ordinary proc-parameter form it already compiles without the `yield`. */ int blk_value_use = !self_rec && !thread_yld && m->blk_param_value_use; /* A proc the method creates that `return`s leaves it through the method's own proc-return frame, which a method spliced into its caller does not have: the splice named the frame's `_h` where none was declared. As a function of its own it has one. */ int ret_proc = !self_rec && !thread_yld && !blk_value_use && scope_creates_returning_proc(c, mi); if (!self_rec && !thread_yld && !blk_value_use && !ret_proc) continue; m->is_lowered_yield = 1; m->lowered_lifted_yield = thread_yld; m->yields = 0; /* `if block_given? ... else ... end` was typed per call form while the method was inlined (Scope.ret_noblock); as one emitted function both arms are live at run time, so its value is the two joined */ /* one function answers both: its own ret, the Enumerator a guard pin stands for joined with the body's values (pinned_guard_unpin_ret) */ scope_set_ret(m, pinned_guard_unpin_ret(c, mi)); if (m->ret_noblock != TY_UNKNOWN) scope_set_ret_noblock(m, TY_UNKNOWN); scope_set_ret_pin_guard(m, -1); /* The return the fixpoint derived stands. This forced TY_INT, which was right only by coincidence: the body's own tail already types the method, and overwriting it made `walk` answer Integer where it answers nil, and made a body ending in a String or an Array emit `return char *` from a function typed sp_int -- no binary at all (#4145). A body that types to nothing usable still needs a declarable slot, which is what the default below is for. */ m->lowered_carries_block_value = !thread_yld && a_scope_returns_a_yield(c, mi); /* The return the fixpoint derived stands, unless the method's value comes out of a `yield`: then the emitted tail reads the block's answer from the poly side-channel as a raw slot, and the function has to be typed to receive it. This used to force TY_INT unconditionally, which was right only by coincidence -- `walk` answered Integer where it answers nil, and a body ending in a String or an Array emitted `return char *` from a function typed sp_int, producing no binary at all (#4145). */ if (!thread_yld && (m->ret == TY_UNKNOWN || m->ret == TY_VOID || m->lowered_carries_block_value)) /* A method whose value comes out of a `yield` hands back whatever the block answered, and one C function serves every call site -- the same `countdown` is called with a String block and an Integer block. TY_INT was the RAW CARRIER for that: the slot's bits, cast back to the real type at the call site. Only some consumer paths emitted that cast (`puts x` did, `p x` did not), so a block answering a String or an Array reached a `const char *` slot as an sp_int and the C did not compile; a block answering poly had no cast that could work, the bits being unable to carry a tag. Box instead, and let method_call_ret agree: a poly return is the one shape every consumer already handles, and it is what "the block answered something only the call site knows" has meant everywhere else. */ scope_set_ret(m, m->lowered_carries_block_value ? TY_POLY : TY_INT); if (!m->blk_param) m->blk_param = strdup("__yblk__"); LocalVar *yblk = scope_local_intern(m, m->blk_param); if (yblk) { lv_set_type(yblk, TY_PROC); yblk->is_param = 1; { yblk->is_cell = 1; comp_touch_scope(yblk->owner); } } lowered_recursive_yield = 1; again_ret = 1; } } /* A method lowered just above (yields=1 -> a real &block-forwarding function) turns any literal block passed to it into a lifted first-class proc. That classification (a_block_is_lifted) reads m->yields, so those blocks were still "unlifted" when mark_proc_captures ran earlier -- their captures of enclosing locals never got heap cells (#3166). Re-run the capture pass now that the yields flags are final; it only sets is_cell and is idempotent. */ if (lowered_recursive_yield) mark_proc_captures(c); return lowered_recursive_yield; } /* Procs: the forced #each lowering, the proc captures, the referenced module sources, the dropped case arms, the lifted block procs and their scopes (analyze_program's steps, in their order) */ static void an_phase_procs(Compiler *c) { /* Force-lower `#each` for any class whose synthesized `__enum_to_a` helper is actually called (an `enum_for`/`to_enum` survived the rewrite). The helper drives `#each` with a collector block; the real (lowered) function form passes that block as a proc. Done BEFORE mark_proc_captures so the collector block lifts and its `__enum_acc` accumulator gets a heap cell. */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, "__enum_to_a")) continue; int recv = nt_ref(c->nt, id, "receiver"); if (recv < 0) continue; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) continue; int ec = comp_method_in_chain(c, ty_object_class(rt), "each", NULL); if (ec < 0) continue; Scope *m = &c->scopes[ec]; if (m->blk_param || !m->yields) continue; /* already lowered, or no yielding each */ m->is_lowered_yield = 1; m->yields = 0; /* A Struct's synthesized each returns self (Struct#each semantics), so keep its object return. A hand-written each's value is whatever its body tail evaluates to -- `self` for the `...; self` idiom, but a bare-yield each (`yield 1; yield 2`) ends in the block's return value (poly), and `def each(&b) = xs.each(&b)` ends in the inner array. Pinning those to the class type truncated the poly tail to an object pointer; type the return poly so any tail coerces (the __enum_to_a driver discards it either way). */ int is_struct = m->class_id >= 0 && c->classes[m->class_id].is_struct; scope_set_ret(m, is_struct ? ty_object(m->class_id) : TY_POLY); m->blk_param = strdup("__yblk__"); LocalVar *yblk = scope_local_intern(m, "__yblk__"); if (yblk) { lv_set_type(yblk, TY_PROC); yblk->is_param = 1; { yblk->is_cell = 1; comp_touch_scope(yblk->owner); } } } /* mark locals captured by escaping procs (they need heap cells) */ AN_STEP_V("mark_proc_captures", mark_proc_captures(c)); /* Reachability: an instance/free method is live only if its name is referenced somewhere -- as a call name, an alias target, or a symbol literal (covering send/method/define_method). Names never mentioned are dead code; skipping them avoids type-checking uninvoked methods (e.g. a never-called method with an uninferrable param). */ AN_STEP_V("compute_reachable", compute_reachable(c, 1)); /* a module method named by Mod.instance_method(:m) is referenced directly */ unmark_referenced_module_sources(c); /* Which exact cls_ids can appear at runtime -- lets the poly-dispatch switch drop `case` arms for classes that are never instantiated (the referenced method then DCEs as an unreferenced static). */ compute_instantiated(c, 0); /* Lower self-recursive yield methods: methods that use `yield` AND call themselves recursively. Their implicit block is forwarded as a proc parameter -- the declared &block name when the def has one, else a synthetic __yblk__ -- so the method is emitted (yields=0) and each `yield` in its body calls sp_proc_call(, ...). A declared &block that only does blk.call/&blk-forward never reaches here (the blk_param pass above already kept it yields=0 when self-recursive); this loop covers bodies with literal `yield`s (an_lower_yielders). */ AN_STEP_V("an_lower_yielders", an_lower_yielders(c)); for (int again = 1; again; ) { again = 0; for (int mi = 1; mi < c->nscopes; mi++) { Scope *m = &c->scopes[mi]; LocalVar *bl = m->yields && m->blk_param && m->blk_param[0] ? scope_local(m, m->blk_param) : NULL; if (!bl || !bl->is_cell || scope_has_yield_node(c, m)) continue; m->yields = 0; scope_set_ret(m, pinned_guard_unpin_ret(c, mi)); if (m->ret_noblock != TY_UNKNOWN) scope_set_ret_noblock(m, TY_UNKNOWN); scope_set_ret_pin_guard(m, -1); again = 1; } if (again) mark_proc_captures(c); } /* Post-fixpoint: propagate include-copy param types back to the source scope so the final infer_type scan (which uses comp_scope_of, mapping body nodes to the ORIGINAL scope) sees the correctly-typed params. Without this, LocalVariableReadNodes inside the body get TY_UNKNOWN because the source scope's params were never updated (no direct calls go through it). */ for (int ci = 0; ci < c->nscopes; ci++) { Scope *copy = &c->scopes[ci]; if (!copy->name || !copy->is_transplanted_source || copy->nparams == 0) continue; /* This is a transplanted SOURCE: find copies (same body, different class_id, params registered and typed) and unify their param types back here. */ for (int k = 0; k < c->nscopes; k++) { if (k == ci) continue; Scope *dst = &c->scopes[k]; if (!dst->name || !sp_streq(dst->name, copy->name)) continue; if (dst->body != copy->body || dst->nparams != copy->nparams) continue; if (!dst->is_transplanted_source) { /* dst is a copy: unify its param types into the source */ for (int p = 0; p < copy->nparams; p++) { if (!copy->pnames[p]) continue; LocalVar *slv = scope_local(copy, copy->pnames[p]); LocalVar *dlv = scope_local(dst, dst->pnames[p]); if (!slv || !dlv || dlv->type == TY_UNKNOWN || slv->rbs_seeded) continue; TyKind mg = ty_unify(slv->type, dlv->type); if (mg != slv->type) lv_set_type(slv, mg); } if (dst->ret != TY_UNKNOWN && copy->ret == TY_UNKNOWN) scope_set_ret(copy, dst->ret); } } } } /* The method backstops: params of a method reached only by method(:sym) (step 1), the ivar up-propagation and param re-binding, the method_missing warning, the block-splat check, params no typed call site bound (step 2), then the write and return re-inference (analyze_program's steps, in their order) */ static void an_phase_method_backstops(Compiler *c) { /* Backstop step 1: a method reached only via method(:sym) is invoked through the bound Method ABI, which passes sp_int args -- default its untyped params/ret to int rather than dropping it (which would leave it undeclared). Done before the drop decision below so the freshly-typed params can propagate through poly-dispatch param binding (e.g. a poke dispatch table entry typed here flows into `@pads[0].poke(data)` -> Pad#poke). */ /* Collect the names referenced by `method(:sym)` nodes once (they are rare), instead of rescanning every node per scope: the old shape was O(scopes * nodes) and dominated the post-fixpoint tail on large trees. */ const char **msym_names = NULL; int msym_n = 0, msym_cap = 0; for (int id = 0; id < c->nt->count; id++) { NodeKind nk = nt_kind(c->nt, id); if (nk != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, "method")) continue; const char *msym = method_sym_arg(c, id); if (!msym) continue; msym_names = an_grow(msym_names, msym_n, &msym_cap, sizeof(*msym_names)); msym_names[msym_n++] = msym; } /* The evidence for those parameters: what the program passes at every DYNAMIC call of a Method value (`.call` / `.()` / `[]` on a receiver typed Method or boxed), per position, over the whole program. A captured method reached only out of a container cannot be tied to one site, so the union over all of them stands in: optcarrot's dispatch table is called as `@store[addr][addr, value]` with two Integers, so its handlers keep sp_int parameters, while `exports.fetch(name).call(*args)` splats a boxed array and widens every position. A position no dynamic site reaches keeps the int default it always had. */ TyKind dyn_arg[16]; int dyn_seen[16]; for (int k = 0; k < 16; k++) { dyn_arg[k] = TY_UNKNOWN; dyn_seen[k] = 0; } if (msym_n > 0) { AnMethodHolders mholders = {0}; an_method_holders_build(c, &mholders); for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || (!sp_streq(nm, "call") && !sp_streq(nm, "()") && !sp_streq(nm, "[]") && !sp_streq(nm, "==="))) continue; int r = nt_ref(c->nt, id, "receiver"); if (r < 0) continue; TyKind rt = c->ntype[r]; if (rt != TY_METHOD && rt != TY_POLY) continue; int a = nt_ref(c->nt, id, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(c->nt, a, "arguments", &an) : NULL; /* `[]` on a boxed value is mostly a Hash or Array read (`h[:k]`, `row["name"]`), and `===` a case comparison, which are no evidence about a Method: count `[]` with numeric arguments, the shape a dispatch table is called in (`@store[addr][addr, value]`), and either with any argument on a value that can be a Method read out of a literal (an_poly_may_be_method). A String into `[method(:len), 1][i]["abc"]` kept the int default, and the call raised TypeError where `.call` answered (#6179). */ if (sp_streq(nm, "===") && an != 1) continue; if ((sp_streq(nm, "[]") || sp_streq(nm, "===")) && rt != TY_METHOD) { int numeric = an > 0 && sp_streq(nm, "[]"); for (int k = 0; k < an && numeric; k++) { TyKind at = c->ntype[av[k]]; if (at != TY_INT && at != TY_FLOAT) numeric = 0; } if (!numeric && !an_poly_may_be_method(c, &mholders, r, 0)) continue; } int splat = nt_argv_has(c->nt, av, an, NT_ARG_SPLAT | NT_ARG_BLOCK | NT_ARG_KEYWORDS | NT_ARG_FORWARDING); if (splat) { for (int k = 0; k < 16; k++) { dyn_arg[k] = TY_POLY; dyn_seen[k] = 1; } continue; } for (int k = 0; k < an && k < 16; k++) { TyKind at = c->ntype[av[k]]; if (at == TY_UNKNOWN || at == TY_VOID) at = TY_POLY; dyn_arg[k] = dyn_seen[k] ? ty_unify(dyn_arg[k], at) : at; dyn_seen[k] = 1; } } free(mholders.scope); free(mholders.name); free(mholders.memo[0]); free(mholders.memo[1]); } /* (the names as a set: compared one by one with every reachable scope's name, thousands of `method(:sym)` names against 118,000 scopes on a Rails application were most of this phase) */ ANameHash msym_set; memset(&msym_set, 0, sizeof msym_set); for (int i = 0; i < msym_n; i++) if (!anh_has(&msym_set, msym_names[i])) anh_add(&msym_set, msym_names[i]); int msym_pinned = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->reachable || !sc->name) continue; int taken = anh_has(&msym_set, sc->name); if (taken) { /* The bound-Method ABI passes sp_int args, so a param nothing else typed is pinned to int rather than left undeclared. The return is NOT pinned any more: the bind site stamps the kind of the C return and every Method call reads it (SP_BM_RET_POLY takes the sp_RbVal cast, d879c8b0 / 21f32341), so a poly-returning target is called correctly as it is. Pinning it to int retyped the method for its DIRECT callers too -- `ip = obj.ip` read a String's to_i once `method(:ip)` appeared anywhere in the program (#4451). An UNKNOWN return still defaults to int, as before, so the method is emitted with a value rather than as void. */ /* POLY, not int, for a method whose parameters nothing typed: the bound-Method ABI describes each argument's kind and carries the poly ones boxed, so it never needed the int guess -- and the guess is wrong for every program that puts a Float, a boolean or a hash in one. A Float read as an integer is how the bit pattern of NaN came to be compared as a number (#4597). This is the judgement #4451 already made for the RETURN of these same methods. A synthesized __bam_ wrapper keeps the int: it is not a user method with no evidence but a wrapper around a builtin whose C signature the adapter emission fixes, and its bind site stamps a legacy sig to match. Widening it left the stamp describing a poly parameter that no call site asks for, and the Method stopped being callable at all. */ int is_bam_wrap = sc->name && strncmp(sc->name, "__bam_", 6) == 0; for (int i = 0; i < sc->nparams; i++) { LocalVar *p = sc->pnames[i] ? scope_local(sc, sc->pnames[i]) : NULL; if (p && p->type == TY_UNKNOWN) { /* the position's evidence (above): Integer-only keeps the int lane, anything else rides the boxed channel; a bam wrapper's signature is fixed by the adapter emission */ TyKind ev = (i < 16 && dyn_seen[i]) ? dyn_arg[i] : TY_INT; /* A bam wrapper's parameter takes the one kind the sites pass when the legacy signature carries it as itself (a String, a Symbol, a bool, a typed array): `method(:Integer).call("42")` stamped an Integer slot the String argument could never match, and with no thunk on a builtin's wrapper the Method was uncallable in the default mode (#4785). Mixed or boxed evidence keeps the int default: poly is the one kind such a wrapper cannot take here. Under --int-overflow=promote the int default is what widens to the poly signature every call site there rides, so it stays. */ if (is_bam_wrap) lv_set_type(p, (!g_promote_mode && (ev == TY_STRING || ev == TY_SYMBOL || ev == TY_BOOL || ty_is_array(ev))) ? ev : TY_INT); else lv_set_type(p, ev == TY_INT ? TY_INT : TY_POLY); msym_pinned = 1; } } if (sc->ret == TY_UNKNOWN) { scope_set_ret(sc, TY_INT); msym_pinned = 1; } } } free(msym_names); anh_free(&msym_set); /* A pin here is a type change after the fixpoint: what the body computes from that parameter -- an ivar it stores, the return it becomes -- and every direct caller of the method still carry the types they had before it. `def poke(a, v) = @latch = v.odd? ? "s" : v` had @latch a String while v was unknown and a poly once v was an int, and `p pad.peek(0)` kept the String reading over a poly-returning callee (the C did not compile). Let the writes settle again. */ if (msym_pinned) for (int k = 0; k < 8; k++) { int ch = infer_write_types(c); ch |= infer_return_types(c); if (!ch) break; } /* Widen inherited ivars to their union across the hierarchy (see propagate_ivars_up). Monotonic, so iterate to a fixpoint. */ for (int iter = 0; iter < 16; iter++) if (!propagate_ivars_up(c)) break; /* Re-run param binding now that method(:sym) targets are int-typed (step 1 above) and ivars carry their inheritance-unioned types: a base method calling `@x.foo(arg)` on a poly @x (only widened to poly by the up-propagation just above) can finally bind foo's params. Without this the bound method would stay TY_UNKNOWN and be dropped below, leaving an undefined function at the poly-dispatch call site. */ AN_STEP_V("build_ie_map", build_ie_map(c)); /* Seed the synthesized compiler_state IR-emit helpers' signature types. They are called only by the synthesized dump_compiler_state_ir (which has no AST), so the param backstop below can't bind them from a call site; unseeded they stay TY_UNKNOWN, get pruned, and the dump then references an undefined function. Seeding BEFORE the backstop lets the binding propagate into the helpers they call (ir_join_ints / ir_escape). */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; const char *snm = sc->name; if (!snm || sc->nparams < 3) continue; if (!sp_streq(snm, "ir_emit_int") && !sp_streq(snm, "ir_emit_str") && !sp_streq(snm, "ir_emit_sa") && !sp_streq(snm, "ir_emit_ia")) continue; LocalVar *pb = scope_local(sc, sc->pnames[0]); LocalVar *pn = scope_local(sc, sc->pnames[1]); LocalVar *pv = scope_local(sc, sc->pnames[2]); if (pb) lv_set_type(pb, TY_STRING); if (pn) lv_set_type(pn, TY_STRING); if (pv) lv_set_type(pv, sp_streq(snm, "ir_emit_int") ? TY_INT : sp_streq(snm, "ir_emit_str") ? TY_STRING : sp_streq(snm, "ir_emit_sa") ? TY_STR_ARRAY : TY_INT_ARRAY); scope_set_ret(sc, TY_STRING); /* each returns the accumulated buf (a string) */ } /* Re-assert BEFORE each binding round, not after. A seeded parameter is a call site of everything its body calls, and this loop is the last place those bindings happen: entering it with the seed already lost, the seeded caller contributed UNKNOWN and the callee narrowed to whatever its one other call site said. `M.wrapper` declared `(untyped)` in an .rbs stopped holding `M.callee`'s parameter open, and the .rbs never mentioned callee (#4165). The fixpoint's own copy runs after its binding, which is why it recovered on the next iteration and this loop never did. */ for (int it = 0; it < 8; it++) { reassert_rbs_param_seeds(c); int ch = infer_param_types(c); /* A binding here is a type change after the fixpoint too: a parameter just typed feeds the callee's locals and its return, and those feed the arguments of the calls that follow, so the round is not settled until the writes and returns are -- and a round that bound nothing may still be carrying a return the previous one changed. Binding alone, the second `m64(l3 + l2)` in a method reachable only through `method(:f)` kept binding from an UNKNOWN `l3` while the first call had typed the parameter int, and codegen then derived the Bignum both sides in fact carry and passed it to the int parameter (the C did not build, #4684). */ ch |= infer_write_types(c); ch |= infer_return_types(c); if (!ch) break; } reassert_rbs_param_seeds(c); /* method_missing is not honored: spinel resolves every call statically and does not route an undefined-method call to method_missing (that would need runtime dispatch foreign to the whole-program model). Warn once per definition so the author isn't misled into relying on it -- the method is still callable explicitly, it just never fires as a missing-method hook. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->class_id < 0 || !sc->name || !sp_streq(sc->name, "method_missing")) continue; int dn = sc->def_node; int ln = dn >= 0 ? (int)nt_int(c->nt, dn, "node_line", 0) : 0; const char *file = c->nt->source_file; if (ln > 0) { const char *f = nt_file_path(c->nt, (int)nt_int(c->nt, dn, "node_file", 0)); if (f && *f) file = f; } if (!file || !*file) file = "source.rb"; fprintf(stderr, "spinel: %s:%d: warning: method_missing is defined but " "spinel does not dispatch undefined-method calls to it; such calls " "raise NoMethodError (method_missing can still be called " "explicitly)\n", file, ln); } /* Block splat params reach only the lowerings that bind them; the rest reject loudly here rather than emitting nil-bound or misdeclared bodies. */ check_block_rest_support(c); /* Backstop step 2: a reachable method whose parameter STILL has TY_UNKNOWN was never bound by a typed call site -- every call reached it with a poly/untyped argument (e.g. an FFI :pointer return). The method is still genuinely called, so it cannot just be dropped: codegen would still emit the call and dangle on the missing symbol (#1606 -- the direct-call sibling of #1583). Widen the unknown parameter to TY_POLY so the body emits with a poly parameter and the call links. A truly-dead method (no real call) gets the same harmless poly body and is dropped by --gc-sections at link. A method that IS called is widened first, and its parameters are then bound onward. Pool#insert, reached only as `db.insert(...)` on a `db` no typed site binds, got a poly `binds` here and hands it to `conn.execute(sql, binds)`. The main fixpoint had seen that argument as unknown, which binds nothing, so execute's `binds` was typed from its other caller alone: a dead `execute("...", [str])` made it a String array, the live Integer-and-nil array was converted to one at the call, and its Integer read as a string pointer. A method no call can reach stays unknown through that re-bind and is widened only afterwards, so its harmless poly body does not widen the methods it would call (a user `[]=` that only a Hash index write names must not box `poke`, #4889). */ { const NodeTable *nt = c->nt; int widened = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->reachable || sc->nparams == 0 || !sc->name) continue; int unknown = 0; for (int i = 0; i < sc->nparams && !unknown; i++) { LocalVar *p = sc->pnames[i] ? scope_local(sc, sc->pnames[i]) : NULL; unknown = p && p->type == TY_UNKNOWN; } if (!unknown) continue; /* a call that can land here: implicit self, a receiver of this class or a subclass, or one whose type is poly or never settled */ int called = 0; /* (the calls of the name, from the index: this walked every node of the program for each such method, and a Rails application has thousands of them over 2.3 million nodes) */ for (int id = an_calls_named_first(c, sc->name); id >= 0 && !called; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!nm || !sp_streq(nm, sc->name)) continue; int rv = nt_ref(nt, id, "receiver"); if (rv < 0) { called = 1; break; } TyKind rt = infer_type(c, rv); if (rt == TY_POLY || rt == TY_UNKNOWN) called = 1; else if (ty_is_object(rt) && sc->class_id >= 0 && !sc->is_cmethod && is_descendant(c, ty_object_class(rt), sc->class_id)) called = 1; } /* an initialize is called by name nowhere: `Klass.new(...)` reaches it, and missing that left its parameter to be widened after the re-bind below, so a boxed value it forwarded reached a parameter typed from the other callers alone and was read as their kind */ if (!called && sc->class_id >= 0 && !sc->is_cmethod && sp_streq(sc->name, "initialize")) { for (int id = an_calls_named_first(c, "new"); id >= 0 && !called; id = an_calls_named_next(id)) { if (nt_kind(nt, id) != NK_CallNode) continue; int rv = nt_ref(nt, id, "receiver"); Scope *cs = comp_scope_of(c, id); int cid = -1; if (rv < 0 || nt_kind(nt, rv) == NK_SelfNode) cid = cs && cs->is_cmethod ? cs->class_id : -1; else if (nt_kind(nt, rv) == NK_ConstantReadNode || nt_kind(nt, rv) == NK_ConstantPathNode) cid = const_node_class_index(c, rv); else { TyKind rt = infer_type(c, rv); if (rt == TY_POLY || rt == TY_UNKNOWN || rt == TY_CLASS) called = 1; continue; } if (cid >= 0 && comp_method_in_chain(c, cid, "initialize", NULL) == s) called = 1; } } if (!called) continue; for (int i = 0; i < sc->nparams; i++) { LocalVar *p = sc->pnames[i] ? scope_local(sc, sc->pnames[i]) : NULL; if (p && p->type == TY_UNKNOWN) { slot_rule(c, p, TY_POLY, -1, "never bound: no call site gives it a type"); widened = 1; } } } /* A widened value travels a call a round, as a type does in the main fixpoint: through a callee's return into its caller's local, and on into the next parameter. A chain of calls through other classes takes about two rounds a link, so the rounds run under that loop's cap (an_round_cap_step), which a chain extends and an oscillation does not. */ if (widened) { AnRoundCap rc = { 128, 0, 0, NULL, 0 }; for (int iter = 0; iter < rc.cap; iter++) { int ch = infer_param_types(c); ch |= infer_write_types(c); /* and into the ivars it is stored in. The main fixpoint can have typed such an ivar from another source: a recursive call handing Hop#run back Secure#wrap's Sealed made its `conn` one, `Sealed.new(conn)` made `@io` one, and `def write(b) = @io.write(b)` then called only itself and had no value. Left to the late passes, `@io` widened after the last one that gives a method with no value its return, and `write`, with each method answering through it, was emitted void and answered nil. */ ch |= infer_ivar_types(c); ch |= infer_return_types(c); if (!ch) break; an_round_cap_step(c, &rc, iter); } free(rc.seen); } } for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->reachable || sc->nparams == 0) continue; for (int i = 0; i < sc->nparams; i++) { LocalVar *p = sc->pnames[i] ? scope_local(sc, sc->pnames[i]) : NULL; if (p && p->type == TY_UNKNOWN) slot_rule(c, p, TY_POLY, -1, "never bound: no call site gives it a type"); } } /* The method-reference backstop and ivar up-propagation above changed param and ivar types after the main fixpoint, so re-run local write-type inference: a local like `xfine = 8 - (data & 0x7)` only becomes int once its method's `data` param is pinned to int by the backstop. Return types alternate with the write re-runs: a method returning through a local (`r = expr; r`) whose expr only settles here re-derives its return, which in turn types callers' locals (`a = m()`) -- and the callers' own returns -- on the next round. A write-only re-run would strand such a caller's return at UNKNOWN, and the method would emit as void, silently dropping its value (#1670). */ /* Settle the container-flow bits ONCE, on the types the main fixpoint left, then let the write/return re-runs below propagate the widening they gate. Not inside the fixpoint: a bit arriving mid-run re-types a call after its neighbours have settled, and whether it arrived at all depended on node order -- a --line-map and a --no-line-map run of the same program then disagreed about whether a program compiled at all. Not before it either: the evidence is a container-typed producer, which the fixpoint is what establishes. (#3459) */ for (int iter = 0; iter < 8; iter++) if (!infer_container_flow(c)) break; g_ret_no_new_poly = 1; for (int iter = 0; iter < 8; iter++) { int ch = infer_write_types(c); ch |= infer_return_types(c); if (!ch) break; } g_ret_no_new_poly = 0; /* Widen an ivar shared with a push-widened helper param -- after the write re-run above (which would re-narrow it from its empty-`[]` write) so the poly widening sticks, then re-derive return types only so a manual reader (`def flags; @flags; end`) reports the now-poly array (#3154). */ AN_STEP_V("widen_ivars_from_pushed_params", widen_ivars_from_pushed_params(c)); AN_STEP_V("widen_ivars_read_into_poly", widen_ivars_read_into_poly(c)); g_ret_no_new_poly = 1; for (int iter = 0; iter < 8; iter++) if (!infer_return_types(c)) break; g_ret_no_new_poly = 0; reassert_rbs_param_seeds(c); /* the post-fixpoint passes narrow too */ /* The returns settled above may have widened past the locals that were derived from them (the write re-run ran first, and its `no new poly` gate kept a return narrow until now): the locals re-join the writes whose values widened (rejoin_local_writes). */ for (int iter = 0; iter < 8; iter++) { int ch = rejoin_local_writes(c); ch |= widen_arrays_from_map_bang(c); ch |= infer_return_types(c); if (!ch) break; } /* The write-type re-run can re-derive a hash/array container type for an iteration-bound block param from its element-index usage (e.g. `a[1]=v`), clobbering the TY_POLY the block-param pass pinned for a poly-collection `.each`. Re-pin block-param types so poly elements stay poly. */ for (int iter = 0; iter < 8; iter++) if (!infer_block_params(c)) break; intern_block_params(c); /* a parameter nothing reads still needs its slot */ /* infer_write_types resets non-param locals, undoing the earlier bigint loop-variable promotion, so re-apply it. */ detect_bigint_loop_vars(c); AN_STEP_V("propagate_bigint_cascade", propagate_bigint_cascade(c)); } /* The ivar slots of every class, counted: a class that gained a slot since the count was taken has a layout the subclasses' must be re-merged with. */ static int an_total_ivars(Compiler *c) { int n = 0; for (int k = 0; k < c->nclasses; k++) n += c->classes[k].nivars; return n; } /* The late widening and narrowing: unresolved locals and returns to poly, ivars fed by nil-only params, the arithmetic widen, the object-array and poly-int narrowing, then the gc-root marks and the full node type cache (analyze_program's steps, in their order) */ static void an_phase_late_widen(Compiler *c, NodeTypes *promote_before) { /* A non-parameter local that inference never resolved holds a value of unknown static type -- a block param bound to an element of a poly receiver, or a local fed by a dynamically-dispatched call (e.g. optcarrot's memory-map procs). Declare it boxed (poly) rather than failing codegen; reads then go through the tag-dispatching poly paths. Method params are excluded: the backstop above already pins or drops those. Set before the node-type cache is rebuilt so reads of the local see poly. */ for (int s = 0; s < c->nscopes; s++) for (int i = 0; i < c->scopes[s].nlocals; i++) { LocalVar *blv = &c->scopes[s].locals[i]; if (!blv->is_param && blv->type == TY_UNKNOWN) lv_set_type(blv, TY_POLY); /* A slot left at TY_NIL only ever saw nil (it never narrowed against an object). It has no object class, so represent it as a boxed-nil poly. Applies to params too (a purely-nil param). An --rbs-seeded slot keeps its pinned type. */ if (blv->type == TY_NIL && !blv->rbs_seeded) { lv_set_type(blv, TY_POLY); blv->nil_only = 1; } } /* A method whose body tail is one of the locals just declared poly still carries an UNKNOWN return, which emits a `void` C function -- the value is then dropped at every call site (#3337: `def txn; r = yield; r; end` called with a block whose own tail is a local read; `r` only settles in the backstop above, after the last return pass). Lift ONLY returns that are still UNKNOWN: widening an established return here is what g_ret_no_new_poly deliberately prevents. Iterated so a caller whose tail is such a call picks the type up too. The top level (scope 0) has no value, as infer_return_types has it. */ for (int iter = 0; iter < 8; iter++) { int lifted = 0; /* A NIL return was read off a local the loop above just made poly: `h = nil; h ||= {}; h` is nil through the fixpoint, because the element-less `{}` stays UNKNOWN, and the method then dropped the hash it built. Its tail or one of its returns now reads poly. */ NT_FOREACH_KIND(c->nt, NK_ReturnNode, rid) { Scope *rs = comp_scope_of(c, rid); if (rs && rs->ret == TY_NIL && return_node_type(c, rid) == TY_POLY && !rs->ret_rbs_seeded && !rs->ret_specialized && !rs->cs_synth && !rs->is_lowered_yield) { scope_set_ret(rs, TY_POLY); lifted = 1; } } for (int s = 1; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->body < 0) continue; if (sc->ret == TY_NIL && !sc->ret_rbs_seeded && !sc->ret_specialized && !sc->cs_synth && !sc->is_lowered_yield) { if (infer_type(c, sc->body) == TY_POLY) { scope_set_ret(sc, TY_POLY); lifted = 1; } continue; } if (sc->ret != TY_UNKNOWN) continue; TyKind rl = infer_type(c, sc->body); if (rl != TY_UNKNOWN && rl != TY_VOID) { scope_set_ret(sc, rl); lifted = 1; } } if (!lifted) break; } /* Re-merge inherited ivar NAMES into subclasses now that the fixpoint has registered every ivar -- including ones a parent only gained from an included module's transplanted methods (`module M; def m; @x = ...; end` included into a base, then subclassed). inherit_members ran once before the fixpoint, when the parent had no such ivar yet, so the subclass struct would otherwise lack the field (a default-arg read `def g(r = @x)` on the subclass then references a non-existent member). Rebuilds parent-first, so the [parent ivars..., own ivars...] cast-compatible layout is preserved. */ inherit_members(c); /* Re-run ivar and cvar inference after late parameter/return widening. An ivar fed by a purely-nil param (`@x = idx` where every `set` call passed nil) was skipped during the fixpoint. A cvar fed by an optional Symbol param's return may still be Symbol; with the return now poly, its write must widen too, or storing nil loses the tag. Limit this late cvar widening to Bool and Symbol slots, which have no nil representation. */ for (int it = 0; it < 8; it++) { int ivars_before = an_total_ivars(c); int ch = infer_ivar_types(c); ch |= infer_cvar_types(c, 1); ch |= infer_inherited_ivars(c); /* ... and back up: the re-run above can widen a subclass's copy of an inherited ivar (a poly-fallen param feeding it), and the up-propagation above has long finished. Without this the base keeps the narrower type, its struct stops being a prefix of the subclass's, and every inherited method writes through the `(sp_Base *)self` cast at the wrong offsets. */ ch |= propagate_ivars_up(c); /* An ivar or a return that widens here (e.g. `@query_log`, whose heterogeneous `= []` / `.push(str)` / `= prev` writes merge to poly) must carry its new type into the locals written from it: `prev = @query_log` (#1793), `parent = Base.defs` (#7602). The locals re-join those writes, monotonically and without the full re-derivation infer_write_types does (which would reset pattern/massign/block-bound locals this late and mistype them). */ ch |= rejoin_local_writes(c); /* A method whose value IS such an ivar (`def peek(a) = @latch`) still carries the return derived before the ivar widened, and its callers read a poly through a String (the C did not build, #4451). Re-derive the returns on the widened ivars; the gate against a new poly is off, since the widening is exactly what the body now says. */ g_ret_no_new_poly = 2; ch |= infer_return_types(c); g_ret_no_new_poly = 0; /* A slot a class first gained in this round sits after every slot it had, in that class alone: an ancestor's new slot then shares its index with a subclass's own, and the subclass never took the name. `k.instance_variable_set(:@warned, true)` on a receiver the widening above just turned poly (a local of an `inherited` hook no subclass reaches) did that to a base class with no ivars of its own, and check_class_layout_prefix refused the program. Re-merge the layouts parent-first, as the pass above did for the fixpoint's slots, and go round once more so the moved slots' types settle. */ if (an_total_ivars(c) != ivars_before) { inherit_members(c); ch = 1; } if (!ch) break; } /* --int-overflow=promote: widen every statically-int slot (param / return / local / ivar / cvar / global) to poly so an arithmetic result that overflows int64 can be carried as a boxed bigint (sp_poly_add/mul promote at runtime). A poly slot still holds a small value inline (SP_TAG_INT, no heap), so this is far cheaper than the legacy "everything becomes sp_Bigint*" widen. Done after the fixpoint and before the final node-type cache so reads pick up the widened slot types. TY_BIGINT loop vars (detect_bigint_loop_vars) stay bigint. EXPERIMENTAL: gated by g_promote_mode; default/wrap untouched. */ if (g_promote_mode) { /* (a Hash literal of the Integers it widens turns poly-valued: the calls it reaches bind again, an_phase_nullable_returns) */ node_types_take(c, promote_before); for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; /* `<=>` yields a bounded -1/0/1 and is consumed as `(a <=> b) 0`; widening its return to poly would force every caller's comparison onto the poly path while the result never overflows. Keep it int. */ int is_spaceship = sc->name && sp_streq(sc->name, "<=>"); /* A lowered self-recursive yield method returns its block's value through a raw sp_int carrier (a string is laundered through the slot, an int rides it directly), and every call site casts that carrier back to its own block's concrete type. Widening the return to poly would emit a `sp_box_str(sp_int)` over that raw carrier and mismatch the per-call casts; keep it as the carrier, exactly as default/wrap mode does. */ if (sc->ret == TY_INT && !is_spaceship && !sc->is_lowered_yield) scope_set_ret(sc, TY_POLY); /* the blockless answer of an `if block_given?` tail (#4659) widens with the rest: its else arm is emitted on the widened locals */ if (sc->ret_noblock == TY_INT && !is_spaceship && !sc->is_lowered_yield) scope_set_ret_noblock(sc, TY_POLY); /* The receiver parameter of a synthesized `.method(:sym)` wrapper (`__bam_N(__bam_r, ...) = __bam_r.sym(...)`) stays typed as well: its bind site stores the receiver raw and the thunk ABI (bm_self_ctype) declares the slot sp_int only while the parameter IS TY_INT, so a widened one had the callee read a 16-byte box out of a pointer-sized slot -- junk cls_id, and every call ended in NoMethodError or a TypeError naming an empty class (#4765). The same treatment as a block parameter; the wrapper's own arithmetic on it follows the typed contract. A boxed self slot in the thunk ABI would be the fuller form. A receiverless Kernel wrapper (`method(:String)`, `__bam_r` is its first ARGUMENT) widens with the rest: its poly signature is what a promote call site rides. */ int is_bam_recv = bam_wrapper_binds_receiver(c, sc); for (int i = 0; i < sc->nlocals; i++) { /* Skip block params: they are typed by the iterated collection's element type (an IntArray yields int elements), and the block emitters already retype the param to that element type for the body (use_shadow). Widening them to poly only creates an int/poly shadow and inconsistent reads (e.g. `x.even?` keeps the stale poly type while `x*2` sees the retyped int). Method params still widen. */ if (sc->locals[i].is_block_param) continue; if (is_bam_recv && sc->locals[i].name && sp_streq(sc->locals[i].name, "__bam_r")) continue; /* an unrolled define_method's loop var is its literal, emitted raw */ if (sc->dm_subst_name && sc->locals[i].name && sp_streq(sc->locals[i].name, sc->dm_subst_name)) continue; if (sc->locals[i].type == TY_INT) lv_set_type(&sc->locals[i], TY_POLY); } } for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int i = 0; i < cl->nivars; i++) if (cl->ivar_types[i] == TY_INT) ivar_set_type(cl, i, TY_POLY); for (int i = 0; i < cl->ncvars; i++) if (cl->cvar_types[i] == TY_INT) cvar_set_type(cl, i, TY_POLY); } for (int i = 0; i < c->ngvars; i++) if (c->gvars[i].type == TY_INT) lv_set_type(&c->gvars[i], TY_POLY); /* the blockless answer of an `if block_given?` tail is its else arm's, which the widened locals may have changed: `Array.new(n, v)` of a widened v is a poly Array, not the Integer one it was. An arm that calls another such method reads that method's new answer, so the pass runs again until nothing changes (each round settles one more method of a chain, so a stop short of that left a caller behind its callee). A poly answer (the widening's own, above) stays. */ for (int round = 0, ch = 1; ch && round <= c->nscopes; round++) { ch = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->ret_noblock == TY_UNKNOWN || sc->ret_noblock == TY_POLY) continue; int el = scope_block_given_else_last(c, sc); TyKind et = el >= 0 ? infer_type(c, el) : TY_UNKNOWN; if (et != TY_UNKNOWN && et != sc->ret_noblock) { scope_set_ret_noblock(sc, et); ch = 1; } } } } /* narrow monomorphic object arrays (POLY_ARRAY -> obj-pointer array) before the node cache is finalized so the rebuild below propagates the new element types to every `arr[i]` / `arr[i].field` site. */ AN_STEP_V("narrow_int_table_ivars", narrow_int_table_ivars(c, 0)); AN_STEP_V("narrow_object_arrays", narrow_object_arrays(c)); AN_STEP_V("narrow_locals_from_arrays", narrow_locals_from_arrays(c)); /* after the locals: a parameter can be fed an element the local rule has just narrowed, and both read the settled array types */ AN_STEP_V("narrow_params_from_arrays", narrow_params_from_arrays(c)); /* and a block parameter a splat of such a row binds (`yield(*row)`) */ AN_STEP_V("narrow_site_boxed_block_params", narrow_site_boxed_block_params(c)); /* An --rbs `Array[Class]` ivar seed the pass could not honour is said so, rather than dropped without a word (#4444): the array is used in a way its unboxed form has no emitter for, or read from outside the class's own instance methods. */ for (int ci = 0; ci < c->nclasses; ci++) { ClassInfo *cl = &c->classes[ci]; for (int iv = 0; iv < cl->nivars; iv++) { int req = cl->ivar_oa_seed[iv]; if (!req) continue; TyKind got = cl->ivar_types[iv]; const char *asked; /* promote: rows of the Integers the program boxes answer the request in the boxed table */ if (req == SEED_OA_INT_TABLE) { if (got == TY_INT_ARRAY_ARRAY || cl->ivar_oa_conflict[iv] == 2) continue; asked = "Array[Integer]"; } else if (req == SEED_OA_FLT_TABLE) { if (got == TY_FLOAT_ARRAY_ARRAY) continue; asked = "Array[Float]"; } else { if (ty_is_obj_array(got)) continue; int want = req - 1; asked = want >= 0 && want < c->nclasses ? c->classes[want].name : "?"; } /* A nested request can fail two ways, and they want different advice. The slot may have stayed boxed because a use has no emitter in the unboxed form, or because a row of the OTHER kind met the seed in the element join. The second is not a missing emitter at all but the signature and the code disagreeing about the element type, and it is the same contradiction a flat seed reports (a seed is trusted, so the emitted table would hand a row of one layout to a reader of the other): refused like one, not warned about (#4484). */ const InlinePin *byp = inline_pin_of_ivar(ci, cl->ivars[iv]); const char *by = byp ? byp->where : NULL; if (req < 0 && (cl->ivar_oa_conflict[iv] == 1 || ty_is_ptr_array(got))) { if (by) fprintf(stderr, "spinel: %s: inline RBS annotation contradicted: %s %s is declared Array[%s] but " "the program gives it rows of another kind\n" " An annotation is trusted, so the emitted table would hand a row of one layout " "to a reader of the other.\n" " Fix the annotation or the rows.\n", by, cl->name, cl->ivars[iv], asked); else fprintf(stderr, "spinel: --rbs seed contradicted: %s %s is declared Array[%s] but the program " "gives it rows of another kind\n" " A seed is trusted, so the emitted table would hand a row of one layout to " "a reader of the other.\n" " Fix the signature or the rows.\n", cl->name, cl->ivars[iv], asked); if (!collect_mode()) exit(1); g_seed_bad = 1; continue; } if (by) /* the request is not applied, and the C is as without it */ fprintf(stderr, "spinel: %s: warning: inline RBS: the type of %s %s is not applied: Array[%s] " "stays a boxed array; every use of it must be one the unboxed array supports " "([], []=, push, pop, shift, delete_at, clear, length, empty?, first, last, min, " "max, sort) from the class's own instance methods or its attr_reader\n", byp->head, cl->name, cl->ivars[iv], asked); else fprintf(stderr, "warning: --rbs: %s %s: Array[%s] stays a boxed array; every use of it must be " "one the unboxed array supports ([], []=, push, pop, shift, delete_at, clear, " "length, empty?, first, last, min, max, sort) from the class's own instance " "methods or its attr_reader\n", cl->name, cl->ivars[iv], asked); } } /* narrow poly locals that are only ever used as ints (drop per-use boxing); the rebuild below re-infers their reads/ops at the narrowed int type. */ AN_STEP_V("narrow_poly_int_locals", narrow_poly_int_locals(c)); /* `@v.between?(lo, hi)` went onto the Integer copy while @v was an Integer, and a backstop above has widened @v since */ AN_STEP_V("revert_widened_builtin_scalar_calls", revert_widened_builtin_scalar_calls(c)); /* finalize: gc-root needs + full node type cache */ for (int s = 0; s < c->nscopes; s++) for (int i = 0; i < c->scopes[s].nlocals; i++) c->scopes[s].locals[i].gc_root = (c->scopes[s].locals[i].type == TY_STRING); AN_STEP_V("mark_empty_array_operands", mark_empty_array_operands(c)); for (int id = 0; id < c->nt->count; id++) AN_STEP_V("infer_type", infer_type(c, id)); } /* The only calls an_call_targets_scope can answer yes for, given a scope's name: a call on that name, a call on a name some class aliases a method under, and `new` for an `initialize`; and, given a call's name, the only scopes. Asking every call of the program about every scope made the passes below scopes times calls, every round; asking just these, in the same order, gives the same answers in the same order. */ typedef struct { int nc, nb, nal, nsc; int *calls; /* the CallNodes, in kind-chain order */ const char **cname; /* their names, by position */ int *byname; /* positions with a name, by (name, position) */ int *aliased; /* positions whose name is an alias, ascending */ int *scopes; /* named scopes past the top, by (name, index) */ const char **sname; /* scope names, by index */ int *buf, *sbuf; /* the candidates of one ask */ } PRCallIdx; static const char **g_prci_key; static int prci_cmp(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; int r = strcmp(g_prci_key[x], g_prci_key[y]); return r ? r : (x > y) - (x < y); } static void prci_build(Compiler *c, PRCallIdx *x) { const NodeTable *nt = c->nt; int cap = 1; for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) cap++; x->nc = x->nb = x->nal = x->nsc = 0; x->calls = (int *)malloc(sizeof(int) * (size_t)cap); x->cname = (const char **)malloc(sizeof(char *) * (size_t)cap); x->byname = (int *)malloc(sizeof(int) * (size_t)cap); x->aliased = (int *)malloc(sizeof(int) * (size_t)cap); x->buf = (int *)malloc(sizeof(int) * (size_t)cap * 3); x->scopes = (int *)malloc(sizeof(int) * (size_t)(c->nscopes + 1)); x->sname = (const char **)malloc(sizeof(char *) * (size_t)(c->nscopes + 1)); x->sbuf = (int *)malloc(sizeof(int) * (size_t)(c->nscopes + 1) * 2); 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; const char *un = nt_str(nt, u, "name"); x->calls[x->nc] = u; x->cname[x->nc] = un; if (un) { x->byname[x->nb++] = x->nc; if (an_alias_name(c, un)) x->aliased[x->nal++] = x->nc; } x->nc++; } for (int s = 0; s < c->nscopes; s++) { x->sname[s] = c->scopes[s].name; if (s > 0 && c->scopes[s].name) x->scopes[x->nsc++] = s; } g_prci_key = x->cname; qsort(x->byname, (size_t)x->nb, sizeof(int), prci_cmp); g_prci_key = x->sname; qsort(x->scopes, (size_t)x->nsc, sizeof(int), prci_cmp); g_prci_key = NULL; } static void prci_free(PRCallIdx *x) { free(x->calls); free(x->cname); free(x->byname); free(x->aliased); free(x->buf); free(x->scopes); free(x->sname); free(x->sbuf); } /* The run of `sorted` (n entries, keyed by key[]) named nm: its start, and its length as the answer. */ static int prci_run(const int *sorted, int n, const char **key, const char *nm, int *at) { int lo = 0, hi = n; while (lo < hi) { int mid = (lo + hi) / 2; if (strcmp(key[sorted[mid]], nm) < 0) lo = mid + 1; else hi = mid; } int e = lo; while (e < n && strcmp(key[sorted[e]], nm) == 0) e++; *at = lo; return e - lo; } /* Merge ascending runs a and b into out, dropping repeats; the count. */ static int prci_merge(const int *a, int na, const int *b, int nb, int *out) { int i = 0, j = 0, n = 0; while (i < na || j < nb) { int v = j >= nb || (i < na && a[i] <= b[j]) ? a[i++] : b[j++]; if (n == 0 || out[n - 1] != v) out[n++] = v; } return n; } /* The positions of the calls that can reach a scope named nm, ascending. */ static int prci_calls_for(PRCallIdx *x, const char *nm) { int at = 0, n1 = prci_run(x->byname, x->nb, x->cname, nm, &at); int *t = x->buf + x->nc, *out = x->buf; int nt = prci_merge(x->byname + at, n1, x->aliased, x->nal, t); if (!sp_streq(nm, "initialize")) { memcpy(out, t, sizeof(int) * (size_t)nt); return nt; } int n3 = prci_run(x->byname, x->nb, x->cname, "new", &at); return prci_merge(t, nt, x->byname + at, n3, out); } /* The scopes a call named un can reach, ascending; -1 for every scope (an alias reaches past the name). */ static int prci_scopes_for(Compiler *c, PRCallIdx *x, const char *un) { if (!un) return 0; if (an_alias_name(c, un)) return -1; int at = 0, n1 = prci_run(x->scopes, x->nsc, x->sname, un, &at); if (!sp_streq(un, "new")) { memcpy(x->sbuf, x->scopes + at, sizeof(int) * (size_t)n1); return n1; } int at3 = 0, n3 = prci_run(x->scopes, x->nsc, x->sname, "initialize", &at3); int *t = x->sbuf + c->nscopes + 1; memcpy(t, x->scopes + at, sizeof(int) * (size_t)n1); return prci_merge(t, n1, x->scopes + at3, n3, x->sbuf); } /* Whether call u's receiver is a builtin value -- an Array, an Enumerator, a String, a number -- that cannot be an instance of scope s's class: a class of the program's own, not a reopened builtin (nor Object) and not a module a builtin may include. an_call_targets_scope answers by the name alone when one method defines it, so `arr.each_slice(1).map(&:first)` targeted a user class's yielding #map, and the block's slice parameter took that method's boxed yield and stopped matching the slice the builtin binds. */ static int an_builtin_recv_misses_scope(Compiler *c, int u, const Scope *s) { int rc = nt_ref(c->nt, u, "receiver"); if (rc < 0 || s->class_id < 0) return 0; TyKind rt = infer_type(c, rc); if (rt == TY_POLY || rt == TY_UNKNOWN || rt == TY_CLASS || rt == TY_EXCEPTION || ty_is_object(rt)) return 0; ClassInfo *k = &c->classes[s->class_id]; return !comp_class_is_module(c, k) && !is_builtin_reopen_name(k->name) && !builtin_class_id(k->name); } /* The proc-return re-derivation: ret_proc_ret and proc_ret from the now-widened bodies, as a focused fixpoint, then the node-type cache refresh (analyze_program's steps, in their order) */ static void an_phase_proc_returns(Compiler *c) { /* --int-overflow=promote: the widen above can change a proc body's return type (a captured int local widened to poly), so a proc's caller-side proc_ret / a factory method's ret_proc_ret must be re-derived from the now-widened body -- else a `.call` reads the wrong return channel (raw sp_int slot vs the poly side-channel) and yields 0. Re-run JUST the proc_ret / ret_proc_ret derivations (mirroring infer_return_types and infer_write_types) as a focused fixpoint; this touches only proc-return metadata, never the widened slot types, so it cannot undo the widen. A factory's ret_proc_ret feeds a caller's proc_ret, hence the loop. */ /* Not only in promote mode. A proc's parameter is typed from its `.call` sites, and a parameter that settles LATE -- an empty-array local argument types it only in the post-convergence pass -- leaves proc_ret derived from the body as it read before, so `->(acc){ acc + [1] }.call(e)` with `e = []` answered an Integer where the body plainly builds an array (#4296). The re-derivation below is the same one, and it only touches proc-return metadata. */ { const NodeTable *nt = c->nt; int changed = 1, iters = 0; while (changed && iters++ < 32) { changed = 0; /* (1) method scopes that return a proc: ret_proc_ret from the body. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->ret != TY_PROC) continue; TyKind pr = TY_UNKNOWN; if (sc->body >= 0) { int bn = 0; const int *bb = nt_arr(nt, sc->body, "body", &bn); if (bn > 0) pr = proc_ret_of(c, bb[bn - 1]); } for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k == NK_ReturnNode && comp_scope_of(c, id) == sc) { int a = nt_ref(nt, id, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an > 0) pr = proc_ret_of(c, av[0]); } } if (pr != TY_UNKNOWN && sc->ret_proc_ret != (int)pr) { scope_set_ret_proc_ret(sc, (int)pr); changed = 1; } } /* (2) proc-typed locals: proc_ret from the assigned proc / factory call. */ changed |= join_local_proc_rets(c); /* (3) a typed-array-returning method whose elements widened now yields a poly array in its body (e.g. `[a/b, a%b]` with poly a,b builds a PolyArray), so its return type must follow to TY_POLY_ARRAY. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; TyKind r = (TyKind)sc->ret; /* promote: so does an Integer-valued hash, whose literal builds its poly-valued form once the values widened */ int ihash = g_promote_mode && ty_is_hash(r) && ty_hash_val(r) == TY_INT; if (r != TY_INT_ARRAY && r != TY_STR_ARRAY && r != TY_FLOAT_ARRAY && !ihash) continue; TyKind br = TY_UNKNOWN; /* infer_type, not the cache: a local the steps below widened in this same pass reads as its new kind only through a fresh inference. A tail that is `if block_given? ... else ... end` answers through its block arm: that arm is what a call with a block receives, and the if's own type unifies it with the blockless Enumerator arm. */ if (sc->body >= 0) { int tail = scope_body_value(c, s); for (int guard = 0; tail >= 0 && guard < 4 && nt_kind(nt, tail) == NK_IfNode; guard++) { int pred = nt_ref(nt, tail, "predicate"); const char *pnm = pred >= 0 && nt_kind(nt, pred) == NK_CallNode ? nt_str(nt, pred, "name") : NULL; if (!pnm || !sp_streq(pnm, "block_given?")) break; int st = nt_ref(nt, tail, "statements"); int sn = 0; const int *sb = st >= 0 ? nt_arr(nt, st, "body", &sn) : NULL; tail = sn > 0 ? sb[sn - 1] : -1; } if (tail >= 0) br = infer_type(c, tail); } if (ihash) { if (ty_is_hash(br) && ty_hash_val(br) == TY_POLY) { scope_set_ret(sc, br); changed = 1; } /* a tail that widened to a box (a local the method rebinds to the boxed-value hash) returns the box: the typed return cast it to the Integer-valued struct and read its boxed values as raw */ else if (br == TY_POLY) { scope_set_ret(sc, TY_POLY); changed = 1; } continue; } /* the scope's own ReturnNodes: this runs per scope (roundhouse#72) */ for (int id = comp_ret_first(c, s); id >= 0 && br != TY_POLY_ARRAY; id = comp_ret_next(c, id)) { int a = nt_ref(nt, id, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an == 1 && infer_type(c, av[0]) == TY_POLY_ARRAY) br = TY_POLY_ARRAY; } if (br == TY_POLY_ARRAY) { scope_set_ret(sc, TY_POLY_ARRAY); changed = 1; } } /* (4) a local whose writes' values widened above follows them */ changed |= rejoin_local_writes(c); /* (5) a constant assigned from a value that widened to poly (a method return widened in step 3, an int constant assigned an arithmetic result, ...) must follow: a `COUNT = obj.m` whose method now returns poly otherwise mismatches the int constant slot. */ for (int id = 0; id < nt->count; id++) { NodeKind ty_k = nt_kind(nt, id); if (ty_k != NK_ConstantWriteNode) continue; const char *nm = nt_str(nt, id, "name"); LocalVar *cv = nm ? comp_const(c, nm) : NULL; if (!cv || cv->type != TY_INT) continue; int vnode = nt_ref(nt, id, "value"); if (vnode < 0) continue; if (infer_type(c, vnode) == TY_POLY) { lv_set_type(cv, TY_POLY); changed = 1; } } /* (6)-(10), --int-overflow=promote only: the slots the widening leaves behind. A producer of an Integer array that reads a widened slot -- `[x, x * 2]`, `[x]`, a map over them -- is a poly array in the final types, while the slot it flows into was decided as an Integer array during the fixpoint and, unlike a plain local (step 4), was re-derived by nothing afterwards: an ivar, a global, a multiple assignment's targets, a callee's parameter, and a local pinned to a container's element kind whose container has since widened. Each is widened here in the same direction step 4 widens, with the array-aware join the ivar write merge already uses (a typed array meets a poly array as the poly ARRAY, not the poly scalar ty_unify answers); a typed-array slot fed a boxed element becomes the box. Default mode never reaches these: its fixpoint saw every producer as it is (#4738). */ if (g_promote_mode) { /* the join: what a slot of kind `cur` becomes when fed `val` */ /* the flat scalar arrays only: a table (Array[Array[Integer]]) and an object array are the narrowing passes' kinds, re-derived by them */ #define PW_TYPED_ARR(t) ((t) == TY_INT_ARRAY || (t) == TY_STR_ARRAY || (t) == TY_FLOAT_ARRAY) /* ...and an Integer-valued hash fed the same hash with boxed values (`@h = { "z" => junk }`, junk's Integer answer boxed above), as step 3 widens a method's return */ #define PW_IHASH_WIDENS(cur, val) (ty_is_hash(cur) && ty_hash_val(cur) == TY_INT && ty_is_hash(val) && \ ty_hash_val(val) == TY_POLY && ty_hash_key(val) == ty_hash_key(cur)) #define PW_JOIN(cur, val) (PW_TYPED_ARR(cur) && (val) == TY_POLY_ARRAY ? TY_POLY_ARRAY \ : PW_TYPED_ARR(cur) && (val) == TY_POLY ? TY_POLY \ : PW_IHASH_WIDENS(cur, val) ? (val) : (cur)) /* (6) an ivar / cvar written a poly array or a boxed element */ for (int id = 0; id < nt->count; id++) { NodeKind k = nt_kind(nt, id); /* an operator write (`@a |= [x]`) stores its operator's answer, which an Integer array meets a poly array operand as */ int iv_write = k == NK_InstanceVariableWriteNode || k == NK_InstanceVariableOrWriteNode || k == NK_InstanceVariableAndWriteNode || k == NK_InstanceVariableOperatorWriteNode; int cv_write = k == NK_ClassVariableWriteNode || k == NK_ClassVariableOrWriteNode || k == NK_ClassVariableOperatorWriteNode; if (!iv_write && !cv_write) continue; int vnode = nt_ref(nt, id, "value"); if (vnode < 0) continue; const char *nm = nt_str(nt, id, "name"); if (!nm) continue; TyKind vt = infer_type(c, vnode); if (vt != TY_POLY_ARRAY && vt != TY_POLY && !(ty_is_hash(vt) && ty_hash_val(vt) == TY_POLY)) continue; if (iv_write) { int cid = comp_ivar_owner(c, id); int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], nm) : -1; if (iv < 0) continue; TyKind cur = c->classes[cid].ivar_types[iv], nw = PW_JOIN(cur, vt); if (nw != cur) { ivar_set_type(&c->classes[cid], iv, nw); changed = 1; } } else { Scope *ws = comp_scope_of(c, id); int cid = ws ? ws->class_id : -1; if (cid < 0) continue; ClassInfo *ci = &c->classes[comp_cvar_owner(c, cid, nm)]; for (int cv = 0; cv < ci->ncvars; cv++) { if (!ci->cvars[cv] || !sp_streq(ci->cvars[cv], nm)) continue; TyKind cur = ci->cvar_types[cv], nw = PW_JOIN(cur, vt); if (nw != cur) { cvar_set_type(ci, cv, nw); changed = 1; } } } } /* (6a) an attribute's operator write (`box.items |= [x]`): the attribute's ivar in the receiver's class takes the answer */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallOperatorWriteNode) continue; int vnode = nt_ref(nt, id, "value"), recv = nt_ref(nt, id, "receiver"); const char *nm = nt_str(nt, id, "name"); if (vnode < 0 || recv < 0 || !nm || infer_type(c, vnode) != TY_POLY_ARRAY) continue; TyKind rt = infer_type(c, recv); if (!ty_is_object(rt)) continue; int cid = ty_object_class(rt); char ivn[300]; snprintf(ivn, sizeof ivn, "@%s", nm); int iv = cid >= 0 && cid < c->nclasses ? comp_ivar_index(&c->classes[cid], ivn) : -1; if (iv < 0) continue; TyKind cur = c->classes[cid].ivar_types[iv], nw = PW_JOIN(cur, TY_POLY_ARRAY); if (nw != cur) { ivar_set_type(&c->classes[cid], iv, nw); changed = 1; } } /* (6c) an attribute writer's argument (`o.s = [o.a]`): the attribute's ivar in the receiver's class, or in every class with such an ivar when the receiver is boxed */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_CallNode) continue; const char *nm = nt_str(nt, id, "name"); if (!name_is_plain_setter(nm)) continue; size_t nl = strlen(nm); int recv = nt_ref(nt, id, "receiver"); int aa = nt_ref(nt, id, "arguments"), argc = 0; const int *argv = aa >= 0 ? nt_arr(nt, aa, "arguments", &argc) : NULL; if (recv < 0 || argc != 1 || infer_type(c, argv[0]) != TY_POLY_ARRAY) continue; char ivn[300]; snprintf(ivn, sizeof ivn, "@%.*s", (int)(nl - 1), nm); TyKind rt = infer_type(c, recv); for (int k = 0; k < c->nclasses; k++) { if (ty_is_object(rt) ? k != ty_object_class(rt) : (rt != TY_POLY && rt != TY_UNKNOWN)) continue; int iv = comp_ivar_index(&c->classes[k], ivn); if (iv < 0) continue; TyKind cur = c->classes[k].ivar_types[iv], nw = PW_JOIN(cur, TY_POLY_ARRAY); if (nw != cur) { ivar_set_type(&c->classes[k], iv, nw); changed = 1; } } } /* (6b) ...and the same field in every class that shares it through inheritance. (6) widens the class whose method writes the ivar, but a subclass holds a copy of the slot (inherit_members) and an ancestor's methods reach it through a cast (propagate_ivars_up's cast-compatibility rule), so the slot widens in both directions: `def initialize(k) = (@a = [k])` in N left M < N holding @a as sp_IntArray * against N's sp_PolyArray *. */ for (int k = 0; k < c->nclasses; k++) { ClassInfo *kc = &c->classes[k]; for (int iv = 0; iv < kc->nivars; iv++) { for (int a = kc->parent; a >= 0; a = c->classes[a].parent) { int ai = comp_ivar_index(&c->classes[a], kc->ivars[iv]); if (ai < 0) continue; TyKind at = c->classes[a].ivar_types[ai], kt = kc->ivar_types[iv]; TyKind na = PW_JOIN(at, kt), nk = PW_JOIN(kt, at); if (na != at) { ivar_set_type(&c->classes[a], ai, na); changed = 1; } if (nk != kt) { ivar_set_type(kc, iv, nk); changed = 1; } } } } /* (7) a global written a poly array or a boxed element */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_GlobalVariableWriteNode && nt_kind(nt, id) != NK_GlobalVariableOrWriteNode && nt_kind(nt, id) != NK_GlobalVariableOperatorWriteNode) continue; int vnode = nt_ref(nt, id, "value"); const char *nm = nt_str(nt, id, "name"); if (vnode < 0 || !nm) continue; LocalVar *gv = comp_gvar(c, nm[0] == '$' ? nm + 1 : nm); /* keyed without the `$` */ if (!gv) continue; TyKind vt = infer_type(c, vnode), nw = PW_JOIN(gv->type, vt); if (nw != gv->type) { lv_set_type(gv, nw); changed = 1; } } /* (8) a multiple assignment: a splat target collects a poly array from a boxed scalar or a poly-array source, and a plain target takes a boxed element out of a poly-array source */ for (int id = 0; id < nt->count; id++) { if (nt_kind(nt, id) != NK_MultiWriteNode) continue; int value = nt_ref(nt, id, "value"); if (value < 0) continue; Scope *ms = comp_scope_of(c, id); if (!ms) continue; /* a tuple right-hand side (`a, b = [], [x]`) pairs element with target: each target follows its own element */ if (masgn_tuple_rhs(nt, value)) { int en = 0; const int *el = nt_arr(nt, value, "elements", &en); int ln0 = 0; const int *lf = nt_arr(nt, id, "lefts", &ln0); for (int i = 0; i < ln0 && i < en; i++) { if (nt_kind(nt, lf[i]) != NK_LocalVariableTargetNode) continue; const char *lnm = nt_str(nt, lf[i], "name"); LocalVar *lv = lnm ? scope_local(ms, lnm) : NULL; if (!lv) continue; TyKind et = infer_type(c, el[i]), nw = PW_JOIN(lv->type, et); if (nw != lv->type) { lv_set_type(lv, nw); changed = 1; } } continue; } TyKind st = infer_type(c, value); if (st != TY_POLY && st != TY_POLY_ARRAY) continue; int rest = nt_ref(nt, id, "rest"); if (rest >= 0 && nt_kind(nt, rest) == NK_SplatNode) { int rin = nt_ref(nt, rest, "expression"); const char *rnm = rin >= 0 ? nt_str(nt, rin, "name") : NULL; LocalVar *rlv = rnm ? scope_local(ms, rnm) : NULL; if (rlv && PW_TYPED_ARR(rlv->type)) { lv_set_type(rlv, TY_POLY_ARRAY); changed = 1; } } if (st != TY_POLY_ARRAY) continue; int ln = 0; const int *lefts = nt_arr(nt, id, "lefts", &ln); for (int i = 0; i < ln; i++) { if (nt_kind(nt, lefts[i]) != NK_LocalVariableTargetNode) continue; const char *lnm = nt_str(nt, lefts[i], "name"); LocalVar *lv = lnm ? scope_local(ms, lnm) : NULL; if (lv && PW_TYPED_ARR(lv->type)) { lv_set_type(lv, TY_POLY); changed = 1; } } } /* (9) a parameter bound from an argument that is a poly array now (a clone's receiver parameter included: the rewrite passes the receiver as the first argument) */ PRCallIdx ix; prci_build(c, &ix); for (int s = 1; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->name || sc->nparams <= 0) continue; /* only a typed-array parameter widens: a method with none has no call to resolve (an_call_targets_scope, asked per call of the name, was most of this pass on a large program) */ int any_arr = 0; for (int j = 0; j < sc->nparams && !any_arr; j++) { LocalVar *pv = sc->pnames[j] ? scope_local(sc, sc->pnames[j]) : NULL; any_arr = pv && PW_TYPED_ARR(pv->type); } if (!any_arr) continue; int nk = prci_calls_for(&ix, sc->name); for (int q = 0; q < nk; q++) { int u = ix.calls[ix.buf[q]]; if (!an_call_targets_scope(c, u, s, sc)) continue; int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; for (int j = 0; j < sc->nparams; j++) { if (!sc->pnames[j]) continue; LocalVar *pv = scope_local(sc, sc->pnames[j]); if (!pv || !PW_TYPED_ARR(pv->type)) continue; int aj = call_param_arg(c, sc, av, an, j); if (aj < 0) continue; /* an empty literal argument is built at the parameter's kind and says nothing about it */ if (nt_kind(nt, aj) == NK_ArrayNode) { int en0 = 0; nt_arr(nt, aj, "elements", &en0); if (en0 == 0) continue; } if (infer_type(c, aj) != TY_POLY_ARRAY) continue; /* a parameter the callee mutates in place is passed by reference at its kind; widened, the argument would be copied into it and the mutation lost (the #4480 rule), and the emitter refuses that shape outright */ if (pw_scope_mutates_param(c, s, sc->pnames[j])) continue; lv_set_type(pv, TY_POLY_ARRAY); changed = 1; } } } /* (9a) a parameter the callee answers as it is (`def ia(a) = a`) hands the caller its argument back, which the caller may change in place: a typed-array local passed to it widens with it, so the call passes the array itself rather than a poly copy of it */ for (int s = 1; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->name || sc->nparams <= 0) continue; int last = scope_body_last(c, s); const char *rn = last >= 0 && nt_kind(nt, last) == NK_LocalVariableReadNode ? nt_str(nt, last, "name") : NULL; int rj = -1; for (int j = 0; rn && j < sc->nparams; j++) if (sc->pnames[j] && sp_streq(sc->pnames[j], rn)) rj = j; if (rj < 0) continue; LocalVar *pv = scope_local(sc, sc->pnames[rj]); if (!pv || pv->type != TY_POLY_ARRAY) continue; int nk = prci_calls_for(&ix, sc->name); for (int q = 0; q < nk; q++) { int u = ix.calls[ix.buf[q]]; if (!an_call_targets_scope(c, u, s, sc)) continue; int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; int aj = call_param_arg(c, sc, av, an, rj); if (aj < 0 || nt_kind(nt, aj) != NK_LocalVariableReadNode) continue; Scope *cs = comp_scope_of(c, aj); LocalVar *alv = cs ? scope_local(cs, nt_str(nt, aj, "name")) : NULL; if (alv && PW_TYPED_ARR(alv->type)) { lv_set_type(alv, TY_POLY_ARRAY); changed = 1; } } } /* (9c) a parameter whose default is such a value (`b: [a, r.size]` over widened slots): the default is filled at the call site into the parameter's slot */ for (int s = 1; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (!sc->pdefault) continue; for (int j = 0; j < sc->nparams; j++) { int d = sc->pdefault[j]; if (d < 0 || !sc->pnames[j]) continue; LocalVar *pv = scope_local(sc, sc->pnames[j]); if (!pv || !PW_TYPED_ARR(pv->type) || infer_type(c, d) != TY_POLY_ARRAY) continue; if (pw_scope_mutates_param(c, s, sc->pnames[j])) continue; lv_set_type(pv, TY_POLY_ARRAY); changed = 1; } } /* (9b) a block's parameter bound from such a value: the sites that bind the block -- each yield of the method the call reaches, or an instance_exec's own arguments -- typed again by the binding plan (block_site_types), a keyword from its value as much as a positional */ block_sites_index(c); for (int u = comp_kind_first(c, NK_CallNode); u >= 0; u = comp_kind_next(c, u)) { int blk = nt_kind(nt, u) == NK_CallNode ? nt_ref(nt, u, "block") : -1; if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; Scope *bs = comp_scope_of(c, blk); BlockSig bsig; block_sig(c, nt_ref(nt, blk, "parameters"), 0, &bsig); int np = bsig.P + bsig.O + bsig.Q, nb = np + bsig.nk; if (!bs || nb == 0) continue; TyKind *pos = calloc((size_t)nb, sizeof(TyKind)); char *absent = calloc((size_t)np + 1, 1); const char *un = nt_str(nt, u, "name"); if (un && sp_streq(un, "instance_exec")) { int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; AN_STEP_V("block_site_types", block_site_types(c, &bsig, av, an, pos, absent, pos + np)); } else for (int q = 0, nq = prci_scopes_for(c, &ix, un); nq < 0 ? q < c->nscopes - 1 : q < nq; q++) { int s = nq < 0 ? q + 1 : ix.sbuf[q]; if (!c->scopes[s].yields || !an_call_targets_scope(c, u, s, &c->scopes[s]) || an_builtin_recv_misses_scope(c, u, &c->scopes[s])) continue; const int *sites = NULL; int ns = block_sites(c, s, &sites); for (int k = 0; k < ns; k++) { int a = block_site_args(c, s, sites[k], u); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; AN_STEP_V("block_site_types", block_site_types(c, &bsig, av, an, pos, absent, pos + np)); } } for (int i = 0; i < nb; i++) { const char *pnm = i < np ? block_sig_name(c, &bsig, i) : block_sig_kw_name(c, &bsig, i - np); LocalVar *pv = pnm ? scope_local(bs, pnm) : NULL; if (!pv) continue; TyKind nw = PW_JOIN(pv->type, pos[i]); if (nw != pv->type) { lv_set_type(pv, nw); changed = 1; } } free(pos); free(absent); } prci_free(&ix); /* (11) a block over a receiver that is a poly array now: the params the fixpoint typed from the receiver's Integer elements are bound from boxed elements (the widening skips block params on purpose, since the emitters retype them from the receiver -- but from the receiver as the fixpoint saw it). Every scalar-typed param takes the box, except an each_with_index / with_index index, which is the emitter's own counter; each_cons / each_slice hand their first param a slice of the receiver, a poly array. */ 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; int blk = nt_ref(nt, u, "block"); if (blk < 0 || nt_kind(nt, blk) != NK_BlockNode) continue; int recv = nt_ref(nt, u, "receiver"); const char *inm = nt_str(nt, u, "name"); if (!inm) continue; /* a Ruby-defined builtin's call, rewritten receiverless with the receiver as its first argument (desugar_builtin_enum_calls) */ if (recv < 0 && strncmp(inm, "__enum_", 7) == 0) { int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an >= 1) recv = av[0]; } int slice_first = is_each_window(inm); int idx_second = is_with_index_alias(inm); /* only an iterator whose block receives ELEMENTS: `fetch(i) { |i| }` hands its block the index, `each_with_index` its second param */ if (!slice_first && !strbuf_elem_first_iterator(inm) && !sp_streq(inm, "inject") && !sp_streq(inm, "reduce") && !sp_streq(inm, "min") && !sp_streq(inm, "max") && !sp_streq(inm, "minmax") && !sp_streq(inm, "sort") && !sp_streq(inm, "sum") && !(strncmp(inm, "__enum_", 7) == 0)) continue; /* `arr.each_cons(2).with_index(1).map { |(x, y), i| }`: the block sits on a chain; walk it down to the each_cons / each_slice whose receiver is the array, and the first param is its slice */ for (int guard = 0; recv >= 0 && guard < 6 && nt_kind(nt, recv) == NK_CallNode && nt_ref(nt, recv, "block") < 0; guard++) { const char *cn = nt_str(nt, recv, "name"); if (!cn) break; if (is_each_window(cn)) { slice_first = 1; recv = nt_ref(nt, recv, "receiver"); break; } if (is_with_index_alias(cn)) idx_second = 1; else if (!(sp_streq(cn, "each") || sp_streq(cn, "with_object") || sp_streq(cn, "lazy") || sp_streq(cn, "each_entry"))) break; recv = nt_ref(nt, recv, "receiver"); } if (recv < 0 || infer_type(c, recv) != TY_POLY_ARRAY) continue; Scope *bs = comp_scope_of(c, blk); if (!bs) continue; for (int k = 0; k < 8; k++) { const char *pnm = block_param_name(c, blk, k); if (!pnm) break; if (idx_second && k == 1) continue; LocalVar *pv = scope_local(bs, pnm); if (!pv) continue; if (slice_first && k == 0) { if (PW_TYPED_ARR(pv->type)) { lv_set_type(pv, TY_POLY_ARRAY); changed = 1; } continue; } if (pv->type == TY_INT || pv->type == TY_FLOAT || pv->type == TY_STRING) { lv_set_type(pv, TY_POLY); changed = 1; } } } /* (12) a typed array local or ivar pushed a boxed value: the push's element evidence widened with the slot it reads, and the array has to hold the box (a Bignum among them is the mode's whole point) */ 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; const char *pn = nt_str(nt, u, "name"); if (!pn || !is_push_unshift(pn)) continue; int recv = nt_ref(nt, u, "receiver"); if (recv < 0) continue; int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; int boxed = 0; for (int j = 0; j < an; j++) if (infer_type(c, av[j]) == TY_POLY) boxed = 1; if (!boxed) continue; NodeKind rk = nt_kind(nt, recv); if (rk == NK_LocalVariableReadNode) { const char *rn = nt_str(nt, recv, "name"); LocalVar *lv = rn ? scope_local(comp_scope_of(c, recv), rn) : NULL; /* a parameter is the caller's array by reference and a block parameter is bound by its iterator: both keep their kind, and the push unboxes (raising on a foreign element, as the typed store always has) */ if (!lv || lv->is_param || lv->is_block_param || !(lv->type == TY_INT_ARRAY || lv->type == TY_FLOAT_ARRAY || lv->type == TY_STR_ARRAY)) continue; /* and only an array the local BUILT: bound from an element or an ivar read it is another name for storage something else holds, and a widened slot would take a converted copy, so the pushes would no longer reach the container (#4412) */ if (!pw_local_owns_array(c, recv, rn)) continue; lv_set_type(lv, TY_POLY_ARRAY); changed = 1; } else if (rk == NK_InstanceVariableReadNode) { const char *rn = nt_str(nt, recv, "name"); int cid = rn ? comp_ivar_owner(c, recv) : -1; int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], rn) : -1; if (iv >= 0) { TyKind cur = c->classes[cid].ivar_types[iv]; if (cur == TY_INT_ARRAY || cur == TY_FLOAT_ARRAY || cur == TY_STR_ARRAY) { ivar_set_type(&c->classes[cid], iv, TY_POLY_ARRAY); changed = 1; } } } } /* (13) a splice into a typed array from a source that is a poly array now -- an object whose #to_ary builds its array out of widened ivars (#4764), or a poly array outright: the fold of the splice source into the receiver's element kind ran inside the fixpoint, before the widening. The receiver, an array the local built, takes the poly kind, whose splice arm reads any array source. */ 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; const char *pn = nt_str(nt, u, "name"); if (!pn || !sp_streq(pn, "[]=")) continue; int a = nt_ref(nt, u, "arguments"); int an = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &an) : NULL; if (an < 2) continue; int recv = nt_ref(nt, u, "receiver"); if (recv < 0 || nt_kind(nt, recv) != NK_LocalVariableReadNode) continue; /* a splice: two index arguments, or a Range index */ if (an == 2 && infer_type(c, av[0]) != TY_RANGE) continue; int val = av[an - 1]; TyKind vt = infer_type(c, val), src = vt; if (ty_is_object(vt)) { int tmi = comp_implicit_to_ary_mi(c, ty_object_class(vt)); src = tmi >= 0 ? (TyKind)c->scopes[tmi].ret : TY_UNKNOWN; } if (src != TY_POLY_ARRAY) continue; const char *rn = nt_str(nt, recv, "name"); LocalVar *lv = rn ? scope_local(comp_scope_of(c, recv), rn) : NULL; if (!lv || lv->is_param || lv->is_block_param || !PW_TYPED_ARR(lv->type)) continue; if (!pw_local_owns_array(c, recv, rn)) continue; lv_set_type(lv, TY_POLY_ARRAY); changed = 1; } #undef PW_JOIN #undef PW_TYPED_ARR #undef PW_IHASH_WIDENS } } /* refresh the node-type cache so a `proc.call` node picks up the updated proc_ret (codegen reads comp_ntype, not lv->proc_ret directly). Re-infer reads scope-local types only -- it does not mutate them, so the widen stands. */ for (int id = 0; id < nt->count; id++) AN_STEP_V("infer_type", infer_type(c, id)); } } /* --share-strings: how many locals of scope s share_default_apply has converted to the poly Array (elems_shared); the storage loop compares the counts around itself to tell the scopes with a conversion made after inference. */ static int share_converted_locals(const Compiler *c, int s) { int n = 0; for (int i = 0; i < c->scopes[s].nlocals; i++) n += c->scopes[s].locals[i].elems_shared && c->scopes[s].locals[i].type == TY_POLY_ARRAY; return n; } /* The instance_eval re-infer and the storage refinements: byref string out-params, handle params, the reader operand marks, the STRBUF promotion, the shared and aliased strings (analyze_program's steps, in their order) */ static void an_phase_storage(Compiler *c) { /* Re-infer nodes inside instance_eval block bodies with the receiver's class context, so ivar reads get correct types in the final c->ntype cache. Call infer_type on each body statement: it recursively re-infers all sub-expressions (including ivar reads) and updates c->ntype. */ for (int id = 0; id < c->nt->count; id++) { NodeKind ty2_k = nt_kind(c->nt, id); if (ty2_k != NK_CallNode) continue; const char *nm2 = nt_str(c->nt, id, "name"); if (!nm2) continue; int blk2 = nt_ref(c->nt, id, "block"); int recv2 = nt_ref(c->nt, id, "receiver"); if (blk2 < 0 || recv2 < 0) continue; TyKind rt2 = c->ntype[recv2]; int cls2 = ty_is_object(rt2) ? ty_object_class(rt2) : ie_poly_mark(c, id, rt2); if (cls2 < 0) continue; int is_ie2 = is_instance_eval_family(nm2); if (is_ie2) { if (call_target(c, id, cls2, nm2, 0, NULL) >= 0) continue; } else if (!comp_trampoline_kind(c, cls2, nm2, NULL)) continue; int bdy2 = nt_ref(c->nt, blk2, "body"); if (bdy2 < 0) continue; int bn2 = 0; const int *bb2 = nt_arr(c->nt, bdy2, "body", &bn2); if (bn2 <= 0 || !bb2) continue; int saved2 = an_ie_class_id; an_ie_class_id = cls2; for (int k2 = 0; k2 < bn2; k2++) infer_type(c, bb2[k2]); /* refresh the splice call's own type from the now-rebound body so a consumer (e.g. truthiness) sees the poly result, not the stale type computed before an_ie_class_id was bound. */ AN_STEP_V("infer_type", infer_type(c, id)); an_ie_class_id = saved2; } /* Byref string out-params: must run before the STRBUF promotion below so the promotion can exclude locals whose address is passed to a byref slot (a STRBUF local is an sp_String*, not the const char* slot byref needs). */ AN_STEP_V("compute_byref_out_params", compute_byref_out_params(c)); AN_STEP_V("strbuf_arg_index_build", strbuf_arg_index_build(c)); /* handle args cannot ride byref's const char** slot: convert such params to the handle representation, cascading through transitive passes. A parameter RETAINED in a shared-handle ivar demands the handle for the same reason and feeds the same propagation, so the two run to a joint fixpoint rather than one after the other (#4363). */ int late_widened = 0; int *conv0 = NULL; { conv0 = malloc(sizeof(int) * (size_t)(c->nscopes > 0 ? c->nscopes : 1)); if (!conv0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int s = 0; s < c->nscopes; s++) conv0[s] = share_converted_locals(c, s); } HandleArgTab hat; /* The refresh below moves node types the rule's facts are built from: a variable an expression over a widened read joins can only now meet a String the rule shares. Under the rule the loop runs again while the refresh moves a type, so the rule decides over the types the seal will see (a holder it shares only by them is refused there otherwise). */ TyKind *nt0 = NULL; /* The types every store's input had before the rule: once the rule has widened a holder, a store whose input has widened since joins its slot again (infer_changed_store_types). */ NodeTypes before_rule; node_types_take(c, &before_rule); g_share_widened_holder = 0; for (int round = 0;; round++) { for (;;) { handle_arg_tab_init(c, &hat); int ch = promote_params_stored_in_shared_ivars(c, &hat); if (convert_byref_handle_params(c, &hat)) ch = 1; if (lift_poly_alias_reads(c, &hat)) ch = 1; if (promote_local_alias_pairs(c)) ch = 1; if (promote_dyncall_string_args(c)) ch = 1; if (promote_spread_string_args(c)) ch = 1; if (promote_default_alias_params(c)) ch = 1; if (promote_forwarded_rest_args(c)) ch = 1; if (promote_ivar_handle_stores(c)) ch = 1; /* --share-strings: a typed String Array the rule converts to the poly Array this late (share_default_apply) is a late widening: a method answering it, and the locals written from that method, follow it as they follow the late ivar and return widenings (rejoin_local_writes) */ if (c->sb_args->changed) { /* Newly reached stores can widen a literal. Bind its parameters again through the existing worklist, including super's chain, until those bindings settle too. */ c->sb_args->changed = infer_param_types_settle(c); if (c->sb_args->changed) ch = late_widened = 1; /* and the block parameters a widened parameter's iteration hands its blocks */ if (c->sb_args->changed && infer_block_params(c)) ch = 1; } if (ch) { g_ret_no_new_poly = 3; if (infer_return_types(c)) ch = late_widened = 1; g_ret_no_new_poly = 0; if (rejoin_local_writes(c)) ch = late_widened = 1; /* ... and so do the instance, class and global variables and the constants written from it. A constant a runtime-name const_get reaches is the poly Array under the rule (one_reflect_widen), and a class variable a class body wrote its copy (`@@p = PATHS.dup`) kept the String Array slot the fixpoint gave it, which the copy no longer fits. Once the rule has widened a holder, each store whose input has widened since the rule began joins again; the rest keep what they settled on. */ if (g_share_widened_holder && infer_changed_store_types(c, &before_rule)) ch = late_widened = 1; } if (!ch) break; handle_arg_tab_free(&hat); } /* ... and the reads of what widened take its type, as the proc-return re-derivation's refresh does (an_phase_proc_returns): everywhere when a return or a re-joined local moved, else the scopes of the locals converted here alone (a local is its scope's; their reads' types, and the types of the expressions over them, are that scope's nodes) */ if (round < 4) { if (!nt0) nt0 = malloc(sizeof(TyKind) * (size_t)(c->nt->count > 0 ? c->nt->count : 1)); if (!nt0) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } memcpy(nt0, c->ntype, sizeof(TyKind) * (size_t)c->nt->count); } if (late_widened) for (int id = 0; id < c->nt->count; id++) infer_type(c, id); else if (conv0) { int any = 0; for (int s = 0; s < c->nscopes; s++) { int moved = share_converted_locals(c, s) != conv0[s]; conv0[s] = moved; any |= moved; } if (any) for (int id = 0; id < c->nt->count; id++) { Scope *sc = comp_scope_of(c, id); if (sc && conv0[sc - c->scopes]) infer_type(c, id); } } if (round >= 4 || !memcmp(nt0, c->ntype, sizeof(TyKind) * (size_t)c->nt->count)) break; handle_arg_tab_free(&hat); late_widened = 0; for (int s = 0; s < c->nscopes; s++) conv0[s] = share_converted_locals(c, s); } free(nt0); free(before_rule.ty); free(conv0); /* A read an is_a? or nil guard narrowed to String (`m(x) if x.is_a?(String)`) unboxed a copy of the String its POLY variable holds: one lifted into the handle for a parameter appended to keeps the box, so the callee appends to the variable's own String. The narrowing ran before the lifts were decided. Under the rule, a lift whose variable cannot hold a String is dropped first, asking once per variable rather than per read. */ AN_STEP_V("strbuf_reconcile_lifted_reads", strbuf_reconcile_lifted_reads(c, &hat)); NT_FOREACH_KIND(c->nt, NK_LocalVariableReadNode, r) { if (c->nilnarrow[r] != TY_STRING) continue; if (c->poly_strbuf_lift[r]) { comp_set_nilnarrow(c, r, TY_UNKNOWN); comp_set_ntype(c, r, TY_POLY); } /* Other narrowed boxes keep the handle under the sharing rule too. Its ordinary String read still uses the byte face, as a handle local's does; identity and mutation take the handle itself. */ else { Scope *s = comp_scope_of(c, r); LocalVar *lv = s ? scope_local(s, nt_str(c->nt, r, "name")) : NULL; if (lv && lv->type == TY_POLY) comp_set_nilnarrow(c, r, comp_set_ntype(c, r, TY_STRBUF)); } } handle_arg_tab_free(&hat); AN_STEP_V("mark_reader_read_only_operands", mark_reader_read_only_operands(c)); /* Promote `<<`-appended string locals to mutable strings (TY_STRBUF) so the append is amortized O(1) instead of an O(n) copy-concat (which makes a build-in-a-loop O(n^2)). This is a storage-only refinement: comp_ntype demotes it to TY_STRING, and every read hands out an immutable copy, so a STRBUF never escapes its sp_String wrapper. Parameters are excluded (their type is part of the function signature). Runs last, after the node-type cache is finalized, so reads keep their TY_STRING cache entry. */ for (int id = 0; id < c->nt->count; id++) { NodeKind k = nt_kind(c->nt, id); if (k != NK_CallNode) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm || !sp_streq(nm, "<<")) continue; int recv = nt_ref(c->nt, id, "receiver"); if (recv < 0 || nt_kind(c->nt, recv) != NK_LocalVariableReadNode) continue; if (c->ntype[recv] != TY_STRING) continue; /* string append only */ const char *vn = nt_str(c->nt, recv, "name"); Scope *s = vn ? comp_scope_of(c, recv) : NULL; LocalVar *lv = s ? scope_local(s, vn) : NULL; /* (one-String mode: a String local is the handle) */ if (!lv || lv->is_param || (lv->type != TY_STRING && !(lv->type == TY_STRBUF))) continue; /* Only promote when every write to this local is a bare string literal: such a value is a fresh mutable string. A `.freeze`/`.dup`/method-result write may carry runtime frozen state that sp_String_new would discard, so `<<` would fail to raise FrozenError. Conservative but sound. Under `# frozen_string_literal: true` (the literal's `fzl` node flag, per file) the premise inverts -- the literal is FROZEN, and copying it into a writable sp_String would let `<<` mutate where CRuby raises FrozenError -- so a frozen contributing literal blocks the promotion and the value path's sp_str_check_mutable raises faithfully. */ /* other_write: an op-, or-, and-write or a multiple-assignment target also assigns the local, so it can hold that write's value (`s += "#{n}"` holds a fresh String): such a local does not "only ever hold frozen literals", and the warning below would be wrong. The promotion still declines it, as before. */ int all_literal_writes = 1, saw_write = 0, frozen_literal_write = 0, other_write = 0; for (int w = 0; w < c->nt->count; w++) { NodeKind wk = nt_kind(c->nt, w); if (wk == NK_LocalVariableOperatorWriteNode || wk == NK_LocalVariableOrWriteNode || wk == NK_LocalVariableAndWriteNode || wk == NK_LocalVariableTargetNode) { const char *on = nt_str(c->nt, w, "name"); if (on && sp_streq(on, vn) && comp_scope_of(c, w) == s) other_write = 1; continue; } if (wk != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(c->nt, w, "name"); if (!wn || !sp_streq(wn, vn) || comp_scope_of(c, w) != s) continue; saw_write = 1; int wv = nt_ref(c->nt, w, "value"); NodeKind wvk = nt_kind(c->nt, wv); if (wvk != NK_StringNode) { all_literal_writes = 0; break; } if (nt_int(c->nt, wv, "fzl", 0)) frozen_literal_write = 1; } if (!saw_write || !all_literal_writes) continue; if (frozen_literal_write) { /* Every value this local can hold is a frozen literal, and this `<<` will raise FrozenError on every execution -- at run time, deep in whatever the program was doing. `text = ""; text << line` is the most common accumulator idiom in Ruby, and spinel's frozen-by-default literals (docs/limitations.md) are exactly the wall it walks into -- so say so at compile time, once per local, with the escape hatch named (#4207). */ int first_shl = !other_write; for (int p2 = 0; p2 < id && first_shl; p2++) { if (nt_kind(c->nt, p2) != NK_CallNode) continue; const char *pn2 = nt_str(c->nt, p2, "name"); int pr2 = nt_ref(c->nt, p2, "receiver"); if (pn2 && sp_streq(pn2, "<<") && pr2 >= 0 && nt_kind(c->nt, pr2) == NK_LocalVariableReadNode && nt_str(c->nt, pr2, "name") && sp_streq(nt_str(c->nt, pr2, "name"), vn) && comp_scope_of(c, pr2) == s) first_shl = 0; } if (first_shl) { int wl = (int)nt_int(c->nt, id, "node_line", 0); int wf = (int)nt_int(c->nt, id, "node_file", 0); const char *wpath = nt_file_path(c->nt, wf); if (!wpath || !*wpath) wpath = c->nt->source_file; char wpos[1200]; if (wpath && *wpath && wl > 0) snprintf(wpos, sizeof wpos, "%s:%d: ", wpath, wl); else if (wpath && *wpath) snprintf(wpos, sizeof wpos, "%s: ", wpath); else wpos[0] = 0; fprintf(stderr, "%swarning: `%s` only ever holds frozen string literals, so this " "`<<` raises FrozenError at run time (string literals are frozen in " "spinel; build a mutable string with +\"...\", String.new, or " "interpolation -- see docs/limitations.md)\n", wpos, vn); } continue; } /* A captured-and-celled string stays TY_STRING so it rides a typed-pointer cell: `<<` then reassigns through the cell (*_cell = concat(...)), which propagates to the enclosing scope. A STRBUF's in-place buffer + read-time demotion don't fit the cell's stable-pointer model. (After the warning above: a celled accumulator raises the same FrozenError, #4207's own repro appends from inside a Thread block.) */ if (lv->is_cell) continue; /* Exclude vars used with a reassigning string mutator (replace/prepend/ insert/clear or a bang method): codegen emits those as `recv = ...`, which needs a plain lvalue, not the copy a STRBUF read produces. */ int has_reassign_mutate = 0; for (int u = 0; u < c->nt->count && !has_reassign_mutate; u++) { NodeKind uk = nt_kind(c->nt, u); if (uk != NK_CallNode) continue; int urecv = nt_ref(c->nt, u, "receiver"); if (urecv < 0 || nt_kind(c->nt, urecv) != NK_LocalVariableReadNode) continue; const char *urn = nt_str(c->nt, urecv, "name"); if (!urn || !sp_streq(urn, vn) || comp_scope_of(c, urecv) != s) continue; const char *un = nt_str(c->nt, u, "name"); if (!un) continue; size_t ul = strlen(un); if (sp_streq(un, "replace") || sp_streq(un, "prepend") || sp_streq(un, "insert") || sp_streq(un, "clear") || (ul > 0 && un[ul - 1] == '!')) has_reassign_mutate = 1; } if (has_reassign_mutate) continue; if (lv->rbs_seeded) continue; /* Exclude vars passed into a byref out-param slot: the call site takes the local's address as const char**, which a promoted sp_String* slot cannot provide. */ int passed_byref = 0; for (int u = 0; u < c->nt->count && !passed_byref; u++) { NodeKind uk = nt_kind(c->nt, u); if (uk != NK_CallNode) continue; if (comp_scope_of(c, u) != s) continue; /* the group's answer, not the unique one: see an_any_scope_by_name -- but not a builtin value's call, which none of them takes */ if (an_call_recv_builtin(c, u)) continue; int mi = an_any_scope_by_name(c, nt_str(c->nt, u, "name")); if (mi < 0) continue; for (int j = 0; j < c->scopes[mi].nparams; j++) { if (!comp_byref_param(c, &c->scopes[mi], j)) continue; int a2 = arg_layout_param_node(c, &c->scopes[mi], u, j, NULL); const char *an2 = a2 >= 0 && nt_kind(c->nt, a2) == NK_LocalVariableReadNode ? nt_str(c->nt, a2, "name") : NULL; if (an2 && sp_streq(an2, vn)) { passed_byref = 1; break; } } } if (passed_byref) continue; lv_set_type(lv, TY_STRBUF); } /* Shared-mutable strings (#3227): re-assert the phase-3 promotion. The fixpoint pass (promote_shared_stored_strings) sets str_shared and the strbuf_box read marks, but a later assign-based inference pass can overwrite the LOCAL's type back to TY_STRING from its literal writes. The marks (and so the container typing) are durable; restore the slot type here so the local declares as the sp_String handle. */ for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; for (int i = 0; i < sc->nlocals; i++) { LocalVar *lv = &sc->locals[i]; if ((lv->str_shared || lv->str_append) && (lv->type == TY_STRING || lv->type == TY_STR_ARRAY)) lv_set_type(lv, TY_STRBUF); } } for (int ci2 = 0; ci2 < c->nclasses; ci2++) { ClassInfo *ci = &c->classes[ci2]; for (int iv = 0; iv < ci->nivars; iv++) if (ci->ivar_str_shared[iv] && ci->ivar_types[iv] == TY_STRING) ivar_set_type(ci, iv, TY_STRBUF); } /* The local read a string-aliasing write bottoms out at: a bare local read, or a value-position `<<`/`concat` chain over one -- the chain's value IS its base object (`q = (v << x)` makes q an alias of v, and a later `v << y` is visible through q). Parens unwrap. -1 if neither. */ /* (helper defined just below as a nested loop-free walk) */ /* Shared-mutable strings (#3227): a string local that is a pure ALIAS of an in-place-mutated string (`s2 = s1` where s1 is now TY_STRBUF) must share s1's one sp_String* handle, so a later `s1 << x` is visible through s2 and `s1.equal?(s2)` is true (CRuby's mutable-String-object semantics). Promote the alias to TY_STRBUF and flag the whole set str_shared, which flips reads from copy-on-read to the live buffer and assignment to a handle copy. Conservative (Phase 1): the alias's every write must be `= `; other shapes keep today's value-copy behavior. Runs after the TY_STRBUF promotion above so the source's type is settled. */ for (int w = 0; w < c->nt->count; w++) { NodeKind wty_k = nt_kind(c->nt, w); if (wty_k != NK_LocalVariableWriteNode) continue; int wv = an_strbuf_alias_source(c, nt_ref(c->nt, w, "value")); if (wv < 0) continue; const char *srcn = nt_str(c->nt, wv, "name"); Scope *ws = comp_scope_of(c, w); LocalVar *src = (srcn && ws) ? scope_local(ws, srcn) : NULL; if (!src || src->type != TY_STRBUF) continue; /* aliases an in-place-mutated string */ const char *tgtn = nt_str(c->nt, w, "name"); LocalVar *tgt = tgtn ? scope_local(ws, tgtn) : NULL; if (!tgt || tgt == src || tgt->is_param || tgt->is_cell || tgt->rbs_seeded) continue; if (tgt->type != TY_STRING && tgt->type != TY_STRBUF) continue; /* every write to the alias must itself be an aliasing shape */ int pure_alias = 1; for (int u = 0; u < c->nt->count; u++) { NodeKind uk = nt_kind(c->nt, u); if (uk != NK_LocalVariableWriteNode) continue; const char *un = nt_str(c->nt, u, "name"); if (!un || !sp_streq(un, tgtn) || comp_scope_of(c, u) != ws) continue; if (an_strbuf_alias_source(c, nt_ref(c->nt, u, "value")) < 0) { pure_alias = 0; break; } } if (!pure_alias) continue; { src->str_shared = 1; comp_touch_scope(src->owner); } lv_set_type(tgt, TY_STRBUF); { tgt->str_shared = 1; comp_touch_scope(tgt->owner); } } } /* The classes an instance of which may be nil, by the nil fact (#7444): a node, a method's value, a local or a parameter, or an ivar of the class's type whose fact says so. A slot holding `nil | W` encodes nil as the heap pointer's NULL, which a by-value struct has no representation for, so such a class keeps its pointer layout. */ static void vt_nil_fact_classes(Compiler *c, unsigned char *fact) { const NodeTable *nt = c->nt; for (int id = 0; id < nt->count; id++) { TyKind t = c->ntype[id]; if (!ty_is_object(t) || !nil_fact_node(c, id)) continue; int q = ty_object_class(t); if (q >= 0 && q < c->nclasses) fact[q] = 1; } for (int si = 0; si < c->nscopes; si++) { Scope *sc = &c->scopes[si]; if (ty_is_object(sc->ret) && sc->ret_obj_may_nil) fact[ty_object_class(sc->ret)] = 1; for (int k = 0; k < sc->nlocals; k++) if (ty_is_object(sc->locals[k].type) && sc->locals[k].obj_may_nil) fact[ty_object_class(sc->locals[k].type)] = 1; } for (int k = 0; k < c->nclasses; k++) for (int i = 0; i < c->classes[k].n_ivar_obj_may_nil && i < c->classes[k].nivars; i++) if (c->classes[k].ivar_obj_may_nil[i] && ty_is_object(c->classes[k].ivar_types[i])) fact[ty_object_class(c->classes[k].ivar_types[i])] = 1; } /* Value-type objects: a small, immutable, scalar-only leaf class is represented by value, unless an instance is boxed, held in a class variable or captured by a proc (analyze_program's steps, in their order) */ /* A String argument of a C function the program binds (ffi_func, a package's native_func) takes a shared or appended String's live buffer. Such a String is an sp_String * handle, and its read face is a full copy, made so that no `const char *` outlives the bytes a growth reallocates. A bound C function reads a :str argument for the length of the call and runs no Ruby code meanwhile, so nothing can grow the String under it, and the copy is never observed. It can run Ruby code only through an ffi_callback, a signal handler or another thread, so a program with any of them keeps the copy (an_ruby_runs_unseen): `LibC.strlen(s)` copied all of a 1 MB `s` at every call. The call's other operands have to be plain reads or literals, so nothing runs between the borrow and the call either. Nothing becomes shared that was not; only the read of a handle stops copying, as mark_reader_read_only_operands does for a builtin accessor. */ static int native_str_operand_plain(Compiler *c, int a) { switch (nt_kind(c->nt, a)) { case NK_LocalVariableReadNode: case NK_InstanceVariableReadNode: case NK_GlobalVariableReadNode: case NK_ConstantReadNode: case NK_SelfNode: case NK_IntegerNode: case NK_FloatNode: case NK_StringNode: case NK_SymbolNode: case NK_NilNode: case NK_TrueNode: case NK_FalseNode: return 1; default: return 0; } } /* Is argument a a read of a String slot held as an sp_String * handle? */ static int native_str_handle_read(Compiler *c, int a) { const NodeTable *nt = c->nt; if (c->strbuf_box[a] || c->strbuf_handle_demand[a] || c->strbuf_read_raw[a]) return 0; if (nt_kind(nt, a) == NK_LocalVariableReadNode) { const char *ln = nt_str(nt, a, "name"); LocalVar *lv = ln ? scope_local(comp_scope_of(c, a), ln) : NULL; return lv && lv->type == TY_STRBUF && repr_of_slot(c, lv).kind == RK_STRBUF; } if (nt_kind(nt, a) == NK_InstanceVariableReadNode) { const char *nm = nt_str(nt, a, "name"); Scope *s = comp_scope_of(c, a); /* a class method's @x is the class's own ivar, another slot */ if (!nm || !s || s->is_cmethod) return 0; int cid = s->class_id >= 0 ? s->class_id : c->toplevel_class; int iv = cid >= 0 ? comp_ivar_index(&c->classes[cid], nm) : -1; return iv >= 0 && c->classes[cid].ivar_types[iv] == TY_STRBUF; } return 0; } static void mark_native_str_operands(Compiler *c) { const NodeTable *nt = c->nt; if (an_ruby_runs_unseen(c)) return; NT_FOREACH_KIND(nt, NK_CallNode, n) { int recv = nt_ref(nt, n, "receiver"); const char *name = nt_str(nt, n, "name"); if (recv < 0 || !name || nt_ref(nt, n, "block") >= 0) continue; NodeKind rk = nt_kind(nt, recv); if (rk != NK_ConstantReadNode && rk != NK_ConstantPathNode) continue; const char *mod = nt_str(nt, recv, "name"); if (!mod || (ffi_find_func(c, mod, name) < 0 && comp_native_find(c, mod, name) < 0)) continue; int args = nt_ref(nt, n, "arguments"); int argc = 0; const int *argv = args >= 0 ? nt_arr(nt, args, "arguments", &argc) : NULL; int plain = 1, any = 0; for (int i = 0; i < argc && plain; i++) { int h = native_str_handle_read(c, argv[i]); any |= h; plain = h || native_str_operand_plain(c, argv[i]); } if (!plain || !any) continue; for (int i = 0; i < argc; i++) if (native_str_handle_read(c, argv[i])) c->strbuf_read_raw[argv[i]] = 1; } } /* ---- Reads that see whether an ivar was assigned ---- instance_variables, instance_variable_defined?, defined?(@x), remove_instance_variable, the default inspect (p, pp, #inspect, a container's inspect, a `super` in an inspect of the program's) and Marshal.dump tell an ivar never assigned from one assigned nil. Each marks the classes whose instances it can reach, from the settled types: the receiver's class, the classes a boxed value can be (pivs), and for inspect and dump the classes the object's ivars and a container's elements hold in turn. An inspect of the program's own shows no ivar, so a class that resolves one is passed over (its body's reads are sites of their own). ivar_set_kind keeps an assigned flag only for ivars of a marked class. */ enum { OBS_SEEN = 1, OBS_INSPECT = 2, OBS_DUMP = 4 }; static void an_presence_type(Compiler *c, TyKind t, int how, int depth); static void an_presence_poly_ivar(Compiler *c, int k, const char *ivn, int how, int depth); /* Class k and the classes below it, whose instances a value typed k can be; how is 0 for a read of presence alone, else OBS_INSPECT or OBS_DUMP, which reach the ivars' values as well. */ static void an_presence_class(Compiler *c, int k, int how, int depth) { if (k < 0 || k >= c->nclasses || depth > 16) return; for (int d = 0; d < c->nclasses; d++) { if (d != k && !is_descendant(c, d, k)) continue; ClassInfo *ci = &c->classes[d]; /* `super` in the program's inspect runs the default one (depth -1) */ if (how == OBS_INSPECT && depth >= 0 && comp_method_in_chain(c, d, "inspect", NULL) >= 0) continue; ci->presence_observed |= OBS_SEEN; c->pres_seen = 1; if (!how || (ci->presence_observed & how)) continue; ci->presence_observed |= how; for (int j = 0; j < ci->nivars; j++) { if (ci->ivar_types[j] == TY_POLY) an_presence_poly_ivar(c, d, ci->ivars[j], how, depth + 1); else an_presence_type(c, ci->ivar_types[j], how, depth + 1); } } } static void an_presence_type(Compiler *c, TyKind t, int how, int depth) { if (ty_is_object(t)) an_presence_class(c, ty_object_class(t), how, depth); else if (ty_is_obj_array(t)) an_presence_class(c, ty_obj_array_class(t), how, depth); else if (ty_is_array(t) && ty_is_object(ty_array_elem(t))) an_presence_type(c, ty_array_elem(t), how, depth); else if (ty_is_hash(t)) { an_presence_type(c, ty_hash_key(t), how, depth); an_presence_type(c, ty_hash_val(t), how, depth); } } /* The boxed ivar ivn of class k holds what its stores put there: the classes the receiver walk bounds them to (none when it cannot). */ static void an_presence_poly_ivar(Compiler *c, int k, const char *ivn, int how, int depth) { char *set = calloc((size_t)(c->nclasses > 0 ? c->nclasses : 1), 1); if (!set) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (pivs_ivar_classes(c, k, ivn, set)) for (int i = 0; i < c->nclasses; i++) if (set[i]) an_presence_class(c, i, how, depth); free(set); } /* What node v holds: a literal's elements one by one, a typed value by its type, a boxed value or a general Array's elements or a Hash's boxed values by the classes pivs bounds them to (none when it cannot). */ static void an_presence_node(Compiler *c, int v, int how) { const NodeTable *nt = c->nt; if (v < 0) return; NodeKind k = nt_kind(nt, v); if (k == NK_ArrayNode || k == NK_HashNode || k == NK_KeywordHashNode) { int n = 0; const int *el = nt_arr(nt, v, "elements", &n); for (int i = 0; i < n; i++) { if (nt_kind(nt, el[i]) == NK_AssocNode) { an_presence_node(c, nt_ref(nt, el[i], "key"), how); an_presence_node(c, nt_ref(nt, el[i], "value"), how); } else if (nt_kind(nt, el[i]) != NK_SplatNode) an_presence_node(c, el[i], how); } return; } TyKind t = comp_ntype(c, v); int elems = t == TY_POLY_ARRAY || (ty_is_hash(t) && ty_hash_val(t) == TY_POLY); if (t != TY_POLY && !elems) { an_presence_type(c, t, how, 0); return; } char *set = calloc((size_t)(c->nclasses > 0 ? c->nclasses : 1), 1); if (!set) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } if (pivs_value_classes(c, v, elems, set)) for (int i = 0; i < c->nclasses; i++) if (set[i]) an_presence_class(c, i, how, 0); free(set); } /* The receiver of reflective call id: self's class, a typed object's, or the classes a boxed one can be. */ static void an_presence_recv(Compiler *c, int id, int how) { int r = nt_ref(c->nt, id, "receiver"); Scope *s = comp_scope_of(c, id); if (r < 0) { if (s && !s->is_cmethod && s->class_id >= 0) an_presence_class(c, s->class_id, how, 0); return; } if (comp_ntype(c, r) != TY_POLY) { an_presence_node(c, r, how); return; } int n = 0; const int *ks = poly_recv_classes(c, id, &n); for (int i = 0; ks && i < n; i++) an_presence_class(c, ks[i], how, 0); } /* Note every ivar the subtree under n writes as one whose writes no emitter marks assigned. */ static void an_presence_unmarked(Compiler *c, int n, int depth) { const NodeTable *nt = c->nt; if (n < 0 || depth > 200) return; switch (nt_kind(nt, n)) { case NK_InstanceVariableWriteNode: case NK_InstanceVariableOrWriteNode: case NK_InstanceVariableAndWriteNode: case NK_InstanceVariableOperatorWriteNode: case NK_InstanceVariableTargetNode: comp_pres_note(c, nt_str(nt, n, "name"), PRES_UNMARKED); break; default: break; } int nr = nt_num_refs(nt, n); for (int i = 0; i < nr; i++) an_presence_unmarked(c, nt_ref_at(nt, n, i), depth + 1); int na = nt_num_arrs(nt, n); for (int i = 0; i < na; i++) { int m = 0; const int *ids = nt_arr_at(nt, n, i, &m); for (int j = 0; j < m; j++) an_presence_unmarked(c, ids[j], depth + 1); } } /* Note ivar "@" + the first len bytes of name, a string made as long as the name needs. */ static void an_presence_note_ivar(Compiler *c, const char *name, size_t len, int flag) { char *ivn = malloc(len + 2); if (!ivn) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } ivn[0] = '@'; memcpy(ivn + 1, name, len); ivn[len + 1] = '\0'; comp_pres_note(c, ivn, flag); free(ivn); } /* The writes and removals the flag cannot follow (Compiler.pres_names): an ivar target of a `for` or a `rescue`, a writer reached by its name, a bitwise op-write through a writer (nil answers `|`), an instance_variable_set of a computed name. */ static void an_presence_writes(Compiler *c, int id) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, id); if (k == NK_ForNode) an_presence_unmarked(c, nt_ref(nt, id, "index"), 0); else if (k == NK_RescueNode) an_presence_unmarked(c, nt_ref(nt, id, "reference"), 0); else if (k == NK_CallNode) { const char *nm = nt_str(nt, id, "name"); int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; NodeKind k0 = ac > 0 ? nt_kind(nt, av[0]) : NK_NONE; if (!nm) return; /* objects made without initialize: the order it assigns the ivars in says nothing of theirs */ { int rr = nt_ref(nt, id, "receiver"); if (is_allocate_name(nm) || (rr >= 0 && nt_kind(nt, rr) == NK_ConstantReadNode && is_marshal_load(nt_str(nt, rr, "name"), nm))) c->pres_noinit = 1; } if (is_ivar_set(nm) && k0 != NK_SymbolNode && k0 != NK_StringNode) c->pres_any = 1; else if (is_ivar_remove_name(nm) && (k0 == NK_SymbolNode || k0 == NK_StringNode)) comp_pres_note(c, nt_str(nt, av[0], k0 == NK_SymbolNode ? "value" : "content"), PRES_REMOVED); else if ((is_send_family(nm) || is_method_ref_name(nm)) && k0 == NK_SymbolNode) { const char *wn = nt_str(nt, av[0], "value"); if (name_is_plain_setter(wn)) an_presence_note_ivar(c, wn, strlen(wn) - 1, PRES_UNMARKED); } } else if (nt_kind(nt, id) == NK_CallOperatorWriteNode) { const char *op = nt_str(nt, id, "binary_operator"), *rn = nt_str(nt, id, "read_name"); if (op && rn && is_bit_op(op)) an_presence_note_ivar(c, rn, strlen(rn), PRES_UNMARKED); } } /* presence_family: the classes linked by a parent or an included module, joined (union-find on the class indexes) */ static int an_family_root(int *up, int k) { while (up[k] != k) { up[k] = up[up[k]]; k = up[k]; } return k; } static void an_presence_families(Compiler *c) { int n = c->nclasses; int *up = malloc(sizeof(int) * (size_t)(n > 0 ? n : 1)); if (!up) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } for (int k = 0; k < n; k++) up[k] = k; for (int k = 0; k < n; k++) { ClassInfo *ci = &c->classes[k]; if (ci->parent >= 0 && ci->parent < n) up[an_family_root(up, k)] = an_family_root(up, ci->parent); for (int m = 0; m < ci->nincluded_mods; m++) { int md = ci->included_mods[m]; if (md >= 0 && md < n) up[an_family_root(up, k)] = an_family_root(up, md); } } for (int k = 0; k < n; k++) c->classes[k].presence_family = an_family_root(up, k) + 1; /* a family Object's layout is part of is every class's */ for (int k = 0; k < n; k++) { if (!is_object_root(c->classes[k].name)) continue; int r = an_family_root(up, k) + 1; for (int j = 0; j < n; j++) if (c->classes[j].presence_family == r) c->classes[j].presence_family = 0; } free(up); } static void an_presence_observe(Compiler *c, int id) { const NodeTable *nt = c->nt; NodeKind k = nt_kind(nt, id); Scope *s = comp_scope_of(c, id); int own = s && !s->is_cmethod && s->class_id >= 0 ? s->class_id : -1; an_presence_writes(c, id); if (k == NK_DefinedNode) { if (nt_kind(nt, nt_ref(nt, id, "value")) == NK_InstanceVariableReadNode) an_presence_class(c, own, 0, 0); return; } /* Object#inspect, through `super` in an inspect of the program's */ if (k == NK_SuperNode || k == NK_ForwardingSuperNode) { /* only where no inspect of the program's sits above: that one runs */ int up = own >= 0 ? comp_super_parent(c, own, 0) : -1; if (own >= 0 && s->name && is_inspect_name(s->name) && (up < 0 || comp_method_in_chain(c, up, s->name, NULL) < 0)) an_presence_class(c, own, OBS_INSPECT, -1); return; } /* an interpolated Array or Hash is its inspect */ if (k == NK_EmbeddedStatementsNode) { int st = nt_ref(nt, id, "statements"), n = 0; const int *b = st >= 0 ? nt_arr(nt, st, "body", &n) : NULL; TyKind t = n > 0 ? comp_ntype(c, b[n - 1]) : TY_UNKNOWN; if (ty_is_array(t) || ty_is_hash(t)) an_presence_node(c, b[n - 1], OBS_INSPECT); return; } if (k != NK_CallNode) return; const char *nm = nt_str(nt, id, "name"); int r = nt_ref(nt, id, "receiver"); int a = nt_ref(nt, id, "arguments"), ac = 0; const int *av = a >= 0 ? nt_arr(nt, a, "arguments", &ac) : NULL; if (!nm) return; if (is_ivar_presence_read(nm)) { if (is_inspect_print(nm)) { if (r < 0) for (int i = 0; i < ac; i++) an_presence_node(c, av[i], OBS_INSPECT); } /* inspect reaches the ivars' values, the readers of presence and the removal do not */ else if (!is_inspect_name(nm)) an_presence_recv(c, id, 0); else if (ac == 0) an_presence_recv(c, id, OBS_INSPECT); } /* an Array's or a Hash's to_s is its inspect; puts writes a Hash so */ else if (r >= 0 && is_to_s_name(nm) && (ty_is_array(comp_ntype(c, r)) || ty_is_hash(comp_ntype(c, r)))) an_presence_node(c, r, OBS_INSPECT); else if (r < 0 && is_text_print(nm)) { for (int i = 0; i < ac; i++) if (ty_is_hash(comp_ntype(c, av[i]))) an_presence_node(c, av[i], OBS_INSPECT); } else if (ac >= 1 && r >= 0 && nt_kind(nt, r) == NK_ConstantReadNode && is_marshal_dump(nt_str(nt, r, "name"), nm)) an_presence_node(c, av[0], OBS_DUMP); } static void an_phase_value_types(Compiler *c) { /* The nil fact (analyze_nil.c, #7444): whether each object-typed node and slot may hold nil, from the settled types, ahead of the layout choice below, which a value that may be nil cannot take (vt_nil_fact_classes). It changes no type and no flag the passes before it read. */ AN_STEP_V("an_nil_facts", an_nil_facts(c)); /* Value-type object detection (Stage 1, conservative). A user class is represented by value (sp_X, no heap/GC) when it is a small, immutable, scalar-only leaf whose instances never need a heap pointer (never boxed, stored, passed, or captured). See reference_legacy_value_type_logic. */ for (int i = 0; i < c->nclasses; i++) { ClassInfo *ci = &c->classes[i]; if (ci->is_struct || ci->cext_extension) continue; if (ci->def_node < 0) continue; /* synthetic / Toplevel */ if (!ci->name || i == c->toplevel_class) continue; if (ci->nivars < 1 || ci->nivars > 8) continue; if (ci->nwriters > 0) continue; if (ci->parent >= 0) continue; /* explicit user superclass */ /* A class with any superclass node -- including a builtin like StandardError -- is not a standalone leaf, so it can't be a value type. The parent check above only catches user superclasses (a builtin parent leaves ci->parent == -1); without this, an Exception subclass with a scalar ivar was wrongly returned by value and never got a struct (#1415). */ if (ci->def_node >= 0 && nt_ref(c->nt, ci->def_node, "superclass") >= 0) continue; int scalar = 1; for (int j = 0; j < ci->nivars; j++) { TyKind t = ci->ivar_types[j]; /* int/float/bool need no GC; string fields are heap pointers but get GC-rooted per value-type local (the field slot is a stable root). */ /* (one String representation: a String ivar is its handle, a pointer as a String's bytes are; the pass below keeps it by value only when every write gives it a frozen literal) */ int one_str = t == TY_STRBUF; if (t != TY_INT && t != TY_FLOAT && t != TY_BOOL && t != TY_STRING && !one_str) { scalar = 0; break; } if (one_str) continue; /* an in-place-mutated string ivar (`@s << x` in a method) needs the mutation visible through every reference; a by-value struct copy would swallow it (#3227 P4) */ if (t == TY_STRING && strbuf_ivar_mut_kind(c, i, ci->ivars[j]) != 0) { scalar = 0; break; } /* the same mutation one call away: `helper(@s, x)` where the helper appends to its parameter writes through the slot's address, and a by-value receiver would hand it the address of a copy */ if (t == TY_STRING && an_ivar_passed_as_arg(c, i, ci->ivars[j])) { scalar = 0; break; } } if (!scalar) continue; int has_sub = 0; for (int j = 0; j < c->nclasses; j++) if (c->classes[j].parent == i) { has_sub = 1; break; } if (has_sub) continue; ci->is_value_type = 1; /* tentative; disqualified below */ } /* a class an instance of which may be nil keeps its pointer, whose NULL is the nil (#1686, by the nil fact since #7444) */ { unsigned char *fact = calloc((size_t)c->nclasses + 1, 1); if (!fact) { fprintf(stderr, "spinel: out of memory\n"); exit(1); } vt_nil_fact_classes(c, fact); for (int q = 0; q < c->nclasses; q++) if (fact[q]) c->classes[q].is_value_type = 0; free(fact); } an_presence_families(c); for (int id = 0; id < c->nt->count; id++) { const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) continue; an_presence_observe(c, id); /* freeze/frozen? reaching a class's instances needs the GC-header frozen bit, which a by-value struct doesn't have: force heap representation and mark the class so codegen guards its ivar stores. A poly-receiver freeze can hit any boxable instance, so it marks every class. */ if (ty_k == NK_CallNode) { const char *fzn = nt_str(c->nt, id, "name"); if (fzn && (is_freeze_family(fzn))) { int frecv = nt_ref(c->nt, id, "receiver"); int q = -1; if (frecv >= 0) { TyKind frt = comp_ntype(c, frecv); if (ty_is_object(frt)) q = ty_object_class(frt); else if (frt == TY_POLY && sp_streq(fzn, "freeze")) /* a poly-receiver freeze can hit any class's instance, so every class must carry the GC-header frozen bit -- disqualify them all from the by-value layout too, matching the single-class path */ for (int q2 = 0; q2 < c->nclasses; q2++) { c->classes[q2].is_value_type = 0; c->classes[q2].freeze_observed = 1; } } else { Scope *fs = comp_scope_of(c, id); if (fs && fs->class_id >= 0 && !fs->is_cmethod) q = fs->class_id; } if (q >= 0 && q < c->nclasses) { c->classes[q].is_value_type = 0; c->classes[q].freeze_observed = 1; } } } /* (one String representation: a String ivar is by value only when every write gives it a frozen literal, which no name can change) */ if (ty_k == NK_InstanceVariableWriteNode) { Scope *s = comp_scope_of(c, id); int q = s ? s->class_id : -1; const char *ivn = nt_str(c->nt, id, "name"); int val = nt_ref(c->nt, id, "value"); if (q >= 0 && q < c->nclasses && c->classes[q].is_value_type && ivn && (val < 0 || nt_kind(c->nt, val) != NK_StringNode)) { ClassInfo *qc = &c->classes[q]; for (int j = 0; j < qc->nivars; j++) if (sp_streq(qc->ivars[j], ivn) && qc->ivar_types[j] == TY_STRBUF) { qc->is_value_type = 0; break; } } } /* immutable check: an ivar write outside `initialize` defeats value type */ if (ty_k == NK_InstanceVariableWriteNode || ty_k == NK_InstanceVariableOperatorWriteNode || ty_k == NK_InstanceVariableOrWriteNode || ty_k == NK_InstanceVariableAndWriteNode || ty_k == NK_InstanceVariableTargetNode) { Scope *s = comp_scope_of(c, id); if (s && s->class_id >= 0 && s->class_id < c->nclasses && c->classes[s->class_id].is_value_type && (!s->name || !sp_streq(s->name, "initialize"))) c->classes[s->class_id].is_value_type = 0; } /* `instance_variable_set` on self mutates the receiver just like `@x = v`; a by-value class would take the write on a copy and silently drop it. */ if (ty_k == NK_CallNode) { const char *ivsn = nt_str(c->nt, id, "name"); if (ivsn && sp_streq(ivsn, "instance_variable_set")) { int ivr = nt_ref(c->nt, id, "receiver"); if (ivr < 0 || (nt_kind(c->nt, ivr) == NK_SelfNode && comp_ntype(c, ivr) != TY_POLY)) { Scope *s = comp_scope_of(c, id); if (s && s->class_id >= 0 && s->class_id < c->nclasses && (!s->name || !sp_streq(s->name, "initialize"))) c->classes[s->class_id].is_value_type = 0; } else { poly_ivar_set_reference(c, id, ivr); } } } /* unsafe uses that would need a heap pointer / boxing (void* slot) */ if (ty_k == NK_ArrayNode || ty_k == NK_HashNode || ty_k == NK_KeywordHashNode) { int n = 0; const int *els = nt_arr(c->nt, id, "elements", &n); for (int k = 0; k < n; k++) { TyKind et = comp_ntype(c, els[k]); if (ty_is_object(et)) { int q = ty_object_class(et); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } } if (ty_k == NK_CallNode) { const char *nm = nt_str(c->nt, id, "name"); int recv = nt_ref(c->nt, id, "receiver"); if (nm && sp_streq(nm, "method") && recv >= 0) { TyKind rt = comp_ntype(c, recv); if (ty_is_object(rt)) { int q = ty_object_class(rt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } /* `method(:foo)` with no receiver captures `self`; a bound Method needs a stable heap pointer, so the enclosing class can't be a value type. */ if (nm && sp_streq(nm, "method") && recv < 0) { Scope *s = comp_scope_of(c, id); if (s && s->class_id >= 0 && s->class_id < c->nclasses) c->classes[s->class_id].is_value_type = 0; } /* instance_eval/exec lifts the block body into a method on the receiver that needs a by-pointer self; exclude such receivers. */ if (nm && (is_instance_eval_family(nm)) && recv >= 0) { TyKind rt = comp_ntype(c, recv); if (ty_is_object(rt)) { int q = ty_object_class(rt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } int args = nt_ref(c->nt, id, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(c->nt, args, "arguments", &an) : NULL; for (int k = 0; k < an; k++) { TyKind at = comp_ntype(c, av[k]); if (ty_is_object(at)) { int q = ty_object_class(at); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } } if (ty_k == NK_InstanceVariableWriteNode || ty_k == NK_GlobalVariableWriteNode || ty_k == NK_ConstantWriteNode || ty_k == NK_ClassVariableWriteNode || ty_k == NK_InstanceVariableOrWriteNode || ty_k == NK_GlobalVariableOrWriteNode || ty_k == NK_ClassVariableOrWriteNode) { int v = nt_ref(c->nt, id, "value"); if (v >= 0) { TyKind vt = comp_ntype(c, v); if (ty_is_object(vt)) { int q = ty_object_class(vt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } } /* a value-type instance written into a poly-typed local would be boxed */ if (ty_k == NK_LocalVariableWriteNode) { int v = nt_ref(c->nt, id, "value"); if (v >= 0) { TyKind vt = comp_ntype(c, v); if (ty_is_object(vt)) { const char *vn = nt_str(c->nt, id, "name"); Scope *s = comp_scope_of(c, id); LocalVar *lv = (vn && s) ? scope_local(s, vn) : NULL; if (lv && lv->type != vt) { int q = ty_object_class(vt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } } } } /* An instance built inside a poly-returning method is liable to be boxed at the (poly) return -- sp_box_obj would carry a stack pointer. And an instance that is a block's result is collected (map/collect -> a boxed array). Both box a value, so exclude such classes. */ for (int id = 0; id < c->nt->count; id++) { TyKind t = comp_ntype(c, id); if (!ty_is_object(t)) continue; int q = ty_object_class(t); if (q < 0 || q >= c->nclasses || !c->classes[q].is_value_type) continue; Scope *s = comp_scope_of(c, id); /* poly scalar return boxes; a tuple return (POLY_ARRAY, e.g. `return a, b, c`) boxes each element. Either way a value instance here would be boxed. */ if (s && (s->ret == TY_POLY || s->ret == TY_POLY_ARRAY)) c->classes[q].is_value_type = 0; } /* An instance held in a class variable: the cvar is a file-scope static initialized to NULL and GC-marked through its pointer, so its class must be a heap object (a value type has no NULL and no pointer to mark). */ for (int i = 0; i < c->nclasses; i++) { ClassInfo *ci = &c->classes[i]; for (int j = 0; j < ci->ncvars; j++) { TyKind t = ci->cvar_types[j]; if (!ty_is_object(t)) continue; int q = ty_object_class(t); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } /* An instance held in a local a proc captures: the local lives in a heap cell the proc keeps, and a cell holds a GC pointer or a builtin struct, not a user struct. Nothing else here disqualified it, so `v = V.new(1); proc { v.a }` was refused as a non-integer capture. */ an_heap_captured_classes(c); for (int id = 0; id < c->nt->count; id++) { NodeKind ty_k = nt_kind(c->nt, id); if (ty_k != NK_BlockNode) continue; int body = nt_ref(c->nt, id, "body"); if (body < 0) continue; int n = 0; const int *st = nt_arr(c->nt, body, "body", &n); if (n <= 0) continue; TyKind lt = comp_ntype(c, st[n - 1]); if (ty_is_object(lt)) { int q = ty_object_class(lt); if (q >= 0 && q < c->nclasses) c->classes[q].is_value_type = 0; } } /* a bound C function's String argument reads a handle's live buffer, off the final slots */ mark_native_str_operands(c); } /* The final reconciliation and checks: hash literals and locals against their variables, operators on poly receivers, the rounds over the ivars' final types, lifted proc captures, array-or-nil slots, late poly arrays, then the seed contradictions and the late refusals (analyze_program's steps, in their order) */ static void an_phase_reconcile_check(Compiler *c) { /* Reconcile a hash literal's node type with the variable it initializes. A literal like `{ begin: ... }` infers a narrow variant (SYM_POLY_HASH) from its own keys, but the variable may later be promoted to a wider hash (e.g. POLY_POLY_HASH from a mixed-key `dic[sym_or_int] = ...`). Codegen emits the literal from its own node type, so without this the constructor (sp_SymPolyHash_new) disagrees with the variable's declared C type (sp_PolyPolyHash *) -- an incompatible-pointer assignment that corrupts the hash at runtime. Align the literal to the variable's type so the right constructor and key/value boxing are emitted. Widen only (the variable type is the union over all writes, so it is never narrower). */ for (int id = 0; id < c->nt->count; id++) { const char *ty = nt_type(c->nt, id); NodeKind ty_k = nt_kind(c->nt, id); if (!ty) continue; int is_lv = ty_k == NK_LocalVariableWriteNode || ty_k == NK_LocalVariableOrWriteNode || ty_k == NK_LocalVariableAndWriteNode || ty_k == NK_LocalVariableOperatorWriteNode; int is_iv = ty_k == NK_InstanceVariableWriteNode || ty_k == NK_InstanceVariableOrWriteNode || ty_k == NK_InstanceVariableAndWriteNode || ty_k == NK_InstanceVariableOperatorWriteNode; if (!is_lv && !is_iv) continue; int v = nt_ref(c->nt, id, "value"); if (v < 0) continue; const char *vty = nt_type(c->nt, v); /* `x ||= expr || {}`: the literal hides one level down in an or/and arm; align that arm instead (doom's `@gfx_weapons ||= ...weapons || {}`, whose {} otherwise defaulted to StrPolyHash against a Sym-keyed slot). */ if (vty && (sp_streq(vty, "OrNode") || sp_streq(vty, "AndNode"))) { int alt = nt_ref(c->nt, v, "right"); const char *aty = alt >= 0 ? nt_type(c->nt, alt) : NULL; NodeKind ak = nt_kind(c->nt, alt); if (ak == NK_HashNode || ak == NK_KeywordHashNode) { v = alt; vty = aty; } } if (!vty || (!sp_streq(vty, "HashNode") && !sp_streq(vty, "KeywordHashNode"))) continue; TyKind litt = c->ntype[v]; /* an EMPTY literal often has no inferred type at all (TY_UNKNOWN) -- `@near_linedefs ||= {}` -- and can adopt any variant, so let it through; the widen-only guard below still applies. */ int lit_n = 0; nt_arr(c->nt, v, "elements", &lit_n); if (!ty_is_hash(litt) && !(lit_n == 0 && (litt == TY_UNKNOWN || litt == TY_POLY))) continue; const char *nm = nt_str(c->nt, id, "name"); if (!nm) continue; TyKind dstt = TY_UNKNOWN; if (is_lv) { Scope *s = comp_scope_of(c, id); LocalVar *lv = s ? scope_local(s, nm) : NULL; if (lv) dstt = lv->type; } else { Scope *s = comp_scope_of(c, id); if (s && s->class_id >= 0 && s->class_id < c->nclasses) { int iv = comp_ivar_index(&c->classes[s->class_id], nm); if (iv >= 0) dstt = c->classes[s->class_id].ivar_types[iv]; } } /* Only widen toward POLY_POLY_HASH, the universally-boxed variant whose constructor accepts any keys/values -- a safe target for any narrower literal. Other cross-variant widenings are left alone. */ if (dstt == TY_POLY_POLY_HASH && litt != TY_POLY_POLY_HASH) comp_set_ntype(c, v, dstt); /* an empty literal adopts ANY destination hash variant (nothing to re-box), fixing e.g. a Sym-keyed slot initialized with `... || {}` */ else if (lit_n == 0 && ty_is_hash(dstt) && litt != dstt) comp_set_ntype(c, v, dstt); /* an Integer-valued literal into the same keys' boxed-value variant: its values box as they go in. The slot widens so after the fixpoint in promote mode, where another write's value answers its Integers boxed (`@h = { "a" => 1 }` beside `@h = { "z" => junk }`). */ else if (ty_is_hash(litt) && ty_is_hash(dstt) && ty_hash_val(litt) == TY_INT && ty_hash_val(dstt) == TY_POLY && ty_hash_key(litt) == ty_hash_key(dstt)) comp_set_ntype(c, v, dstt); } int widened_a_read = 0; /* Final safety pass: a hash-typed LOCAL assigned from a plain TY_POLY value, or from a reader whose backing ivar finished on a different type, cannot assume its variant -- codegen would cast the boxed pointer to the inferred variant and index a different struct layout (doom's `opts = @menu.options`: a PolyPolyHash_get over the actual SymPolyHash object segfaulted in respawn_player). Runs LAST so it sees final ivar types; widens the local to TY_POLY and repoints the node-type cache so subscripts go through the runtime dispatch. Rational and Complex need the same reconciliation: they are by-value structs, so a local that kept the value type while the slot finished poly is a C type error rather than a bad cast, and the program does not build at all (#3500). */ for (int id = 0; id < c->nt->count; id++) { NodeKind nk = nt_kind(c->nt, id); if (nk != NK_LocalVariableWriteNode) continue; const char *nm = nt_str(c->nt, id, "name"); Scope *lsc = comp_scope_of(c, id); LocalVar *lv = nm && lsc ? scope_local(lsc, nm) : NULL; int val_struct = (lv && (lv->type == TY_RATIONAL || lv->type == TY_COMPLEX)); if (!lv || (!ty_is_hash(lv->type) && !val_struct)) continue; int v = nt_ref(c->nt, id, "value"); if (v < 0) continue; NodeKind vty2_k = nt_kind(c->nt, v); if (vty2_k == NK_HashNode || vty2_k == NK_KeywordHashNode) continue; int widen2 = (comp_ntype(c, v) == TY_POLY); if (!widen2 && vty2_k == NK_CallNode) { int vrecv = nt_ref(c->nt, v, "receiver"); int vargs = nt_ref(c->nt, v, "arguments"); int vac = 0; if (vargs >= 0) nt_arr(c->nt, vargs, "arguments", &vac); const char *vnm = nt_str(c->nt, v, "name"); if (vrecv >= 0 && vac == 0 && vnm && nt_ref(c->nt, v, "block") < 0) { TyKind rt3 = comp_ntype(c, vrecv); for (int ci3 = 0; ci3 < c->nclasses && !widen2; ci3++) { if (ty_is_object(rt3) && ty_object_class(rt3) != ci3) continue; if (!ty_is_object(rt3) && rt3 != TY_POLY) break; if (!ty_is_object(rt3) && !c->classes[ci3].instantiated) continue; int pdc3 = -1; if (!comp_reader_in_chain(c, ci3, vnm, &pdc3)) continue; char ivn3[300]; snprintf(ivn3, sizeof ivn3, "@%s", comp_resolve_alias(c, pdc3, vnm)); int ix3 = comp_ivar_index(&c->classes[pdc3], ivn3); if (ix3 < 0) continue; TyKind it3 = c->classes[pdc3].ivar_types[ix3]; /* an instance of a Hash subclass starts with its Hash by value: the local's variant is the one the chain embeds, no other layout */ int emb3 = ty_is_object(it3) && comp_ary_is_hash(c, ty_object_class(it3)) && comp_ary_kind(c, ty_object_class(it3)) == lv->type; if (it3 != lv->type && !emb3) widen2 = 1; } } } if (widen2) { lv_set_type(lv, TY_POLY); /* repoint the cache for this local's read/write nodes in this scope */ for (int nid2 = 0; nid2 < c->nt->count; nid2++) { const char *t4 = nt_type(c->nt, nid2); NodeKind t4_k = nt_kind(c->nt, nid2); if (!t4) continue; if (t4_k != NK_LocalVariableReadNode && t4_k != NK_LocalVariableWriteNode && t4_k != NK_LocalVariableTargetNode && t4_k != NK_LocalVariableOrWriteNode) continue; const char *n4 = nt_str(c->nt, nid2, "name"); if (!n4 || !sp_streq(n4, nm)) continue; if (comp_scope_of(c, nid2) != lsc) continue; if (ty_is_hash(c->ntype[nid2]) || c->ntype[nid2] == TY_RATIONAL || c->ntype[nid2] == TY_COMPLEX) comp_set_ntype(c, nid2, TY_POLY); widened_a_read = 1; } } } /* The reads are repointed above, but what CONSUMED them still carries the type it was inferred with. An arithmetic operator on a poly receiver lowers to sp_poly_, whose value is boxed, so a node still typed Rational or Complex from before the widening met the emission at its consumer and the C did not compile. Bring those in line with the rule inference already states for a poly receiver. Iterated: a chain widens one link at a time. */ if (widened_a_read) { for (int iter = 0; iter < 16; iter++) { int ch2 = 0; NT_FOREACH_KIND(c->nt, NK_CallNode, id) { const char *nm = nt_str(c->nt, id, "name"); if (!nm || !is_arith_op(nm)) continue; int r2 = nt_ref(c->nt, id, "receiver"); int a2 = nt_ref(c->nt, id, "arguments"); int an2 = 0; if (a2 >= 0) nt_arr(c->nt, a2, "arguments", &an2); if (r2 < 0 || an2 != 1) continue; if (c->ntype[r2] != TY_POLY || c->ntype[id] == TY_POLY) continue; if (c->ntype[id] == TY_UNKNOWN || c->ntype[id] == TY_VOID) continue; comp_set_ntype(c, id, TY_POLY); ch2 = 1; } if (!ch2) break; } } /* A self-returning Array mutator answers its receiver, so a local capturing one has the receiver's type -- including a widening the receiver took after this local was first typed. `a = [1, 2]; c = a.push(:x)` widens a to a poly array and left c an int array, so the capture read the same object at the wrong layout and printed raw memory. Same shape as the ivar reader below, and the same fix: run last, when the receiver's type is final. */ { /* Only the mutators that ALWAYS answer the receiver. select!/reject!/ uniq!/compact!/flatten! answer nil when nothing changed, so their capture is nilable and does not take the receiver's type. */ static const char *self_ret_mut[] = { "push", "unshift", "<<", "concat", "insert", "append", "prepend", "clear", "replace", "fill", "sort!", "reverse!", "shuffle!", "rotate!", "map!", "collect!", "keep_if", "delete_if", NULL }; NT_FOREACH_KIND(c->nt, NK_LocalVariableWriteNode, id) { int v = nt_ref(c->nt, id, "value"); if (v >= 0) v = nt_paren_single(c->nt, v); /* `c = (a << x)` */ if (v < 0 || nt_kind(c->nt, v) != NK_CallNode) continue; const char *cn = nt_str(c->nt, v, "name"); if (!cn) continue; int hit = 0; for (int k = 0; self_ret_mut[k]; k++) if (sp_streq(cn, self_ret_mut[k])) { hit = 1; break; } if (!hit) continue; int r = nt_ref(c->nt, v, "receiver"); if (r < 0 || nt_kind(c->nt, r) != NK_LocalVariableReadNode) continue; Scope *sc2 = comp_scope_of(c, id); const char *rn = nt_str(c->nt, r, "name"); const char *wn = nt_str(c->nt, id, "name"); if (!sc2 || !rn || !wn) continue; LocalVar *rv = scope_local(sc2, rn); LocalVar *wv = scope_local(sc2, wn); if (!rv || !wv || rv->type == wv->type) continue; if (!ty_is_array(rv->type) && rv->type != TY_POLY) continue; /* a local already unified to poly holds other kinds too (`r = x.pop; r = x.concat([3])`); it stays poly */ if (!ty_is_array(wv->type)) continue; lv_set_type(wv, rv->type); comp_set_ntype(c, v, rv->type); NT_FOREACH_KIND(c->nt, NK_LocalVariableReadNode, rid) { const char *n2 = nt_str(c->nt, rid, "name"); if (!n2 || !sp_streq(n2, wn) || comp_scope_of(c, rid) != sc2) continue; comp_set_ntype(c, rid, rv->type); } } } /* A method whose value is an instance variable read has to answer that slot's FINAL type. The return fixpoint settles before the ivar types do, so a slot seeded `@a = []` and later widened by the explicit `@a = @a + [x]` form leaves its reader pinned to the seed's layout, and codegen then casts the boxed slot to it -- `(sp_IntArray *)(self->iv_a).v.p` reads an sp_RbVal at the wrong layout and every element comes back as its raw bits (#3514). Only widening toward POLY, so a narrowing the passes above established is never undone. */ /* Rounds, because the widening propagates: a method whose value IS one of these calls answers the widened type too, and so does its own caller. One pass left `def m = self.class.key` declared sp_sym while the dispatch it returns had become sp_RbVal, which does not compile (#4053). */ char *wnode = calloc((size_t)c->nt->count, 1); for (int wround = 0; wround < 8; wround++) { int wdid = 0; for (int s = 0; s < c->nscopes; s++) { Scope *sc = &c->scopes[s]; if (sc->ret == TY_POLY || sc->ret == TY_UNKNOWN || sc->ret == TY_VOID) continue; if (sc->class_id < 0 || sc->class_id >= c->nclasses || sc->body < 0) continue; int bn = 0; const int *bb = nt_arr(c->nt, sc->body, "body", &bn); if (!bb || bn < 1) continue; int tail = bb[bn - 1]; const char *tt = nt_type(c->nt, tail); NodeKind tk = nt_kind(c->nt, tail); if (!tt) continue; if (tk == NK_InstanceVariableReadNode) { const char *ivn = nt_str(c->nt, tail, "name"); int iv = ivn ? comp_ivar_index(&c->classes[sc->class_id], ivn) : -1; if (iv < 0 || c->classes[sc->class_id].ivar_types[iv] != TY_POLY) continue; } /* the value is a call THIS pass widened: the method answers it. Only a call this pass touched -- a tail call that was already poly says nothing about a return the passes above deliberately narrowed, and an --rbs seed pinned the return on purpose. */ else if (!(wnode && tk == NK_CallNode && wnode[tail])) continue; if (sc->ret_rbs_seeded) continue; scope_set_ret(sc, TY_POLY); wdid = 1; comp_set_ntype(c, tail, TY_POLY); if (c->ntype[sc->body] != TY_UNKNOWN) comp_set_ntype(c, sc->body, TY_POLY); /* the callers were typed from the old return, and a call still carrying the container type hands the boxed value to a typed helper, which does not compile */ NT_FOREACH_KIND(c->nt, NK_CallNode, cid) { const char *cn = nt_str(c->nt, cid, "name"); if (!cn || !sc->name || !sp_streq(cn, sc->name)) continue; int recv = nt_ref(c->nt, cid, "receiver"); int args = nt_ref(c->nt, cid, "arguments"); int ac = 0; if (args >= 0) nt_arr(c->nt, args, "arguments", &ac); if (recv < 0 || ac != 0 || nt_ref(c->nt, cid, "block") >= 0) continue; TyKind rt = c->ntype[recv]; int hits = (rt == TY_POLY); /* the receiver's own chain resolving the name HERE, rather than the receiver being exactly this class: a subclass that inherits the method unchanged calls this very scope, and its call was left at the old type */ if (!hits && ty_is_object(rt)) hits = call_target(c, cid, ty_object_class(rt), cn, 0, NULL) == s; /* `Klass.m` / `obj.class.m`: a class-method call reaching this scope. */ if (!hits && rt == TY_CLASS && sc->is_cmethod) { int rci = -1; if (nt_kind(c->nt, recv) == NK_ConstantReadNode || nt_kind(c->nt, recv) == NK_ConstantPathNode) rci = const_node_class_index(c, recv); else if (nt_kind(c->nt, recv) == NK_CallNode && nt_str(c->nt, recv, "name") && sp_streq(nt_str(c->nt, recv, "name"), "class")) { int robj = nt_ref(c->nt, recv, "receiver"); TyKind rot = robj >= 0 ? c->ntype[robj] : TY_UNKNOWN; if (ty_is_object(rot)) rci = ty_object_class(rot); } if (rci >= 0) hits = call_target(c, cid, rci, cn, CT_CMETHOD, NULL) == s; } if (!hits) continue; if (c->ntype[cid] != TY_UNKNOWN && c->ntype[cid] != TY_VOID && c->ntype[cid] != TY_POLY) { comp_set_ntype(c, cid, TY_POLY); if (wnode) wnode[cid] = 1; } } } if (!wdid) break; } free(wnode); /* A proc form holds its block as a real parameter, so a nested block that yields and is itself lifted to a standalone proc has to capture it -- the same shape a lowered yield method has, and the same cell it needs. The YieldNode is not a local read, so the capture pass cannot see it; mark the parameter here and codegen's force adds it to the capture set. */ for (int s = 1; s < c->nscopes; s++) { Scope *d = &c->scopes[s]; if (!d->is_proc_form || !d->blk_param || !d->blk_param[0]) continue; for (int id = 0; id < c->nt->count; id++) { if (c->nscope[id] != s || !a_proc_create_or_lifted(c, id)) continue; int pb = a_proc_body(c, id); if (pb < 0 || !subtree_has_yield_node(c, pb, 0)) continue; LocalVar *bl = scope_local_intern(d, d->blk_param); if (bl) { bl->is_cell = 1; comp_touch_scope(bl->owner); } break; } } /* An --rbs seed the settled types statically contradict is a compile error, not something to emit a reinterpretation for. */ AN_STEP_V("mark_nullable_int_locals", mark_nullable_int_locals(c)); sync_captured_nullable(c); AN_STEP_V("widen_nullable_keyed_hash_literals", widen_nullable_keyed_hash_literals(c)); mark_array_or_nil_slots(c); /* A local's array KIND has to agree with what its writes actually build. A value whose type widens to a poly array LATE -- a map whose block value became boxed because a Proc composition widened the method it calls -- left the slot at the kind it had settled on, and the assignment did not compile (#4009). Widen the slot to match; the reverse never happens, since a poly array is the widest kind. */ for (int id = 0; id < c->nt->count; id++) { if (nt_kind(c->nt, id) != NK_LocalVariableWriteNode) continue; const char *wn = nt_str(c->nt, id, "name"); int v = nt_ref(c->nt, id, "value"); if (!wn || v < 0) continue; Scope *sc = comp_scope_of(c, id); LocalVar *lv = sc ? scope_local(sc, wn) : NULL; if (!lv || lv->is_param || lv->is_block_param || lv->rbs_seeded) continue; if (lv->oa_pin != TY_UNKNOWN) continue; /* a narrowed slot keeps its kind */ /* An OBJECT slot whose write is boxed: an attribute that holds a base class and its subclass reads as poly, and a slot that settled on one of the two kept the pointer type while the read handed back an sp_RbVal (#4023). */ if (ty_is_object(lv->type) && comp_ntype(c, v) == TY_POLY) { lv_set_type(lv, TY_POLY); NT_FOREACH_KIND(c->nt, NK_LocalVariableReadNode, rid2) { const char *n4 = nt_str(c->nt, rid2, "name"); if (!n4 || !sp_streq(n4, wn) || comp_scope_of(c, rid2) != sc) continue; comp_set_ntype(c, rid2, TY_POLY); } continue; } if (lv->type != TY_INT_ARRAY && lv->type != TY_FLOAT_ARRAY && lv->type != TY_STR_ARRAY) continue; if (comp_ntype(c, v) != TY_POLY_ARRAY) continue; /* only where the value is a BLOCK-collecting call: those build the array fresh, so nothing else holds it at the old kind */ if (nt_kind(c->nt, v) != NK_CallNode || nt_ref(c->nt, v, "block") < 0) continue; lv_set_type(lv, TY_POLY_ARRAY); NT_FOREACH_KIND(c->nt, NK_LocalVariableReadNode, rid) { const char *n3 = nt_str(c->nt, rid, "name"); if (!n3 || !sp_streq(n3, wn) || comp_scope_of(c, rid) != sc) continue; comp_set_ntype(c, rid, TY_POLY_ARRAY); } } AN_STEP_V("check_seed_contradictions", check_seed_contradictions(c)); /* A global the program only ever mentions holding nil -- `$log = nil` and nothing else, or a read of one never assigned at all -- settles at no type, and a global with no type gets no slot, so the reference does not compile. Ruby's answer for both is nil, which the boxed form holds. Done after the fixpoint so a global that any write gives a real type keeps it. */ { int promoted = 0; for (int i = 0; i < c->ngvars; i++) if (c->gvars[i].type == TY_UNKNOWN) { lv_set_type(&c->gvars[i], TY_POLY); promoted = 1; } /* Stamp the reads too: a node's type is cached from the round that typed it, and every round so far saw this global as having no type. */ if (promoted) { NodeTable *ntg = (NodeTable *)c->nt; for (int id = 0; id < ntg->count && id < c->node_cap; id++) { if (nt_kind(ntg, id) != NK_GlobalVariableReadNode) continue; if (c->ntype[id] != TY_UNKNOWN) continue; const char *nm = nt_str(ntg, id, "name"); const char *rn = nm ? comp_resolve_gvar(c, nm + 1) : NULL; LocalVar *g = rn ? comp_gvar(c, rn) : NULL; if (g && g->type == TY_POLY) comp_set_ntype(c, id, TY_POLY); } } } /* Hash#store is []= (desugar_enum_method_recv), but a receiver typed a hash only after the fixpoint -- a boxed parameter every caller of which the binding widened to one hash kind, retyped by the re-narrow -- kept the name, and a typed hash has no other `store` arm. */ NT_FOREACH_KIND(c->nt, NK_CallNode, sid) { const char *snm = nt_str(c->nt, sid, "name"); if (!snm || !sp_streq(snm, "store") || nt_ref(c->nt, sid, "block") >= 0) continue; int src = nt_ref(c->nt, sid, "receiver"), sa = nt_ref(c->nt, sid, "arguments"), sac = 0; if (sa >= 0) nt_arr(c->nt, sa, "arguments", &sac); if (src >= 0 && sac == 2 && ty_is_hash(comp_ntype(c, src)) && an_reopen_owner(c, sid, CORE_HASH) < 0) nt_node_set_str((NodeTable *)c->nt, sid, "name", "[]="); } mark_scan_rtchecks(c); AN_STEP_V("mark_struct_aset_targets", mark_struct_aset_targets(c)); AN_STEP_V("share_struct_aset_handles", share_struct_aset_handles(c)); /* Refuse lent ivar copies through calls and super only after sharing analysis settles (#6998). */ AN_STEP_V("refuse_set_identity_members", refuse_set_identity_members(c)); refuse_mixin_after_calls(c); refuse_reinit_mismatch(c); /* Last: the capture pass again, on the settled types. a_block_is_lifted asks whether the receiver is poly, and a receiver that widened after the earlier run answered no then and yes now -- so codegen routes the call to the cls_id dispatch (a real proc) while analyze left its captures uncelled, and the emit refuses. Only sets is_cell, and is idempotent. */ AN_STEP_V("mark_proc_captures", mark_proc_captures(c)); /* ...and a local it cells there may hold a value-type instance the detection above let through: a receiver typed poly only by the late widenings (a parameter every caller passes nil) lifts the block now. Nothing between there and here reads is_value_type, so clearing it is still in time for the emit. */ an_heap_captured_classes(c); /* a read-only parameter takes a shared handle's live buffer, read off the final types and flags (#7482) */ mark_param_read_only_operands(c); /* A capped run emits from whatever the last round left, which need not be a fixpoint; that is a compiler bug worth hearing about, not a quiet log. */ if (g_fixpoint_capped) fprintf(stderr, "spinel: warning: type inference did not converge in %d rounds; " "the output may be built from unsettled types (please report this program)\n", g_fixpoint_rounds); } /* an analysis phase under --timing: `phase=an_`; a step named `an_` around the passes of its list that are no step of their own */ #define AN_PHASE(nm, call) do { double _ph0 = sp_timing_now(); infer_memo_boundary(); AN_STEP_V("an_" nm, call); \ infer_memo_boundary(); sp_timing_end(_ph0, "an_" nm, ""); } while (0) /* Does a node hold a Hash of another shape than at the snapshots: one the marks, or promote's widening before them, made poly-valued? */ static int an_hash_reshaped(Compiler *c, const NodeTypes *settled, const NodeTypes *calls) { for (int id = 0; id < c->nt->count; id++) { TyKind now = infer_type(c, id); if (!ty_is_hash(now)) continue; const NodeTypes *at[2] = { settled, calls }; for (int k = 0; k < 2; k++) { TyKind was = id < at[k]->n ? at[k]->ty[id] : TY_UNKNOWN; if (now != was && (ty_is_hash(was) || was == TY_UNKNOWN)) return 1; } } return 0; } /* The nullable marks settle after every slot has (the reconcile check reads them again): a Hash built from a nullable number turns poly-valued only here (infer_type), as one built from the Integers promote widens does in the late widening. What holds it follows -- the returns, locals, parameters and block parameters it reaches, and the instance, class and global variables and constants it is stored in. A call whose inputs have widened since promote's widening (or the marks) binds again, and a store whose input has widened since the marks joins again; the late widening's other changes it leaves as they settled. Both ask against a fixed snapshot, so a widening a later pass of the round finds still reaches a call or a store an earlier one visited. A program where no Hash changed shape skips the rounds: its slots keep what the late widening settled. */ static void an_phase_nullable_returns(Compiler *c, const NodeTypes *promoted) { NodeTypes settled; node_types_take(c, &settled); const NodeTypes *calls = promoted->ty ? promoted : &settled; AN_STEP_V("mark_nullable_int_locals", mark_nullable_int_locals(c)); sync_captured_nullable(c); infer_memo_boundary(); if (!an_hash_reshaped(c, &settled, calls)) { free(settled.ty); return; } g_ret_no_new_poly = 4; AnRoundCap rc = { 128, 0, 0, NULL, 0 }; for (int iter = 0; iter < rc.cap; iter++) { int ch = infer_return_types(c); ch |= rejoin_local_writes(c); ch |= rejoin_changed_masgn(c, &settled); ch |= rejoin_changed_pattern_locals(c, &settled); ch |= infer_changed_param_types(c, calls); ch |= infer_block_params(c); if (infer_changed_store_types(c, &settled)) { ch = 1; /* and a class hierarchy's copies of a widened ivar with it, so the layouts stay cast-compatible (an_phase_late_widen) */ for (int k = 0; k < 16; k++) if (!(infer_inherited_ivars(c) | propagate_ivars_up(c))) break; } if (!ch) break; an_round_cap_step(c, &rc, iter); } free(settled.ty); free(rc.seen); g_ret_no_new_poly = 0; } /* the node table's size when the analysis began: the calls the program wrote are below it, the ones a rewrite made above (an_written_call) */ static int g_an_src_nodes = -1; int an_written_call(int id) { return g_an_src_nodes < 0 || id < g_an_src_nodes; } /* An Enumerable call whose receiver only the post-fixpoint backstops typed (`out = []` with no typed push, defaulted to an Array there) was never rewritten onto its builtins/enumerable.rb copy: the rewrite runs inside the fixpoint, which saw the receiver untyped. It is rewritten now. The copy existed from the start (desugar_builtins makes it before the scopes, and the fixpoint's rewrites went over its body), so what it lacks is types: its parameters from the call, its locals, its block's parameters and its answer, and what reads that answer. The passes that give them run until none changes, with the rewrite among them for a call the copy's own body makes. Types only widen, so the rounds end; a slot the re-narrow would have narrowed stays wider, which costs speed, never an answer. A program with no such call does none of this. */ int g_enum_late; /* desugar_builtin_enum_calls runs for an_phase_enum_late */ static void an_phase_enum_late(Compiler *c) { g_enum_late = 1; int any = AN_PASS("desugar_builtin_enum_calls_late", desugar_builtin_enum_calls(c)); if (!any) { g_enum_late = 0; return; } AN_PASS_V("mark_empty_literal_args", mark_empty_literal_args(c)); /* The types widen until they settle, and then the rewrite looks again for a call they settled for: the loop ends only where both found nothing, never right after a rewrite, so every copy the rewrite calls is typed. Each call is rewritten once, so the rewrites end; the widening rounds are capped as the fixpoint's are (an_round_cap_step), which tells an oscillation from a long chain. */ AnRoundCap rc = { 128, 0, 0, NULL, 0 }; for (int iter = 0;; iter++) { int ch = 0; infer_memo_boundary(); ch |= infer_param_types(c); infer_memo_boundary(); ch |= infer_default_param_types(c); infer_memo_boundary(); ch |= infer_write_types(c); infer_memo_boundary(); ch |= infer_block_params(c); infer_memo_boundary(); ch |= infer_ivar_types(c); infer_memo_boundary(); ch |= infer_return_types(c); infer_memo_boundary(); ch |= infer_global_const_types(c); infer_memo_boundary(); if (ch) { an_round_cap_step(c, &rc, iter); if (iter + 1 < rc.cap) continue; break; } if (!AN_STEP("desugar_builtin_enum_calls", desugar_builtin_enum_calls(c))) break; AN_STEP_V("mark_empty_literal_args", mark_empty_literal_args(c)); } free(rc.seen); infer_memo_boundary(); g_enum_late = 0; } /* A String mutator's receiver that a program method answers (a reader's String, `c.name << "x"`) is mutated through the handle the call's view carries: the String the method's slot holds, not a copy of it. */ static void an_phase_mutated_answers(Compiler *c) { const NodeTable *nt = c->nt; NT_FOREACH_KIND(nt, NK_CallNode, u) { const char *nm = nt_str(nt, u, "name"); int r = nt_ref(nt, u, "receiver"); if (!nm || r < 0 || !(sp_str_mutator(nm, 0) || bop_name_mutates(nm, BOP_MUT_STATE))) continue; r = nt_paren_single(nt, r); if (nt_kind(nt, r) != NK_CallNode || infer_type(c, r) != TY_STRING) continue; /* (one that may answer nil keeps the view's path, which raises the NoMethodError a nil receiver gets) */ if (nil_fact_node(c, r) == NF_MAY_NIL) continue; /* (only a method of the program's: a reader hands out its slot's String; a builtin's answer is a String of its own) */ char ivb[300]; int defc = -1; int tg[CPT_MAX]; if (!call_reaches_program_method(c, r) && cplan_targets(c, r, tg, CPT_MAX) <= 0 && !an_reader_ivar_of(c, r, &defc, ivb, sizeof ivb)) continue; c->strbuf_handle_demand[r] = 1; } } void analyze_program(Compiler *c) { double tm_an = sp_timing_now(); g_an_src_nodes = c->nt->count; an_note_written_names(c); elab_note_const_writes(c); /* before any pass drops or rewrites one (lr_const_seen) */ AN_PHASE("desugar_register", an_phase_desugar_register(c)); AN_PHASE("class_structure", an_phase_class_structure(c)); if (c->n_cext_extensions) { AN_STEP_V("register_cext_layouts", register_cext_layouts(c)); inherit_cext_layouts(c); } AN_PHASE("block_inline", an_phase_block_inline(c)); AN_PHASE("pre_fixpoint", an_phase_pre_fixpoint(c)); /* the classes a value can hold, decided once from the program's shape (#8530): the fixpoint's binding of a boxed receiver's call reads it */ AN_PHASE("cls_facts", an_cls_facts(c)); double tm_fp = sp_timing_now(); AN_STEP_V("an_phase_infer_fixpoint", an_phase_infer_fixpoint(c)); { char ex[64]; snprintf(ex, sizeof ex, " rounds=%d%s", g_fixpoint_rounds, g_fixpoint_capped ? " capped=1" : ""); sp_timing_end(tm_fp, "analysis_fixpoint", ex); } an_pass_report(); an_shadow_report(); AN_PHASE("post_fixpoint", an_phase_post_fixpoint(c)); AN_PHASE("enum_late", an_phase_enum_late(c)); AN_PHASE("procs", an_phase_procs(c)); AN_PHASE("method_backstops", an_phase_method_backstops(c)); NodeTypes promoted = { NULL, 0 }; AN_PHASE("late_widen", an_phase_late_widen(c, &promoted)); AN_PHASE("nullable_returns", an_phase_nullable_returns(c, &promoted)); free(promoted.ty); /* a receiver the backstops or the late widening turned poly now lifts a block whose `yield` the procs pass judged spliced; lower its method as that pass would */ AN_PHASE("procs_settled", an_lower_yielders(c)); AN_PHASE("proc_returns", an_phase_proc_returns(c)); AN_PHASE("storage", an_phase_storage(c)); AN_PHASE("value_types", an_phase_value_types(c)); AN_PHASE("mutated_answers", an_phase_mutated_answers(c)); AN_PHASE("reconcile_check", an_phase_reconcile_check(c)); /* once more on the settled types, which tell a builtin receiver's call from a program method's (cr_recv_builtin_value) */ AN_STEP_V("compute_reachable", compute_reachable(c, 2)); /* The census again, on what this pass reached: it opens the instances of a class reached code is typed with (an object built only where nothing runs), and a construction in one of its methods, now reached, builds its class too. Such a class gets its constructor (ClassInfo.ctor_late) and nothing else: the dispatch's arms and the types were settled on the census above, and a class added to them now gave a call typed Integer an arm of another class's String reader (#10649). Without it a reached `SSLSocket#initialize` called `SSLContext.new`, whose constructor the C left out (#10643). */ { int nk = c->nclasses; unsigned char *was = malloc((size_t)(nk ? nk : 1) * 3); for (int k = 0; k < nk; k++) { was[3 * k] = (unsigned char)c->classes[k].instantiated; was[3 * k + 1] = (unsigned char)c->classes[k].ctor_site; was[3 * k + 2] = (unsigned char)c->classes[k].ctor_reachable; } compute_instantiated(c, 0); for (int k = 0; k < nk; k++) { c->classes[k].ctor_late = c->classes[k].instantiated && !was[3 * k]; c->classes[k].instantiated = was[3 * k]; c->classes[k].ctor_site = was[3 * k + 1]; c->classes[k].ctor_reachable = was[3 * k + 2]; } free(was); } if (g_dump_implicit) { ic_dump(c, stdout); exit(0); } /* the methods CRuby calls that the program does not name, and the defs kept */ if (getenv("SP_FIXPOINT_LOG")) fprintf(stderr, "[fp] rounds=%d%s\n", g_fixpoint_rounds, g_fixpoint_capped ? " (CAP -- did not converge)" : ""); /* --share-strings: a String-keyed Hash's key borrows a handle's bytes, read off the final types and flags */ share_mark_borrows(c); /* --share-strings: the methods whose value is a shared String's handle */ AN_STEP_V("an_mark_handle_returns", an_mark_handle_returns(c)); /* the String methods whose value no call reads, once the handle flags are */ an_mark_unread_returns(c); /* Identity operands can ask the settled call-return fact now. */ AN_STEP_V("mark_reader_identity_operands", mark_reader_identity_operands(c)); /* the representation flags are final from here (repr.h) */ /* The final share query sees every caller without rebuilding per receiver. */ HandleArgTab hat; { handle_arg_tab_init(c, &hat); c->share_args = &hat; } repr_seal(c); { c->share_args = NULL; handle_arg_tab_free(&hat); } /* the block bodies codegen types per arm, typed here once (#7483) */ AN_PHASE("arm_facts", an_arm_facts(c)); if (elab_check_on(c)) elab_check_visibility(c); sp_timing_end(tm_an, "analysis", ""); } /* Is `id` (a CallNode) `x.instance_method(arg).parameters` with a receiver or a name only the run time knows? Then the parameters come from sp_refl_instance_method_parameters (Thor checks a command's arity this way: `klass.instance_method(name.to_sym).parameters`). */ int refl_im_params_call(Compiler *c, int id) { const NodeTable *nt = c->nt; if (nt_kind(nt, id) != NK_CallNode || !nt_str(nt, id, "name") || !sp_streq(nt_str(nt, id, "name"), "parameters")) return 0; if (nt_ref(nt, id, "arguments") >= 0) return 0; int im = nt_ref(nt, id, "receiver"); if (im < 0 || nt_kind(nt, im) != NK_CallNode || !nt_str(nt, im, "name") || !sp_streq(nt_str(nt, im, "name"), "instance_method")) return 0; int args = nt_ref(nt, im, "arguments"); int an = 0; const int *av = args >= 0 ? nt_arr(nt, args, "arguments", &an) : NULL; if (an != 1 || !av) return 0; NodeKind ak = nt_kind(nt, av[0]); int recv = nt_ref(nt, im, "receiver"); NodeKind rk = nt_kind(nt, recv); int lit_name = (ak == NK_SymbolNode || ak == NK_StringNode); int lit_recv = (rk == NK_ConstantReadNode || rk == NK_ConstantPathNode); return !(lit_name && lit_recv); }