closure implementation: first steps

This commit is contained in:
Araq 2012-02-04 15:47:48 +01:00
commit 2633e3fb27
10 changed files with 330 additions and 299 deletions

View file

@ -218,7 +218,7 @@ Backend issues
However the biggest problem is that dead code elimination breaks modularity!
To see why, consider this scenario: The module ``G`` (for example the huge
Gtk2 module...) is compiled with dead code elimination turned on. So no
Gtk2 module...) is compiled with dead code elimination turned on. So none
of ``G``'s procs is generated at all.
Then module ``B`` is compiled that requires ``G.P1``. Ok, no problem,
@ -366,11 +366,27 @@ comparisons).
Code generation for closures
============================
Code generation for closures is implemented by `lambda lifting`:idx:.
Design
------
A ``closure`` proc var can call ordinary procs of the default Nimrod calling
convention. But not the other way round! A closure is implemented as a
``tuple[prc, data]``. ``data`` can be nil implying a call without a closure.
This means that a call through a closure generates an ``if`` but the
interoperability is worth the cost of the ``if``. Thunk generation would be
possible too, but it's slightly more effort to implement.
Tests with GCC on Amd64 showed that it's really beneficical if the
'environment' pointer is passed as the last argument, not as the first argument.
Example code:
.. code-block:: nimrod
proc add(x: int): proc (y: int): int {.closure.} =
return lambda (y: int): int =
return proc (y: int): int =
return x + y
var add2 = add(2)
@ -380,21 +396,21 @@ This should produce roughly this code:
.. code-block:: nimrod
type
PClosure = ref object
fn: proc (x: int, c: PClosure): int
PEnv = ref object
x: int # data
proc wasLambda(y: int, c: PClosure): int =
proc anon(y: int, c: PClosure): int =
return y + c.x
proc add(x: int): PClosure =
var c: PClosure
new(c)
c.x = x
c.fn = wasLambda
proc add(x: int): tuple[prc, data] =
var env: PEnv
new env
env.x = x
result = (anon, env)
var add2 = add(2)
echo add2.fn(5, add2)
let tmp = if add2.data == nil: add2.prc(5) else: add2.prc(5, add2.data)
echo tmp
Beware of nesting:
@ -412,36 +428,46 @@ This should produce roughly this code:
.. code-block:: nimrod
type
PClosure1 = ref object
fn: proc (x: int, c: PClosure1): int
PEnvX = ref object
x: int # data
PClosure2 = ref object
fn: proc (x: int, c: PClosure2): int
PEnvY = ref object
y: int
c1: PClosure1
ex: PEnvX
proc lambdaZ(z: int, ey: PEnvY): int =
return ey.ex.x + ey.y + z
proc innerLambda(z: int, c2: PClosure2): int =
return c2.c1.x + c2.y + z
proc lambdaY(y: int, ex: PEnvX): tuple[prc, data: PEnvY] =
var ey: PEnvY
new ey
ey.y = y
ey.ex = ex
result = (lambdaZ, ey)
proc outerLambda1(y: int, c1: PClosure1): PClosure2 =
new(result)
result.c1 = c1
result.y = y
result.fn = innerLambda
proc add(x: int): PClosure1 =
new(result)
result.x = x
result.fn = outerLambda
proc add(x: int): tuple[prc, data: PEnvX] =
var ex: PEnvX
ex.x = x
result = (labmdaY, ex)
var tmp = add(2)
var tmp2 = tmp.fn(4, tmp)
var add24 = tmp2.fn(4, tmp2)
var tmp2 = tmp.fn(4, tmp.data)
var add24 = tmp2.fn(4, tmp2.data)
echo add24(5)
We could get rid of nesting environments by always inlining inner anon procs.
More useful is escape analysis and stack allocation of the environment,
however.
Alternative
-----------
Process the closure of all inner procs in one pass and accumulate the
environments. This is however not always possible.
Accumulator
-----------
@ -451,3 +477,18 @@ Accumulator
return lambda: int =
inc i
return i
proc p =
var delta = 7
proc accumulator(start: int): proc(): int =
var x = start-1
result = proc (): int =
x = x + delta
inc delta
return x
var a = accumulator(3)
var b = accumulator(4)
echo a() + b()