• tail call optimisation, lisp implemented in Forth

    From none) (albert@21:1/5 to All on Mon Jul 17 10:56:19 2023
    XPost: comp.lang.lisp

    I'm following the instructions of mal (clojure)
    https://github.com/kanaka/mal

    The instruction in mal step 5 :
    Several of the special forms that you have defined in EVAL end up
    calling back into EVAL. For those forms that call EVAL as the last
    thing that they do before returning (tail call) you will just loop
    back to the beginning of eval rather than calling it again.

    I come to realize that an implementation of a lisp without tail cal optimisation is a joke. So this is all but mandatory.

    In the context of my Forth implementation a function, say `` if ''
    is looked up in the environment, it is special, so it leaves
    evaluation of the rest of the list to the discretion of `` if ''.
    The bottom line is that there is no way to construct an infinite
    loop around special functions, as proposed, in the vein of
    "
    while true: // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    if not list?(ast): return eval_ast(ast, env)
    if empty?(ast): return ast
    switch ast[0]:
    'def!: return env.set(ast[1], EVAL(ast[2], env))
    'let*: env = ...; ast = ast[2] // TCO
    'do: ast = eval_ast(ast[1..-1], env)[-1] // TCO
    'if: EVAL(ast[1], env) ? ast = ast[2] : ast = ast[3] // TCO
    'fn*: return new MalFunc(...)
    _default_: f, args = eval_ast(ast, env)
    if malfunc?(f): ast = f.fn; env = ... // TCO
    else: return apply(f, args)
    "
    In Forth `` eval '' is a colon definition , that is, a machine code
    pointer and an array of tokens. The machine code takes care that
    the tokens are interpreted via the SI register (x86)
    Nesting is accomplished by storing SI in the return stack
    pointed to by the EBP register.
    These are the instructions.

    DOCOL: LEA EBP,[EBP - (CW*(1))] ; *
    MOV [EBP],ESI ; *
    MOV ESI,[EAX+(CW*(D_HOFFSET - C_HOFFSET))] ; *
    LODSD ; NEXT
    JMP DWORD[EAX]

    Nesting is marked with *.
    The NEXT code takes care that the following token is fetched an
    executed. This changes SI.

    TCO (tail call optimisation) is accomplished by leaving out the
    nesting.
    DOCOLPRIME:
    LODSD ; NEXT
    JMP DWORD[EAX]

    So we have
    : eval .... ;
    and we can define an alias and we fix the code pointer CFA .
    'eval ALIAS eval-prime
    DOCOLPRIME 'eval-prime >CFA !

    So we can change each terminating eval with eval-prime without
    bothering with other functions that may or may not be special.

    How cool is that!

    Groetjes Albert
    --
    Don't praise the day before the evening. One swallow doesn't make spring.
    You must not say "hey" before you have crossed the bridge. Don't sell the
    hide of the bear until you shot it. Better one bird in the hand than ten in
    the air. First gain is a cat spinning. - the Wise from Antrim -

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From minforth@21:1/5 to All on Mon Jul 17 03:17:15 2023
    TCO through not adding a new stack frame is a well-known technique: https://en.wikipedia.org/wiki/Tail_call

    Patching execution tokens / function pointers on the fly is analogous to self-modifying code. Can work well as long as the affected memory address allows it. In some cases memory protection mechanisms may cut in.

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From none) (albert@21:1/5 to minforth@arcor.de on Mon Jul 17 13:09:28 2023
    In article <de4f9e06-64bd-4447-8a57-c173faf76546n@googlegroups.com>,
    minforth <minforth@arcor.de> wrote:
    TCO through not adding a new stack frame is a well-known technique: >https://en.wikipedia.org/wiki/Tail_call

    Patching execution tokens / function pointers on the fly is analogous to >self-modifying code. Can work well as long as the affected memory address >allows it. In some cases memory protection mechanisms may cut in.

    Although my mal implementation abounds with self modifying code
    (as long as adding new definitions to a Forth is self modifying)
    this is not patching.
    You could define a :C analogous to : in a kernel and use :C
    hencetoforth. In this case it doesn't require the Forth segment
    to be executable and memory protection doesn't apply at all.

    In the case I add a :C lateron I need the Forth segment to
    be executable. That is not properly described as patching
    execution tokens / function pointers.
    The phrase "analogous to self-modifying code" is particularly
    unenlightening. Usage of DEFER/IS in Forth or usage and modification
    of pointers to functions in c is commonly not described as
    "self modifying".

    Groetjes Albert
    --
    Don't praise the day before the evening. One swallow doesn't make spring.
    You must not say "hey" before you have crossed the bridge. Don't sell the
    hide of the bear until you shot it. Better one bird in the hand than ten in
    the air. First gain is a cat spinning. - the Wise from Antrim -

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)
  • From none) (albert@21:1/5 to albert on Mon Jul 17 19:37:58 2023
    XPost: comp.lang.lisp

    In article <nnd$7d844dab$52db8b4e@a0541ff0868dc0dd>,
    none) (albert <albert@cherry.> wrote:
    I'm following the instructions of mal (clojure)
    https://github.com/kanaka/mal

    The instruction in mal step 5 :
    Several of the special forms that you have defined in EVAL end up
    calling back into EVAL. For those forms that call EVAL as the last
    thing that they do before returning (tail call) you will just loop
    back to the beginning of eval rather than calling it again.

    <SNIP>
    Nesting is accomplished by storing SI in the return stack
    pointed to by the EBP register.
    These are the instructions.

    DOCOL: LEA EBP,[EBP - (CW*(1))] ; *
    MOV [EBP],ESI ; *
    MOV ESI,[EAX+(CW*(D_HOFFSET - C_HOFFSET))] ; *
    LODSD ; NEXT
    JMP DWORD[EAX]

    Nesting is marked with *.

    This must be, of course:
    DOCOL: LEA EBP,[EBP - (CW*(1))] ; *
    MOV [EBP],ESI ; *
    MOV ESI,[EAX+(CW*(D_HOFFSET - C_HOFFSET))]
    LODSD ; NEXT
    JMP DWORD[EAX]

    (EAX points to the header and ESI must be loaded anew.)


    How cool is that!

    Groetjes Albert
    --
    Don't praise the day before the evening. One swallow doesn't make spring.
    You must not say "hey" before you have crossed the bridge. Don't sell the
    hide of the bear until you shot it. Better one bird in the hand than ten in
    the air. First gain is a cat spinning. - the Wise from Antrim -

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