• Re: Nested functions and representation of closures

    From David Brown@21:1/5 to Stefan Monnier on Wed Dec 20 23:51:40 2023
    On 20/12/2023 23:26, Stefan Monnier wrote:
    Anton Ertl [2023-12-20 21:37:44] wrote:
    mitchalsup@aol.com (MitchAlsup) writes:
    Anton Ertl wrote:
    Stephen Fuld <sfuld@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. 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 ??
    The static scope level is independent of recursion. Note that this
    kind of implementation has a static link in addition to the dynamic
    link.

    Maybe I'm missing something but I think Pascal's display is "orthogonal"
    to the issue discussed before of how to stuff together a code pointer together with a pointer to its associated data.

    Pascal's display is one possible representation of closures, but it
    doesn't save you from the need to use a kind of "fat pointer" for the first-class functions.

    What saves Pascal is that first-class functions aren't first class at
    all, so Pascal can safely and easily represent them any way it likes, including as a pair of two values (a pointer to the code, and a pointer
    to a stack frame, typically).

    It's harder in C because some code wants to be able to convert
    a function pointer to a `void*` and back, so you usually want to make it
    fit into the size of a normal pointer.


    Converting between function pointers and data pointers (like void*) is undefined behaviour in C. Of course, some people want to do that
    regardless, and on many platforms you can probably get away with it if
    you are lucky. On some platforms, however, data pointers and function
    pointers are different sizes. (There are even some platforms where
    pointers to different types of data are different sizes.)

    But you /are/ allowed to convert back and forth between different
    function pointer types in C, which means function pointers are always
    the same size. And for efficiency, that would generally be the smallest
    size that works for normal functions - i.e., typically just the address
    in memory of the function's code.

    So what saves C is that nested functions are not part of the language!
    And when you have extensions that support nested functions, such as
    supported by gcc, if the function really needs a fat pointer you have a trampoline on the stack to hide that and make it look like a normal
    "thin" pointer.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Thomas Koenig@21:1/5 to David Brown on Thu Dec 21 13:02:44 2023
    David Brown <david.brown@hesbynett.no> schrieb:

    So what saves C is that nested functions are not part of the language!
    And when you have extensions that support nested functions, such as
    supported by gcc, if the function really needs a fat pointer you have a trampoline on the stack to hide that and make it look like a normal
    "thin" pointer.

    I looked at what the Fortran compilers I could lay my hands on did
    with the following code, which uses a contained subroutine inside
    a subroutine:

    module test
    contains
    subroutine t(f)
    interface
    subroutine f()
    end subroutine
    end interface
    call f()
    end subroutine
    end module

    subroutine foo(i)
    use test
    integer, intent(inout):: i
    call t(bar)
    contains
    subroutine bar()
    call xxx(i)
    end
    end

    Interestingly, every compiler I could lay my hands on on short
    notice (gfortran for several architectures, ifort, xlf, the new
    flang for llvm) used trampolines.

    See https://godbolt.org/z/nv5nr934q , which is for POWER and
    contains a call to a subroutine suggestively called
    __trampoline_setup.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From Michael S@21:1/5 to David Brown on Thu Dec 21 15:22:38 2023
    On Wed, 20 Dec 2023 23:51:40 +0100
    David Brown <david.brown@hesbynett.no> wrote:

    On 20/12/2023 23:26, Stefan Monnier wrote:
    Anton Ertl [2023-12-20 21:37:44] wrote:
    mitchalsup@aol.com (MitchAlsup) writes:
    Anton Ertl wrote:
    Stephen Fuld <sfuld@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. 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 ??
    The static scope level is independent of recursion. Note that this
    kind of implementation has a static link in addition to the dynamic
    link.

    Maybe I'm missing something but I think Pascal's display is
    "orthogonal" to the issue discussed before of how to stuff together
    a code pointer together with a pointer to its associated data.

    Pascal's display is one possible representation of closures, but it
    doesn't save you from the need to use a kind of "fat pointer" for
    the first-class functions.

    What saves Pascal is that first-class functions aren't first class
    at all, so Pascal can safely and easily represent them any way it
    likes, including as a pair of two values (a pointer to the code,
    and a pointer to a stack frame, typically).

    It's harder in C because some code wants to be able to convert
    a function pointer to a `void*` and back, so you usually want to
    make it fit into the size of a normal pointer.


    Converting between function pointers and data pointers (like void*)
    is undefined behaviour in C. Of course, some people want to do that regardless, and on many platforms you can probably get away with it
    if you are lucky.

    On both leading general-purpose computing platforms, i.e. on Windows and
    on Posix-compatibles, you don't have to be lucky: this type of
    conversion is guaranteed to work. Period. In both cases it is an
    integral part of dynamic linking APIs.

    On some platforms, however, data pointers and
    function pointers are different sizes. (There are even some
    platforms where pointers to different types of data are different
    sizes.)

    But you /are/ allowed to convert back and forth between different
    function pointer types in C, which means function pointers are always
    the same size. And for efficiency, that would generally be the
    smallest size that works for normal functions - i.e., typically just
    the address in memory of the function's code.

    So what saves C is that nested functions are not part of the
    language! And when you have extensions that support nested functions,
    such as supported by gcc, if the function really needs a fat pointer
    you have a trampoline on the stack to hide that and make it look like
    a normal "thin" pointer.



    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From David Brown@21:1/5 to Michael S on Thu Dec 21 15:28:23 2023
    On 21/12/2023 14:22, Michael S wrote:
    On Wed, 20 Dec 2023 23:51:40 +0100
    David Brown <david.brown@hesbynett.no> wrote:

    On 20/12/2023 23:26, Stefan Monnier wrote:
    Anton Ertl [2023-12-20 21:37:44] wrote:
    mitchalsup@aol.com (MitchAlsup) writes:
    Anton Ertl wrote:
    Stephen Fuld <sfuld@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. 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 ??
    The static scope level is independent of recursion. Note that this
    kind of implementation has a static link in addition to the dynamic
    link.

    Maybe I'm missing something but I think Pascal's display is
    "orthogonal" to the issue discussed before of how to stuff together
    a code pointer together with a pointer to its associated data.

    Pascal's display is one possible representation of closures, but it
    doesn't save you from the need to use a kind of "fat pointer" for
    the first-class functions.

    What saves Pascal is that first-class functions aren't first class
    at all, so Pascal can safely and easily represent them any way it
    likes, including as a pair of two values (a pointer to the code,
    and a pointer to a stack frame, typically).

    It's harder in C because some code wants to be able to convert
    a function pointer to a `void*` and back, so you usually want to
    make it fit into the size of a normal pointer.


    Converting between function pointers and data pointers (like void*)
    is undefined behaviour in C. Of course, some people want to do that
    regardless, and on many platforms you can probably get away with it
    if you are lucky.

    On both leading general-purpose computing platforms, i.e. on Windows and
    on Posix-compatibles, you don't have to be lucky: this type of
    conversion is guaranteed to work. Period. In both cases it is an
    integral part of dynamic linking APIs.

    Nope.

    POSIX requires that an object of type "void *" can hold a pointer to a function. But it does not require that a conversion between a function
    and a data pointer is defined. Not only is this code not guaranteed to
    have defined behaviour or work as you might expect, but a conforming C
    compiler is obliged to issue a diagnostic about it:

    void (*foo)(void);

    foo = (void (*)(void) dlsym(handle, "foo");

    What /does/ work, for any POSIX system, is:

    *(void ** )(&foo) = dlsym(handle, "foo");

    <https://pubs.opengroup.org/onlinepubs/009696899/functions/dlsym.html>


    Note that having the same size - and even the same representation - is
    not all that is needed for things to work as naïvely as you might assume
    in C.

    And of course, the C world is vastly greater than just the "leading general-purpose computing platforms", and it includes targets where
    function pointers and data pointers are significantly different. Some platforms (including quite popular ones in their day, like DOS) support different memory models where data and code pointers can be big and
    flexible or small and efficient. Things that work on one platform might
    not work on others. The C language - defined by the standards - gives a
    common subset. If you are writing in C, and you have not specified a particular target but you rely on target-specific features, your code
    relies on luck.




    On some platforms, however, data pointers and
    function pointers are different sizes. (There are even some
    platforms where pointers to different types of data are different
    sizes.)

    But you /are/ allowed to convert back and forth between different
    function pointer types in C, which means function pointers are always
    the same size. And for efficiency, that would generally be the
    smallest size that works for normal functions - i.e., typically just
    the address in memory of the function's code.

    So what saves C is that nested functions are not part of the
    language! And when you have extensions that support nested functions,
    such as supported by gcc, if the function really needs a fat pointer
    you have a trampoline on the stack to hide that and make it look like
    a normal "thin" pointer.





    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From EricP@21:1/5 to Thomas Koenig on Thu Dec 21 11:28:47 2023
    Thomas Koenig wrote:
    David Brown <david.brown@hesbynett.no> schrieb:

    So what saves C is that nested functions are not part of the language!
    And when you have extensions that support nested functions, such as
    supported by gcc, if the function really needs a fat pointer you have a
    trampoline on the stack to hide that and make it look like a normal
    "thin" pointer.

    I looked at what the Fortran compilers I could lay my hands on did
    with the following code, which uses a contained subroutine inside
    a subroutine:

    module test
    contains
    subroutine t(f)
    interface
    subroutine f()
    end subroutine
    end interface
    call f()
    end subroutine
    end module

    subroutine foo(i)
    use test
    integer, intent(inout):: i
    call t(bar)
    contains
    subroutine bar()
    call xxx(i)
    end
    end

    Interestingly, every compiler I could lay my hands on on short
    notice (gfortran for several architectures, ifort, xlf, the new
    flang for llvm) used trampolines.

    See https://godbolt.org/z/nv5nr934q , which is for POWER and
    contains a call to a subroutine suggestively called
    __trampoline_setup.

    BTW this requires a RWE stack.
    The x64 gfortran is considerably simpler.
    It stuffs two MOV reg,imm and a CALL instructions on the stack at RSP+8,
    copies RSP+8 to RDI, calls test_MOD which jumps to RDI.

    bar.0:
    mov rdi, QWORD PTR [r10]
    jmp xxx_
    __test_MOD_t:
    jmp rdi
    foo_:
    sub rsp, 56
    mov edx, -17599 // REX MOV reg,imm
    mov ecx, -17847 // REX MOV reg,imm
    mov QWORD PTR [rsp], rdi
    lea rdi, [rsp+8]
    mov WORD PTR [rsp+8], dx
    mov edx, OFFSET FLAT:bar.0
    mov QWORD PTR [rsp+40], 0
    mov DWORD PTR [rsp+10], edx
    mov WORD PTR [rsp+14], cx
    mov QWORD PTR [rsp+16], rsp
    mov DWORD PTR [rsp+24], -1864106167 // REX CALL
    call __test_MOD_t
    add rsp, 56
    ret

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From MitchAlsup@21:1/5 to EricP on Thu Dec 21 18:20:20 2023
    EricP wrote:

    Thomas Koenig wrote:
    David Brown <david.brown@hesbynett.no> schrieb:

    So what saves C is that nested functions are not part of the language!
    And when you have extensions that support nested functions, such as
    supported by gcc, if the function really needs a fat pointer you have a
    trampoline on the stack to hide that and make it look like a normal
    "thin" pointer.

    I looked at what the Fortran compilers I could lay my hands on did
    with the following code, which uses a contained subroutine inside
    a subroutine:

    module test
    contains
    subroutine t(f)
    interface
    subroutine f()
    end subroutine
    end interface
    call f()
    end subroutine
    end module

    subroutine foo(i)
    use test
    integer, intent(inout):: i
    call t(bar)
    contains
    subroutine bar()
    call xxx(i)
    end
    end

    Interestingly, every compiler I could lay my hands on on short
    notice (gfortran for several architectures, ifort, xlf, the new
    flang for llvm) used trampolines.

    See https://godbolt.org/z/nv5nr934q , which is for POWER and
    contains a call to a subroutine suggestively called
    __trampoline_setup.

    BTW this requires a RWE stack.
    The x64 gfortran is considerably simpler.
    It stuffs two MOV reg,imm and a CALL instructions on the stack at RSP+8, copies RSP+8 to RDI, calls test_MOD which jumps to RDI.

    bar.0:
    mov rdi, QWORD PTR [r10]

    Where is R10 made to point at i ??

    jmp xxx_
    __test_MOD_t:
    jmp rdi
    foo_:
    sub rsp, 56
    mov edx, -17599 // REX MOV reg,imm
    mov ecx, -17847 // REX MOV reg,imm
    mov QWORD PTR [rsp], rdi
    lea rdi, [rsp+8]
    mov WORD PTR [rsp+8], dx
    mov edx, OFFSET FLAT:bar.0
    mov QWORD PTR [rsp+40], 0
    mov DWORD PTR [rsp+10], edx
    mov WORD PTR [rsp+14], cx
    mov QWORD PTR [rsp+16], rsp
    mov DWORD PTR [rsp+24], -1864106167 // REX CALL
    call __test_MOD_t
    add rsp, 56
    ret

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From EricP@21:1/5 to MitchAlsup on Thu Dec 21 14:31:34 2023
    MitchAlsup wrote:
    EricP wrote:

    Thomas Koenig wrote:
    David Brown <david.brown@hesbynett.no> schrieb:

    So what saves C is that nested functions are not part of the
    language! And when you have extensions that support nested
    functions, such as supported by gcc, if the function really needs a
    fat pointer you have a trampoline on the stack to hide that and make
    it look like a normal "thin" pointer.

    I looked at what the Fortran compilers I could lay my hands on did
    with the following code, which uses a contained subroutine inside
    a subroutine:

    module test
    contains
    subroutine t(f)
    interface
    subroutine f()
    end subroutine
    end interface
    call f()
    end subroutine
    end module

    subroutine foo(i)
    use test
    integer, intent(inout):: i
    call t(bar)
    contains
    subroutine bar()
    call xxx(i)
    end end

    Interestingly, every compiler I could lay my hands on on short
    notice (gfortran for several architectures, ifort, xlf, the new
    flang for llvm) used trampolines.

    See https://godbolt.org/z/nv5nr934q , which is for POWER and
    contains a call to a subroutine suggestively called
    __trampoline_setup.

    BTW this requires a RWE stack.
    The x64 gfortran is considerably simpler.
    It stuffs two MOV reg,imm and a CALL instructions on the stack at RSP+8,
    copies RSP+8 to RDI, calls test_MOD which jumps to RDI.

    bar.0:
    mov rdi, QWORD PTR [r10]

    Where is R10 made to point at i ??

    jmp xxx_
    __test_MOD_t:
    jmp rdi
    foo_:
    sub rsp, 56
    mov edx, -17599 // REX MOV reg,imm
    mov ecx, -17847 // REX MOV reg,imm
    mov QWORD PTR [rsp], rdi
    lea rdi, [rsp+8]
    mov WORD PTR [rsp+8], dx
    mov edx, OFFSET FLAT:bar.0
    mov QWORD PTR [rsp+40], 0
    mov DWORD PTR [rsp+10], edx
    mov WORD PTR [rsp+14], cx
    mov QWORD PTR [rsp+16], rsp
    mov DWORD PTR [rsp+24], -1864106167 // REX CALL
    call __test_MOD_t
    add rsp, 56
    ret

    -17847 = BA4A = REX.WX MOV r10,imm64
    mov QWORD PTR [rsp],rdi copies i in rdi to the bottom of stack [RSP].
    mov WORD PTR [rsp+14],cx stuffs REX.WX MOV r10,imm64 onto stack
    mov QWORD PTR [rsp+16],rsp copies RSP onto stack after REX.WX MOV, r10,imm64

    At execution REX.WX MOV r10,imm64 loads prior rsp that points to i into r10 bar.0:
    mov rdi, QWORD PTR [r10] loads i into rdi

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)