On Wed, 20 Dec 2023 21:09:44 +0000,
mitch...@aol.com (MitchAlsup)
wrote:
>Anton Ertl wrote:
>
>> Stephen Fuld <sf...@alumni.cmu.edu.invalid> writes:
>>>I believe Pascal, back in the 1970s implemented nested functions, though
>>>I don't know how any specific implementation accomplished it.
>
>> AFAIK Pascal implementations all just use static links chains to the
>> enclosing scopes.
@Anton: implementation dependent - some use displays instead.
>So you access an outer local by following the link
>> chain as many levels as you cross scopes.
>
>How does Pascal do this counting when there are recursive frames on
>the stack ??
To access non-local, non-global things (variables, functions, etc.)
nested functions require that the definition scopes be tracked through
the chain of function calls.
To this end there are 2 different kinds of links: by convention
referred to as 'static' and 'dynamic'.
dynamic = who called who
static = who defined who
The stack frame in a language - such as Pascal - having nested
functions contains both kinds of links. The dynamic link is the same
as in C: it just points to the immediate caller. The static link,
however, points to the last created frame of the function that
/defined/ the currently executing function.
[view with a fixed width font]
Expanding a bit on Anton's example, consider:
A defines B and C
B defines D
C defines E
A runs
A calls B
B calls D
D calls C
C calls E
E calls E
E calls A
A calls B
B calls D
D calls B
dynamic: A <- B <- D <- C <- E <- E <- A <- B <- D <- B
static : ^
: ^-----
: ^-----
: ^---------------
: ^-----
: ^----------
: ^
: ^-----
: ^-----
: ^---------------
You can see the dynamic links simply reflect the call chain. The
static links however, jump around and appear to form multiple chains.
The top level function A has no enclosing scope, so its static link is
to self. However, B, C, D and E all can access things defined in
enclosing scopes by following the chain of static links: e.g., a D
instance has a link to the last B instance, and that B instance has a
link to the last A instance.
Obviously there has to be a way to FIND the last instance of a scope
as it may be deep in the stack. The /simplest/ way is to mark each
frame with a small integer identifying their static nest level: e.g.,
A = 1, B = 2, C = 2, D = 3, E = 3
A 'display' is another way to implement non-local, non-global access
which uses an array of frame pointers rather than a 'chain' of static
links woven through the stack frames. Accessing an enclosing scope is
simply an indirection through display[target scope].
When entering a function at nest level N, the value of display[N] is
saved in the frame and display[N] set to point to the last instance of
the enclosing level N-1 frame. Exiting the function, display[N] is
restored from the saved value.
This makes /maintaining/ a display a bit most costly than using static
links - ie. a static link can simply be abandoned. However, if there
is significant use of things from multiple deep scopes [where 'deep'
is 2 or more scopes away], the display will be faster to use.
Only display elements at or (numerically) lower than the nest level of
the current function can (legally) be used - e.g., a function at level
3 can use display[1] .. display[3], whereas using display[4] or higher
would constiture an error.
So, assuming the same example as above:
A defines B and C
B defines D
C defines E
display[ 1 2 3 4 5 6 ]
A runs [ A . . . . . ] L = 1
A calls B [ A A . . . . ] L = 2
B calls D [ A A B . . . ] L = 3
D calls C [ A A b . . . ] L = 2
C calls E [ A A C . . . ] L = 3
E calls E [ A A C . . . ] L = 3
E calls A [ A a c . . . ] L = 1
A calls B [ A A c . . . ] L = 2
B calls D [ A A B . . . ] L = 3
D calls B [ A A b . . . ] L = 2
'L' indicates the current run level (after the call). Display values
at nest levels greater than the current run level are shown as
lowercase: they ARE still in the array, but (theoretically) are not
accessible from the currently running function.
Although it sort of /looks/ like the display is a stack, the elements
at indices greater than the current nest level still are in the array
and MAY still have meaning.
Unfortunately, there is a limit to how complex an example can be shown
here ... a far more complicated example would be required to show
saving and restoring values.
Using displays also interacts with threading: the display is part of
the thread state. However, thread local storage works fine for this -
in use displays tend to be cached quite effectively and so there is
little or no need for special hardware to handle them.
You might ask "who makes multiple deep accesses to different frames?"
That's a fair question to which I don't have a good answer. Displays
fell out of favor ... not because they were troublesome, but rather
because better compilation methods were developed to turn nested
functions into flat closures (which could be in heap or on stack) and
these mostly removed the need for either displays or static links.
YMMV.
>> Pascal allows passing
>> nested functions/procedures as parameters, but does not support
>> variables containing them
@Anton: again, implementation dependent. Standard Pascal (ISO and
ANSI) does not recognize 'function pointer' variables, but some
extended Pascals did.
>> , or any other first-class handling of them.
>> This avoids the upward funarg problem (as the Lispers call it). These
>> parameters consist of the code pointer and the static link pointer.
Yes, in Standard Pascal you could only call functions defined at the
same nesting level or in an enclosing levels WITHIN THE SAME SCOPE
CHAIN. In the example above E couldn't call D because their
definition scopes are disjoint.
However, again, some extended Pascals did allow cross scope calls
using pointers provided the caller and callee both were within a
common enclosing scope. IOW there were implementations in which you
could create a pointer to E in A and then call E from D.
>> Modula-2 has nested procedures, and allows putting procedures in
>> variables, fields, etc., but eliminates the upward funarg problem by
>> only allowing to store global-level procedures in variables, not
>> nested procedures. So it went closer to C.
>
>> Oberon originally had nested procedures, and AFAIK similar rules to
>> Modula-2. When I asked Wirth in 2020 if he has a good example for the
>> use of nested procedures (I thought he would have, given that he kept
>> this feature for so many years), he told me that he had removed nested
>> functions in 2013 or so.
Yes, for complex code, simple nested functions often are inadequate
and you need (not necessarily 'first-class' but) persistent closures.
[Consider that closures in Lisp really are 2nd class because they must
be bound to a symbol in order to be stored/persisted. In Scheme where
they ARE 1st class, they can be stored and passed around - including
upward - just like any other value.]
>> - anton