Non-lexical scope?


#1
(defun test (x code)
  (eval code)
  x)

(test 21 '(setf x 1))

Just what is the scope for variables in uLisp, exactly? 🤨


#2

uLisp is designed to use lexical scoping. This is demonstrated by:

> (let ((y 7)) (defvar test (lambda (x) (list x y))))
test
> (let ((y 5)) (test 3))
(3 7)

The value of y is the value it had when the function was defined, not the value it had when the function was called.

Your example probably reveals a bug.


#3

Well, that depends on whether it’s intentional or not. eval kind of plays havoc with lexical scope no matter how you do it. Common Lisp specifies eval as always using an empty lexical environment. That’s probably the sanest option.

There’s also the inverse issue, in a way, where defun creates functions that don’t close over their lexical environment. I haven’t gone looking for weird edge cases there, but I’m fairly sure they exist.


#4

While thinking about an unrelated problem, I think I’ve figured out what the issue is here. It’s to do with how closures and functions get passed an environment - specifically, that they always get passed the current lexical environment. The result is somewhere between lexical and dynamic scoping. Closures look like they’re working, but that’s because they search the environment they closed over first. See:

> (defvar test (lambda (x) (list x y)))
test

> (let ((y 5)) (test 32))
(32 5)

That should choke on y being an undefined variable, by the rules of lexical scope. The obvious way to seal this hole is to make closures only search their enclosed environment, and return closures over the empty environment for lambda expressions in the null lexical environment, rather than just returning the lambda expression. Doing this would, however, result in functions no longer being easily viewable in the REPL. I also haven’t looked deeply into the changes needed, but will do so.


#5

Although I don’t think this will make any practical difference to most uLisp users, it would be good to find a way to fix it.


#6

Doing this would, however, result in functions no longer being easily viewable in the REPL.

That could be solved by making printobject() treat closures over an empty environment as a special case, and do what it does at the moment.


#7

I think it’s one of those cases where it’s exceedingly rare that it matters, but in any case where it does it’s likely to do something the user didn’t expect, and in a place where they won’t think to look.

Not a bad idea. I have one problem with it, which is that reading that representation back in could in theory result in a closure that behaves differently, if it’s now in a non-null lexical environment.