summaryrefslogtreecommitdiff
path: root/docs/chapter30/escape-flow.md
blob: 8ad49f15401ddd60d33d5a44b455b3f5fc6a6855 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# Outside-world escape flow

The `escape-flow` branch developed this design in Chapter29 and backported it
to Chapters 28a/28b. Chapter28a uses `LocalIds` and its existing initializer
root/lowering protocol; Chapter28b adds saved summaries and cofinite identity
remapping. Neither port imports Chapter29's deferred-construction model.

## The cycle

The outside world's access to local objects and functions is a value in the
Start/Stop cycle. Public declarations are not that value. The frozen public
interface inventory has been removed; public-method and public-alias tests
check the converged outside escape sets directly.

At Opto setup, the outer Start receives a constant foreign-identity seed:

```
foreign identities = FULL minus identities owned by this compilation
outside identities = foreign identities union discovered local escapes
```

Known native function and storage identities are foreign too. The infinite
tail permits future foreign functions even when no local callback has escaped.
The finite exclusions include registered imported identities, including bodies
represented by summaries. This scans identity tables, not the IR. Opto's
existing reset/worklist solve discovers the local escapes.

StartCU includes the shared outside state. Infinite FIDX sets must retain their
exclusions: `Opto.linkStart` checks each local function's membership rather than
treating every cofinite set as FULL. Serialization remaps a cofinite set by
remapping its finite exclusions and complementing again.

Before Opto, outside queries remain pessimistic. During SCCP, a consumer can
read the foreign seed before Start has run: otherwise a temporary finite set
can turn a generic foreign callback into an unrelated local function with the
same arity. No previous escape result is accumulated to repair value flow.

## Consumers and dependencies

* Memory and callback defaults for unknown callers use Start's memory
  projection. Parm applies its declared callback signature to the escaping
  FIDX set. Defaults whose values do not depend on escapes remain constants.
* Unknown/native CallEnd results use the outside state and the current call's
  exposed arguments. Callback results are bounded by their declared signature
  and the outside FIDX set. Explicit linked Return effects are retained.
* CallEnd registers a direct dependency on Start. Depending only on Call is
  insufficient: Call can stay CONTROL while the outside set changes. This
  first implementation uses `addDepForwards`; it does not change CallEnd's
  input layout or introduce a synthetic Fun/Return pair.
* Infinite calls use the outside continuation. Escaped local functions remain
  callable through Start, and their effects return through Stop. Attaching
  every same-arity body to an infinite call is incorrect: argument/result
  families may differ despite the equal arity.
* Outgoing Call argument exposure is independent of CallEnd, including a call
  that never returns. The shared fixed point also permits immediate callback
  round trips and reentrant calls.
* Opaque external values in Escape, Store, Return, and imported-summary paths
  use the current outside/input-memory bound. Explicit private values and
  already accumulated escapes remain included. Arbitrary local BOTTOM values
  still require FULL.
* Function-valued Loads use their memory input's FIDX bound, joined with the
  field declaration. They do not independently manufacture an escape set.

CLInit was another unconditional producer of FULL. During Opto it now depends
on the known initializer's Return and merges those effects with incoming
memory. It waits for live memory before publishing effects. Lowering computes
the replacement CallEnd's linked type before exposing its projections, so a
temporary unlinked MemBot cannot widen the already analyzed caller.

## Why the recursive tests needed local precision too

All four former Chapter12 printed-IR failures pass their original expectations.
The problem was not solely an outside-call default:

1. Convert widened a non-null pointer to a nullable declaration until its later
   identity rewrite. It now preserves non-nullness in compute. Preserving the
   entire source shape conditionally failed the existing monotonicity test;
   shape and access still follow the declaration.
2. A Load could wait until post-SCCP Store forwarding to recover that non-null
   pointer. In the meantime the next dereference produced BOTTOM, which fed
   FULL around the memory cycle and revived private constructors.
3. Forwarded addresses, resolved field aliases, and already-bypassed CLInit
   projections were also waiting for graph rewrites.

Load's existing forwarding search now supplies pointer value facts during
compute, without constructing or rewriting nodes. It can recognize a stored
address through a valid pointer conversion or a proven forwarded Load, use a
resolved named field before layout, and follow an existing CLInit bypass.
The same dependency mechanism reruns the search when its evidence changes.
Nullable/incomplete access checks still precede this sharpening; arbitrary
memory Phis and unknown stores remain barriers. This adds local queries, not a
new whole-program pass, analysis map, or second IR.

## Hidden class receivers

A class function's hidden `#self` defaults to its owning class singleton.
Seeding that input with TypePtr.PTR used to give FULL escapes when forwarded
to an unknown callback. For example:

```simple
val _hidden_noInline = { int n -> return n+2; };
val apply_noInline = { {int->int} f,int n -> return f(n); };
return apply_noInline;
```

`FunNode.defaultArg` now supplies the Test class singleton for this receiver,
both during parsing and when Opto reconnects unknown callers. The callback
parameter still reads Start dynamically. The printed-IR regression verifies
that only `<clinit>` and `apply_noInline` escape; `_hidden_noInline` stays private.

The function signature retains TypePtr.PTR in this slot. The backends use it
to identify the hidden ABI argument, and callback annotations must not bind
the receiver to the class where the annotation appears. Explicit calls still
supply their actual receiver; instance methods keep their declared object
type. This sharpens the default input, not every abstract pointer: a local
merge of a private object and private callback can also have an abstract
pointer type, so capping arbitrary TypePtr values at outside escapes is unsound.

## Remaining design question

Late imported identities also deserve further precision work: identities not
registered when Opto starts lie in the foreign tail. Existing import tests
exercise correctness, but this can remain conservative when bodies arrive
later. This limitation is not repaired by resetting the whole IR.

## Validation

The combined `make -k tests CHAPTERS="chapter28a chapter28b chapter29" CTAGS=`
run passes with assertions: 538 active tests in Chapter28a, 578 in Chapter28b,
and 601 in Chapter29. Shared printer and ISA checks pass too. The log is saved
under ignored `build/escape-backport-final.log`.

`EscapeFlowTest` covers opaque wrappers with three worklist seeds, foreign
callback parameters, private callbacks passed to and returned from native code,
and a CallEnd dependency that responds when only Start's escape set changes.
It also checks that public allocators escape while their private-memory helpers
do not. Class-init tests cover unknown callback cycles; saved-library tests in
28b/29 verify private-function exclusions survive identity remapping.
The existing suites cover nonreturning native calls, class-init cycles,
constructor rejection, serialization, imported callbacks, and three backends.
The four recursive-class examples also pass a separate 16-case worklist-seed
check. Backport IR expectations now retain public allocator Returns instead of
private-memory helper Returns. The String allocation/encoding/export fixtures
in 28a/28b/29 now declare `_hashCodeString` before the public `hashCode` wrapper,
so the implicit return exposes the wrapper. Their measured spill expectations
are 12/8/6 (x86/RISC-V/ARM); native x86 export expects 12. The reordered fixture
also measures 8 on RISC-V before the backport, versus the old order's 6.