| I don't think I've seen anyone "solve" this yet, and it seems like one of those things that gets brought up a lot (e.g., when I had troubles doing it before: http://arclanguage.org/item?id=10504), but I somehow saw how this might be done after seeing waterhouse's aps macro again (http://arclanguage.org/item?id=12881). It's pretty easy to get access to the lexical variables. You just need to add a new special form (similar to if, fn, etc.) that compiles to an expression that calculates the environment -- almost like a macro. There are some snafus with ac being so unwieldy, and I'm not sure I got them all, but here's the patch I came up with: $ diff -u old-ac.scm new-ac.scm
--- old-ac.scm 2010-12-03 15:55:02.000000000 -0800
+++ new-ac.scm 2010-12-03 15:57:56.000000000 -0800
@@ -26,6 +26,7 @@
((eq? (xcar s) 'if) (ac-if (cdr s) env))
((eq? (xcar s) 'fn) (ac-fn (cadr s) (cddr s) env))
((eq? (xcar s) 'assign) (ac-set (cdr s) env))
+ ((eq? (xcar s) 'locals) (ac-locals env (cdr s)))
; the next three clauses could be removed without changing semantics
; ... except that they work for macros (so prob should do this for
; every elt of s, not just the car)
@@ -1485,5 +1486,20 @@
(cons (car cs) (unesc (cdr cs))))))))
(unesc (string->list s)))))
+; remove stupid cruft from env
+
+(define (clean-env env)
+ (keep (lambda (v) (and (not (pair? v)) ; due to ac-dbname
+ (not (eq? v 'nil)))) ; due to, e.g., (def foo () ...)
+ env))
+
+(define (ac-locals env args)
+ (unless (null? args) (err "Too many arguments to (locals)"))
+ (list 'fill-table
+ '(make-hash-table 'equal)
+ (cons 'list
+ (map (lambda (v) (list 'list (list 'quote v) v))
+ (clean-env env)))))
+
)
In action: arc> (def scope (var env)
(if (env var)
(prn var " is in scope")
(prn var " is out of scope")))
#<procedure: scope>
arc> (let x (locals)
(pr "Outside: ") (scope 'x x)
(pr "Inside: ") (scope 'x (locals))
nil)
Outside: x is out of scope
Inside: x is in scope
nil
Notice that it's kind of an automagic macroexpansion: arc> (def foo (x) (locals)) ; this is like (def foo (x) (obj x x))
#<procedure: foo>
arc> (foo 5)
#hash((x . 5))
arc> (let x 10 (foo 5))
#hash((x . 5)) ; the x comes from inside the scope of foo
arc> (def bar () (locals)) ; similarly, this is like (def bar () (obj))
#<procedure: bar>
arc> (bar)
#hash()
arc> (let x 10 (bar))
#hash() ; no locals at the time bar was defined
arc> (let quux 10 ; this will be a locally-scoped variable ...
(def baz () (locals))) ; so this is like (def baz () (obj quux quux))
#<procedure: baz>
arc> (baz)
#hash((quux . 10)) ; quux was in scope at the time baz was defined
arc> (let x 10 (baz))
#hash((quux . 10)) ; x was NOT in scope at the time baz was defined
Note that this doesn't give us a full-on lexical eval. But it's still useful for code instrumentation (i.e., some sort of code-walking macro or other could insert bits that look over (locals)). If we're careful, we can use it to good effect. E.g., (mac lexbound (x)
`(if ((locals) ,x)
t
(bound ,x)))
Notice that if we used def, (locals) would be "contaminated" with x in the body. We don't want that, so we sneak the call to (locals) in the macroexpansion, giving us the following behavior. arc> (load "macdebug.arc") ; see http://arclanguage.org/item?id=11806
Expression:
(let x 10 (lexbound 'x))
Macro Expansion:
(let x 10 (lexbound 'x))
==> (with (x 10) (lexbound 'x))
Expression:
(with (x 10) (lexbound 'x))
Macro Expansion:
(with (x 10) (lexbound 'x))
==> ((fn (x) (lexbound 'x)) 10)
Expression:
((fn (x) (lexbound 'x)) 10)
Macro Expansion:
(lexbound 'x)
==> (if ((locals) 'x) t (bound 'x))
Expression:
((fn (x)
(if ((locals) 'x) t (bound 'x)))
10)
nil
arc> (let x 10 (lexbound 'x))
t
but arc> (lexbound 'x)
nil
arc> (= x 100)
100
arc> (lexbound 'x)
t
We could also add the complementary function to ac.scm, which is easier since globals can be accessed without the env parameter. (This is really just a Scheme version of what waterhouse was doing.) (xdef globals
(lambda ()
(fill-table (make-hash-table 'equal)
(map (lambda (v) (list (string->symbol
(substring (symbol->string v) 1))
(namespace-variable-value v)))
(keep (lambda (s)
(and (eqv? (string-ref (symbol->string s) 0) #\_)
(not (eqv? s '_))))
(namespace-mapped-symbols))))))
Then we can get access to the full environment with a macro. (mac env ()
(w/uniq (lex bindings)
`(let ,lex (locals) ; want to bind (locals) first, so next let-
; expr for (globals) won't "contaminate" them
(let ,bindings (globals)
(each (var val) ,lex (= (,bindings var) val))
,bindings))))
arc> (= glob* 100)
100
arc> ((env) 'glob*)
100
arc> (let glob* 500 ((globals) 'glob*))
100
arc> (let glob* 500 ((locals) 'glob*))
500
arc> (let glob* 500 ((env) 'glob*))
500
arc> ((env) 'local)
nil
arc> (let local 5 ((locals) 'local))
5
arc> (let local 5 ((globals) 'local))
nil
arc> (let local 5 ((env) 'local))
5
Strictly, you could make (locals) look like a variable instead (just change the clause in ac), but I decided against that because lexical bindings aren't static, like a variable might imply. |