1 | Exception Update Proposal
|
---|
2 | =========================
|
---|
3 |
|
---|
4 | Exceptions have pretty constant since the end of Andrew's project (*), with
|
---|
5 | the one major addition being some convenience macros. Experience, though, has
|
---|
6 | highlighted problems, particular some difficult to understand sections.
|
---|
7 |
|
---|
8 | (* I'm Andrew, I am writing this in third person so other people can update
|
---|
9 | it easily, but I'm not beating on myself.)
|
---|
10 |
|
---|
11 | A lot of this should be viewed in the context of the "Proposal For Use of
|
---|
12 | Virtual Tables" (see `vtable.md`), and will refer to parts of it. In fact,
|
---|
13 | a lot of what to be discussed about exceptions is actually how it uses
|
---|
14 | that proposal, and at the same time, enabling exceptions is the primary
|
---|
15 | motivation behind virtual tables, with others being largely abstract.
|
---|
16 |
|
---|
17 | Note that the following problem areas are also inter-related with each other,
|
---|
18 | the virtual table code (as above) and the exception feature list.
|
---|
19 |
|
---|
20 | Type/Function Binding
|
---|
21 | ---------------------
|
---|
22 | Exceptions (and the virtual table system) attempt to use the location based
|
---|
23 | binding like the rest of Cforall. However, because exceptions are created and
|
---|
24 | used in very different places, without stepping through all the intermediate
|
---|
25 | locations. This is the largest user issue the system faces right now.
|
---|
26 |
|
---|
27 | The full list of things you have to do:
|
---|
28 | + Define the structure (in the header).
|
---|
29 | + Implement the required functions (in implementation).
|
---|
30 | + Forward declare a virtual table instance (in the header).
|
---|
31 | + Define the virtual table (in implementation).
|
---|
32 | + Pass in the table reference to a constructed exception (at usage).
|
---|
33 |
|
---|
34 | Even this example has some automation. For example, defining the virtual
|
---|
35 | table automatically defines some of the associated functions. This means that
|
---|
36 | you cannot control the implementation of those functions.
|
---|
37 |
|
---|
38 | Helper macros have been plastered all over this process to try and reduce
|
---|
39 | the number of steps. It is annoying, repetitive and error prone. It does open
|
---|
40 | up new options for customization, similar to call-site binding, but this does
|
---|
41 | not get used very often (as of yet, no examples have come up naturally).
|
---|
42 |
|
---|
43 | It turns out that you usually, almost always even, want a single binding
|
---|
44 | between a type and its implementing function. Call-site binding has some
|
---|
45 | similar issues, but the extra work for the flexibility is almost entirely
|
---|
46 | on the compiler. (See Stack Unwinding for information on why we can't just
|
---|
47 | resolve at the throw site.) We have also currently fixed implementations for
|
---|
48 | that one binding because of all the short-cut.
|
---|
49 |
|
---|
50 | There are a couple of directions to go with this, but the root cause is just
|
---|
51 | that contextual binding does not work great with large shifts in context.
|
---|
52 | There are a couple of ways
|
---|
53 | + Add universal (non-contextual) type/function binding. If a given type
|
---|
54 | always have the same assertions associated with it, which then must be
|
---|
55 | static, this problem goes away. This is a slight loss of functionality,
|
---|
56 | but is the strategy used by most type-class systems as well as in
|
---|
57 | object-orientated programming, which fits well with exceptions.
|
---|
58 | + Improve the user interface. It may not be a logical change so much as
|
---|
59 | just a user interface improvement to the system. The limits around
|
---|
60 | life-time, memory allocation and resolution timing remain, but new tools
|
---|
61 | could at least make the simple use cases easier and less error prone.
|
---|
62 | + Remove the need for a stable binding entirely. This would allow the
|
---|
63 | contextual binding to be used as it is in the rest of Cforall. However,
|
---|
64 | it would likely require the largest changes to exceptions. It would be
|
---|
65 | something like separately resolving the assertions at both sites.
|
---|
66 |
|
---|
67 | Stack Unwinding and Lifetime
|
---|
68 | ----------------------------
|
---|
69 | Termination exceptions unwind the stack between the throw and the catch.
|
---|
70 | This runs all the destructors and releases stored memory. The memory used
|
---|
71 | by an exception must remain in scope past the unwind while the handler is
|
---|
72 | run.
|
---|
73 |
|
---|
74 | There are two main ways to make sure memory/data remains live. First, is to
|
---|
75 | make sure that the data already has a sufficient life-time. Because it is
|
---|
76 | usually not known where an exception will be caught, that usually just means
|
---|
77 | something with a static life-time. The second option is to manually extend
|
---|
78 | the life-time by copying the data. Most exception handling mechanisms (EHMs)
|
---|
79 | use both of these, copying some core data around that might refer to out to
|
---|
80 | static information (usually code).
|
---|
81 |
|
---|
82 | The problem in Cforall, assertions can often be in neither of these areas.
|
---|
83 | While the value of the assertion itself is a local value that could be
|
---|
84 | copied around, but they often refer to stack allocated thunks, created when
|
---|
85 | a polymorphic function was specialized (monomorphized) to a less polymorphic
|
---|
86 | form. These are not static and we don't have the layout information to copy
|
---|
87 | them either.
|
---|
88 |
|
---|
89 | The explicit virtual tables is an attempt to address this, because they
|
---|
90 | create a minimum on the life-time of the assertions and thunks. This still
|
---|
91 | could be unwound if the table is on the stack, but it explicitly shows where
|
---|
92 | that limit is, unlike the automatic monomorphiation.
|
---|
93 |
|
---|
94 | This problem is really dependant on the constraints on it. There are minor
|
---|
95 | improvements to be made, however large improvements may only be possible
|
---|
96 | by changing the problem. Making non-contextual bindings
|
---|
97 | (see Type/Function Bindings) means the information can be static which could
|
---|
98 | allow it to be automatically located and reduce the issue.
|
---|
99 |
|
---|
100 | The only way to remove the life-time limit would be to avoid unwinding the
|
---|
101 | stack. If the exception handling code only removes stack frames during
|
---|
102 | clean-up, most of these problems go away.
|
---|
103 | Resumption exceptions already does this. Termination exceptions might be able
|
---|
104 | to work if you unwind after the handler runs.
|
---|
105 |
|
---|
106 | Exception Matching and Hierarchy
|
---|
107 | --------------------------------
|
---|
108 | When exceptions are caught, the primary tool to see if a given handler
|
---|
109 | should handle a given exception is hierarchical matching.
|
---|
110 |
|
---|
111 | In terms of the underlying paradigms, this is the most fundamental mismatch,
|
---|
112 | because it is borrowed from object-orientated programming, and Cforall is not
|
---|
113 | an object-orientated programming language. But we added a limited system that
|
---|
114 | mimics that, the virtual type hierarchy, and currently exceptions are the
|
---|
115 | only supported use of that system. Also it isn't entirely implemented and
|
---|
116 | would require a lot of features with relatively narrow use cases to implement it.
|
---|
117 |
|
---|
118 | Which does make updating it based on experience a bit harder, so far the only
|
---|
119 | real problem is allocating memory in the header. (Currently addressed by the
|
---|
120 | cfa_linkonce attribute.) But the missing implementation highlights that maybe
|
---|
121 | we don't need the full hierarchy.
|
---|
122 |
|
---|
123 | If you can handle the exception fully, you can catch the leaf exception and
|
---|
124 | run the handling code. If it doesn't matter because the guarded code is
|
---|
125 | isolated, then you catch any exception, log it, and then move on.
|
---|
126 | In other words, without the hierarchy and just exact matches and a separate
|
---|
127 | catch all, that could handle the vast majority of cases.
|
---|
128 |
|
---|
129 | Even within this solution, there are variants in what do these two cases
|
---|
130 | look like. The exact match case is fairly simple, it may not even have to
|
---|
131 | pass assertions because the concrete type is known at both locations.
|
---|
132 | The catch-all case is the tricky one, because we still have to manipulate
|
---|
133 | the exception in a generic form. It does however mean there is a single
|
---|
134 | generic interface in that case, which might still be simpler than the current
|
---|
135 | system.
|
---|
136 |
|
---|
137 | (Also in this two layer system, it might also be worth revisiting checked
|
---|
138 | exceptions for the exact match cases, because there is a more direct than
|
---|
139 | usual logical binding there.)
|
---|