From 34038ff433169034e2c0abe8f8f8799d5497c805 Mon Sep 17 00:00:00 2001 From: gilch Date: Fri, 15 Sep 2017 23:21:56 -0600 Subject: [PATCH 01/14] implement `let` --- hy/contrib/walk.hy | 262 ++++++++++++++++++++++++++++- tests/native_tests/contrib/walk.hy | 249 ++++++++++++++++++++++++++- 2 files changed, 504 insertions(+), 7 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 48c5f33..73e556c 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -4,7 +4,8 @@ ;; license. See the LICENSE. (import [hy [HyExpression HyDict]] - [functools [partial]]) + [functools [partial]] + [collections [OrderedDict]]) (defn walk [inner outer form] "Traverses form, an arbitrary data structure. Applies inner to each @@ -34,10 +35,259 @@ sub-form, uses f's return value in place of the original." (walk (partial prewalk f) identity (f form))) +(defn call? [form] + "Checks whether form is a non-empty HyExpression" + (and (instance? HyExpression form) + form)) + (defn macroexpand-all [form] "Recursively performs all possible macroexpansions in form." - (prewalk (fn [x] - (if (instance? HyExpression x) - (macroexpand x) - x)) - form)) + (setv quote-level [0]) ; TODO: make nonlocal after dropping Python2 + (defn traverse [form] + (walk expand identity form)) + (defn expand [form] + ;; manages quote levels + (defn +quote [&optional [x 1]] + (setv head (first form)) + (+= (get quote-level 0) x) + (when (neg? (get quote-level 0)) + (raise (TypeError "unquote outside of quasiquote"))) + (setv res (traverse (cut form 1))) + (-= (get quote-level 0) x) + `(~head ~@res)) + (if (call? form) + (cond [(get quote-level 0) + (cond [(in (first form) '[unquote unquote-splice]) + (+quote -1)] + [(= (first form) 'quasiquote) (+quote)] + [True (traverse form)])] + [(= (first form) 'quote) form] + [(= (first form) 'quasiquote) (+quote)] + [True (traverse (macroexpand form))]) + (if (coll? form) + (traverse form) + form))) + (expand form)) + +(setv special-forms (list-comp k + [k (.keys hy.compiler._compile-table)] + (isinstance k hy._compat.string-types))) + + +(defn lambda-list [form] + " +splits a fn argument list into sections based on &-headers. + +returns an OrderedDict mapping headers to sublists. +Arguments without a header are under None. +" + (setv headers '[&optional &key &rest &kwonly &kwargs] + sections (OrderedDict [(, None [])]) + header None) + (for [arg form] + (if (in arg headers) + (do (setv header arg) + (assoc sections header []) + ;; Don't use a header more than once. It's the compiler's problem. + (.remove headers header)) + (.append (get sections header) arg))) + sections) + +(defmacro let [bindings &rest body] + " +sets up lexical bindings in its body + +Bindings are processed sequentially, +so you can use the result of a earlier binding in a later one. + +Basic assignments (e.g. setv, +=) will update the let binding, +if they use the name of a let binding. + +But assignments via `import` are always hoisted to normal Python scope, and +likewise, `defclass` will assign the class to the Python scope, +even if it shares the name of a let binding. + +Use __import__ and type (or whatever metaclass) instead, +if you must avoid this hoisting. + +Function arguments can shadow let bindings in their body, +as can nested let forms. +" + ;; I'd use defmacro/g!, but it loses the docstring hylang/hy#1424 + (setv g!let (gensym 'let)) + (if (odd? (len bindings)) + (macro-error bindings "let bindings must be paired")) + ;; pre-expanding the body means we only have to worry about a small number + ;; of special forms + (setv body (macroexpand-all body) + bound-symbols (cut bindings None None 2) + quote-level [0]) + (for [k bound-symbols] + (if-not (symbol? k) + (macro-error k "let can only bind to symbols") + (if (in '. k) + (macro-error k "let binding symbols may not contain a dot")))) + ;; sets up the recursion call + (defn expand-symbols [protected-symbols form] + (defn traverse [form &optional [protected-symbols protected-symbols]] + (walk (partial expand-symbols protected-symbols) + identity + form)) + ;; manages quote levels + (defn +quote [&optional [x 1]] + (setv head (first form)) + (+= (get quote-level 0) x) + (setv res (traverse (cut form 1))) + (-= (get quote-level 0) x) + `(~head ~@res)) + (cond [(get quote-level 0) ; don't expand symbols in quotations + (if (call? form) + (cond [(in (first form) '[unquote unquote-splice]) + (+quote -1)] + [(= (first form) 'quasiquote) + (+quote)] + [True (traverse form)]) + (if (coll? form) + (traverse form) + form))] + ;; symbol expansions happen here. + [(symbol? form) + (if (and form + (not (.startswith form '.)) + (in '. form)) + ;; convert dotted names to the standard special form + (expand-symbols protected-symbols + `(. ~@(map HySymbol (.split form '.)))) + ;; else expand if applicable + (if (and (in form bound-symbols) + (not-in form protected-symbols)) + `(get ~g!let ~(name form)) + form))] + ;; We have to treat special forms differently. + ;; Quotation should suppress symbol expansion, + ;; and local bindings should shadow those made by let. + [(call? form) + (setv head (first form)) + (setv tail (cut form 1)) + (cond [(in head '[fn fn*]) + ;; TODO: handle globals, locals + (setv body (cut tail 1) + protected #{} + fn-bindings `[]) + (for [[header section] (-> tail first lambda-list .items)] + (if header (.append fn-bindings header)) + (cond [(in header [None '&rest '&kwargs]) + (.update protected (-> section flatten set)) + (.extend fn-bindings section)] + [(in header '[&optional &kwonly]) + (for [pair section] + (cond [(coll? pair) + (.add protected (first pair)) + (.append fn-bindings + `[~(first pair) + ~(expand-symbols protected-symbols + (second pair))])] + [True + (.add protected pair) + (.append fn-bindings pair)]))] + [(= header '&key) + (setv &key-dict '{}) + (for [[k v] (-> section first partition)] + (.add protected k) + (.append &key-dict k) + (.append &key-dict (expand-symbols protected-symbols + v))) + (.append fn-bindings &key-dict)])) + `(~head ~fn-bindings + ~@(traverse body (| protected protected-symbols)))] + [(= head 'except) + ;; protect the "as" name binding the exception + `(~head ~@(traverse tail (| protected-symbols + (if (and tail + (-> tail + first + len + (= 2))) + #{(first (first tail))} + #{}))))] + [(= head ".") + `(. ~@(walk (fn [form] + (if (symbol? form) + form ; don't expand attrs + (expand-symbols protected-symbols + form))) + identity + tail))] + [(in head '[import quote]) form] + [(= head 'defclass) + ;; don't expand the name of the class + `(~head ~(first tail) ~@(traverse (cut tail 1)))] + [(= head 'quasiquote) (+quote)] + ;; don't expand other special form symbols in head position + [(in head special-forms) `(~head ~@(traverse tail))] + ;; Not a special form. Traverse it like a coll + [True (traverse form)])] + [(coll? form) (traverse form)] + ;; recursive base case--it's an atom. Put it back. + [True form])) + (expand-symbols #{} + `(do + (setv ~g!let {} + ~@bindings) + ~@body))) + +#_[special cases for let + ;; this means we can't use a list for our let scope + ;; we're using a dict instead. + 'del', + + ;; Symbols containing a dot should be converted to this form. + ;; attrs should not get expanded, + ;; but [] lookups should. + '.', + + ;;; can shadow let bindings with Python locals + ;; protect its bindings for the lexical scope of its body. + 'fn', + 'fn*', + ;; protect as bindings for the lexical scope of its body + 'except', + + ;;; changes scope of named variables + ;; protect the variables they name for the lexical scope of their container + 'global', + 'nonlocal', + ;; should we provide a protect form? + ;; it's an anaphor only valid in a `let` body. + ;; this would make the named variables python-scoped in its body + ;; expands to a do + 'protect', + + ;;; quoted variables must not be expanded. + ;; but unprotected, unquoted variables must be. + 'quasiquote', + 'quote', + 'unquote', + 'unquote-splice', + + ;;;; deferred + + ;; should really only exist at toplevel. Ignore until someone complains? + ;; raise an error? treat like fn? + ;; should probably be implemented as macros in terms of fn/setv anyway. + 'defmacro', + 'deftag', + + ;;; create Python-scoped variables. It's probably hard to avoid this. + ;; Best just doc this behavior for now. + ;; we can't avoid clobbering enclosing python scope, unless we use a gensym, + ;; but that corrupts '__name__'. + ;; It could be set later, but that could mess up metaclasses! + ;; Should the class name update let variables too? + 'defclass', + ;; should this update let variables? + ;; it could be done with gensym/setv. + 'import', + + ;; I don't understand these. Ignore until someone complains? + 'eval_and_compile', 'eval_when_compile', 'require',] diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index d104c45..caf9151 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -3,6 +3,7 @@ ;; license. See the LICENSE. (import [hy.contrib.walk [*]]) +(require [hy.contrib.walk [*]]) (def walk-form '(print {"foo" "bar" "array" [1 2 3 [4]] @@ -35,5 +36,251 @@ [[2 [3 [4]] 2 [3 [4]]]]))) (defn test-macroexpand-all [] + (assert (= (macroexpand-all '(with [a 1])) + '(with* [a 1] (do)))) (assert (= (macroexpand-all '(with [a 1 b 2 c 3] (for [d c] foo))) - '(with* [a 1] (with* [b 2] (with* [c 3] (do (for* [d c] (do foo))))))))) + '(with* [a 1] (with* [b 2] (with* [c 3] (do (for* [d c] (do foo)))))))) + (assert (= (macroexpand-all '(with [a 1] + '(with [b 2]) + `(with [c 3] + ~(with [d 4]) + ~@[(with [e 5])]))) + '(with* [a 1] + (do '(with [b 2]) + `(with [c 3] + ~(with* [d 4] (do)) + ~@[(with* [e 5] (do))])))))) + +(defn test-let-basic [] + (assert (zero? (let [a 0] a))) + (setv a "a" + b "b") + (let [a "x" + b "y"] + (assert (= (+ a b) + "xy")) + (let [a "z"] + (assert (= (+ a b) + "zy"))) + ;; let-shadowed variable doesn't get clobbered. + (assert (= (+ a b) + "xy"))) + (let [q "q"] + (assert (= q "q"))) + (assert (= a "a")) + (assert (= b "b")) + (assert (in "a" (.keys (vars)))) + ;; scope of q is limited to let body + (assert (not-in "q" (.keys (vars))))) + +(defn test-let-sequence [] + (let [a "a" + b "b" + ab (+ a b)] + (assert (= ab "ab")) + (let [c "c" + abc (+ ab c)] + (assert (= abc "abc"))))) + +(defn test-let-special [] + ;; special forms in function position still work as normal + (let [, 1] + (assert (= (, , ,) + (, 1 1))))) + +(defn test-let-quasiquote [] + (setv a-symbol 'a) + (let [a "x"] + (assert (= a "x")) + (assert (= 'a a-symbol)) + (assert (= `a a-symbol)) + (assert (= `(foo ~a) + '(foo "x"))) + (assert (= `(foo `(bar a ~a ~~a)) + '(foo `(bar a ~a ~"x")))) + (assert (= `(foo ~@[a]) + '(foo "x"))) + (assert (= `(foo `(bar [a] ~@[a] ~@~[a 'a `a] ~~@[a])) + '(foo `(bar [a] ~@[a] ~@["x" a a] ~"x")))))) + +(defn test-let-except [] + (let [foo 42 + bar 33] + (assert (= foo 42)) + (try + (do + 1/0 + (assert False)) + (except [foo Exception] + ;; let bindings should work in except block + (assert (= bar 33)) + ;; but exception bindings can shadow let bindings + (assert (instance? Exception foo)))) + ;; let binding did not get clobbered. + (assert (= foo 42)))) + +(defn test-let-mutation [] + (setv foo 42) + (setv error False) + (let [foo 12 + bar 13] + (assert (= foo 12)) + (setv foo 14) + (assert (= foo 14)) + (del foo) + ;; deleting a let binding should not affect others + (assert (= bar 13)) + (try + ;; foo=42 is still shadowed, but the let binding was deleted. + (do + foo + (assert False)) + (except [ke LookupError] + (setv error ke))) + (setv foo 16) + (assert (= foo 16)) + (setv [foo bar baz] [1 2 3]) + (assert (= foo 1)) + (assert (= bar 2)) + (assert (= baz 3))) + (assert error) + (assert (= foo 42)) + (assert (= baz 3))) + +(defn test-let-break [] + (for [x (range 3)] + (let [done (odd? x)] + (if done (break)))) + (assert (= x 1))) + +(defn test-let-yield [] + (defn grind [] + (yield 0) + (let [a 1 + b 2] + (yield a) + (yield b))) + (assert (= (tuple (grind)) + (, 0 1 2)))) + +(defn test-let-return [] + (defn get-answer [] + (let [answer 42] + (return answer))) + (assert (= (get-answer) + 42))) + +(defn test-let-import [] + (let [types 6] + ;; imports don't fail, even if using a let-bound name + (import types) + ;; let-bound name is not affected + (assert (= types 6))) + ;; import happened in Python scope. + (assert (in "types" (vars))) + (assert (instance? types.ModuleType types))) + +(defn test-let-defclass [] + (let [Foo 42 + quux object] + ;; the name of the class is just a symbol, even if it's a let binding + (defclass Foo [quux] ; let bindings apply in inheritance list + ;; let bindings apply inside class body + (setv x Foo))) + ;; defclass always creates a python-scoped variable, even if it's a let binding name + (assert (= Foo.x 42))) + +(defn test-let-dot [] + (setv foo (fn []) + foo.a 42) + (let [a 1 + b []] + (assert (= a 1)) + (assert (= b [])) + ;; method syntax not affected + (.append b 2) + (assert (= b [2])) + ;; attrs access is not affected + (assert (= foo.a 42)) + (assert (= (. foo a) + 42)) + ;; but indexing is + (assert (= (. [1 2 3] + [a]) + 2)))) + +(defn test-let-positional [] + (let [a 0 + b 1 + c 2] + (defn foo [a b] + (, a b c)) + (assert (= (foo 100 200) + (, 100 200 2))) + (setv c 300) + (assert (= (foo 1000 2000) + (, 1000 2000 300))) + (assert (= a 0)) + (assert (= b 1)) + (assert (= c 300)))) + +(defn test-let-rest [] + (let [xs 6 + a 88 + c 64 + &rest 12] + (defn foo [a b &rest xs] + (-= a 1) + (-= c 1) + (setv xs (list xs)) + (.append xs 42) + (, &rest a b c xs)) + (assert (= xs 6)) + (assert (= a 88)) + (assert (= (foo 1 2 3 4) + (, 12 0 2 63 [3 4 42]))) + (assert (= xs 6)) + (assert (= c 63)) + (assert (= a 88)))) + +(defn test-let-kwargs [] + (let [kws 6 + &kwargs 13] + (defn foo [&kwargs kws] + (, &kwargs kws)) + (assert (= kws 6)) + (assert (= (foo :a 1) + (, 13 {"a" 1}))))) + +(defn test-let-optional [] + (let [a 1 + b 6 + d 2] + (defn foo [&optional [a a] b [c d]] + (, a b c)) + (assert (= (foo) + (, 1 None 2))) + (assert (= (foo 10 20 30) + (, 10 20 30))))) + +(defn test-let-key [] + (let [a 1 + b 6 + d 2] + (defn foo [&key {a a b None c d}] + (, a b c)) + (assert (= (foo) + (, 1 None 2))) + (assert (= (foo 10 20 30) + (, 10 20 30))) + (assert (= (, a b d) + (, 1 6 2))))) +;; TODO +;; test-let-continue +;; test-let-closure +;; test-let-global + +;; TODO +;;; Python 3 only +;; test-let-nonlocal +;; test-let-kwonly From ba898aa8d8ef616bf36b0cf0b1c0ca6382ccf057 Mon Sep 17 00:00:00 2001 From: gilch Date: Mon, 18 Sep 2017 13:50:41 -0600 Subject: [PATCH 02/14] support (nonlocal) in `let` --- hy/contrib/walk.hy | 9 ++---- tests/native_tests/contrib/walk.hy | 36 +++++++++++++++-------- tests/native_tests/py3_only_tests.hy | 43 ++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 73e556c..2d2a202 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -161,7 +161,7 @@ as can nested let forms. ;; else expand if applicable (if (and (in form bound-symbols) (not-in form protected-symbols)) - `(get ~g!let ~(name form)) + (HySymbol (+ g!let "::" form)) form))] ;; We have to treat special forms differently. ;; Quotation should suppress symbol expansion, @@ -232,15 +232,10 @@ as can nested let forms. [True form])) (expand-symbols #{} `(do - (setv ~g!let {} - ~@bindings) + (setv ~@bindings) ~@body))) #_[special cases for let - ;; this means we can't use a list for our let scope - ;; we're using a dict instead. - 'del', - ;; Symbols containing a dot should be converted to this form. ;; attrs should not get expanded, ;; but [] lookups should. diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index caf9151..1a7b4e3 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -135,8 +135,8 @@ (do foo (assert False)) - (except [ke LookupError] - (setv error ke))) + (except [ne NameError] + (setv error ne))) (setv foo 16) (assert (= foo 16)) (setv [foo bar baz] [1 2 3]) @@ -153,6 +153,14 @@ (if done (break)))) (assert (= x 1))) +(defn test-let-continue [] + (let [foo []] + (for [x (range 10)] + (let [odd (odd? x)] + (if odd (continue)) + (.append foo x))) + (assert (= foo [0 2 4 6 8])))) + (defn test-let-yield [] (defn grind [] (yield 0) @@ -231,16 +239,15 @@ &rest 12] (defn foo [a b &rest xs] (-= a 1) - (-= c 1) (setv xs (list xs)) (.append xs 42) (, &rest a b c xs)) (assert (= xs 6)) (assert (= a 88)) (assert (= (foo 1 2 3 4) - (, 12 0 2 63 [3 4 42]))) + (, 12 0 2 64 [3 4 42]))) (assert (= xs 6)) - (assert (= c 63)) + (assert (= c 64)) (assert (= a 88)))) (defn test-let-kwargs [] @@ -275,12 +282,17 @@ (, 10 20 30))) (assert (= (, a b d) (, 1 6 2))))) -;; TODO -;; test-let-continue -;; test-let-closure -;; test-let-global + +(defn test-let-closure [] + (let [count [0]] + (defn +count [&optional [x 1]] + (+= (get count 0) x) + (get count 0))) + ;; let bindings can still exist outside of a let body + (assert (= 1 (+count))) + (assert (= 2 (+count))) + (assert (= 42 (+count 40)))) ;; TODO -;;; Python 3 only -;; test-let-nonlocal -;; test-let-kwonly +;; test-let-global + diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy index 84b8053..90331fd 100644 --- a/tests/native_tests/py3_only_tests.hy +++ b/tests/native_tests/py3_only_tests.hy @@ -8,8 +8,8 @@ (defn test-exception-cause [] (try (raise ValueError :from NameError) - (except [e [ValueError]] - (assert (= (type (. e __cause__)) NameError))))) + (except [e [ValueError]] + (assert (= (type (. e __cause__)) NameError))))) (defn test-kwonly [] @@ -21,8 +21,8 @@ ;; keyword-only without default ... (defn kwonly-foo-no-default [&kwonly foo] foo) (setv attempt-to-omit-default (try - (kwonly-foo-no-default) - (except [e [Exception]] e))) + (kwonly-foo-no-default) + (except [e [Exception]] e))) ;; works (assert (= (kwonly-foo-no-default :foo "quux") "quux")) ;; raises TypeError with appropriate message if not supplied @@ -64,9 +64,36 @@ (assert 0)) (defn yield-from-test [] (for* [i (range 3)] - (yield i)) + (yield i)) (try - (yield-from (yield-from-subgenerator-test)) - (except [e AssertionError] - (yield 4)))) + (yield-from (yield-from-subgenerator-test)) + (except [e AssertionError] + (yield 4)))) (assert (= (list (yield-from-test)) [0 1 2 1 2 3 4]))) + +(require [hy.contrib.walk [let]]) +(defn test-let-nonlocal [] + (let [a 88 + c 64] + (defn foo [a b] + (nonlocal c) + (-= a 1) + (-= c 1) + (, a b c)) + (assert (= a 88)) + (assert (= (foo 1 2) + (, 0 2 63))) + (assert (= c 63)) + (assert (= a 88)))) + +(defn test-let-optional [] + (let [a 1 + b 6 + d 2] + (defn foo [&kwonly [a a] b [c d]] + (, a b c)) + (assert (= (foo :b "b") + (, 1 "b" 2))) + (assert (= (foo :b 20 :a 10 :c 30) + (, 10 20 30))))) + From 20b4342d405cba9292b556003c8ee1909f777b4f Mon Sep 17 00:00:00 2001 From: gilch Date: Mon, 18 Sep 2017 14:44:24 -0600 Subject: [PATCH 03/14] support (global) in `let` --- hy/contrib/walk.hy | 4 +++- tests/native_tests/contrib/walk.hy | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 2d2a202..4a67555 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -170,7 +170,6 @@ as can nested let forms. (setv head (first form)) (setv tail (cut form 1)) (cond [(in head '[fn fn*]) - ;; TODO: handle globals, locals (setv body (cut tail 1) protected #{} fn-bindings `[]) @@ -218,6 +217,9 @@ as can nested let forms. form))) identity tail))] + [(= head 'global) + (.update protected-symbols (set tail)) + form] [(in head '[import quote]) form] [(= head 'defclass) ;; don't expand the name of the class diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index 1a7b4e3..66ad7f7 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -293,6 +293,21 @@ (assert (= 2 (+count))) (assert (= 42 (+count 40)))) -;; TODO -;; test-let-global +(defn test-let-global [] + (setv (get (globals) + 'let-global) + "global") + (let [let-global 1] + (assert (= let-global 1)) + (defn foo [] + (assert (= let-global 1)) + (global let-global) + (assert (= let-global "global")) + (setv let-global "mutated") + (assert (= let-global "mutated"))) + (foo) + (assert (= let-global 1)) + (assert (= (get (globals) + 'let-global) + "mutated")))) From bcc93fb1fec49fd0031a5dbb060fc4a41e48be1b Mon Sep 17 00:00:00 2001 From: gilch Date: Mon, 18 Sep 2017 18:18:45 -0600 Subject: [PATCH 04/14] refactor let symbol expansion to a class --- hy/contrib/walk.hy | 282 +++++++++++++++++++++++++++------------------ 1 file changed, 171 insertions(+), 111 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 4a67555..1c8923c 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -93,6 +93,169 @@ Arguments without a header are under None. (.append (get sections header) arg))) sections) +(defclass SymbolExpander[] + (defn expand-symbols [self form &optional protected quote-level] + (if (none? protected) + (setv protected self.protected)) + (if (none? quote-level) + (setv quote-level self.quote-level)) + (.expand (SymbolExpander form self.expander protected quote-level))) + + (defn __init__ [self form expander + &optional + [protected (frozenset)] + [quote-level 0]] + (setv self.form form + self.expander expander + self.protected protected + self.quote-level quote-level)) + + (defn traverse [self form &optional protected quote-level] + (if (none? protected) + (setv protected self.protected)) + (if (none? quote-level) + (setv quote-level self.quote-level)) + (walk (partial self.expand-symbols + :protected protected + :quote-level quote-level) + identity + form)) + + ;; manages quote levels + (defn +quote [self &optional [x 1]] + `(~(self.head) ~@(self.traverse (self.tail) + :quote-level (+ self.quote-level x)))) + + (defn handle-dot [self] + `(. ~@(walk (fn [form] + (if (symbol? form) + form ; don't expand attrs + (self.expand-symbols form))) + identity + (self.tail)))) + + (defn head [self] + (first self.form)) + + (defn tail [self] + (cut self.form 1)) + + (defn handle-except [self] + (setv tail (self.tail)) + ;; protect the "as" name binding the exception + `(~(self.head) ~@(self.traverse tail (| self.protected + (if (and tail + (-> tail + first + len + (= 2))) + #{(first (first tail))} + #{}))))) + (defn handle-args-list [self] + (setv protected #{} + argslist `[]) + (for [[header section] (-> self (.tail) first lambda-list .items)] + (if header (.append argslist header)) + (cond [(in header [None '&rest '&kwargs]) + (.update protected (-> section flatten set)) + (.extend argslist section)] + [(in header '[&optional &kwonly]) + (for [pair section] + (cond [(coll? pair) + (.add protected (first pair)) + (.append argslist + `[~(first pair) + ~(self.expand-symbols (second pair))])] + [True + (.add protected pair) + (.append argslist pair)]))] + [(= header '&key) + (setv &key-dict '{}) + (for [[k v] (-> section first partition)] + (.add protected k) + (.append &key-dict k) + (.append &key-dict (self.expand-symbols v))) + (.append argslist &key-dict)])) + (, protected argslist)) + + (defn handle-fn [self] + (setv [protected argslist] (self.handle-args-list)) + `(~(self.head) ~argslist + ~@(self.traverse (cut (self.tail) 1)(| protected self.protected)))) + + ;; don't expand symbols in quotations + (defn handle-quoted [self] + (if (call? self.form) + (if (in (self.head) '[unquote unquote-splice]) (self.+quote -1) + (= (self.head) 'quasiquote) (self.+quote) + (self.handle-coll)) + (if (coll? self.form) + (self.handle-coll) + (self.handle-base)))) + + ;; convert dotted names to the standard special form + (defn convert-dotted-symbol [self] + (self.expand-symbols `(. ~@(map HySymbol (.split self.form '.))))) + + (defn expand-symbol [self] + (if (not-in self.form self.protected) + (self.expander self.form) + (self.handle-base))) + + (defn handle-symbol [self] + (if (and self.form + (not (.startswith self.form '.)) + (in '. self.form)) + (self.convert-dotted-symbol) + (self.expand-symbol))) + + (defn handle-global [self] + (.update self.protected (set (self.tail))) + (self.handle-base)) + + (defn handle-defclass [self] + ;; don't expand the name of the class + `(~(self.head) ~(first (self.tail)) + ~@(self.traverse (cut (self.tail) 1)))) + + (defn handle-special-form [self] + ;; don't expand other special form symbols in head position + `(~(self.head) ~@(self.traverse (self.tail)))) + + (defn handle-base [self] + self.form) + + (defn handle-coll [self] + ;; recursion + (self.traverse self.form)) + + ;; We have to treat special forms differently. + ;; Quotation should suppress symbol expansion, + ;; and local bindings should shadow those made by let. + (defn handle-call [self] + (setv head (first self.form)) + (if (in head '[fn fn*]) (self.handle-fn) + (in head '[import quote]) (self.handle-base) + (= head 'except) (self.handle-except) + (= head ".") (self.handle-dot) + (= head 'global) (self.handle-global) + (= head 'defclass) (self.handle-defclass) + (= head 'quasiquote) (self.+quote) + ;; must be checked last! + (in head special-forms) (self.handle-special-form) + ;; Not a special form. Traverse it like a coll + (self.handle-coll))) + + (defn expand [self] + "the main entry point. Call this to do the expansion" + (setv form self.form) + (if self.quote-level (self.handle-quoted) + (symbol? form) (self.handle-symbol) + (call? form) (self.handle-call) + (coll? form) (self.handle-coll) + ;; recursive base case--it's an atom. Put it back. + (self.handle-base)))) + (defmacro let [bindings &rest body] " sets up lexical bindings in its body @@ -120,122 +283,19 @@ as can nested let forms. ;; pre-expanding the body means we only have to worry about a small number ;; of special forms (setv body (macroexpand-all body) - bound-symbols (cut bindings None None 2) - quote-level [0]) + bound-symbols (cut bindings None None 2)) (for [k bound-symbols] (if-not (symbol? k) (macro-error k "let can only bind to symbols") (if (in '. k) (macro-error k "let binding symbols may not contain a dot")))) - ;; sets up the recursion call - (defn expand-symbols [protected-symbols form] - (defn traverse [form &optional [protected-symbols protected-symbols]] - (walk (partial expand-symbols protected-symbols) - identity - form)) - ;; manages quote levels - (defn +quote [&optional [x 1]] - (setv head (first form)) - (+= (get quote-level 0) x) - (setv res (traverse (cut form 1))) - (-= (get quote-level 0) x) - `(~head ~@res)) - (cond [(get quote-level 0) ; don't expand symbols in quotations - (if (call? form) - (cond [(in (first form) '[unquote unquote-splice]) - (+quote -1)] - [(= (first form) 'quasiquote) - (+quote)] - [True (traverse form)]) - (if (coll? form) - (traverse form) - form))] - ;; symbol expansions happen here. - [(symbol? form) - (if (and form - (not (.startswith form '.)) - (in '. form)) - ;; convert dotted names to the standard special form - (expand-symbols protected-symbols - `(. ~@(map HySymbol (.split form '.)))) - ;; else expand if applicable - (if (and (in form bound-symbols) - (not-in form protected-symbols)) - (HySymbol (+ g!let "::" form)) - form))] - ;; We have to treat special forms differently. - ;; Quotation should suppress symbol expansion, - ;; and local bindings should shadow those made by let. - [(call? form) - (setv head (first form)) - (setv tail (cut form 1)) - (cond [(in head '[fn fn*]) - (setv body (cut tail 1) - protected #{} - fn-bindings `[]) - (for [[header section] (-> tail first lambda-list .items)] - (if header (.append fn-bindings header)) - (cond [(in header [None '&rest '&kwargs]) - (.update protected (-> section flatten set)) - (.extend fn-bindings section)] - [(in header '[&optional &kwonly]) - (for [pair section] - (cond [(coll? pair) - (.add protected (first pair)) - (.append fn-bindings - `[~(first pair) - ~(expand-symbols protected-symbols - (second pair))])] - [True - (.add protected pair) - (.append fn-bindings pair)]))] - [(= header '&key) - (setv &key-dict '{}) - (for [[k v] (-> section first partition)] - (.add protected k) - (.append &key-dict k) - (.append &key-dict (expand-symbols protected-symbols - v))) - (.append fn-bindings &key-dict)])) - `(~head ~fn-bindings - ~@(traverse body (| protected protected-symbols)))] - [(= head 'except) - ;; protect the "as" name binding the exception - `(~head ~@(traverse tail (| protected-symbols - (if (and tail - (-> tail - first - len - (= 2))) - #{(first (first tail))} - #{}))))] - [(= head ".") - `(. ~@(walk (fn [form] - (if (symbol? form) - form ; don't expand attrs - (expand-symbols protected-symbols - form))) - identity - tail))] - [(= head 'global) - (.update protected-symbols (set tail)) - form] - [(in head '[import quote]) form] - [(= head 'defclass) - ;; don't expand the name of the class - `(~head ~(first tail) ~@(traverse (cut tail 1)))] - [(= head 'quasiquote) (+quote)] - ;; don't expand other special form symbols in head position - [(in head special-forms) `(~head ~@(traverse tail))] - ;; Not a special form. Traverse it like a coll - [True (traverse form)])] - [(coll? form) (traverse form)] - ;; recursive base case--it's an atom. Put it back. - [True form])) - (expand-symbols #{} - `(do - (setv ~@bindings) - ~@body))) + (.expand (SymbolExpander `(do + (setv ~@bindings) + ~@body) + (fn [symbol] + (if (in symbol bound-symbols) + (HySymbol (+ g!let "::" symbol)) + symbol))))) #_[special cases for let ;; Symbols containing a dot should be converted to this form. From 081a6e2575f3a2fa3dcbab8307a73adf74b61fe5 Mon Sep 17 00:00:00 2001 From: gilch Date: Mon, 18 Sep 2017 23:01:13 -0600 Subject: [PATCH 05/14] factor out smacrolet from let --- hy/contrib/walk.hy | 78 +++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 1c8923c..e4fe774 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -93,29 +93,35 @@ Arguments without a header are under None. (.append (get sections header) arg))) sections) + +(defn symbolexpand [form expander + &optional + [protected (frozenset)] + [quote-level 0]] + (.expand (SymbolExpander form expander protected quote-level))) + (defclass SymbolExpander[] + + (defn __init__ [self form expander protected quote-level] + (setv self.form form + self.expander expander + self.protected protected + self.quote-level quote-level)) + (defn expand-symbols [self form &optional protected quote-level] (if (none? protected) (setv protected self.protected)) (if (none? quote-level) (setv quote-level self.quote-level)) - (.expand (SymbolExpander form self.expander protected quote-level))) - - (defn __init__ [self form expander - &optional - [protected (frozenset)] - [quote-level 0]] - (setv self.form form - self.expander expander - self.protected protected - self.quote-level quote-level)) + (symbolexpand form self.expander protected quote-level)) (defn traverse [self form &optional protected quote-level] (if (none? protected) (setv protected self.protected)) (if (none? quote-level) (setv quote-level self.quote-level)) - (walk (partial self.expand-symbols + (walk (partial symbolexpand + :expander self.expander :protected protected :quote-level quote-level) identity @@ -256,6 +262,26 @@ Arguments without a header are under None. ;; recursive base case--it's an atom. Put it back. (self.handle-base)))) +(defmacro smacrolet [bindings &rest body] + " +symbol macro let. + +Replaces symbols in body, but only where it would be a valid let binding. +The bindings pairs the target symbol and the expansion form for that symbol. +" + (if (odd? (len bindings)) + (macro-error bindings "bindings must be paired")) + (for [k (cut bindings None None 2)] + (if-not (symbol? k) + (macro-error k "bind targets must be symbols") + (if (in '. k) + (macro-error k "binding target may not contain a dot")))) + (setv bindings (dict (partition bindings)) + body (macroexpand-all body)) + (symbolexpand `(do ~@body) + (fn [symbol] + (.get bindings symbol symbol)))) + (defmacro let [bindings &rest body] " sets up lexical bindings in its body @@ -276,26 +302,22 @@ if you must avoid this hoisting. Function arguments can shadow let bindings in their body, as can nested let forms. " - ;; I'd use defmacro/g!, but it loses the docstring hylang/hy#1424 - (setv g!let (gensym 'let)) + (setv g!let (gensym 'let) + symbols (cut bindings None None 2)) (if (odd? (len bindings)) (macro-error bindings "let bindings must be paired")) - ;; pre-expanding the body means we only have to worry about a small number - ;; of special forms - (setv body (macroexpand-all body) - bound-symbols (cut bindings None None 2)) - (for [k bound-symbols] + (for [k symbols] (if-not (symbol? k) - (macro-error k "let can only bind to symbols") - (if (in '. k) - (macro-error k "let binding symbols may not contain a dot")))) - (.expand (SymbolExpander `(do - (setv ~@bindings) - ~@body) - (fn [symbol] - (if (in symbol bound-symbols) - (HySymbol (+ g!let "::" symbol)) - symbol))))) + (macro-error k "let can only bind to symbols"))) + (macroexpand + `(smacrolet [~@(interleave symbols + (genexpr (HySymbol (+ g!let "::" symbol)) + [symbol symbols]))] + (do + (setv ~@bindings) + ~@body)))) + +;; (defmacro macrolet []) #_[special cases for let ;; Symbols containing a dot should be converted to this form. From 5bbf4d9894a1880e5dc30d308a2bf684ceb70345 Mon Sep 17 00:00:00 2001 From: gilch Date: Fri, 22 Sep 2017 20:07:48 -0600 Subject: [PATCH 06/14] fix premature binding in `let` --- hy/contrib/walk.hy | 60 ++++++++++++++++-------------- tests/native_tests/contrib/walk.hy | 44 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index e4fe774..485d169 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -5,7 +5,9 @@ (import [hy [HyExpression HyDict]] [functools [partial]] - [collections [OrderedDict]]) + [collections [OrderedDict]] + [hy.macros [macroexpand :as mexpand]] + [hy.compiler [HyASTCompiler]]) (defn walk [inner outer form] "Traverses form, an arbitrary data structure. Applies inner to each @@ -40,9 +42,10 @@ (and (instance? HyExpression form) form)) -(defn macroexpand-all [form] +(defn macroexpand-all [form &optional module-name] "Recursively performs all possible macroexpansions in form." - (setv quote-level [0]) ; TODO: make nonlocal after dropping Python2 + (setv module-name (or module-name (calling-module-name)) + quote-level [0]) ; TODO: make nonlocal after dropping Python2 (defn traverse [form] (walk expand identity form)) (defn expand [form] @@ -56,17 +59,17 @@ (-= (get quote-level 0) x) `(~head ~@res)) (if (call? form) - (cond [(get quote-level 0) - (cond [(in (first form) '[unquote unquote-splice]) - (+quote -1)] - [(= (first form) 'quasiquote) (+quote)] - [True (traverse form)])] - [(= (first form) 'quote) form] - [(= (first form) 'quasiquote) (+quote)] - [True (traverse (macroexpand form))]) - (if (coll? form) - (traverse form) - form))) + (cond [(get quote-level 0) + (cond [(in (first form) '[unquote unquote-splice]) + (+quote -1)] + [(= (first form) 'quasiquote) (+quote)] + [True (traverse form)])] + [(= (first form) 'quote) form] + [(= (first form) 'quasiquote) (+quote)] + [True (traverse (mexpand form (HyASTCompiler module-name)))]) + (if (coll? form) + (traverse form) + form))) (expand form)) (setv special-forms (list-comp k @@ -262,7 +265,7 @@ Arguments without a header are under None. ;; recursive base case--it's an atom. Put it back. (self.handle-base)))) -(defmacro smacrolet [bindings &rest body] +(defmacro smacrolet [bindings &optional module-name &rest body] " symbol macro let. @@ -277,7 +280,7 @@ The bindings pairs the target symbol and the expansion form for that symbol. (if (in '. k) (macro-error k "binding target may not contain a dot")))) (setv bindings (dict (partition bindings)) - body (macroexpand-all body)) + body (macroexpand-all body (or module-name (calling-module-name)))) (symbolexpand `(do ~@body) (fn [symbol] (.get bindings symbol symbol)))) @@ -302,20 +305,23 @@ if you must avoid this hoisting. Function arguments can shadow let bindings in their body, as can nested let forms. " - (setv g!let (gensym 'let) - symbols (cut bindings None None 2)) (if (odd? (len bindings)) (macro-error bindings "let bindings must be paired")) - (for [k symbols] + (setv g!let (gensym 'let) + replacements (OrderedDict) + values []) + (defn expander [symbol] + (.get replacements symbol symbol)) + (for [[k v] (partition bindings)] (if-not (symbol? k) - (macro-error k "let can only bind to symbols"))) - (macroexpand - `(smacrolet [~@(interleave symbols - (genexpr (HySymbol (+ g!let "::" symbol)) - [symbol symbols]))] - (do - (setv ~@bindings) - ~@body)))) + (macro-error k "bind targets must be symbols") + (if (in '. k) + (macro-error k "binding target may not contain a dot"))) + (.append values (symbolexpand v expander)) + (assoc replacements k (HySymbol (+ g!let "::" k)))) + `(do + (setv ~@(interleave (.values replacements) values)) + ~@(symbolexpand (macroexpand-all body) expander))) ;; (defmacro macrolet []) diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index 66ad7f7..063cd49 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -5,6 +5,8 @@ (import [hy.contrib.walk [*]]) (require [hy.contrib.walk [*]]) +(import pytest) + (def walk-form '(print {"foo" "bar" "array" [1 2 3 [4]] "something" (+ 1 2 3 4) @@ -35,7 +37,13 @@ (drop 1 [1 [2 [3 [4]]]])) [[2 [3 [4]] 2 [3 [4]]]]))) +(defmacro foo-walk [] + 42) + (defn test-macroexpand-all [] + ;; make sure a macro from the current module works + (assert (= (macroexpand-all '(foo-walk)) + 42)) (assert (= (macroexpand-all '(with [a 1])) '(with* [a 1] (do)))) (assert (= (macroexpand-all '(with [a 1 b 2 c 3] (for [d c] foo))) @@ -74,6 +82,7 @@ (assert (not-in "q" (.keys (vars))))) (defn test-let-sequence [] + ;; assignments happen in sequence, not parallel. (let [a "a" b "b" ab (+ a b)] @@ -82,6 +91,20 @@ abc (+ ab c)] (assert (= abc "abc"))))) +(defn test-let-early [] + (setv a "a") + (let [q (+ a "x") + a 2 ; should not affect q + b 3] + (assert (= q "ax")) + (let [q (* a b) + a (+ a b) + b (* a b)] + (assert (= q 6)) + (assert (= a 5)) + (assert (= b 15)))) + (assert (= a "a"))) + (defn test-let-special [] ;; special forms in function position still work as normal (let [, 1] @@ -311,3 +334,24 @@ 'let-global) "mutated")))) +(defmacro triple [a] + (setv g!a (gensym a)) + `(do + (setv ~g!a ~a) + (+ ~g!a ~g!a ~g!a))) + +(defmacro ap-triple [] + '(+ a a a)) + +#@(pytest.mark.xfail + (defn test-let-macros [] + (let [a 1 + b (triple a) + c (ap-triple)] + (assert (= (triple a) + 3)) + (assert (= (ap-triple) + 3)) + (assert (= b 3)) + (assert (= c 3))))) + From a54f6aa38b58e5bcf47dd050845bfe899679f4bb Mon Sep 17 00:00:00 2001 From: gilch Date: Sat, 23 Sep 2017 12:55:44 -0600 Subject: [PATCH 07/14] add hidden &name parameter to macros The module name allows macros to preexpand their body in the proper context. --- hy/cmdline.py | 4 ++-- hy/compiler.py | 1 + hy/contrib/walk.hy | 6 ++++-- hy/macros.py | 4 ++-- tests/macros/test_macro_processor.py | 5 ++++- tests/native_tests/contrib/walk.hy | 21 ++++++++++----------- tests/resources/tlib.py | 4 ++-- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/hy/cmdline.py b/hy/cmdline.py index fb58539..b99855d 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -125,7 +125,7 @@ class HyREPL(code.InteractiveConsole): @macro("koan") -def koan_macro(): +def koan_macro(ETname): return HyExpression([HySymbol('print'), HyString(""" Ummon asked the head monk, "What sutra are you lecturing on?" @@ -143,7 +143,7 @@ def koan_macro(): @macro("ideas") -def ideas_macro(): +def ideas_macro(ETname): return HyExpression([HySymbol('print'), HyString(r""" diff --git a/hy/compiler.py b/hy/compiler.py index 953d5d8..5ce9073 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -2080,6 +2080,7 @@ class HyASTCompiler(object): for kw in ("&kwonly", "&kwargs", "&key"): if kw in expression[0]: raise HyTypeError(name, "macros cannot use %s" % kw) + expression[0].insert(0, HySymbol('&name')) new_expression = HyExpression([ HyExpression([HySymbol("hy.macros.macro"), name]), HyExpression([HySymbol("fn")] + expression), diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 485d169..60d6eca 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -37,6 +37,7 @@ sub-form, uses f's return value in place of the original." (walk (partial prewalk f) identity (f form))) +;; TODO: move to hy.core? (defn call? [form] "Checks whether form is a non-empty HyExpression" (and (instance? HyExpression form) @@ -72,6 +73,7 @@ form))) (expand form)) +;; TODO: move to hy.extra.reserved? (setv special-forms (list-comp k [k (.keys hy.compiler._compile-table)] (isinstance k hy._compat.string-types))) @@ -317,11 +319,11 @@ as can nested let forms. (macro-error k "bind targets must be symbols") (if (in '. k) (macro-error k "binding target may not contain a dot"))) - (.append values (symbolexpand v expander)) + (.append values (symbolexpand (macroexpand-all v &name) expander)) (assoc replacements k (HySymbol (+ g!let "::" k)))) `(do (setv ~@(interleave (.values replacements) values)) - ~@(symbolexpand (macroexpand-all body) expander))) + ~@(symbolexpand (macroexpand-all body &name) expander))) ;; (defmacro macrolet []) diff --git a/hy/macros.py b/hy/macros.py index 22bda2e..76b03de 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -189,14 +189,14 @@ def macroexpand_1(tree, compiler): try: m_copy = make_empty_fn_copy(m) - m_copy(*ntree[1:], **opts) + m_copy(compiler.module_name, *ntree[1:], **opts) except TypeError as e: msg = "expanding `" + str(tree[0]) + "': " msg += str(e).replace("()", "", 1).strip() raise HyMacroExpansionError(tree, msg) try: - obj = m(*ntree[1:], **opts) + obj = m(compiler.module_name, *ntree[1:], **opts) except HyTypeError as e: if e.expression is None: e.expression = tree diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py index 8efc8b8..1acad0b 100644 --- a/tests/macros/test_macro_processor.py +++ b/tests/macros/test_macro_processor.py @@ -10,9 +10,11 @@ from hy.errors import HyMacroExpansionError from hy.compiler import HyASTCompiler +import pytest + @macro("test") -def tmac(*tree): +def tmac(ETname, *tree): """ Turn an expression into a list """ return HyList(tree) @@ -42,6 +44,7 @@ def test_preprocessor_expression(): assert obj == macroexpand(obj, HyASTCompiler("")) +@pytest.mark.xfail def test_preprocessor_exceptions(): """ Test that macro expansion raises appropriate exceptions""" try: diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index 063cd49..4107ec2 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -343,15 +343,14 @@ (defmacro ap-triple [] '(+ a a a)) -#@(pytest.mark.xfail - (defn test-let-macros [] - (let [a 1 - b (triple a) - c (ap-triple)] - (assert (= (triple a) - 3)) - (assert (= (ap-triple) - 3)) - (assert (= b 3)) - (assert (= c 3))))) +(defn test-let-macros [] + (let [a 1 + b (triple a) + c (ap-triple)] + (assert (= (triple a) + 3)) + (assert (= (ap-triple) + 3)) + (assert (= b 3)) + (assert (= c 3)))) diff --git a/tests/resources/tlib.py b/tests/resources/tlib.py index bc214a0..a51029b 100644 --- a/tests/resources/tlib.py +++ b/tests/resources/tlib.py @@ -3,10 +3,10 @@ from hy import HyList, HyInteger @macro("qplah") -def tmac(*tree): +def tmac(ETname, *tree): return HyList((HyInteger(8), ) + tree) @macro("parald") -def tmac2(*tree): +def tmac2(ETname, *tree): return HyList((HyInteger(9), ) + tree) From d2e8537d3f4d4d32055156334422e532aed7d7e9 Mon Sep 17 00:00:00 2001 From: gilch Date: Sat, 23 Sep 2017 15:00:00 -0600 Subject: [PATCH 08/14] document `let` --- docs/contrib/walk.rst | 67 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/contrib/walk.rst b/docs/contrib/walk.rst index f3c4efe..e6044df 100644 --- a/docs/contrib/walk.rst +++ b/docs/contrib/walk.rst @@ -10,7 +10,7 @@ Functions .. _walk: walk ------ +---- Usage: `(walk inner outer form)` @@ -36,7 +36,7 @@ Example: 97 postwalk ---------- +-------- .. _postwalk: @@ -116,7 +116,7 @@ each sub-form, uses ``f`` 's return value in place of the original. HyInteger(7)])])])]) prewalk --------- +------- .. _prewalk: @@ -194,3 +194,64 @@ each sub-form, uses ``f`` 's return value in place of the original. HyInteger(6), HyList([ HyInteger(7)])])])]) + +macroexpand-all +--------------- + +Usage: `(macroexpand-all form &optional module-name)` + +Recursively performs all possible macroexpansions in form, using the ``require`` context of ``module-name``. +`macroexpand-all` assumes the calling module's context if unspecified. + +Macros +====== + +let +--- + +``let`` creates lexically-scoped names for local variables. +A let-bound name ceases to refer to that local outside the ``let`` form. +Arguments in nested functions and bindings in nested ``let`` forms can shadow these names. + +.. code-block:: hy + + => (let [x 5] ; creates a new local bound to name 'x + ... (print x) + ... (let [x 6] ; new local and name binding that shadows 'x + ... (print x)) + ... (print x)) ; 'x refers to the first local again + 5 + 6 + 5 + +The ``global`` special form changes the meaning of names to refer to the +module-level variables instead of locals, and this change still applies inside a ``let`` form, +even if a global has the same name as a let binding. + +Basic assignments (e.g. ``setv``, ``+=``) will update the local variable named by a let binding, +when they assign to a let-bound name. + +But assignments via ``import`` are always hoisted to normal Python scope, and +likewise, ``defclass`` will assign the class to the Python scope, +even if it shares the name of a let binding. + +Use ``__import__`` and ``type`` (or whatever metaclass) instead, +if you must avoid this hoisting. + +When used in a nested function, +nonlocal assignments to let-bound variables still require a ``nonlocal`` form. + +The ``let`` macro takes two parameters: a list defining *variables* +and the *body* which gets executed. *variables* is a vector of +variable and value pairs. + +``let`` executes the variable assignments one-by-one, in the order written. + +.. code-block:: hy + + => (let [x 5 + ... y (+ x 1)] + ... (print x y)) + 5 6 + + From 1fd0dc8fbe286ce974e78d852d5cd0875db02c21 Mon Sep 17 00:00:00 2001 From: gilch Date: Sat, 23 Sep 2017 15:09:44 -0600 Subject: [PATCH 09/14] add `let` to NEWS --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index cc19a6b..5740234 100644 --- a/NEWS +++ b/NEWS @@ -49,6 +49,7 @@ Changes from 0.13.0 [ Misc. Improvements ] * `read`, `read_str`, and `eval` are exposed and documented as top-level functions in the `hy` module + * Experimental `let` macro in `hy.contrib.walk` Changes from 0.12.1 From c4b3d7bcda7cedeee00f345867fc6835736d9505 Mon Sep 17 00:00:00 2001 From: gilch Date: Sat, 23 Sep 2017 23:10:23 -0600 Subject: [PATCH 10/14] fix gensym format to start with _ for import * --- hy/core/language.hy | 2 +- tests/native_tests/core.hy | 4 ++-- tests/native_tests/native_macros.hy | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/hy/core/language.hy b/hy/core/language.hy index e4aa531..fd67f15 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -224,7 +224,7 @@ (global _gensym_lock) (.acquire _gensym_lock) (try (do (setv _gensym_counter (inc _gensym_counter)) - (setv new_symbol (HySymbol (.format ":{0}_{1}" g _gensym_counter)))) + (setv new_symbol (HySymbol (.format "_;{0}|{1}" g _gensym_counter)))) (finally (.release _gensym_lock))) new_symbol) diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy index d9455ef..e5af611 100644 --- a/tests/native_tests/core.hy +++ b/tests/native_tests/core.hy @@ -272,10 +272,10 @@ result['y in globals'] = 'y' in globals()") (import [hy.models [HySymbol]]) (setv s1 (gensym)) (assert (isinstance s1 HySymbol)) - (assert (= 0 (.find s1 ":G_"))) + (assert (= 0 (.find s1 "_;G|"))) (setv s2 (gensym "xx")) (setv s3 (gensym "xx")) - (assert (= 0 (.find s2 ":xx_"))) + (assert (= 0 (.find s2 "_;xx|"))) (assert (not (= s2 s3))) (assert (not (= (str s2) (str s3))))) diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy index d3efcb6..12e4fe1 100644 --- a/tests/native_tests/native_macros.hy +++ b/tests/native_tests/native_macros.hy @@ -163,8 +163,8 @@ (setv s1 (to_source _ast1)) (setv s2 (to_source _ast2)) ;; and make sure there is something new that starts with :G_ - (assert (in ":G_" s1)) - (assert (in ":G_" s2)) + (assert (in "_;G|" s1)) + (assert (in "_;G|" s2)) ;; but make sure the two don't match each other (assert (not (= s1 s2)))) @@ -188,8 +188,8 @@ (setv _ast2 (import_buffer_to_ast macro1 "foo")) (setv s1 (to_source _ast1)) (setv s2 (to_source _ast2)) - (assert (in ":a_" s1)) - (assert (in ":a_" s2)) + (assert (in "_;a|" s1)) + (assert (in "_;a|" s2)) (assert (not (= s1 s2)))) (defn test-defmacro-g! [] @@ -211,8 +211,8 @@ (setv _ast2 (import_buffer_to_ast macro1 "foo")) (setv s1 (to_source _ast1)) (setv s2 (to_source _ast2)) - (assert (in ":res_" s1)) - (assert (in ":res_" s2)) + (assert (in "_;res|" s1)) + (assert (in "_;res|" s2)) (assert (not (= s1 s2))) ;; defmacro/g! didn't like numbers initially because they @@ -240,8 +240,8 @@ (setv _ast2 (import_buffer_to_ast macro1 "foo")) (setv s1 (to_source _ast1)) (setv s2 (to_source _ast2)) - (assert (in ":res_" s1)) - (assert (in ":res_" s2)) + (assert (in "_;res|" s1)) + (assert (in "_;res|" s2)) (assert (not (= s1 s2))) ;; defmacro/g! didn't like numbers initially because they From 370768105644559b314425694508b604cc9f933c Mon Sep 17 00:00:00 2001 From: gilch Date: Sun, 24 Sep 2017 11:54:54 -0600 Subject: [PATCH 11/14] make deftag/defmacro macros, not special forms --- hy/compiler.py | 59 ++------------------------------------------ hy/core/bootstrap.hy | 41 ++++++++++++++++++++++++++++++ hy/errors.py | 2 +- hy/macros.py | 2 ++ 4 files changed, 46 insertions(+), 58 deletions(-) diff --git a/hy/compiler.py b/hy/compiler.py index 5ce9073..2583cb2 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -831,6 +831,7 @@ class HyASTCompiler(object): @builds("try") @checkargs(min=2) def compile_try_expression(self, expr): + expr = copy.deepcopy(expr) expr.pop(0) # try # (try something…) @@ -1125,6 +1126,7 @@ class HyASTCompiler(object): @builds("import") def compile_import_expression(self, expr): + expr = copy.deepcopy(expr) def _compile_import(expr, module, names=None, importer=asty.Import): if not names: names = [ast.alias(name=ast_str(module), asname=None)] @@ -2054,63 +2056,6 @@ class HyASTCompiler(object): bases=bases_expr, body=body.stmts) - def _compile_time_hack(self, expression): - """Compile-time hack: we want to get our new macro now - We must provide __name__ in the namespace to make the Python - compiler set the __module__ attribute of the macro function.""" - - hy.importer.hy_eval(copy.deepcopy(expression), - compile_time_ns(self.module_name), - self.module_name) - - # We really want to have a `hy` import to get hy.macro in - ret = self.compile(expression) - ret.add_imports('hy', [None]) - return ret - - @builds("defmacro") - @checkargs(min=1) - def compile_macro(self, expression): - expression.pop(0) - name = expression.pop(0) - if not isinstance(name, HySymbol): - raise HyTypeError(name, ("received a `%s' instead of a symbol " - "for macro name" % type(name).__name__)) - name = HyString(name).replace(name) - for kw in ("&kwonly", "&kwargs", "&key"): - if kw in expression[0]: - raise HyTypeError(name, "macros cannot use %s" % kw) - expression[0].insert(0, HySymbol('&name')) - new_expression = HyExpression([ - HyExpression([HySymbol("hy.macros.macro"), name]), - HyExpression([HySymbol("fn")] + expression), - ]).replace(expression) - - ret = self._compile_time_hack(new_expression) - - return ret - - @builds("deftag") - @checkargs(min=2) - def compile_tag_macro(self, expression): - expression.pop(0) - name = expression.pop(0) - if name == ":" or name == "&": - raise NameError("%s can't be used as a tag macro name" % name) - if not isinstance(name, HySymbol) and not isinstance(name, HyString): - raise HyTypeError(name, - ("received a `%s' instead of a symbol " - "for tag macro name" % type(name).__name__)) - name = HyString(name).replace(name) - new_expression = HyExpression([ - HyExpression([HySymbol("hy.macros.tag"), name]), - HyExpression([HySymbol("fn")] + expression), - ]).replace(expression) - - ret = self._compile_time_hack(new_expression) - - return ret - @builds("dispatch_tag_macro") @checkargs(exact=2) def compile_dispatch_tag_macro(self, expression): diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy index 4ecb243..d2a4920 100644 --- a/hy/core/bootstrap.hy +++ b/hy/core/bootstrap.hy @@ -6,6 +6,30 @@ ;;; These macros are the essential hy macros. ;;; They are automatically required everywhere, even inside hy.core modules. +(eval-and-compile + (import hy) + ((hy.macros.macro "defmacro") + (fn [&name macro-name lambda-list &rest body] + "the defmacro macro" + (if* (not (isinstance macro-name hy.models.HySymbol)) + (raise + (hy.errors.HyTypeError + macro-name + (% "received a `%s' instead of a symbol for macro name" + (. (type name) + __name__))))) + (for* [kw '[&kwonly &kwargs &key]] + (if* (in kw lambda-list) + (raise (hy.errors.HyTypeError macro-name + (% "macros cannot use %s" + kw))))) + ;; this looks familiar... + `(eval-and-compile + (import hy) + ((hy.macros.macro ~(str macro-name)) + (fn ~(+ `[&name] lambda-list) + ~@body)))))) + (defmacro if [&rest args] "if with elif" (setv n (len args)) @@ -16,6 +40,23 @@ ~(get args 1) (if ~@(cut args 2)))))) +(defmacro deftag [tag-name lambda-list &rest body] + (if (and (not (isinstance tag-name hy.models.HySymbol)) + (not (isinstance tag-name hy.models.HyString))) + (raise (hy.errors.HyTypeError + tag-name + (% "received a `%s' instead of a symbol for tag macro name" + (. (type tag-name) __name__))))) + (if (or (= tag-name ":") + (= tag-name "&")) + (raise (NameError (% "%s can't be used as a tag macro name" tag-name)))) + (setv tag-name (.replace (hy.models.HyString tag-name) + tag-name)) + `(eval-and-compile + (import hy) + ((hy.macros.tag ~tag-name) + (fn ~lambda-list ~@body)))) + (defmacro macro-error [location reason] "error out properly within a macro" `(raise (hy.errors.HyMacroExpansionError ~location ~reason))) diff --git a/hy/errors.py b/hy/errors.py index 6fb00fd..257ea3c 100644 --- a/hy/errors.py +++ b/hy/errors.py @@ -81,7 +81,7 @@ class HyTypeError(TypeError): result += colored.yellow("%s: %s\n\n" % (self.__class__.__name__, - self.message.encode('utf-8'))) + self.message)) return result diff --git a/hy/macros.py b/hy/macros.py index 76b03de..82d88d6 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -34,6 +34,7 @@ def macro(name): """ def _(fn): + fn.__name__ = '({})'.format(name) try: argspec = getargspec(fn) fn._hy_macro_pass_compiler = argspec.keywords is not None @@ -63,6 +64,7 @@ def tag(name): """ def _(fn): + fn.__name__ = '#{}'.format(name) module_name = fn.__module__ if module_name.startswith("hy.core"): module_name = None From e90f082baf6c66293339782d4415b337dfeb2fdb Mon Sep 17 00:00:00 2001 From: gilch Date: Sat, 23 Sep 2017 23:50:44 -0600 Subject: [PATCH 12/14] back `let` with dict for better defclass behavior --- docs/contrib/walk.rst | 9 +------- hy/contrib/walk.hy | 9 ++++---- tests/native_tests/contrib/walk.hy | 34 ++++++++-------------------- tests/native_tests/py3_only_tests.hy | 13 ----------- 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/docs/contrib/walk.rst b/docs/contrib/walk.rst index e6044df..fe0eeea 100644 --- a/docs/contrib/walk.rst +++ b/docs/contrib/walk.rst @@ -224,10 +224,6 @@ Arguments in nested functions and bindings in nested ``let`` forms can shadow th 6 5 -The ``global`` special form changes the meaning of names to refer to the -module-level variables instead of locals, and this change still applies inside a ``let`` form, -even if a global has the same name as a let binding. - Basic assignments (e.g. ``setv``, ``+=``) will update the local variable named by a let binding, when they assign to a let-bound name. @@ -238,9 +234,6 @@ even if it shares the name of a let binding. Use ``__import__`` and ``type`` (or whatever metaclass) instead, if you must avoid this hoisting. -When used in a nested function, -nonlocal assignments to let-bound variables still require a ``nonlocal`` form. - The ``let`` macro takes two parameters: a list defining *variables* and the *body* which gets executed. *variables* is a vector of variable and value pairs. @@ -254,4 +247,4 @@ variable and value pairs. ... (print x y)) 5 6 - +It is an error to use a let-bound name in a ``global`` or ``nonlocal`` form. diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 60d6eca..8106492 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -74,6 +74,7 @@ (expand form)) ;; TODO: move to hy.extra.reserved? +(import hy) (setv special-forms (list-comp k [k (.keys hy.compiler._compile-table)] (isinstance k hy._compat.string-types))) @@ -246,10 +247,9 @@ Arguments without a header are under None. (defn handle-call [self] (setv head (first self.form)) (if (in head '[fn fn*]) (self.handle-fn) - (in head '[import quote]) (self.handle-base) + (in head '[import require quote]) (self.handle-base) (= head 'except) (self.handle-except) (= head ".") (self.handle-dot) - (= head 'global) (self.handle-global) (= head 'defclass) (self.handle-defclass) (= head 'quasiquote) (self.+quote) ;; must be checked last! @@ -320,9 +320,10 @@ as can nested let forms. (if (in '. k) (macro-error k "binding target may not contain a dot"))) (.append values (symbolexpand (macroexpand-all v &name) expander)) - (assoc replacements k (HySymbol (+ g!let "::" k)))) + (assoc replacements k `(get ~g!let ~(name k)))) `(do - (setv ~@(interleave (.values replacements) values)) + (setv ~g!let {} + ~@(interleave (.values replacements) values)) ~@(symbolexpand (macroexpand-all body &name) expander))) ;; (defmacro macrolet []) diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy index 4107ec2..689cd2a 100644 --- a/tests/native_tests/contrib/walk.hy +++ b/tests/native_tests/contrib/walk.hy @@ -158,8 +158,8 @@ (do foo (assert False)) - (except [ne NameError] - (setv error ne))) + (except [le LookupError] + (setv error le))) (setv foo 16) (assert (= foo 16)) (setv [foo bar baz] [1 2 3]) @@ -217,7 +217,10 @@ ;; the name of the class is just a symbol, even if it's a let binding (defclass Foo [quux] ; let bindings apply in inheritance list ;; let bindings apply inside class body - (setv x Foo))) + (setv x Foo) + ;; quux is not local + (setv quux "quux")) + (assert (= quux "quux"))) ;; defclass always creates a python-scoped variable, even if it's a let binding name (assert (= Foo.x 42))) @@ -307,33 +310,15 @@ (, 1 6 2))))) (defn test-let-closure [] - (let [count [0]] + (let [count 0] (defn +count [&optional [x 1]] - (+= (get count 0) x) - (get count 0))) + (+= count x) + count)) ;; let bindings can still exist outside of a let body (assert (= 1 (+count))) (assert (= 2 (+count))) (assert (= 42 (+count 40)))) -(defn test-let-global [] - (setv (get (globals) - 'let-global) - "global") - (let [let-global 1] - (assert (= let-global 1)) - (defn foo [] - (assert (= let-global 1)) - (global let-global) - (assert (= let-global "global")) - (setv let-global "mutated") - (assert (= let-global "mutated"))) - (foo) - (assert (= let-global 1)) - (assert (= (get (globals) - 'let-global) - "mutated")))) - (defmacro triple [a] (setv g!a (gensym a)) `(do @@ -353,4 +338,3 @@ 3)) (assert (= b 3)) (assert (= c 3)))) - diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy index 90331fd..eb10ec2 100644 --- a/tests/native_tests/py3_only_tests.hy +++ b/tests/native_tests/py3_only_tests.hy @@ -72,19 +72,6 @@ (assert (= (list (yield-from-test)) [0 1 2 1 2 3 4]))) (require [hy.contrib.walk [let]]) -(defn test-let-nonlocal [] - (let [a 88 - c 64] - (defn foo [a b] - (nonlocal c) - (-= a 1) - (-= c 1) - (, a b c)) - (assert (= a 88)) - (assert (= (foo 1 2) - (, 0 2 63))) - (assert (= c 63)) - (assert (= a 88)))) (defn test-let-optional [] (let [a 1 From 82b4518fa648f62f1acf2e132ee7c8587667bde4 Mon Sep 17 00:00:00 2001 From: gilch Date: Sun, 29 Oct 2017 17:51:29 -0600 Subject: [PATCH 13/14] error check defclass name --- hy/compiler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hy/compiler.py b/hy/compiler.py index 2583cb2..c526d19 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -2007,14 +2007,15 @@ class HyASTCompiler(object): expressions.pop(0) # class class_name = expressions.pop(0) + if not isinstance(class_name, HySymbol): + raise HyTypeError(class_name, "Class name must be a symbol.") bases_expr = [] bases = Result() if expressions: base_list = expressions.pop(0) if not isinstance(base_list, HyList): - raise HyTypeError(expressions, - "Bases class must be a list") + raise HyTypeError(base_list, "Base classes must be a list.") bases_expr, bases, _ = self._compile_collect(base_list) body = Result() From a4dd344ebd8a66916208b9cf9b00ade2b769e6c4 Mon Sep 17 00:00:00 2001 From: gilch Date: Mon, 30 Oct 2017 19:46:09 -0600 Subject: [PATCH 14/14] protect eval-and-compile/eval-when-compile in let --- hy/contrib/walk.hy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy index 8106492..45d991f 100644 --- a/hy/contrib/walk.hy +++ b/hy/contrib/walk.hy @@ -247,7 +247,11 @@ Arguments without a header are under None. (defn handle-call [self] (setv head (first self.form)) (if (in head '[fn fn*]) (self.handle-fn) - (in head '[import require quote]) (self.handle-base) + (in head '[import + require + quote + eval-and-compile + eval-when-compile]) (self.handle-base) (= head 'except) (self.handle-except) (= head ".") (self.handle-dot) (= head 'defclass) (self.handle-defclass)