# emit_escape.ludic — 25.3: which allocations never outlive the frame that made them. Every value a # function handles is in a class (flow-insensitive: a local, a parameter, a function's result, an # allocating construct), joined by two kinds of edge: # flow a -> b a's value can become b's (a let, an assignment, an argument into its parameter, # a returned value into the function's result, that result into the call's value) # store v -> t v is written into t's contents (a field, an element, a push) # HEAP: the class may alias memory that outlives a frame (a parameter, a state, a global, a value # an unknown call hands back); it flows forward. ESC: the class's values may be kept - stored into # something HEAP, into a global, handed to an unknown callee; it flows backward, along a flow edge # from the escaping class and along a store edge from an escaping or HEAP target. A load is its # base's class (field-insensitive), so what a record holds escapes with it. # An allocating construct that ends up neither ESC nor in a function that spans frames (one that # reaches Mem.frame) is LOCAL: uns = ES_SCRATCH marks its node, and it allocates from the current # scratch (the frame's arena, emit_fence_ir). A text built by + copies its pieces, so they stay apart. const ES_SCRATCH: int = 77 # Node.uns on an allocating expression: scratch const ES_ESC: int = 1 const ES_HEAP: int = 2 const ES_FREED: int = 4 # given back with free(): not a leak at birth var g_es_flag: []int = new []int var g_es_why: []Node = new []Node # per class: the statement that made it escape var g_es_own: []pointer = new []pointer # per class: the state that holds it (25.5c's census) var g_es_gname: []pointer = new []pointer # a class per state global, so each state is its own var g_es_gcls: []int = new []int var g_es_ok: []pointer = new []pointer # file:line of a kept site -> the state that holds it var g_es_ov: []Node = new []Node var g_es_swhy: []Node = new []Node # per store edge: its statement var g_es_stmt: Node = null # the statement being walked var g_es_aok: int = 0 # inside a statement under @alloc_ok var g_es_site_aok: []bool = new []bool # the site is (not reported by the region rule) var g_es_site_grow: []bool = new []bool # the site is a push's growth (25.5's capacities, not the rule) var g_es_done: bool = false var g_es_fnrefs: []pointer = new []pointer # functions taken as values (fn f): called from anywhere var g_es_consumed: []Node = new []Node # text used up where it is made (a + or == operand): freed at once var g_es_ft: []int = new []int # flow edges var g_es_ff: []int = new []int var g_es_st: []int = new []int # store edges: value, target var g_es_sv: []int = new []int var g_es_site: []Node = new []Node # allocating constructs and their classes var g_es_site_cls: []int = new []int var g_es_site_fn: []int = new []int # the function each site is in var g_es_fk: []pointer = new []pointer # function records by name (ck_tab) var g_es_fv: []Node = new []Node var g_es_fparam: []int = new []int # per function: its first parameter's class var g_es_fret: []int = new []int # its result's class var g_es_fnode: []Node = new []Node var g_es_fspans: []bool = new []bool # it reaches Mem.frame: its sites stay on the heap var g_es_calls_f: []int = new []int # direct calls, caller -> callee (function indexes) var g_es_calls_t: []int = new []int var g_es_ln: []pointer = new []pointer # the current function's locals and their classes var g_es_lc: []int = new []int var g_es_lt: []pointer = new []pointer # ... and their types, where known var g_es_cur: int = -1 var g_es_heap: int = -1 # the one class for globals, states and the unknown function es_new(flags: int) -> int { push(g_es_flag, flags); push(g_es_why, null); push(g_es_own, null); return len(g_es_flag) - 1 } function es_flow(a: int, b: int) -> void { if a >= 0 and b >= 0 { push(g_es_ff, a); push(g_es_ft, b) } } function es_store(v: int, t: int) -> void { if v < 0 { return } if t < 0 { es_escape(v); return } push(g_es_sv, v) push(g_es_st, t) push(g_es_swhy, g_es_stmt) } function es_escape(v: int) -> void { if v < 0 { return } g_es_flag[v] = g_es_flag[v] | ES_ESC if g_es_why[v] == null { g_es_why[v] = g_es_stmt } } function es_local(name: pointer) -> int { var i = len(g_es_ln) - 1 while i >= 0 { if (g_es_ln[i] == name) { return g_es_lc[i] }; i -= 1 } return -1 } function es_bind_local(name: pointer) -> int { let c = es_local(name) if c >= 0 { return c } let n = es_new(0) push(g_es_ln, name) push(g_es_lc, n) push(g_es_lt, null) return n } # a value of one of these types holds no reference: it takes part in no flow and no store, so a # number copied out of a frame's list into a state's does not make the list kept function es_prim(t: pointer) -> bool { if t == null { return false } return (t == "int") or (t == "float") or (t == "long") or (t == "double") or (t == "fixed") or (t == "bool") or (t == "byte") } function es_elem_of(t: pointer) -> pointer { if t == null { return null } if (t == "words") { return "int" } if (t == "floats") { return "float" } if (t == "doubles") { return "double" } if (t == "fixeds") { return "fixed" } if len(t) > 2 and t[0] == '[' and t[1] == ']' { return t[2..len(t)] } return null } function es_local_ty(name: pointer) -> pointer { var i = len(g_es_ln) - 1 while i >= 0 { if (g_es_ln[i] == name) { return g_es_lt[i] }; i -= 1 } return null } # the static type of an expression, as far as the analysis can tell without emitting it function es_type(e: Node) -> pointer { if e == null { return null } let k = e.kind if k == E_INT { return "int" } if k == E_FLOAT { return "float" } if k == E_BOOL { return "bool" } if k == E_STR or k == E_SLICE { return "string" } if k == E_ID { if es_local(e.s) >= 0 { return es_local_ty(e.s) } let g = find_global(e.s) if g != null { return g.ty } return null } if k == E_MEMBER { let bt = es_type(e.a) if bt != null { let c = find_comp(bt) if c != null { return field_type(c, e.s) } } return null } if k == E_INDEX { return es_elem_of(es_type(e.a)) } if k == E_NEW { return e.s } if k == E_BIN { let o = e.s if (o == "==") or (o == "!=") or (o == "<") or (o == ">") or (o == "<=") or (o == ">=") or (o == "and") or (o == "or") { return "bool" } if (o == "+") and fr_is_textish(e) { return "string" } let at = es_type(e.a) if es_prim(at) { return at } return null } if k == E_CALL and e.a != null and e.a.kind == E_ID { let nm = e.a.s if (nm == "len") { return "int" } if (nm == "string") { return "string" } if (nm == "int") or (nm == "float") or (nm == "long") or (nm == "double") or (nm == "fixed") { return nm } if (nm == "words") or (nm == "floats") or (nm == "doubles") or (nm == "fixeds") { return nm } let fi = es_fn(nm) if fi >= 0 { return g_es_fnode[fi].ty } } return null } # LOCAL: never kept (ESC); and for a push's growth, whose buffer is its list's, the list may not be # one that outlives the frame (HEAP) either. A fresh value that flows into a local also holding kept # memory is still the frame's - storing it anywhere kept would have made it ESC function es_local_class(c: int, grow: bool) -> bool { if (g_es_flag[c] & ES_ESC) != 0 { return false } if grow and (g_es_flag[c] & ES_HEAP) != 0 { return false } return true } # a value's class, or -1 when its type holds no reference function es_ref(e: Node) -> int { let v = es_val(e) if es_prim(es_type(e)) { return -1 } return v } function es_fn(name: pointer) -> int { let n = ck_tab_get(g_es_fk, g_es_fv, name) if n == null { return -1 } return n.ival } function es_site(n: Node) -> int { let c = es_new(0) push(g_es_site, n) push(g_es_site_cls, c) push(g_es_site_fn, g_es_cur) push(g_es_site_aok, g_es_aok > 0) push(g_es_site_grow, false) return c } function es_is_mem_frame(n: Node) -> bool { return n.kind == E_CALL and n.a != null and n.a.kind == E_MEMBER and n.a.a != null and n.a.a.kind == E_ID and (n.a.a.s == "Mem") and (n.a.s == "frame") } # the class of an expression's value (-1: not a reference worth following), walking its parts function es_val(e: Node) -> int { if e == null { return -1 } let k = e.kind if k == E_FNREF { # a function handed on as a value: whoever calls it keeps what it returns if e.s != null { push(g_es_fnrefs, e.s) } return -1 } if k == E_STR or k == E_INT or k == E_BOOL or k == E_FLOAT or k == E_NULL { return -1 } if k == E_ID { let c = es_local(e.s) if c >= 0 { return c } let g = find_global(e.s) if g != null and is_state_ty(g.ty) { var i = 0 while i < len(g_es_gname) { if (g_es_gname[i] == e.s) { return g_es_gcls[i] }; i += 1 } let sc = es_new(ES_ESC | ES_HEAP) g_es_own[sc] = g.ty push(g_es_gname, e.s) push(g_es_gcls, sc) return sc } return g_es_heap } if k == E_MEMBER { return es_val(e.a) } if k == E_INDEX { es_val(e.b) return es_val(e.a) } if k == E_SLICE { es_val(e.a); es_val(e.b); es_val(e.c) return es_site(e) } if k == E_BIN { es_val(e.a) es_val(e.b) if (e.s == "+") or (e.s == "==") or (e.s == "!=") { if e.a != null and (e.a.kind == E_BIN or e.a.kind == E_CALL or e.a.kind == E_SLICE) { push(g_es_consumed, e.a) } if e.b != null and (e.b.kind == E_BIN or e.b.kind == E_CALL or e.b.kind == E_SLICE) { push(g_es_consumed, e.b) } } if (e.s == "+") and fr_is_textish(e) { return es_site(e) } return -1 } if k == E_UN { es_val(e.a); return -1 } if k == E_NEW and e.b != null { # a dispatch fills the queue's kept record: kept if e.a != null and e.a.kids != null { var i = 0 while i < len(e.a.kids) { es_escape(es_val(e.a.kids[i].a)); i += 1 } } return g_es_heap } if k == E_NEW { let s = es_site(e) if e.a != null and e.a.kids != null { var i = 0 while i < len(e.a.kids) { es_store(es_ref(e.a.kids[i].a), s); i += 1 } } # a field's default is made with the record and held by it (`facts: Queue = queue_new(...)`) let lay = find_comp(e.s) if lay != null and lay.kids != null { var j = 0 while j < len(lay.kids) { let fd = lay.kids[j] if fd.a != null and not es_overridden(e, fd.s) { es_store(es_ref(fd.a), s) } j += 1 } } return s } if k == E_LIST { let s = es_site(e) var i = 0 while i < len(e.kids) { es_store(es_ref(e.kids[i]), s); i += 1 } return s } if k == E_CALL { return es_call(e) } # anything else: walk it, and what it hands back may be anything es_walk(e.a); es_walk(e.b); es_walk(e.c) if e.kids != null { var j = 0 while j < len(e.kids) { es_walk(e.kids[j]); j += 1 } } return g_es_heap } function es_overridden(e: Node, field: pointer) -> bool { if e.a == null or e.a.kids == null { return false } var i = 0 while i < len(e.a.kids) { if (e.a.kids[i].s == field) { return true }; i += 1 } return false } # the function records a call to `nm` reaches: nm itself, or every instance of a generic nm function es_fns_for(nm: pointer) -> []int { let out = new []int let f = es_fn(nm) if f >= 0 { push(out, f) return out } let pre = `{nm}$` var i = 0 while i < len(g_es_fnode) { let d = g_es_fnode[i] if d.s != null and str_starts(d.s, pre) { push(out, i) } i += 1 } return out } function es_keeps_nothing(nm: pointer) -> bool { if (nm == "quit") or (nm == "exit") or (nm == "panic") or (nm == "assert") or (nm == "expect") or (nm == "expect_eq") or (nm == "expect_near") { return true } if (nm == "min") or (nm == "max") or (nm == "abs") or (nm == "clamp") or (nm == "sqrt") or (nm == "floor") or (nm == "ceil") or (nm == "round") { return true } if (nm == "int") or (nm == "float") or (nm == "long") or (nm == "double") or (nm == "fixed") or (nm == "as_int") or (nm == "as_fixed") { return true } if (nm == "float_bits") or (nm == "float_from_bits") or (nm == "sin") or (nm == "cos") or (nm == "atan2") or (nm == "file_write") or (nm == "file_close") { return true } return false } function es_args_escape(e: Node) -> void { var i = 0 while i < len(e.kids) { es_escape(es_val(e.kids[i])); i += 1 } } function es_args_walk(e: Node) -> void { var i = 0 while i < len(e.kids) { es_val(e.kids[i]); i += 1 } } function es_call(e: Node) -> int { let c = e.a if c != null and c.kind == E_ID and c.s != null { let nm = c.s if es_local(nm) >= 0 { # a function value in a local: anything es_args_escape(e) return g_es_heap } if (nm == "string") { es_args_walk(e) return es_site(e) } if (nm == "keep") or (nm == "intern") { # a heap copy on purpose: a site for the census's owner, never scratch es_args_walk(e) let kc = es_site(e) g_es_flag[kc] = g_es_flag[kc] | ES_ESC g_es_site_aok[len(g_es_site_aok) - 1] = true # deliberate: not a keep to report, nor a birth return kc } if (nm == "push") and len(e.kids) >= 2 { let t = es_val(e.kids[0]) es_store(es_ref(e.kids[1]), t) # the grown buffer is the list's own push(g_es_site, e); push(g_es_site_cls, t); push(g_es_site_fn, g_es_cur); push(g_es_site_aok, g_es_aok > 0); push(g_es_site_grow, true) return -1 } if (nm == "words") or (nm == "floats") or (nm == "doubles") or (nm == "buffer") or (nm == "bytes") or (nm == "fixeds") or (nm == "pointers") { es_args_walk(e) return es_site(e) } if (nm == "free") and len(e.kids) == 1 { # given back: what flows into it is not kept by nothing let v = es_val(e.kids[0]) if v >= 0 { g_es_flag[v] = g_es_flag[v] | ES_FREED } return -1 } if (nm == "print") { # print frees a fresh text it is handed es_args_walk(e) var i = 0 while i < len(e.kids) { push(g_es_consumed, e.kids[i]); i += 1 } return -1 } if (nm == "len") { es_args_walk(e); return -1 } if (nm == "view") and len(e.kids) >= 1 { # a view shares its list's storage let base = es_val(e.kids[0]) var vi = 1 while vi < len(e.kids) { es_val(e.kids[vi]); vi += 1 } return base } # a generic's call names the generic; its instances (ui_kept$UiNode) are what is analysed, and # the call reaches every one of them let fs = es_fns_for(nm) if len(fs) > 0 { let r = es_new(0) var fi = 0 while fi < len(fs) { let f = fs[fi] push(g_es_calls_f, g_es_cur) push(g_es_calls_t, f) es_flow(g_es_fret[f], r) fi += 1 } var i = 0 while i < len(e.kids) { let v = es_ref(e.kids[i]) var fj = 0 while fj < len(fs) { let d = g_es_fnode[fs[fj]] if i < len(d.kids) and d.kids[i].kind == N_PARAM { if not es_prim(d.kids[i].ty) { es_flow(v, g_es_fparam[fs[fj]] + i) } } else { es_escape(v) } fj += 1 } i += 1 } return r } # an intrinsic known to keep nothing it is handed; anything else unknown keeps it all - the # arena is only as safe as this default if es_keeps_nothing(nm) { es_args_walk(e) return g_es_heap } es_args_escape(e) return g_es_heap } if es_is_mem_frame(e) { if g_es_cur >= 0 { g_es_fspans[g_es_cur] = true } return -1 } if c != null and c.kind == E_MEMBER and c.a != null and c.a.kind == E_ID and ns_alias_find(c.a.s, c.s) >= 0 { let al = ns_alias_find(c.a.s, c.s) let f = es_fn(g_al_target[al]) if f >= 0 { push(g_es_calls_f, g_es_cur) push(g_es_calls_t, f) var i = 0 let d = g_es_fnode[f] while i < len(e.kids) { let v = es_ref(e.kids[i]) if i < len(d.kids) and d.kids[i].kind == N_PARAM { if not es_prim(d.kids[i].ty) { es_flow(v, g_es_fparam[f] + i) } } else { es_escape(v) } i += 1 } let r = es_new(0) es_flow(g_es_fret[f], r) return r } } if c != null and c.kind == E_MEMBER and c.a != null and c.a.kind == E_ID and es_local(c.a.s) < 0 and ck_global(c.a.s) == null { # Ns.method, the emitter's own: what it is handed after the first may be kept in the first # (List.insert, Dict.put, ...); a namespace that only reads (Text, Math) costs a false escape if len(e.kids) > 0 { let t = es_val(e.kids[0]) var i = 1 while i < len(e.kids) { es_store(es_val(e.kids[i]), t); i += 1 } } return g_es_heap } # a function value in a field: anything es_val(c) es_args_escape(e) return g_es_heap } function es_walk(n: Node) -> void { if n == null { return } if n.uns == FR_AOK_STMT and n.kind != E_ID { g_es_aok += 1 es_walk_in(n) g_es_aok -= 1 return } es_walk_in(n) } function es_walk_in(n: Node) -> void { let k = n.kind if k == S_LET or k == S_ASSIGN or k == S_RETURN or k == S_EXPR or k == S_EMIT { g_es_stmt = n } if k == S_LET { var t = n.ty if t == null { t = es_type(n.a) } let fresh = es_local(n.s) < 0 let c = es_bind_local(n.s) if fresh { g_es_lt[len(g_es_lt) - 1] = t } if es_prim(t) { es_val(n.a); return } es_flow(es_val(n.a), c) return } if k == S_ASSIGN { let v = es_ref(n.b) let t = n.a if t != null and t.kind == E_ID { let c = es_local(t.s) if c >= 0 { es_flow(v, c) } else { es_escape(v) } return } es_store(v, es_val(t)) return } if k == S_RETURN { if n.a != null and g_es_cur >= 0 { es_flow(es_ref(n.a), g_es_fret[g_es_cur]) } return } if k == S_FOR { if es_local(n.s) < 0 { es_bind_local(n.s) g_es_lt[len(g_es_lt) - 1] = "int" } es_val(n.a); es_val(n.b) es_walk(n.c) return } if k == S_EXPR { es_val(n.a); return } if k == S_EMIT { # an event's fields reach its listeners: kept if n.a != null and n.a.kids != null { var i = 0 while i < len(n.a.kids) { es_escape(es_val(n.a.kids[i].a)); i += 1 } } return } if k == E_REC { # named values handed on (a dispatch, a spawn, ...): kept if n.kids != null { var i = 0 while i < len(n.kids) { es_escape(es_val(n.kids[i].a)); i += 1 } } return } if k == E_CALL or k == E_NEW or k == E_BIN or k == E_LIST or k == E_SLICE or k == E_ID or k == E_MEMBER or k == E_INDEX { es_val(n) return } es_walk(n.a) es_walk(n.b) es_walk(n.c) if n.kids != null { var i = 0 while i < len(n.kids) { es_walk(n.kids[i]); i += 1 } } } # HEAP forward along flows; ESC backward along flows, and from an ESC or HEAP target along stores function es_solve() -> void { var changed = true while changed { changed = false var i = 0 while i < len(g_es_ff) { let a = g_es_ff[i] let b = g_es_ft[i] if (g_es_flag[a] & ES_HEAP) != 0 and (g_es_flag[b] & ES_HEAP) == 0 { g_es_flag[b] = g_es_flag[b] | ES_HEAP; changed = true } if (g_es_flag[b] & ES_FREED) != 0 and (g_es_flag[a] & ES_FREED) == 0 { g_es_flag[a] = g_es_flag[a] | ES_FREED; changed = true } if (g_es_flag[b] & ES_ESC) != 0 and (g_es_flag[a] & ES_ESC) == 0 { g_es_flag[a] = g_es_flag[a] | ES_ESC g_es_why[a] = g_es_why[b] changed = true } i += 1 } # kept memory outlives the frame: an ESC class is HEAP too, so every alias of it (let x = y, # a load, a call's result) is, and what is stored through the alias is kept i = 0 while i < len(g_es_flag) { if (g_es_flag[i] & ES_ESC) != 0 and (g_es_flag[i] & ES_HEAP) == 0 { g_es_flag[i] = g_es_flag[i] | ES_HEAP; changed = true } i += 1 } i = 0 while i < len(g_es_sv) { let v = g_es_sv[i] let t = g_es_st[i] if (g_es_flag[t] & (ES_ESC | ES_HEAP)) != 0 and (g_es_flag[v] & ES_ESC) == 0 { g_es_flag[v] = g_es_flag[v] | ES_ESC g_es_why[v] = g_es_swhy[i] changed = true } i += 1 } } # 25.5c: which state holds what - a container's owner flows forward, a stored value takes its # target's, and what flowed into a value takes the value's; the first found stays changed = true while changed { changed = false var o = 0 while o < len(g_es_ff) { let a = g_es_ff[o] let b = g_es_ft[o] if g_es_own[a] != null and g_es_own[b] == null { g_es_own[b] = g_es_own[a]; changed = true } if g_es_own[b] != null and g_es_own[a] == null and (g_es_flag[a] & ES_ESC) != 0 { g_es_own[a] = g_es_own[b]; changed = true } o += 1 } o = 0 while o < len(g_es_sv) { let v = g_es_sv[o] let t = g_es_st[o] if g_es_own[t] != null and g_es_own[v] == null { g_es_own[v] = g_es_own[t]; changed = true } o += 1 } } # a function that reaches Mem.frame spans frames: so does every caller changed = true while changed { changed = false var j = 0 while j < len(g_es_calls_f) { let f = g_es_calls_f[j] let t = g_es_calls_t[j] if f >= 0 and g_es_fspans[t] and not g_es_fspans[f] { g_es_fspans[f] = true; changed = true } j += 1 } } } # mark every LOCAL allocating construct; the count of them, and of those that escape var g_es_local_n: int = 0 var g_es_esc_n: int = 0 function escape_analyse() -> void { if g_es_done { return } g_es_done = true ck_tab_init(g_es_ok, g_es_ov) g_es_heap = es_new(ES_ESC | ES_HEAP) ck_tab_init(g_es_fk, g_es_fv) var i = 0 while i < len(prog) { let d = prog[i] # an entry has no name, and must be walked all the same: what it keeps, it keeps for good var dn = d.s if d.kind == N_MAIN and dn == null { dn = "$entry" } if (d.kind == N_FN or d.kind == N_SYS or d.kind == N_MAIN) and dn != null and ck_tab_get(g_es_fk, g_es_fv, dn) == null { let h = new Node h.ival = len(g_es_fnode) ck_tab_put(g_es_fk, g_es_fv, dn, h) push(g_es_fnode, d) push(g_es_fspans, d.kind == N_MAIN) push(g_es_fparam, len(g_es_flag)) var p = 0 while p < len(d.kids) { let pc = es_new(ES_HEAP) if d.kids[p].kind == N_PARAM and is_state_ty(d.kids[p].ty) { g_es_own[pc] = d.kids[p].ty } p += 1 } push(g_es_fret, es_new(0)) } i += 1 } var f = 0 while f < len(g_es_fnode) { let d = g_es_fnode[f] g_es_cur = f g_es_ln = new []pointer g_es_lc = new []int g_es_lt = new []pointer var p = 0 while p < len(d.kids) { if d.kids[p].kind == N_PARAM { push(g_es_ln, d.kids[p].s); push(g_es_lc, g_es_fparam[f] + p); push(g_es_lt, d.kids[p].ty) } p += 1 } es_walk(d.a) f += 1 } g_es_cur = -1 # a global's initializer - a state's instance, a module's table - is made to be kept var gi = 0 while gi < len(prog) { let gd = prog[gi] if gd.kind == N_VAR and gd.a != null { g_es_ln = new []pointer g_es_lc = new []int g_es_lt = new []pointer es_escape(es_ref(gd.a)) } gi += 1 } # an @On body keeps nothing it makes unless it stores it; it has no function record of its own var li = 0 while li < len(g_onlisten) { g_es_ln = new []pointer g_es_lc = new []int g_es_lt = new []pointer es_walk(g_onlisten[li].a) li += 1 } # a function taken as a value is called where the analysis cannot see (a UiClass's make, a step # list, a System's tick): its result is kept by whoever calls it, whatever it returns var fr = 0 while fr < len(g_es_fnrefs) { let fs = es_fns_for(g_es_fnrefs[fr]) var fq = 0 while fq < len(fs) { es_escape(g_es_fret[fs[fq]]); fq += 1 } fr += 1 } es_solve() # a node analysed more than once (a field's default, walked at every `new` of its record) is # scratch only if every walk found it LOCAL: mark the LOCAL ones, then take the mark off any node # one walk found kept var s = 0 while s < len(g_es_site) { let n = g_es_site[s] let c = g_es_site_cls[s] let fi = g_es_site_fn[s] let spans = fi < 0 or g_es_fspans[fi] # LOCAL only when it can neither be kept (ESC) nor share storage with what is (HEAP): a push's # growth into a parameter's list is the list's own buffer, and the list may be a state's if c >= 0 and es_local_class(c, g_es_site_grow[s]) and not spans { n.uns = ES_SCRATCH g_es_local_n += 1 } else { g_es_esc_n += 1 if c >= 0 and g_es_own[c] != null and n.file != null { let h = new Node h.s = g_es_own[c] ck_tab_put(g_es_ok, g_es_ov, `{n.file}:{itoa(n.line)}`, h) } } s += 1 } var u = 0 while u < len(g_es_site) { let n2 = g_es_site[u] let c2 = g_es_site_cls[u] let fi2 = g_es_site_fn[u] let spans2 = fi2 < 0 or g_es_fspans[fi2] if n2.uns == ES_SCRATCH and (c2 < 0 or not es_local_class(c2, g_es_site_grow[u]) or spans2) { n2.uns = 0 } u += 1 } } var g_es_report: bool = false # --escape-report: the analysis on its own (the arena that uses it is not wired yet), each site that # escapes or stays LOCAL as a line on stderr, then the totals function escape_report() -> void { escape_analyse() var s = 0 while s < len(g_es_site) { let n = g_es_site[s] var what = "kept" if n.uns == ES_SCRATCH { what = "local" } var fname = "?" let fi = g_es_site_fn[s] if fi >= 0 { fname = g_es_fnode[fi].s } var file = n.file if file == null { file = "?" } let line = `escape: {what} {file}:{itoa(n.line)} {fname}\n` file_write(file_stderr(), line, len(line)) s += 1 } var fl = 0 while fl < len(g_es_fnrefs) { let fm = `escape: fn value {g_es_fnrefs[fl]} ({itoa(len(es_fns_for(g_es_fnrefs[fl])))} functions)\n` file_write(file_stderr(), fm, len(fm)) fl += 1 } let tot = `escape: {itoa(g_es_local_n)} local, {itoa(g_es_esc_n)} kept\n` file_write(file_stderr(), tot, len(tot)) } # keep(x): x copied onto the heap - a string's bytes, a slice's header and elements, a record's # fields (shallow) - so a frame's value can become a state's on purpose (25.3's region rule) function emit_keep(a: Val) -> Val { if (a.ty == "string") { let n = emit_bind(`call i64 @strlen(ptr {a.code})`) let n1 = emit_bind(`add i64 {n}, 1`) let p = emit_bind(`call ptr @lp_malloc(i64 {n1})`) emit(` call ptr @memcpy(ptr {p}, ptr {a.code}, i64 {n1})\n`) return val(p, "string") } if is_slice_ty(a.ty) { let esz = emit_sizeof(llty(slice_elem(a.ty))) let hsz = emit_sizeof("%LSlice") let h = emit_bind(`call ptr @lp_malloc(i64 {hsz})`) let od = emit_bind(`load ptr, ptr {slice_field(a.code, 0)}`) let ln = emit_bind(`load i32, ptr {slice_field(a.code, 1)}`) let ln64 = emit_bind(`zext i32 {ln} to i64`) let bytes = emit_bind(`mul i64 {ln64}, {esz}`) let b1 = emit_bind(`add i64 {bytes}, 1`) let d = emit_bind(`call ptr @lp_malloc(i64 {b1})`) emit(` call ptr @memcpy(ptr {d}, ptr {od}, i64 {bytes})\n`) emit(` store ptr {d}, ptr {slice_field(h, 0)}\n`) emit(` store i32 {ln}, ptr {slice_field(h, 1)}\n`) emit(` store i32 {ln}, ptr {slice_field(h, 2)}\n`) return val(h, a.ty) } if layout_node(a.ty) != null { let sz = emit_sizeof(layout_ty(a.ty)) let p = emit_bind(`call ptr @lp_malloc(i64 {sz})`) emit(` call ptr @memcpy(ptr {p}, ptr {a.code}, i64 {sz})\n`) return val(p, a.ty) } return a } # the state that holds what a site makes, when the analysis ran and found one function es_owner_at(file: pointer, line: int) -> pointer { if not g_es_done or file == null { return null } let h = ck_tab_get(g_es_ok, g_es_ov, `{file}:{itoa(line)}`) if h == null { return null } return h.s }