Search code examples
scopelispdynamic-scopepicolisp

How to write a PicoLisp function that does not shadow variables with it's parameters


I am idly exploring PicoLisp, and find myself perplexed about how to write meta-programming functions that would traditionally be handled with macros (in other lisp dialects). The biggest source of concern for me is that I do not see how I can prevent variable name shadowing. Reviewing the examples in Metaprogramming 101 has, if anything, just left me more confused.

Examples on how to implement the function mapeach, as seen in the linked article:

   [de mapeach "Args"    # expression
      (let [(@Var @List . @Body)  "Args"]
         (macro
            (mapcar
               '((@Var) . @Body)
               @List ]

   (de mapeach "Args"
      (mapcar
         (cons (cons (car "Args")) (cddr "Args"))
         (eval (cadr "Args")) ) )

   (de mapeach "Args"
      (mapcar
         '(("E")
            (bind (car "Args")
               (set (car "Args") "E")
               (run (cddr "Args")) ) )
         (eval (cadr "Args")) ) )

   (de mapeach "Args"
      (let "Vars" (pop '"Args")
         (apply mapcar
            (mapcar eval (cut (length "Vars") '"Args"))
            (cons "Vars" "Args") ) ) )

I have tested each of these with the call (let "Args" * (mapeach N (1 2 3) ("Args" N N))). As expected, the PicoLisp interpreter (started with the command pil +) experiences a segfault and crashes. I assume this is because mapeach's "Args" shadows the "Args" defined at call point.

I also tried both of their implementations of map@ (the "cuter" alternative to mapeach).

   (de map@ "Args"
      (mapcar
         '(("E") (and "E" (run (cdr "Args"))))  # 'and' sets '@'
         (eval (car "Args")) ) )
   
   (de map@ "Args"
      (mapcar
         '((@) (run (cdr "Args")))
         (eval (car "Args")) ) )

I used (let "Args" * (map@ (1 2 3) ("Args" @ @))) to test each of those implementations. Bizarrely, the first time I tested the first implementation, not only did it not segfault, it actually produced the correct result (1 4 9). Each subsequent test has resulted in a segfault. For clarity, the snippet from the prompt:

:  (de map@ "Args"
      (mapcar
         '(("E") (and "E" (run (cdr "Args"))))  # 'and' sets '@'
         (eval (car "Args")) ) )
-> map@
: (let "Args" * (mapeach N (1 2 3) ("Args" N N)))
!? (mapeach N (1 2 3) ("Args" N N))
mapeach -- Undefined
?                                                
: (let "Args" * (map@ (1 2 3) ("Args" @ @)))     
-> (1 4 9)

I believe that the segfault was somehow prevented by the call to the (then) undefined function mapeach, I also tried (ooga booga), which similarly prevented the segfault. If I do not have the erroneous call separating the definition from the proper call, a segfault always occurs.

This ultimately culminates in 2 questions:

  1. How can I prevent name shadowing? Clearly the examples do not succeed in that regard.
  2. Why does that call to map@ not result in a segfault?

Solution

  • According to this "The index for transient symbols is cleared automatically before and after loading a source file, or it can be reset explicitly with the ==== function". It doesn't specify any way that it is automatically cleared during regular REPL usage, which is the context in which I was testing this.

    This code runs properly:

    [de mapeach "Args"    # expression
          (let [(@Var @List . @Body)  "Args"]
             (macro
                (mapcar
                   '((@Var) . @Body)
                   @List ]
    (====)
    (let "Args" * (mapeach N (1 2 3) ("Args" N N)))
    

    It also runs as expected without the call to ====, but only if the call to mapeach is not in the same file.

    To address the 2 parts of my question:

    1. You can prevent name shadowing by using transient symbols either in different files, or followed by a call to ====.
    2. Those calls likely worked because the debugger clears the index which contains the transient symbols.