Closures DIP

Dmitry Olshansky
2 min readJun 25, 2023

Make closures great for once”

First — statically detect scopes of stack and allocate them as Context objects on heap or via allocator. Stick a ref-count in there if we must.

Second — chain contexts with next pointer stored at the end of context object (if has parent, statically my we know it)

Third — all access to variables while doing codegen in these scopes goes through context pointer (but we may save references to deeply nested contexts).

Last — have qualifiers for delegate to indicate if its context has to be shared or const. If it is — verify we do not access mutable data in this closure. We can start with turtless all the way up — if deepest level is immutable, the whole chain must be, same with shared.

Code examples:

1st — 2 separate scopes with common parent scope, pass them to “blackhole”. Blackhole calls them N times. by taking from array as ring buffer. Picture, discuss how code is generated by this DIP.

2nd — find code that doesn’t compile today (2 context pointers!!!) and show out new way.

3rd — &instance.foo and its codegen now, compare with 1st.

4th — longer chains with loop bodies, context is allocated multiple times.

5th — sometimes we must allocate contexts for scopes (and use them!) even if due to dynamic execution we fo not use that closure (inside if block!). Our context allocation is static analysis.

Enough for now. Codegen rules are such that context pointer is equivalent to this pointer, for particular functions there are cases:

  1. no vtbl struct — offset to fields calculated as usual
  2. vtbl struct — well we had to skip vtbl even before this DIP +1 word on all fields
  3. context — like struct but has ‘next’at _the end_ of struct, it may also not exist(!) if only first level of context is used (ststic analysis). To generalize and for optimizations (see below) it may have +K words offset to access fields
  4. delegate is a context where next doesn’t exist and codegen has K=0 or K=+1.

So actually number 3 is general enough to cover all 4 ways to do ‘T delegate(ARGS)’. Therefore we can have single ABI with code compiled under assumption of certsin offset from ‘this’ (struct or class!) or just a scope.

Optimization can be done to merge scopes if they are never used in loops etc. This DIP will not talk about it, but in some common(!) cases we can just allocate the stack frame on GC heap or better allocate with RC.

RC for closures is also beyond this DIP but we show how we can easily sneak count into each of context objects — after next or any place really as long as function’s we can have. same code run for. plain this.foo(a,b,c) and auto dg = this.foo; dg(a,b,c);

This basically says that delegate ABI should be the same as “this call” and we must do that already(!).

--

--