hy/tests/native_tests/native_macros.hy

515 lines
16 KiB
Hy

;; Copyright 2020 the authors.
;; This file is part of Hy, which is free software licensed under the Expat
;; license. See the LICENSE.
(import pytest
[hy.errors [HyTypeError HyMacroExpansionError]])
(defmacro rev [&rest body]
"Execute the `body` statements in reverse"
(quasiquote (do (unquote-splice (list (reversed body))))))
(defn test-rev-macro []
"NATIVE: test stararged native macros"
(setv x [])
(rev (.append x 1) (.append x 2) (.append x 3))
(assert (= x [3 2 1])))
(defn test-macros-returning-constants []
(defmacro an-int [] 42)
(assert (= (an-int) 42))
(defmacro a-true [] True)
(assert (= (a-true) True))
(defmacro a-false [] False)
(assert (= (a-false) False))
(defmacro a-float [] 42.)
(assert (= (a-float) 42.))
(defmacro a-complex [] 42j)
(assert (= (a-complex) 42j))
(defmacro a-string [] "foo")
(assert (= (a-string) "foo"))
(defmacro a-bytes [] b"foo")
(assert (= (a-bytes) b"foo"))
(defmacro a-list [] [1 2])
(assert (= (a-list) [1 2]))
(defmacro a-tuple [&rest b] b)
(assert (= (a-tuple 1 2) [1 2]))
(defmacro a-dict [] {1 2})
(assert (= (a-dict) {1 2}))
(defmacro a-set [] #{1 2})
(assert (= (a-set) #{1 2}))
(defmacro a-none [])
(assert (= (a-none) None)))
; A macro calling a previously defined function
(eval-when-compile
(defn foo [x y]
(quasiquote (+ (unquote x) (unquote y)))))
(defmacro bar [x y]
(foo x y))
(defn test-macro-kw []
"NATIVE: test that an error is raised when &kwonly or &kwargs is used in a macro"
(try
(eval '(defmacro f [&kwonly a b]))
(except [e HyTypeError]
(assert (= e.msg "macros cannot use &kwonly")))
(else (assert False)))
(try
(eval '(defmacro f [&kwargs kw]))
(except [e HyTypeError]
(assert (= e.msg "macros cannot use &kwargs")))
(else (assert False))))
(defn test-fn-calling-macro []
"NATIVE: test macro calling a plain function"
(assert (= 3 (bar 1 2))))
(defn test-optional-and-unpacking-in-macro []
; https://github.com/hylang/hy/issues/1154
(defn f [&rest args]
(+ "f:" (repr args)))
(defmacro mac [&optional x]
`(f #* [~x]))
(assert (= (mac) "f:(None,)")))
(defn test-macro-autoboxing-docstring []
(defmacro m []
(setv mystring "hello world")
`(fn [] ~mystring (+ 1 2)))
(setv f (m))
(assert (= (f) 3))
(assert (= f.__doc__ "hello world")))
(defn test-midtree-yield []
"NATIVE: test yielding with a returnable"
(defn kruft [] (yield) (+ 1 1)))
(defn test-midtree-yield-in-for []
"NATIVE: test yielding in a for with a return"
(defn kruft-in-for []
(for [i (range 5)]
(yield i))
(+ 1 2)))
(defn test-midtree-yield-in-while []
"NATIVE: test yielding in a while with a return"
(defn kruft-in-while []
(setv i 0)
(while (< i 5)
(yield i)
(setv i (+ i 1)))
(+ 2 3)))
(defn test-multi-yield []
"NATIVE: testing multiple yields"
(defn multi-yield []
(for [i (range 3)]
(yield i))
(yield "a")
(yield "end"))
(assert (= (list (multi-yield)) [0 1 2 "a" "end"])))
; Macro that checks a variable defined at compile or load time
(setv phase "load")
(eval-when-compile
(setv phase "compile"))
(defmacro phase-when-compiling [] phase)
(assert (= phase "load"))
(assert (= (phase-when-compiling) "compile"))
(setv initialized False)
(eval-and-compile
(setv initialized True))
(defmacro test-initialized [] initialized)
(assert initialized)
(assert (test-initialized))
(defn test-gensym-in-macros []
(import ast)
(import [astor.code-gen [to-source]])
(import [hy.compiler [hy-compile]])
(import [hy.lex [hy-parse]])
(setv macro1 "(defmacro nif [expr pos zero neg]
(setv g (gensym))
`(do
(setv ~g ~expr)
(cond [(pos? ~g) ~pos]
[(zero? ~g) ~zero]
[(neg? ~g) ~neg])))
(print (nif (inc -1) 1 0 -1))
")
;; expand the macro twice, should use a different
;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
;; and make sure there is something new that starts with _G\uffff
(assert (in (mangle "_G\uffff") s1))
(assert (in (mangle "_G\uffff") s2))
;; but make sure the two don't match each other
(assert (not (= s1 s2))))
(defn test-with-gensym []
(import ast)
(import [astor.code-gen [to-source]])
(import [hy.compiler [hy-compile]])
(import [hy.lex [hy-parse]])
(setv macro1 "(defmacro nif [expr pos zero neg]
(with-gensyms [a]
`(do
(setv ~a ~expr)
(cond [(pos? ~a) ~pos]
[(zero? ~a) ~zero]
[(neg? ~a) ~neg]))))
(print (nif (inc -1) 1 0 -1))
")
;; expand the macro twice, should use a different
;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_a\uffff") s1))
(assert (in (mangle "_a\uffff") s2))
(assert (not (= s1 s2))))
(defn test-defmacro/g! []
(import ast)
(import [astor.code-gen [to-source]])
(import [hy.compiler [hy-compile]])
(import [hy.lex [hy-parse]])
(setv macro1 "(defmacro/g! nif [expr pos zero neg]
`(do
(setv ~g!res ~expr)
(cond [(pos? ~g!res) ~pos]
[(zero? ~g!res) ~zero]
[(neg? ~g!res) ~neg])))
(print (nif (inc -1) 1 0 -1))
")
;; expand the macro twice, should use a different
;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_res\uffff") s1))
(assert (in (mangle "_res\uffff") s2))
(assert (not (= s1 s2)))
;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro/g! two-point-zero [] `(+ (float 1) 1.0))")
(assert (hy-compile (hy-parse macro2) __name__)))
(defn test-defmacro! []
;; defmacro! must do everything defmacro/g! can
(import ast)
(import [astor.code-gen [to-source]])
(import [hy.compiler [hy-compile]])
(import [hy.lex [hy-parse]])
(setv macro1 "(defmacro! nif [expr pos zero neg]
`(do
(setv ~g!res ~expr)
(cond [(pos? ~g!res) ~pos]
[(zero? ~g!res) ~zero]
[(neg? ~g!res) ~neg])))
(print (nif (inc -1) 1 0 -1))
")
;; expand the macro twice, should use a different
;; gensym each time
(setv _ast1 (hy-compile (hy-parse macro1) __name__))
(setv _ast2 (hy-compile (hy-parse macro1) __name__))
(setv s1 (to_source _ast1))
(setv s2 (to_source _ast2))
(assert (in (mangle "_res\uffff") s1))
(assert (in (mangle "_res\uffff") s2))
(assert (not (= s1 s2)))
;; defmacro/g! didn't like numbers initially because they
;; don't have a startswith method and blew up during expansion
(setv macro2 "(defmacro! two-point-zero [] `(+ (float 1) 1.0))")
(assert (hy-compile (hy-parse macro2) __name__))
(defmacro! foo! [o!foo] `(do ~g!foo ~g!foo))
;; test that o! becomes g!
(assert (= "Hy" (foo! "Hy")))
;; test that o! is evaluated once only
(setv foo 40)
(foo! (+= foo 1))
(assert (= 41 foo))
;; test &optional args
(defmacro! bar! [o!a &optional [o!b 1]] `(do ~g!a ~g!a ~g!b ~g!b))
;; test that o!s are evaluated once only
(bar! (+= foo 1) (+= foo 1))
(assert (= 43 foo))
;; test that the optional arg works
(assert (= (bar! 2) 1)))
(defn test-if-not []
(assert (= (if-not True :yes :no)
:no))
(assert (= (if-not False :yes :no)
:yes))
(assert (none? (if-not True :yes)))
(assert (= (if-not False :yes)
:yes)))
(defn test-lif []
"test that lif works as expected"
;; None is false
(assert (= (lif None "true" "false") "false"))
;; But everything else is True! Even falsey things.
(assert (= (lif True "true" "false") "true"))
(assert (= (lif False "true" "false") "true"))
(assert (= (lif 0 "true" "false") "true"))
(assert (= (lif "some-string" "true" "false") "true"))
(assert (= (lif "" "true" "false") "true"))
(assert (= (lif (+ 1 2 3) "true" "false") "true"))
(assert (= (lif None "true" "false") "false"))
(assert (= (lif 0 "true" "false") "true"))
;; Test ellif [sic]
(assert (= (lif None 0
None 1
0 2
3)
2)))
(defn test-lif-not []
"test that lif-not works as expected"
; None is false
(assert (= (lif-not None "false" "true") "false"))
; But everything else is True! Even falsey things.
(assert (= (lif-not True "false" "true") "true"))
(assert (= (lif-not False "false" "true") "true"))
(assert (= (lif-not 0 "false" "true") "true"))
(assert (= (lif-not "some-string" "false" "true") "true"))
(assert (= (lif-not "" "false" "true") "true"))
(assert (= (lif-not (+ 1 2 3) "false" "true") "true"))
(assert (= (lif-not None "false" "true") "false"))
(assert (= (lif-not 0 "false" "true") "true")))
(defn test-defmain []
"NATIVE: make sure defmain is clean"
(global --name--)
(setv oldname --name--)
(setv --name-- "__main__")
(defn main [x]
(print (integer? x))
x)
(try
(defmain [&rest args]
(main 42))
(assert False)
(except [e SystemExit]
(assert (= (str e) "42"))))
;; Try a `defmain` without args
(try
(defmain []
(main 42))
(assert False)
(except [e SystemExit]
(assert (= (str e) "42"))))
;; Try a `defmain` with only one arg
(import sys)
(setv oldargv sys.argv)
(try
(setv sys.argv [1])
(defmain [x]
(main x))
(assert False)
(except [e SystemExit]
(assert (= (str e) "1"))))
(setv sys.argv oldargv)
(setv --name-- oldname))
(defn test-macro-namespace-resolution []
"Confirm that local versions of macro-macro dependencies do not shadow the
versions from the macro's own module, but do resolve unbound macro references
in expansions."
;; `nonlocal-test-macro` is a macro used within
;; `tests.resources.macro-with-require.test-module-macro`.
;; Here, we introduce an equivalently named version in local scope that, when
;; used, will expand to a different output string.
(defmacro nonlocal-test-macro [x]
(print "this is the local version of `nonlocal-test-macro`!"))
;; Was the above macro created properly?
(assert (in "nonlocal_test_macro" __macros__))
(setv nonlocal-test-macro (get __macros__ "nonlocal_test_macro"))
(require [tests.resources.macro-with-require [*]])
(setv module-name-var "tests.native_tests.native_macros.test-macro-namespace-resolution")
(assert (= (+ "This macro was created in tests.resources.macros, "
"expanded in tests.native_tests.native_macros.test-macro-namespace-resolution "
"and passed the value 2.")
(test-module-macro 2)))
(assert (= (+ "This macro was created in tests.resources.macros, "
"expanded in tests.native_tests.native_macros.test-macro-namespace-resolution "
"and passed the value 2.")
#test-module-tag 2))
;; Now, let's use a `require`d macro that depends on another macro defined only
;; in this scope.
(defmacro local-test-macro [x]
(.format "This is the local version of `nonlocal-test-macro` returning {}!" (int x)))
(assert (= "This is the local version of `nonlocal-test-macro` returning 3!"
(test-module-macro-2 3)))
(assert (= "This is the local version of `nonlocal-test-macro` returning 3!"
#test-module-tag-2 3)))
(defn test-macro-from-module []
"Macros loaded from an external module, which itself `require`s macros, should
work without having to `require` the module's macro dependencies (due to
[minimal] macro namespace resolution).
In doing so we also confirm that a module's `__macros__` attribute is correctly
loaded and used.
Additionally, we confirm that `require` statements are executed via loaded bytecode."
(import os sys marshal types)
(import importlib)
(setv pyc-file (importlib.util.cache-from-source
(os.path.realpath
(os.path.join
"tests" "resources" "macro_with_require.hy"))))
;; Remove any cached byte-code, so that this runs from source and
;; gets evaluated in this module.
(when (os.path.isfile pyc-file)
(os.unlink pyc-file)
(.clear sys.path_importer_cache)
(when (in "tests.resources.macro_with_require" sys.modules)
(del (get sys.modules "tests.resources.macro_with_require"))
(__macros__.clear)
(__tags__.clear)))
;; Ensure that bytecode isn't present when we require this module.
(assert (not (os.path.isfile pyc-file)))
(defn test-requires-and-macros []
(require [tests.resources.macro-with-require
[test-module-macro]])
;; Make sure that `require` didn't add any of its `require`s
(assert (not (in "nonlocal-test-macro" __macros__)))
;; and that it didn't add its tags.
(assert (not (in "test_module_tag" __tags__)))
;; Now, require everything.
(require [tests.resources.macro-with-require [*]])
;; Again, make sure it didn't add its required macros and/or tags.
(assert (not (in "nonlocal-test-macro" __macros__)))
;; Its tag(s) should be here now.
(assert (in "test_module_tag" __tags__))
;; The test macro expands to include this symbol.
(setv module-name-var "tests.native_tests.native_macros")
(assert (= (+ "This macro was created in tests.resources.macros, "
"expanded in tests.native_tests.native_macros "
"and passed the value 1.")
(test-module-macro 1)))
(assert (= (+ "This macro was created in tests.resources.macros, "
"expanded in tests.native_tests.native_macros "
"and passed the value 1.")
#test-module-tag 1)))
(test-requires-and-macros)
;; Now that bytecode is present, reload the module, clear the `require`d
;; macros and tags, and rerun the tests.
(assert (os.path.isfile pyc-file))
;; Reload the module and clear the local macro context.
(.clear sys.path_importer_cache)
(del (get sys.modules "tests.resources.macro_with_require"))
(.clear __macros__)
(.clear __tags__)
;; XXX: There doesn't seem to be a way--via standard import mechanisms--to
;; ensure that an imported module used the cached bytecode. We'll simply have
;; to trust that the .pyc loading convention was followed.
(test-requires-and-macros))
(defn test-recursive-require-star []
"(require [foo [*]]) should pull in macros required by `foo`."
(require [tests.resources.macro-with-require [*]])
(test-macro)
(assert (= blah 1)))
(defn test-macro-errors []
(import traceback
[hy.importer [hy-parse]])
(setv test-expr (hy-parse "(defmacro blah [x] `(print ~@z)) (blah y)"))
(with [excinfo (pytest.raises HyMacroExpansionError)]
(eval test-expr))
(setv output (traceback.format_exception_only
excinfo.type excinfo.value))
(setv output (cut (.splitlines (.strip (first output))) 1))
(setv expected [" File \"<string>\", line 1"
" (defmacro blah [x] `(print ~@z)) (blah y)"
" ^------^"
"expanding macro blah"
" NameError: global name 'z' is not defined"])
(assert (= (cut expected 0 -1) (cut output 0 -1)))
(assert (or (= (get expected -1) (get output -1))
;; Handle PyPy's peculiarities
(= (.replace (get expected -1) "global " "") (get output -1))))
;; This should throw a `HyWrapperError` that gets turned into a
;; `HyMacroExpansionError`.
(with [excinfo (pytest.raises HyMacroExpansionError)]
(eval '(do (defmacro wrap-error-test []
(fn []))
(wrap-error-test))))
(assert (in "HyWrapperError" (str excinfo.value))))