diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a238df8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,11 @@ +[run] +omit = + */python?.?/* + */lib-python/?.?/*.py + */lib_pypy/_*.py + */site-packages/nose/* + */pypy/* + +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0ba4a9e..b6e97e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,13 +7,15 @@ python: - "2.6" # command to install dependencies install: - - pip install -r requirements.txt --use-mirrors - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse importlib unittest2 astor --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install astor --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install astor --use-mirrors; fi + - pip install -r requirements.txt + - pip install coveralls + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse importlib unittest2 astor; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install astor; fi + - if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install astor; fi - python setup.py -q install # # command to run tests script: make travis +after_success: coveralls notifications: email: - paultag@gmail.com diff --git a/AUTHORS b/AUTHORS index 311903b..fe17b8c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,3 +18,28 @@ * Ralph Möritz * Josh McLaughlin * Berker Peksag +* Henrique Carvalho Alves +* Joe Hakim Rahme +* Kenan Bölükbaşı +* Abhishek L +* Christopher Browne +* Clinton N. Dreisbach +* D. Joe +* Duncan McGreggor +* E. Anders Lannerback +* Jack +* Johan Euphrosine +* Kevin Zita +* Matt Fenwick +* Sean B. Palmer +* Thom Neale +* Tuukka Turto +* Vasudev Kamath +* Yuval Langer +* Fatih Kadir Akın +* Jack Hooper +* Brian McKenna +* Halit Alptekin +* Richard Parsons +* han semaj +* kirbyfan64 diff --git a/Makefile b/Makefile index 07ae6db..21f2b21 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ all: @echo " - tox" @echo " - d" @echo " - r" + @echo " - clean" @echo "" docs: @@ -53,11 +54,19 @@ diff: r: d tox diff travis: - nosetests -s + nosetests -s --with-coverage --cover-package hy ifeq (PyPy,$(findstring PyPy,$(shell python -V 2>&1 | tail -1))) @echo "skipping flake8 on pypy" else flake8 hy bin tests endif +clean: + @find . -name "*.pyc" -exec rm {} \; + @find -name __pycache__ -delete + @${RM} -r -f .tox + @${RM} -r -f dist + @${RM} -r -f *.egg-info + @${RM} -r -f docs/_build + .PHONY: docs diff --git a/NEWS b/NEWS index 9f199da..3670fca 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,76 @@ +Changes from Hy 0.9.11 + + tl;dr: + + 0.9.12 comes with some massive changes, + We finally took the time to implement gensym, as well as a few + other bits that help macro writing. Check the changelog for + what exactly was added. + + The biggest feature, Reader Macros, landed later + in the cycle, but were big enough to warrent a release on it's + own. A huge thanks goes to Foxboron for implementing them + and a massive hug goes out to olasd for providing ongoing + reviews during the development. + + Welcome to the new Hy contributors, Henrique Carvalho Alves, + Kevin Zita and Kenan Bölükbaşı. Thanks for your work so far, + folks! + + Hope y'all enjoy the finest that 2013 has to offer, + - Hy Society + + + * Special thanks goes to Willyfrog, Foxboron and theanalyst for writing + 0.9.12's NEWS. Thanks, y'all! (PT) + + + [ Language Changes ] + * Translate foo? -> is_foo, for better Python interop. (PT) + * Reader Macros! + * Operators + and * now can work without arguments + * Define kwapply as a macro + * Added apply as a function + * Instant symbol generation with gensym + * Allow macros to return None + * Add a method for casting into byte string or unicode depending on python version + * flatten function added to language + * Add a method for casting into byte string or unicode depending on python version + * Added type coercing to the right integer for the platform + + + [ Misc. Fixes ] + * Added information about core team members + * Documentation fixed and extended + * Add astor to install_requires to fix hy --spy failing on hy 0.9.11. + * Convert stdout and stderr to UTF-8 properly in the run_cmd helper. + * Update requirements.txt and setup.py to use rply upstream. + * tryhy link added in documentation and README + * Command line options documented + * Adding support for coverage tests at coveralls.io + * Added info about tox, so people can use it prior to a PR + * Added the start of hacking rules + * Halting Problem removed from example as it was nonfree + * Fixed PyPI is now behind a CDN. The --use-mirrors option is deprecated. + * Badges for pypi version and downloads. + + + [ Syntax Fixes ] + * get allows multiple arguments + + + [ Bug Fixes ] + * OSX: Fixes for readline Repl problem which caused HyREPL not allowing 'b' + * Fix REPL completions on OSX + * Make HyObject.replace more resilient to prevent compiler breakage. + + + [ Contrib changes ] + * Anaphoric macros added to contrib + * Modified eg/twisted to follow the newer hy syntax + * Added (experimental) profile module + + Changes from Hy 0.9.10 * Many thanks to Guillermo Vayá (Willyfrog) for preparing this release's diff --git a/README.md b/README.md index dbf6c99..bd66b81 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ Hy == -![](https://raw.github.com/hylang/shyte/18f6925e08684b0e1f52b2cc2c803989cd62cd91/imgs/xkcd.png) - -Lisp and Python should love each other. Let's make it happen. - [![Build Status](https://travis-ci.org/hylang/hy.png?branch=master)](https://travis-ci.org/hylang/hy) [![Downloads](https://pypip.in/d/hy/badge.png)](https://crate.io/packages/hy) [![version](https://pypip.in/v/hy/badge.png)](https://crate.io/packages/hy) +[![Coverage Status](https://coveralls.io/repos/hylang/hy/badge.png)](https://coveralls.io/r/hylang/hy) +![](https://raw.github.com/hylang/shyte/18f6925e08684b0e1f52b2cc2c803989cd62cd91/imgs/xkcd.png) + +Lisp and Python should love each other. Let's make it happen. [Try it](http://try-hy.appspot.com/). Hylarious Hacks --------------- @@ -19,6 +19,7 @@ Hylarious Hacks [Hy IRC bot](https://github.com/hylang/hygdrop) +[miniKanren in Hy](https://github.com/algernon/adderall) OK, so, why? ------------ @@ -30,7 +31,7 @@ Why? Well, I wrote Hy to help people realize one thing about Python: -It's really goddamn awesome. +It's really awesome. Oh, and lisps are neat. @@ -42,7 +43,7 @@ Project ------- * Code: https://github.com/hylang/hy -* Docs: http://hy.rtfd.org/ -* Quickstart: http://hy.rtfd.org/quickstart +* Docs: http://hylang.org/ +* Quickstart: http://hylang.org/en/latest/quickstart.html * Bug reports: We have no bugs! Your bugs are your own! (https://github.com/hylang/hy/issues) * License: MIT (Expat) diff --git a/bin/hy2py b/bin/hy2py index 125fd26..3bcade4 100755 --- a/bin/hy2py +++ b/bin/hy2py @@ -1,25 +1,39 @@ #!/usr/bin/env python from __future__ import print_function +from hy.importer import (import_file_to_ast, import_file_to_hst) -from hy.importer import (import_file_to_ast, import_file_to_module, - import_file_to_hst) +import argparse +import sys import astor.codegen -import sys -import ast module_name = "" -hst = import_file_to_hst(sys.argv[1]) -print(hst) -print("") -print("") -_ast = import_file_to_ast(sys.argv[1], module_name) -print("") -print("") -print(ast.dump(_ast)) -print("") -print("") -print(astor.codegen.to_source(_ast)) +parser = argparse.ArgumentParser( + prog="hy2py", + usage="%(prog)s [options] FILE", + formatter_class=argparse.RawDescriptionHelpFormatter) +parser.add_argument("--with-source", "-s", action="store_true", + help="Show the parsed source structure") +parser.add_argument("--with-ast", "-a", action="store_true", + help="Show the generated AST") +parser.add_argument("--without-python", "-np", action="store_true", + help="Do not show the python code generated from the AST") +parser.add_argument('args', nargs=argparse.REMAINDER, help=argparse.SUPPRESS) -import_file_to_module(module_name, sys.argv[1]) +options = parser.parse_args(sys.argv[1:]) + +if options.with_source: + hst = import_file_to_hst(options.args[0]) + print(str(hst).encode("utf-8")) + print() + print() + +_ast = import_file_to_ast(options.args[0], module_name) +if options.with_ast: + print(astor.dump(_ast).encode("utf-8")) + print() + print() + +if not options.without_python: + print(astor.codegen.to_source(_ast).encode("utf-8")) diff --git a/docs/_static/cuddles-transparent-small.png b/docs/_static/cuddles-transparent-small.png new file mode 100644 index 0000000..1570db6 Binary files /dev/null and b/docs/_static/cuddles-transparent-small.png differ diff --git a/docs/_static/cuddles-transparent.png b/docs/_static/cuddles-transparent.png new file mode 100644 index 0000000..8f16ebd Binary files /dev/null and b/docs/_static/cuddles-transparent.png differ diff --git a/docs/_static/hy-logo-full.png b/docs/_static/hy-logo-full.png new file mode 100644 index 0000000..5f7d92f Binary files /dev/null and b/docs/_static/hy-logo-full.png differ diff --git a/docs/_static/hy-logo-small.png b/docs/_static/hy-logo-small.png new file mode 100644 index 0000000..b6a3c9c Binary files /dev/null and b/docs/_static/hy-logo-small.png differ diff --git a/docs/_static/hy-logo.svg b/docs/_static/hy-logo.svg new file mode 100644 index 0000000..2eab75b --- /dev/null +++ b/docs/_static/hy-logo.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/docs/_static/hy_logo-about.txt b/docs/_static/hy_logo-about.txt index 203a1c3..b72c081 100644 --- a/docs/_static/hy_logo-about.txt +++ b/docs/_static/hy_logo-about.txt @@ -13,3 +13,6 @@ into the public domain under CC0... see CC0_1.0.txt! You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . + +The new Hy logo is Copyright CC-BY-SA 4.0 Kenan Bölükbaşı, thanks, Kenan! +http://www.kenanb.com/posts/Hy-Programming-Language-Logo.html diff --git a/docs/conf.py b/docs/conf.py index 40eb062..4744800 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys +import time sys.path.append(os.path.abspath("..")) import hy @@ -28,7 +30,7 @@ import hy # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -44,7 +46,7 @@ master_doc = 'index' # General information about the project. project = u'hy' -copyright = u'2013, Paul Tagliamonte' +copyright = u'2013-%s, Paul Tagliamonte' % time.strftime('%Y') # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -67,7 +69,7 @@ release = hy.__version__ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ['_build', 'coreteam.rst'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None diff --git a/docs/contrib/anaphoric.rst b/docs/contrib/anaphoric.rst new file mode 100644 index 0000000..7255aee --- /dev/null +++ b/docs/contrib/anaphoric.rst @@ -0,0 +1,197 @@ +================ +Anaphoric Macros +================ + +.. versionadded:: 0.9.12 + +The anaphoric macros module makes functional programming in Hy very +concise and easy to read. + + An anaphoric macro is a type of programming macro that + deliberately captures some form supplied to the macro which may be + referred to by an anaphor (an expression referring to another). + + -- Wikipedia (http://en.wikipedia.org/wiki/Anaphoric_macro) + +Macros +====== + + +.. _ap-if: + +ap-if +------- + +Usage: ``(ap-if (foo) (print it))`` + +Evaluate the first form for trutheyness, and bind it to ``it`` in both the +true and false branch. + + +.. _ap-each: + +ap-each +------- + +Usage: ``(ap-each [1 2 3 4 5] (print it))`` + +Evaluate the form for each element in the list for side-effects. + + +.. _ap-each-while: + +ap-each-while +============= + +Usage: ``(ap-each-while list pred body)`` + +Evaluate the form for each element where the predicate form returns +True. + +.. code-block:: clojure + + => (ap-each-while [1 2 3 4 5 6] (< it 4) (print it)) + 1 + 2 + 3 + +.. _ap-map: + +ap-map +====== + +Usage: ``(ap-map form list)`` + +The anaphoric form of map works just like regular map except that +instead of a function object it takes a Hy form. The special name, +``it`` is bound to the current object from the list in the iteration. + +.. code-block:: clojure + + => (list (ap-map (* it 2) [1 2 3])) + [2, 4, 6] + + +.. _ap-map-when: + +ap-map-when +=========== + +Usage: ``(ap-map-when predfn rep list)`` + +Evaluate a mapping over the list using a predicate function to +determin when to apply the form. + +.. code-block:: clojure + + => (list (ap-map-when odd? (* it 2) [1 2 3 4])) + [2, 2, 6, 4] + + => (list (ap-map-when even? (* it 2) [1 2 3 4])) + [1, 4, 3, 8] + + +.. _ap-filter: + +ap-filter +========= + +Usage: ``(ap-filter form list)`` + +As with ``ap-map`` we take a special form instead of a function to +filter the elements of the list. The special name ``it`` is bound to +the current element in the iteration. + +.. code-block:: clojure + + => (list (ap-filter (> (* it 2) 6) [1 2 3 4 5])) + [4, 5] + + +.. _ap-reject: + +ap-reject +========= + +Usage: ``(ap-reject form list)`` + +This function does the opposite of ``ap-filter``, it rejects the +elements passing the predicate . The special name ``it`` is bound to +the current element in the iteration. + +.. code-block:: clojure + + => (list (ap-reject (> (* it 2) 6) [1 2 3 4 5])) + [1, 2, 3] + + +.. _ap-dotimes: + +ap-dotimes +========== + +Usage ``(ap-dotimes n body)`` + +This function evaluates the body *n* times, with the special +variable ``it`` bound from *0* to *1-n*. It is useful for side-effects. + +.. code-block:: clojure + + => (setv n []) + => (ap-dotimes 3 (.append n it)) + => n + [0, 1, 2] + + +.. _ap-first: + +ap-first +======== + +Usage ``(ap-first predfn list)`` + +This function returns the first element that passes the predicate or +``None``, with the special variable ``it`` bound to the current element in +iteration. + +.. code-block:: clojure + + =>(ap-first (> it 5) (range 10)) + 6 + + +.. _ap-last: + +ap-last +======== + +Usage ``(ap-last predfn list)`` + +This function returns the last element that passes the predicate or +``None``, with the special variable ``it`` bound to the current element in +iteration. + +.. code-block:: clojure + + =>(ap-last (> it 5) (range 10)) + 9 + + +.. _ap-reduce: + +ap-reduce +========= + +Usage ``(ap-reduce form list &optional initial-value)`` + +This function returns the result of applying form to the first 2 +elements in the body and applying the result and the 3rd element +etc. until the list is exhausted. Optionally an initial value can be +supplied so the function will be applied to initial value and the +first element instead. This exposes the element being iterated as +``it`` and the current accumulated value as ``acc``. + +.. code-block:: clojure + + =>(ap-reduce (+ it acc) (range 10)) + 45 diff --git a/docs/contrib/index.rst b/docs/contrib/index.rst new file mode 100644 index 0000000..53dcf23 --- /dev/null +++ b/docs/contrib/index.rst @@ -0,0 +1,12 @@ + +Contrib Modules Index +===================== + +Contents: + +.. toctree:: + :maxdepth: 3 + + anaphoric + loop + multi diff --git a/docs/contrib/loop.rst b/docs/contrib/loop.rst new file mode 100644 index 0000000..9c2c859 --- /dev/null +++ b/docs/contrib/loop.rst @@ -0,0 +1,56 @@ +========== +loop/recur +========== + +.. versionadded:: 0.9.13 + +The loop/recur macro gives programmers a simple way to use tail-call +optimization (TCO) in their Hy code. + + A tail call is a subroutine call that happens inside another + procedure as its final action; it may produce a return value which + is then immediately returned by the calling procedure. If any call + that a subroutine performs, such that it might eventually lead to + this same subroutine being called again down the call chain, is in + tail position, such a subroutine is said to be tail-recursive, + which is a special case of recursion. Tail calls are significant + because they can be implemented without adding a new stack frame + to the call stack. Most of the frame of the current procedure is + not needed any more, and it can be replaced by the frame of the + tail call. The program can then jump to the called + subroutine. Producing such code instead of a standard call + sequence is called tail call elimination, or tail call + optimization. Tail call elimination allows procedure calls in tail + position to be implemented as efficiently as goto statements, thus + allowing efficient structured programming. + + -- Wikipedia (http://en.wikipedia.org/wiki/Tail_call) + +Macros +====== + +.. _loop: + +loop +----- + +``loop`` establishes a recursion point. With ``loop``, ``recur`` +rebinds the variables set in the recursion point and sends code +execution back to that recursion point. If ``recur`` is used in a +non-tail position, an exception is thrown. + +Usage: `(loop bindings &rest body)` + +Example: + +.. code-block:: clojure + + (require hy.contrib.loop) + + (defn factorial [n] + (loop [[i n] [acc 1]] + (if (zero? i) + acc + (recur (dec i) (* acc i))))) + + (factorial 1000) diff --git a/docs/contrib/multi.rst b/docs/contrib/multi.rst new file mode 100644 index 0000000..b8f3619 --- /dev/null +++ b/docs/contrib/multi.rst @@ -0,0 +1,23 @@ +======== +defmulti +======== + +.. versionadded:: 0.9.13 + +`defmulti` lets you arity-overload a function by the given number of +args and/or kwargs. Inspired by clojures take on `defn`. + +.. code-block:: clj + + => (require hy.contrib.multi) + => (defmulti fun + ... ([a] a) + ... ([a b] "a b") + ... ([a b c] "a b c")) + => (fun 1 2 3) + 'a b c' + => (fun a b) + "a b" + => (fun 1) + 1 + diff --git a/docs/coreteam.rst b/docs/coreteam.rst new file mode 100644 index 0000000..982df9d --- /dev/null +++ b/docs/coreteam.rst @@ -0,0 +1,15 @@ +* `Julien Danjou `_ +* `Morten Linderud `_ +* `J Kenneth King `_ +* `Gergely Nagy `_ +* `Tuukka Turto `_ +* `Karen Rustad `_ +* `Abhishek L `_ +* `Christopher Allan Webber `_ +* `Konrad Hinsen `_ +* `Will Kahn-Greene `_ +* `Paul Tagliamonte `_ +* `Nicolas Dandrimont `_ +* `Bob Tolbert `_ +* `Berker Peksag `_ +* `Clinton N. Dreisbach `_ diff --git a/docs/hacking.rst b/docs/hacking.rst index 77e0036..4329c68 100644 --- a/docs/hacking.rst +++ b/docs/hacking.rst @@ -70,6 +70,10 @@ To run the tests:: Write tests---tests are good! +Also, it is good to run the tests for all the platforms supported and for pep8 compliant code. +You can do so by running tox:: + + $ tox Document! ========= @@ -83,3 +87,26 @@ To build the docs in HTML:: $ make html Write docs---docs are good! Even this doc! + + +Core Development Rules +====================== + +All incoming changes need to be acked by 2 different members of Hylang's +core team. Additional review is clearly welcome, but we need a minimum of +2 signoffs for any change. + +If a core member is sending in a PR, please find 2 core members that don't +include the PR submitter. The idea here is that one can work with the PR +author, and a second acks the entire change set. + +If the change is adding documentation, feel free to just merge after one +ACK. We've got low coverage, so it'd be great to keep that barrier low. + + +Core Team +========= + +Core development team of hy consists of following developers. + +.. include:: coreteam.rst diff --git a/docs/index.rst b/docs/index.rst index 299c59e..1fc685e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,41 +1,26 @@ Welcome to Hy's documentation! ============================== - - - -.. image:: _static/hy_logo-smaller.png - :alt: Hy logo +.. image:: _static/hy-logo-small.png + :alt: Hy :align: left -Welcome to `Hy `_! +:Try Hy: https://try-hy.appspot.com +:PyPI: https://pypi.python.org/pypi/hy +:Source: https://github.com/hylang/hy +:List: `hylang-discuss `_ +:IRC: ``#hy`` on Freenode +:Build status: + .. image:: https://secure.travis-ci.org/hylang/hy.png + :alt: Travis CI + :target: http://travis-ci.org/hylang/hy + Hy is a wonderful dialect of Lisp that's embedded in Python. -Since Hy transforms its lisp code into the python Abstract Syntax -Tree, you have the whole beautiful world of python at your fingertips, -in lisp form! -Meet our mascot, "Cuddles": +Since Hy transforms its Lisp code into the Python Abstract Syntax +Tree, you have the whole beautiful world of Python at your fingertips, +in Lisp form! -.. image:: http://fc07.deviantart.net/fs70/i/2013/138/f/0/cuddles_the_hacker_by_doctormo-d65l7lq.png - :alt: Paul riding cuddles into the distance - - -.. Our old ascii art mascot version -.. Retained as an easter egg for those who read the docs via .rst! -.. -.. LET'S CUDDLEFISH -.. ______ -.. _.----'#' # ' -.. ,' #' ,# ; -.. (' (w) _,-'_/ -.. /// / /'.____.' -.. \|\||/ - - -Read more about Hy in these docs! - -We're also on IRC! Join -`#hy on irc.freenode.net `_! Documentation Index =================== @@ -47,5 +32,6 @@ Contents: quickstart tutorial - hacking language/index + contrib/index + hacking diff --git a/docs/language/api.rst b/docs/language/api.rst index bac3138..02823a1 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -28,16 +28,49 @@ languages. and `i♥u` will become `hy_iu_t0x`. * Symbols that contain dashes will have them replaced with underscores. For - example, `render-template` will become `render_template`. + example, `render-template` will become `render_template`. This means that + symbols with dashes will shadow their underscore equivalents, and vice + versa. Builtins ======== -Hy features a number special forms that are used to help generate +Hy features a number of special forms that are used to help generate correct Python AST. The following are "special" forms, which may have behavior that's slightly unexpected in some situations. +. +- + +.. versionadded:: 0.9.13 + + +`.` is used to perform attribute access on objects. It uses a small DSL +to allow quick access to attributes and items in a nested datastructure. + +For instance, + +.. code-block:: clj + + (. foo bar baz [(+ 1 2)] frob) + +Compiles down to + +.. code-block:: python + + foo.bar.baz[1 + 2].frob + +`.` compiles its first argument (in the example, `foo`) as the object on +which to do the attribute dereference. It uses bare symbols as +attributes to access (in the example, `bar`, `baz`, `frob`), and +compiles the contents of lists (in the example, ``[(+ 1 2)]``) for +indexation. Other arguments throw a compilation error. + +Access to unknown attributes throws an :exc:`AttributeError`. Access to +unknown keys throws an :exc:`IndexError` (on lists and tuples) or a +:exc:`KeyError` (on dicts). + -> -- @@ -56,7 +89,7 @@ The following code demonstrates this: --- `->>` or `threading tail macro` is similar to `threading macro` but instead of -inserting each expression into the next expression’s first argument place it +inserting each expression into the next expression’s first argument place, it appends it as the last argument. The following code demonstrates this: .. code-block:: clj @@ -66,6 +99,37 @@ appends it as the last argument. The following code demonstrates this: 5 10 +apply +----- + +`apply` is used to apply an optional list of arguments and an optional +dictionary of kwargs to a function. + +Usage: `(apply fn-name [args] [kwargs])` + +Examples: + +.. code-block:: clj + + (defn thunk [] + "hy there") + + (apply thunk) + ;=> "hy there" + + (defn total-purchase [price amount &optional [fees 1.05] [vat 1.1]] + (* price amount fees vat)) + + (apply total-purchase [10 15]) + ;=> 173.25 + + (apply total-purchase [10 15] {"vat" 1.05}) + ;=> 165.375 + + (apply total-purchase [] {"price" 10 "amount" 15 "vat" 1.05}) + ;=> 165.375 + + and --- @@ -205,11 +269,12 @@ however is called only for every other value in the list. ;; assuming that (side-effect1) and (side-effect2) are functions and ;; collection is a list of numerical values - (for (x collection) (do - (side-effect1 x) - (if (% x 2) - (continue)) - (side-effect2 x))) + (for [x collection] + (do + (side-effect1 x) + (if (% x 2) + (continue)) + (side-effect2 x))) do / progn @@ -218,7 +283,7 @@ do / progn the `do` and `progn` forms are used to evaluate each of their arguments and return the last one. Return values from every other than the last argument are discarded. It can be used in `lambda` or `list-comp` to perform more complex -logic as show by one of the examples. +logic as shown by one of the examples. Some example usage: @@ -287,6 +352,8 @@ below: Meow +.. _defn: + defn / defun ------------ @@ -357,13 +424,38 @@ Parameters may have following keywords in front of them: => (zig-zag-sum 1 2 3 4 5 6) -3 +.. _defn-alias / defun-alias: + +defn-alias / defun-alias +------------------------ + +.. versionadded:: 0.9.13 + +The `defn-alias` and `defun-alias` macros are much like `defn`_ above, +with the difference that instead of defining a function with a single +name, these can also define aliases. Other than taking a list of +symbols for function names as the first parameter, `defn-alias` and +`defun-alias` have no other differences compared to `defn` and +`defun`. + +.. code-block:: clj + + => (defn-alias [main-name alias] [] + ... (print "Hello!")) + => (main-name) + "Hello!" + => (alias) + "Hello!" + +.. _defmacro: + defmacro -------- `defmacro` is used to define macros. The general format is -`(defmacro [parameters] expr)`. +`(defmacro name [parameters] expr)`. -Following example defines a macro that can be used to swap order of elements in +The following example defines a macro that can be used to swap order of elements in code, allowing the user to write code in infix notation, where operator is in between the operands. @@ -378,6 +470,102 @@ between the operands. => (infix (1 + 1)) 2 +.. _defmacro-alias: + +defmacro-alias +-------------- + +`defmacro-alias` is used to define macros with multiple names +(aliases). The general format is `(defmacro-alias [names] [parameters] +expr)`. It creates multiple macros with the same parameter list and +body, under the specified list of names. + +The following example defines two macros, both of which allow the user +to write code in infix notation. + +.. code-block:: clj + + => (defmacro-alias [infix infi] [code] + ... (quasiquote ( + ... (unquote (get code 1)) + ... (unquote (get code 0)) + ... (unquote (get code 2))))) + + => (infix (1 + 1)) + 2 + => (infi (1 + 1)) + 2 + +.. _defmacro/g!: + +defmacro/g! +------------ + +.. versionadded:: 0.9.12 + +`defmacro/g!` is a special version of `defmacro` that is used to +automatically generate :ref:`gensym` for any symbol that +starts with ``g!``. + +So ``g!a`` would become ``(gensym "a")``. + +.. seealso:: + + Section :ref:`using-gensym` + +defreader +--------- + +.. versionadded:: 0.9.12 + +`defreader` defines a reader macro, enabling you to restructure or +modify syntax. + +.. code-block:: clj + + => (defreader ^ [expr] (print expr)) + => #^(1 2 3 4) + (1 2 3 4) + => #^"Hello" + "Hello" + +.. seealso:: + + Section :ref:`Reader Macros ` + +del +--- + +.. versionadded:: 0.9.12 + +`del` removes an object from the current namespace. + +.. code-block:: clj + + => (setv foo 42) + => (del foo) + => foo + Traceback (most recent call last): + File "", line 1, in + NameError: name 'foo' is not defined + +`del` can also remove objects from a mapping, a list, ... + +.. code-block:: clj + + => (setv test (list (range 10))) + => test + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + => (del (slice test 2 4)) ;; remove items from 2 to 4 excluded + => test + [0, 1, 4, 5, 6, 7, 8, 9] + => (setv dic {"foo" "bar"}) + => dic + {"foo": "bar"} + => (del (get dic "foo")) + => dic + {} + eval ---- @@ -408,52 +596,38 @@ first / car for ---- - -`for` macro is used to build nested `foreach` loops. The macro takes two -parameters, first being a vector specifying collections to iterate over and -variables to bind. The second parameter is a statement which is executed during -each loop: - -.. code-block:: clj - - (for [x iter y iter] stmt) - - (foreach [x iter] - (foreach [y iter] stmt)) - - -foreach ------- -`foreach` is used to call a function for each element in a list or vector. -Results are discarded and None is returned instead. Example code iterates over -collection and calls side-effect to each element in the collection: +`for` is used to call a function for each element in a list or vector. +The results of each call are discarded and the for expression returns +None instead. The example code iterates over `collection` and +for each `element` in `collection` calls the `side-effect` +function with `element` as its argument: .. code-block:: clj ;; assuming that (side-effect) is a function that takes a single parameter - (foreach [element collection] (side-effect element)) + (for [element collection] (side-effect element)) - ;; foreach can have an optional else block - (foreach [element collection] (side-effect element) - (else (side-effect-2))) + ;; for can have an optional else block + (for [element collection] (side-effect element) + (else (side-effect-2))) -The optional `else` block is executed only if the `foreach` loop terminates +The optional `else` block is executed only if the `for` loop terminates normally. If the execution is halted with `break`, the `else` does not execute. .. code-block:: clj - => (foreach [element [1 2 3]] (if (< element 3) - ... (print element) - ... (break)) + => (for [element [1 2 3]] (if (< element 3) + ... (print element) + ... (break)) ... (else (print "loop finished"))) 1 2 - => (foreach [element [1 2 3]] (if (< element 4) - ... (print element) - ... (break)) + => (for [element [1 2 3]] (if (< element 4) + ... (print element) + ... (break)) ... (else (print "loop finished"))) 1 2 @@ -461,6 +635,28 @@ normally. If the execution is halted with `break`, the `else` does not execute. loop finished +.. _gensym: + +gensym +------ + +.. versionadded:: 0.9.12 + +`gensym` form is used to generate a unique symbol to allow writing macros +without accidental variable name clashes. + +.. code-block:: clj + + => (gensym) + u':G_1235' + + => (gensym "x") + u':x_1236' + +.. seealso:: + + Section :ref:`using-gensym` + get --- @@ -481,8 +677,8 @@ Example usages: .. note:: `get` raises a KeyError if a dictionary is queried for a non-existing key. -.. note:: `get` raises an IndexError if a list is queried for an index that is - out of bounds. +.. note:: `get` raises an IndexError if a list or a tuple is queried for an index + that is out of bounds. global @@ -508,13 +704,16 @@ would thrown a `NameError`. (set-a 5) (print-a) -if --- +if / if-not +----------- the `if` form is used to conditionally select code to be executed. It has to contain the condition block and the block to be executed if the condition evaluates `True`. Optionally it may contain a block that is executed in case -the evaluation of the condition is `False`. +the evaluation of the condition is `False`. The `if-not` form (*new in +0.9.13*) is similar, but the first block after the test will be +executed when the test fails, while the other, conditional one, when +the test succeeds - opposite of the order of the `if` form. Example usage: @@ -524,6 +723,10 @@ Example usage: (print "lets go shopping") (print "lets go and work")) + (if-not (money-left? account) + (print "lets go and work") + (print "lets go shopping")) + Truth values of Python objects are respected. Values `None`, `False`, zero of any numeric type, empty sequence and empty dictionary are considered `False`. Everything else is considered `True`. @@ -559,6 +762,9 @@ of import you can use. [os.path [exists isdir isfile]] [sys :as systest]) + ;; Import all module functions into current namespace + (import [sys [*]]) + kwapply ------- @@ -602,12 +808,28 @@ function is defined and passed to another function for filtering output. ... {:name "Dave" :age 5}]) => (defn display-people [people filter] - ... (foreach [person people] (if (filter person) (print (:name person))))) + ... (for [person people] (if (filter person) (print (:name person))))) => (display-people people (fn [person] (< (:age person) 25))) Alice Dave +Just as in normal function definitions, if the first element of the +body is a string, it serves as docstring. This is useful for giving +class methods docstrings. + + => (setv times-three + ... (fn [x] + ... "Multiplies input by three and returns the result." + ... (* x 3))) + + => (help times-three) + Help on function times_three: + + times_three(x) + Multiplies input by three and returns result + (END) + let --- @@ -736,6 +958,7 @@ using the backquote (`) symbol. .. code-block:: clj + ;; let `qux' be a variable with value (bar baz) `(foo ~qux) ; equivalent to '(foo (bar baz)) @@ -751,6 +974,7 @@ be alternatively written using the (') symbol .. code-block:: clj + => (setv x '(print "Hello World")) ; variable x is set to expression & not evaluated => x @@ -758,6 +982,7 @@ be alternatively written using the (') symbol => (eval x) Hello World + require ------- @@ -877,6 +1102,42 @@ given conditional is False. The following shows how the macro expands into code. None (do statement)) + +unquote +------- + +Within a quasiquoted form, `unquote` forces evaluation of a symbol. `unquote` +is aliased to the `~` symbol. + +.. code-block:: clj + + (def name "Cuddles") + (quasiquote (= name (unquote name))) + ;=> (u'=' u'name' u'Cuddles') + + `(= name ~name) + ;=> (u'=' u'name' u'Cuddles') + + +unquote-splice +-------------- + +`unquote-splice` forces the evaluation of a symbol within a quasiquoted form, +much like `unquote`. `unquote-splice` can only be used when the symbol being +unquoted contains an iterable value, as it "splices" that iterable into the +quasiquoted form. `unquote-splice` is aliased to the `~@` symbol. + +.. code-block:: clj + + (def nums [1 2 3 4]) + (quasiquote (+ (unquote-splice nums))) + ;=> (u'+' 1L 2L 3L 4L) + + `(+ ~@nums) + ;=> (u'+' 1L 2L 3L 4L) + + + when ---- @@ -913,16 +1174,18 @@ context to an argument or ignore it completely, as shown below: .. code-block:: clj - (with [arg (expr)] block) + (with [[arg (expr)]] block) - (with [(expr)] block) + (with [[(expr)]] block) + + (with [[arg (expr)] [(expr)]] block) The following example will open file `NEWS` and print its content on screen. The file is automatically closed after it has been processed. .. code-block:: clj - (with [f (open "NEWS")] (print (.read f))) + (with [[f (open "NEWS")]] (print (.read f))) with-decorator @@ -947,6 +1210,35 @@ values that are incremented by 1. When decorated `addition` is called with value 4 +.. _with-gensyms: + +with-gensyms +------------- + +.. versionadded:: 0.9.12 + +`with-gensym` form is used to generate a set of :ref:`gensym` for use +in a macro. + +.. code-block:: clojure + + (with-gensyms [a b c] + ...) + +expands to: + +.. code-block:: clojure + + (let [[a (gensym) + [b (gensym) + [c (gensym)]] + ...) + +.. seealso:: + + Section :ref:`using-gensym` + + yield ----- @@ -954,13 +1246,13 @@ yield The generator is iterable and therefore can be used in loops, list comprehensions and other similar constructs. -Especially the second example shows how generators can be used to generate +The function random-numbers shows how generators can be used to generate infinite series without consuming infinite amount of memory. .. code-block:: clj => (defn multiply [bases coefficients] - ... (foreach [(, base coefficient) (zip bases coefficients)] + ... (for [[(, base coefficient) (zip bases coefficients)]] ... (yield (* base coefficient)))) => (multiply (range 5) (range 5)) @@ -974,3 +1266,24 @@ infinite series without consuming infinite amount of memory. ... (while True (yield (.randint random low high)))) => (list-comp x [x (take 15 (random-numbers 1 50))])]) [7, 41, 6, 22, 32, 17, 5, 38, 18, 38, 17, 14, 23, 23, 19] + +.. _zipwith: + +zipwith +------- + +.. versionadded:: 0.9.13 + +`zipwith` zips multiple lists and maps the given function over the result. It is +equilavent to calling ``zip``, followed by calling ``map`` on the result. + +In the following example, `zipwith` is used to add the contents of two lists +together. The equilavent ``map`` and ``zip`` calls follow. + +.. code-block:: clj + + => (import operator.add) + => (zipwith operator.add [1 2 3] [4 5 6]) ; using zipwith + [5, 7, 9] + => (map operator.add (zip [1 2 3] [4 5 6])) ; using map+zip + [5, 7, 9] diff --git a/docs/language/cli.rst b/docs/language/cli.rst new file mode 100644 index 0000000..5b9dc73 --- /dev/null +++ b/docs/language/cli.rst @@ -0,0 +1,73 @@ +====================== +Command Line Interface +====================== + +hy +-- + +Command line options +^^^^^^^^^^^^^^^^^^^^ + +.. cmdoption:: -c + + Execute the Hy code in *command*. + + .. code-block:: bash + + $ hy -c "(print (+ 2 2))" + 4 + +.. cmdoption:: -i + + Execute the Hy code in *command*, then stay in REPL. + +.. cmdoption:: --spy + + Print equivalent Python code before executing. For example:: + + => (defn salutationsnm [name] (print (+ "Hy " name "!"))) + def salutationsnm(name): + return print(((u'Hy ' + name) + u'!')) + => (salutationsnm "YourName") + salutationsnm(u'YourName') + Hy YourName! + => + + .. versionadded:: 0.9.11 + +.. cmdoption:: --show-tracebacks + + Print extended tracebacks for Hy exceptions. + + .. versionadded:: 0.9.12 + +.. cmdoption:: -v + + Print the Hy version number and exit. + + +hyc +--- + +Command line options +^^^^^^^^^^^^^^^^^^^^ + +.. cmdoption:: file[, fileN] + + Compile Hy code to Python bytecode. For example, save the + following code as ``hyname.hy``: + + .. code-block:: clojure + + (defn hy-hy [name] + (print (+ "Hy " name "!"))) + + (hy-hy "Afroman") + + Then run: + + .. code-block:: bash + + $ hyc hyname.hy + $ python hyname.pyc + Hy Afroman! diff --git a/docs/language/core.rst b/docs/language/core.rst index acdc025..7781d21 100644 --- a/docs/language/core.rst +++ b/docs/language/core.rst @@ -6,6 +6,71 @@ Hy Core Core Functions =============== +.. _is-coll-fn: + +coll? +----- + +.. versionadded:: 0.9.13 + +Usage: ``(coll? x)`` + +Returns true if argument is iterable and not a string. + +.. code-block:: clojure + + => (coll? [1 2 3 4]) + True + + => (coll? {"a" 1 "b" 2}) + True + + => (coll? "abc") + False + + +cons +---- + +.. versionadded:: 0.9.13 + +Usage: ``(cons a b)`` + +Returns a fresh :ref:`cons cell ` with car `a` and cdr `b`. + +.. code-block:: clojure + + => (setv a (cons 'hd 'tl)) + + => (= 'hd (car a)) + True + + => (= 'tl (cdr a)) + True + + +cons? +----- + +.. versionadded:: 0.9.13 + +Usage: ``(cons? foo)`` + +Checks whether ``foo`` is a :ref:`cons cell `. + +.. code-block:: clojure + + => (setv a (cons 'hd 'tl)) + + => (cons? a) + True + + => (cons? nil) + False + + => (cons? [1 2 3]) + False + .. _dec-fn: dec @@ -29,6 +94,29 @@ Raises ``TypeError`` if ``(not (numeric? x))``. 11.3 +.. _disassemble-fn: + +disassemble +----------- + +.. versionadded:: 0.9.13 + +Usage: ``(disassemble tree &optional [codegen false])`` + +Dump the Python AST for given Hy ``tree`` to standard output. If *codegen* +is ``true`` function prints Python code instead. + +.. code-block:: clojure + + => (disassemble '(print "Hello World!")) + Module( + body=[ + Expr(value=Call(func=Name(id='print'), args=[Str(s='Hello World!')], keywords=[], starargs=None, kwargs=None))]) + + => (disassemble '(print "Hello World!") true) + print('Hello World!') + + .. _emtpy?-fn: empty? @@ -50,6 +138,32 @@ Return True if ``coll`` is empty, i.e. ``(= 0 (len coll))``. False +.. _every?-fn: + +every? +------ + +.. versionadded:: 0.9.13 + +Usage: ``(every? pred coll)`` + +Return True if ``(pred x)`` is logical true for every ``x`` in ``coll``, otherwise False. Return True if ``coll`` is empty. + +.. code-block:: clojure + + => (every? even? [2 4 6]) + True + + => (every? even? [1 3 5]) + False + + => (every? even? [2 4 5]) + False + + => (every? even? []) + True + + .. _float?-fn: float? @@ -91,6 +205,24 @@ Raises ``TypeError`` if ``(not (numeric? x))``. True +.. _identity-fn: + +identity +-------- + +Usage: ``(identity x)`` + +Returns argument supplied to the function + +.. code-block:: clojure + + => (identity 4) + 4 + + => (list (map identity [1 2 3 4])) + [1 2 3 4] + + .. _inc-fn: inc @@ -198,7 +330,7 @@ iterator? Usage: ``(iterator? x)`` -Return True if x is an iterator. Iterators are objects that return +Return True if x is an iterator. Iterators are objects that return themselves as an iterator when ``(iter x)`` is called. Contrast with :ref:`iterable?-fn`. @@ -220,6 +352,63 @@ Contrast with :ref:`iterable?-fn`. => (iterator? (iter {:a 1 :b 2 :c 3})) True +list* +----- + +Usage: ``(list* head &rest tail)`` + +Generate a chain of nested cons cells (a dotted list) containing the +arguments. If the argument list only has one element, return it. + +.. code-block:: clojure + + => (list* 1 2 3 4) + (1 2 3 . 4) + + => (list* 1 2 3 [4]) + [1, 2, 3, 4] + + => (list* 1) + 1 + + => (cons? (list* 1 2 3 4)) + True + +.. _macroexpand-fn: + +macroexpand +----------- + +.. versionadded:: 0.9.13 + +Usage: ``(macroexpand form)`` + +Returns the full macro expansion of form. + +.. code-block:: clojure + + => (macroexpand '(-> (a b) (x y))) + (u'x' (u'a' u'b') u'y') + + => (macroexpand '(-> (a b) (-> (c d) (e f)))) + (u'e' (u'c' (u'a' u'b') u'd') u'f') + +.. _macroexpand-1-fn: + +macroexpand-1 +------------- + +.. versionadded:: 0.9.13 + +Usage: ``(macroexpand-1 form)`` + +Returns the single step macro expansion of form. + +.. code-block:: clojure + + => (macroexpand-1 '(-> (a b) (-> (c d) (e f)))) + (u'_>' (u'a' u'b') (u'c' u'd') (u'e' u'f')) + .. _neg?-fn: neg? @@ -242,6 +431,36 @@ Raises ``TypeError`` if ``(not (numeric? x))``. => (neg? 0) False + +.. _nil?-fn: + +nil? +----- + +Usage: ``(nil? x)`` + +Return True if x is nil/None. + +.. code-block:: clojure + + => (nil? nil) + True + + => (nil? None) + True + + => (nil? 0) + False + + => (setf x nil) + => (nil? x) + True + + => ;; list.append always returns None + => (nil? (.append [1 2 3] 4)) + True + + .. _none?-fn: none? @@ -292,7 +511,7 @@ if the `n` is outside the range of `coll`. => (nth (take 3 (drop 2 [1 2 3 4 5 6])) 2)) 5 - + .. _numeric?-fn: numeric? @@ -377,6 +596,32 @@ Return the second member of ``coll``. Equivalent to 1 +.. _some-fn: + +some +---- + +.. versionadded:: 0.9.13 + +Usage: ``(some pred coll)`` + +Return True if ``(pred x)`` is logical true for any ``x`` in ``coll``, otherwise False. Return False if ``coll`` is empty. + +.. code-block:: clojure + + => (some even? [2 4 6]) + True + + => (some even? [1 3 5]) + False + + => (some even? [1 3 6]) + True + + => (some even? []) + False + + .. _string?-fn: string? @@ -397,7 +642,7 @@ Return True if x is a string. .. _zero?-fn: zero? ----- +----- Usage: ``(zero? x)`` @@ -469,7 +714,7 @@ To get the Fibonacci number at index 9, (starting from 0): .. code-block:: clojure => (nth (fib) 9) - 34 + 34 .. _cycle-fn: @@ -487,7 +732,7 @@ Return an infinite iterator of the members of coll. [1, 2, 3, 1, 2, 3, 1] => (list (take 2 (cycle [1 2 3]))) - [1, 2] + [1, 2] .. _distinct-fn: @@ -575,6 +820,26 @@ See also :ref:`remove-fn`. => (list (filter even? [1 2 3 -4 5 -7])) [2, -4] +.. _flatten-fn: + +flatten +------- + +.. versionadded:: 0.9.12 + +Usage: ``(flatten coll)`` + +Return a single list of all the items in ``coll``, by flattening all +contained lists and/or tuples. + +.. code-block:: clojure + + => (flatten [1 2 [3 4] 5]) + [1, 2, 3, 4, 5] + + => (flatten ["foo" (, 1 2) [1 [2 3] 4] "bar"]) + ['foo', 1, 2, 1, 2, 3, 4, 'bar'] + .. _iterate-fn: @@ -667,7 +932,7 @@ Return an iterator containing the first ``n`` members of ``coll``. => (list (take 4 (repeat "s"))) [u's', u's', u's', u's'] - + => (list (take 0 (repeat "s"))) [] @@ -693,7 +958,7 @@ Return an iterator containing every ``nth`` member of ``coll``. => (list (take-nth 10 [1 2 3 4 5 6 7])) [1] - + .. _take-while-fn: @@ -714,5 +979,3 @@ Return an iterator from ``coll`` as long as predicate, ``pred`` returns True. => (list (take-while neg? [ 1 2 3 -4 5])) [] - - diff --git a/docs/language/index.rst b/docs/language/index.rst index dc06211..d55032d 100644 --- a/docs/language/index.rst +++ b/docs/language/index.rst @@ -7,6 +7,8 @@ Contents: .. toctree:: :maxdepth: 3 + cli api core + readermacros internals diff --git a/docs/language/internals.rst b/docs/language/internals.rst index ccf8263..f294ca6 100644 --- a/docs/language/internals.rst +++ b/docs/language/internals.rst @@ -2,26 +2,469 @@ Internal Hy Documentation ========================= -.. info:: - These bits are for folks who hack on Hy it's self, mostly! +.. note:: These bits are mostly useful for folks who hack on Hy itself, + but can also be used for those delving deeper in macro programming. +.. _models: Hy Models ========= -.. TODO:: - Write this. +Introduction to Hy models +------------------------- + +Hy models are a very thin layer on top of regular Python objects, +representing Hy source code as data. Models only add source position +information, and a handful of methods to support clean manipulation of +Hy source code, for instance in macros. To achieve that goal, Hy models +are mixins of a base Python class and :ref:`HyObject`. + +.. _hyobject: + +HyObject +~~~~~~~~ + +``hy.models.HyObject`` is the base class of Hy models. It only +implements one method, ``replace``, which replaces the source position +of the current object with the one passed as argument. This allows us to +keep track of the original position of expressions that get modified by +macros, be that in the compiler or in pure hy macros. + +``HyObject`` is not intended to be used directly to instantiate Hy +models, but only as a mixin for other classes. + +Compound models +--------------- + +Parenthesized and bracketed lists are parsed as compound models by the +Hy parser. + +.. _hylist: + +HyList +~~~~~~ + +``hy.models.list.HyList`` is the base class of "iterable" Hy models. Its +basic use is to represent bracketed ``[]`` lists, which, when used as a +top-level expression, translate to Python list literals in the +compilation phase. + +Adding a HyList to another iterable object reuses the class of the +left-hand-side object, a useful behavior when you want to concatenate Hy +objects in a macro, for instance. + +.. _hyexpression: + +HyExpression +~~~~~~~~~~~~ + +``hy.models.expression.HyExpression`` inherits :ref:`HyList` for +parenthesized ``()`` expressions. The compilation result of those +expressions depends on the first element of the list: the compiler +dispatches expressions between compiler special-forms, user-defined +macros, and regular Python function calls. + +.. _hydict: + +HyDict +~~~~~~ + +``hy.models.dict.HyDict`` inherits :ref:`HyList` for curly-bracketed ``{}`` +expressions, which compile down to a Python dictionary literal. + +The decision of using a list instead of a dict as the base class for +``HyDict`` allows easier manipulation of dicts in macros, with the added +benefit of allowing compound expressions as dict keys (as, for instance, +the :ref:`HyExpression` Python class isn't hashable). + +Atomic models +------------- + +In the input stream, double-quoted strings, respecting the Python +notation for strings, are parsed as a single token, which is directly +parsed as a :ref:`HyString`. + +An ininterrupted string of characters, excluding spaces, brackets, +quotes, double-quotes and comments, is parsed as an identifier. + +Identifiers are resolved to atomic models during the parsing phase in +the following order: + + - :ref:`HyInteger ` + - :ref:`HyFloat ` + - :ref:`HyComplex ` (if the atom isn't a bare ``j``) + - :ref:`HyKeyword` (if the atom starts with ``:``) + - :ref:`HyLambdaListKeyword` (if the atom starts with ``&``) + - :ref:`HySymbol` + +.. _hystring: + +HyString +~~~~~~~~ + +``hy.models.string.HyString`` is the base class of string-equivalent Hy +models. It also represents double-quoted string literals, ``""``, which +compile down to unicode string literals in Python. ``HyStrings`` inherit +unicode objects in Python 2, and string objects in Python 3 (and are +therefore not encoding-dependent). + +``HyString`` based models are immutable. + +Hy literal strings can span multiple lines, and are considered by the +parser as a single unit, respecting the Python escapes for unicode +strings. + +.. _hy_numeric_models: + +Numeric models +~~~~~~~~~~~~~~ + +``hy.models.integer.HyInteger`` represents integer literals (using the +``long`` type on Python 2, and ``int`` on Python 3). + +``hy.models.float.HyFloat`` represents floating-point literals. + +``hy.models.complex.HyComplex`` represents complex literals. + +Numeric models are parsed using the corresponding Python routine, and +valid numeric python literals will be turned into their Hy counterpart. + +.. _hysymbol: + +HySymbol +~~~~~~~~ + +``hy.models.symbol.HySymbol`` is the model used to represent symbols +in the Hy language. It inherits :ref:`HyString`. + +``HySymbol`` objects are mangled in the parsing phase, to help Python +interoperability: + + - Symbols surrounded by asterisks (``*``) are turned into uppercase; + - Dashes (``-``) are turned into underscores (``_``); + - One trailing question mark (``?``) is turned into a leading ``is_``. + +Caveat: as the mangling is done during the parsing phase, it is possible +to programmatically generate HySymbols that can't be generated with Hy +source code. Such a mechanism is used by :ref:`gensym` to generate +"uninterned" symbols. + +.. _hykeyword: + +HyKeyword +~~~~~~~~~ + +``hy.models.keyword.HyKeyword`` represents keywords in Hy. Keywords are +symbols starting with a ``:``. The class inherits :ref:`HyString`. + +To distinguish :ref:`HyKeywords ` from :ref:`HySymbols +`, without the possibility of (involuntary) clashes, the +private-use unicode character ``"\uFDD0"`` is prepended to the keyword +literal before storage. + +.. _hylambdalistkeyword: + +HyLambdaListKeyword +~~~~~~~~~~~~~~~~~~~ + +``hy.models.lambdalist.HyLambdaListKeyword`` represents lambda-list +keywords, that is keywords used by the language definition inside +function signatures. Lambda-list keywords are symbols starting with a +``&``. The class inherits :ref:`HyString` + +.. _hycons: + +Cons Cells +========== + +``hy.models.cons.HyCons`` is a representation of Python-friendly `cons +cells`_. Cons cells are especially useful to mimic features of "usual" +LISP variants such as Scheme or Common Lisp. + +.. _cons cells: http://en.wikipedia.org/wiki/Cons + +A cons cell is a 2-item object, containing a ``car`` (head) and a +``cdr`` (tail). In some Lisp variants, the cons cell is the fundamental +building block, and S-expressions are actually represented as linked +lists of cons cells. This is not the case in Hy, as the usual +expressions are made of Python lists wrapped in a +``HyExpression``. However, the ``HyCons`` mimicks the behavior of +"usual" Lisp variants thusly: + + - ``(cons something nil)`` is ``(HyExpression [something])`` + - ``(cons something some-list)`` is ``((type some-list) (+ [something] + some-list))`` (if ``some-list`` inherits from ``list``). + - ``(get (cons a b) 0)`` is ``a`` + - ``(slice (cons a b) 1)`` is ``b`` + +Hy supports a dotted-list syntax, where ``'(a . b)`` means ``(cons 'a +'b)`` and ``'(a b . c)`` means ``(cons 'a (cons 'b 'c))``. If the +compiler encounters a cons cell at the top level, it raises a +compilation error. + +``HyCons`` wraps the passed arguments (car and cdr) in Hy types, to ease +the manipulation of cons cells in a macro context. + +Hy Internal Theory +================== + +.. _overview: + +Overview +-------- + +The Hy internals work by acting as a front-end to Python bytecode, so +that Hy itself compiles down to Python Bytecode, allowing an unmodified +Python runtime to run Hy code, without even noticing it. + +The way we do this is by translating Hy into an internal Python AST +datastructure, and building that AST down into Python bytecode using +modules from the Python standard library, so that we don't have to +duplicate all the work of the Python internals for every single Python +release. + +Hy works in four stages. The following sections will cover each step of Hy +from source to runtime. + +.. _lexing: + +Steps 1 and 2: Tokenizing and parsing +------------------------------------- + +The first stage of compiling hy is to lex the source into tokens that we can +deal with. We use a project called rply, which is a really nice (and fast) +parser, written in a subset of Python called rpython. + +The lexing code is all defined in ``hy.lex.lexer``. This code is mostly just +defining the Hy grammer, and all the actual hard parts are taken care of by +rply -- we just define "callbacks" for rply in ``hy.lex.parser``, which take +the tokens generated, and return the Hy models. + +You can think of the Hy models as the "AST" for Hy, it's what Macros operate +on (directly), and it's what the compiler uses when it compiles Hy down. + +.. seealso:: + + Section :ref:`models` for more information on Hy models and what they mean. + +.. _compiling: + +Step 3: Hy compilation to Python AST +------------------------------------ + +This is where most of the magic in Hy happens. This is where we take Hy AST +(the models), and compile them into Python AST. A couple of funky things happen +here to work past a few problems in AST, and working in the compiler is some +of the most important work we do have. + +The compiler is a bit complex, so don't feel bad if you don't grok it on the +first shot, it may take a bit of time to get right. + +The main entry-point to the Compiler is ``HyASTCompiler.compile``. This method +is invoked, and the only real "public" method on the class (that is to say, +we don't really promise the API beyond that method). + +In fact, even internally, we don't recurse directly hardly ever, we almost +always force the Hy tree through ``compile``, and will often do this with +sub-elements of an expression that we have. It's up to the Type-based dispatcher +to properly dispatch sub-elements. + +All methods that preform a compilation are marked with the ``@builds()`` +decorator. You can either pass the class of the Hy model that it compiles, +or you can use a string for expressions. I'll clear this up in a second. + +First stage type-dispatch +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's start in the ``compile`` method. The first thing we do is check the +Type of the thing we're building. We look up to see if we have a method that +can build the ``type()`` that we have, and dispatch to the method that can +handle it. If we don't have any methods that can build that type, we raise +an internal ``Exception``. + +For instance, if we have a ``HyString``, we have an almost 1-to-1 mapping of +Hy AST to Python AST. The ``compile_string`` method takes the ``HyString``, and +returns an ``ast.Str()`` that's populated with the correct line-numbers and +content. + +Macro-expand +~~~~~~~~~~~~ + +If we get a ``HyExpression``, we'll attempt to see if this is a known +Macro, and push to have it expanded by invoking ``hy.macros.macroexpand``, then +push the result back into ``HyASTCompiler.compile``. + +Second stage expression-dispatch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The only special case is the ``HyExpression``, since we need to create different +AST depending on the special form in question. For instance, when we hit an +``(if true true false)``, we need to generate a ``ast.If``, and properly +compile the sub-nodes. This is where the ``@builds()`` with a String as an +argument comes in. + +For the ``compile_expression`` (which is defined with an +``@builds(HyExpression)``) will dispatch based on the string of the first +argument. If, for some reason, the first argument is not a string, it will +properly handle that case as well (most likely by raising an ``Exception``). + +If the String isn't known to Hy, it will default to create an ``ast.Call``, +which will try to do a runtime call (in Python, something like ``foo()``). + +Issues hit with Python AST +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Python AST is great; it's what's enabled us to write such a powerful project +on top of Python without having to fight Python too hard. Like anything, we've +had our fair share of issues, and here's a short list of the common ones you +might run into. + +*Python differentiates between Statements and Expressions*. + +This might not sound like a big deal -- in fact, to most Python programmers, +this will shortly become a "Well, yeah" moment. + +In Python, doing something like: + +``print for x in range(10): pass``, because ``print`` prints expressions, and +``for`` isn't an expression, it's a control flow statement. Things like +``1 + 1`` are Expressions, as is ``lambda x: 1 + x``, but other language +features, such as ``if``, ``for``, or ``while`` are statements. + +Since they have no "value" to Python, this makes working in Hy hard, since +doing something like ``(print (if true true false))`` is not just common, it's +expected. + +As a result, we auto-mangle things using a ``Result`` object, where we offer +up any ``ast.stmt`` that need to get run, and a single ``ast.expr`` that can +be used to get the value of whatever was just run. Hy does this by forcing +assignment to things while running. + +As example, the Hy:: + + (print (if true true false)) + +Will turn into:: + + if True: + _mangled_name_here = True + else: + _mangled_name_here = False + + print _mangled_name_here + + +OK, that was a bit of a lie, since we actually turn that statement +into:: + + print True if True else False + +By forcing things into an ``ast.expr`` if we can, but the general idea holds. + + +Step 4: Python bytecode output and runtime +------------------------------------------ + +After we have a Python AST tree that's complete, we can try and compile it to +Python bytecode by pushing it through ``eval``. From here on out, we're no +longer in control, and Python is taking care of everything. This is why things +like Python tracebacks, pdb and django apps work. Hy Macros ========= -.. TODO:: - Write this. +.. _using-gensym: + +Using gensym for safer macros +------------------------------ + +When writing macros, one must be careful to avoid capturing external variables +or using variable names that might conflict with user code. + +We will use an example macro ``nif`` (see http://letoverlambda.com/index.cl/guest/chap3.html#sec_5 +for a more complete description.) ``nif`` is an example, something like a numeric ``if``, +where based on the expression, one of the 3 forms is called depending on if the +expression is positive, zero or negative. + +A first pass might be someting like: + +.. code-block:: clojure + + (defmacro nif [expr pos-form zero-form neg-form] + `(let [[obscure-name ~expr]] + (cond [(pos? obscure-name) ~pos-form] + [(zero? obscure-name) ~zero-form] + [(neg? obscure-name) ~neg-form]))) + +where ``obsure-name`` is an attempt to pick some variable name as not to +conflict with other code. But of course, while well-intentioned, +this is no guarantee. + +The method :ref:`gensym` is designed to generate a new, unique symbol for just +such an occasion. A much better version of ``nif`` would be: + +.. code-block:: clojure + + (defmacro nif [expr pos-form zero-form neg-form] + (let [[g (gensym)]] + `(let [[~g ~expr]] + (cond [(pos? ~g) ~pos-form] + [(zero? ~g) ~zero-form] + [(neg? ~g) ~neg-form])))) + +This is an easy case, since there is only one symbol. But if there is +a need for several gensym's there is a second macro :ref:`with-gensyms` that +basically expands to a series of ``let`` statements: + +.. code-block:: clojure + + (with-gensyms [a b c] + ...) + +expands to: + +.. code-block:: clojure + + (let [[a (gensym) + [b (gensym) + [c (gensym)]] + ...) + +so our re-written ``nif`` would look like: + +.. code-block:: clojure + + (defmacro nif [expr pos-form zero-form neg-form] + (with-gensyms [g] + `(let [[~g ~expr]] + (cond [(pos? ~g) ~pos-form] + [(zero? ~g) ~zero-form] + [(neg? ~g) ~neg-form])))) + +Finally, though we can make a new macro that does all this for us. :ref:`defmacro/g!` +will take all symbols that begin with ``g!`` and automatically call ``gensym`` with the +remainder of the symbol. So ``g!a`` would become ``(gensym "a")``. + +Our final version of ``nif``, built with ``defmacro/g!`` becomes: + +.. code-block:: clojure + + (defmacro/g! nif [expr pos-form zero-form neg-form] + `(let [[~g!res ~expr]] + (cond [(pos? ~g!res) ~pos-form] + [(zero? ~g!res) ~zero-form] + [(neg? ~g!res) ~neg-form])))) + + + +Checking macro arguments and raising exceptions +----------------------------------------------- + Hy Compiler Builtins ==================== -.. TODO:: +.. todo:: Write this. diff --git a/docs/language/readermacros.rst b/docs/language/readermacros.rst new file mode 100644 index 0000000..392feb6 --- /dev/null +++ b/docs/language/readermacros.rst @@ -0,0 +1,73 @@ +.. _reader-macros: + +.. highlight:: clj + +============= +Reader Macros +============= + +Reader macros gives LISP the power to modify and alter syntax on the fly. +You don't want polish notation? A reader macro can easily do just that. Want +Clojure's way of having a regex? Reader macros can also do this easily. + + +Syntax +====== + +:: + + => (defreader ^ [expr] (print expr)) + => #^(1 2 3 4) + (1 2 3 4) + => #^"Hello" + "Hello" + => #^1+2+3+4+3+2 + 1+2+3+4+3+2 + +Hy has no literal for tuples. Lets say you dislike `(, ...)` and want something +else. This is a problem reader macros are able to solve in a neat way. + +:: + + => (defreader t [expr] `(, ~@expr)) + => #t(1 2 3) + (1, 2, 3) + +You could even do like clojure, and have a literal for regular expressions! + +:: + + => (import re) + => (defreader r [expr] `(re.compile ~expr)) + => #r".*" + <_sre.SRE_Pattern object at 0xcv7713ph15#> + + +Implementation +============== + +``defreader`` takes a single character as symbol name for the reader macro, +anything longer will return an error. Implementation wise, ``defreader`` +expands into a lambda covered with a decorator, this decorater saves the +lambda in a dict with its module name and symbol. + +:: + + => (defreader ^ [expr] (print expr)) + ;=> (with_decorator (hy.macros.reader ^) (fn [expr] (print expr))) + +``#`` expands into ``(dispatch_reader_macro ...)`` where the symbol +and expression is passed to the correct function. + +:: + + => #^() + ;=> (dispatch_reader_macro ^ ()) + => #^"Hello" + "Hello" + + +.. warning:: + Because of a limitation in Hy's lexer and parser, reader macros can't + redefine defined syntax such as ``()[]{}``. This will most likely be + adressed in the future. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 18c554c..79a9255 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -2,21 +2,20 @@ Quickstart ========== -.. image:: _static/cuddles.png +.. image:: _static/cuddles-transparent-small.png :alt: Karen Rustard's Cuddles - :align: left -(thanks to Karen Rustad for Cuddles!) +(Thanks to Karen Rustad for Cuddles!) -HOW TO GET HY REAL FAST: +**HOW TO GET HY REAL FAST**: -1. create a `Python virtual environment +1. Create a `Virtual Python Environment `_ -2. activate your Python virtual environment -3. ``pip install hy`` -4. start a REPL with ``hy`` -5. type stuff in the REPL:: +2. Activate your Virtual Python Environment +3. Install `hy from PyPI `_ with ``pip install hy`` +4. Start a REPL with ``hy`` +5. Type stuff in the REPL:: => (print "Hy!") Hy! @@ -26,20 +25,19 @@ HOW TO GET HY REAL FAST: etc -6. hit CTRL-D when you're done +6. Hit CTRL-D when you're done OMG! That's amazing! I want to write a hy program. -7. open up an elite programming editor -8. type:: +7. Open up an elite programming editor and type:: - (print "i was going to code in python syntax, but then i got hy") + (print "I was going to code in python syntax, but then I got hy.") -9. save as ``test_program_of_awesome.hy`` -10. run:: +8. Save as ``awesome.hy`` +9. And run your first Hy program:: - hy test_program_of_awesome.hy + hy awesome.hy -11. take a deep breath so as to not hyperventilate -12. smile villainously and sneak off to your hydeaway and do +10. Take a deep breath so as to not hyperventilate +11. Smile villainously and sneak off to your hydeaway and do unspeakable things diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c856cf7..4acb72b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -276,7 +276,7 @@ You might notice above that if you have code like: (body-if-true) (body-if-false)) -But wait! What if you want to execute more than one statment in the +But wait! What if you want to execute more than one statement in the body of one of these? You can do the following: @@ -289,7 +289,7 @@ You can do the following: (print "and why not, let's keep talking about how true it is!)) (print "this one's still simply just false")) -You can see that we used "do" to wrap multiple statments. If you're +You can see that we used "do" to wrap multiple statements. If you're familiar with other lisps, this is the equivalent of "progn" elsewhere. @@ -311,8 +311,8 @@ The equivalent in hy would be: .. code-block:: clj - (for (i (range 10)) - (print (+ "'i' is now at " (str i)))) + (for [i (range 10)] + (print (+ "'i' is now at " (str i)))) You can also import and make use of various python libraries. For @@ -330,13 +330,13 @@ Python's context managers ('with' statements) are used like this: .. code-block:: clj - (with [f (file "/tmp/data.in")] - (print (.read f))) + (with [[f (open "/tmp/data.in")]] + (print (.read f))) which is equivalent to:: - with file("/tmp/data.in") as f: - print f.read() + with open("/tmp/data.in") as f: + print f.read() And yes, we do have lisp comprehensions! In Python you might do:: @@ -363,7 +363,7 @@ In hy, you could do these like: (list-comp (, x y) - (x (range 9) + (x (range 8) y "ABCDEFGH")) ; [(0, 'A'), (0, 'B'), (0, 'C'), (0, 'D'), (0, 'E'), (0, 'F'), (0, 'G'), (0, 'H'), @@ -373,8 +373,7 @@ In hy, you could do these like: ; (4, 'A'), (4, 'B'), (4, 'C'), (4, 'D'), (4, 'E'), (4, 'F'), (4, 'G'), (4, 'H'), ; (5, 'A'), (5, 'B'), (5, 'C'), (5, 'D'), (5, 'E'), (5, 'F'), (5, 'G'), (5, 'H'), ; (6, 'A'), (6, 'B'), (6, 'C'), (6, 'D'), (6, 'E'), (6, 'F'), (6, 'G'), (6, 'H'), - ; (7, 'A'), (7, 'B'), (7, 'C'), (7, 'D'), (7, 'E'), (7, 'F'), (7, 'G'), (7, 'H'), - ; (8, 'A'), (8, 'B'), (8, 'C'), (8, 'D'), (8, 'E'), (8, 'F'), (8, 'G'), (8, 'H')] + ; (7, 'A'), (7, 'B'), (7, 'C'), (7, 'D'), (7, 'E'), (7, 'F'), (7, 'G'), (7, 'H')] Python has support for various fancy argument and keyword arguments. @@ -406,7 +405,7 @@ The same thing in Hy:: ... [3, 2, 1, 4] -See how we use kwapply to handle the fancy pssing? :) +See how we use kwapply to handle the fancy passing? :) There's also a dictionary-style keyword arguments construction that looks like: @@ -436,12 +435,18 @@ The Hy equivalent: Finally, of course we need classes! In python we might have a class like:: - class FooBar (object): - def __init__(self, x): - self.x = x + class FooBar(object): + """ + Yet Another Example Class + """ + def __init__(self, x): + self.x = x - def get_x(self): - return self.x + def get_x(self): + """ + Return our copy of x + """ + return self.x In Hy: @@ -449,6 +454,7 @@ In Hy: .. code-block:: clj (defclass FooBar [object] + "Yet Another Example Class" [[--init-- (fn [self x] (setv self.x x) @@ -458,6 +464,7 @@ In Hy: [get-x (fn [self] + "Return our copy of x" self.x)]]) diff --git a/eg/flask/meth_example.hy b/eg/flask/meth_example.hy new file mode 100644 index 0000000..4df70d7 --- /dev/null +++ b/eg/flask/meth_example.hy @@ -0,0 +1,27 @@ +;;; Simple Flask application +;;; +;;; Requires to have Flask installed +;;; +;;; You can test it via: +;;; +;;; $ curl 127.0.0.1:5151 +;;; $ curl -X POST 127.0.0.1:5151/post +;;; $ curl -X POST 127.0.0.1:5151/both +;;; $ curl 127.0.0.1:5151/both + +(import [flask [Flask]]) + +(require hy.contrib.meth) + +(setv app (Flask "__main__")) + +(route get-index "/" [] + (str "Hy world!")) + +(post-route post-index "/post" [] + (str "Hy post world!")) + +(route-with-methods both-index "/both" ["GET" "POST"] [] + (str "Hy to both worlds!")) + +(apply app.run [] {"port" 5151}) diff --git a/eg/nonfree/halting-problem/halting.hy b/eg/nonfree/halting-problem/halting.hy deleted file mode 100755 index 9c33c08..0000000 --- a/eg/nonfree/halting-problem/halting.hy +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env hy - -;; Very much a knockoff (straight port) of Dan Gulotta's 2013 MIT Mystery Hunt -;; puzzle "The Halting Problem". His Copyright terms are unclear, so presume -;; that this is distributable, but not free. - - -(defn evaluate [f] ((f (lambda [x] (+ x 1))) 0)) - -(defn successor [n] (lambda [f] (lambda [x] (f ((n f) x))))) -(defn plus [m n] ((n successor) m)) -(defn exponent [m n] (n m)) -(defn zero [f] (lambda [x] x)) -(defn one [f] (lambda [x] (f x))) - -(defn predecessor [n] (lambda [f] (lambda [x] - (((n (lambda [g] (lambda [h] (h (g f))))) (lambda [y] x)) (lambda [z] z))))) - -(defn subtract [m n] ((m predecessor) n)) - -(setv two (plus one one)) -(setv three (plus two one)) - -(print (evaluate (exponent three three))) diff --git a/eg/python3/futures/hello-world.hy b/eg/python3/futures/hello-world.hy index 19d99d4..99f691f 100644 --- a/eg/python3/futures/hello-world.hy +++ b/eg/python3/futures/hello-world.hy @@ -7,5 +7,5 @@ (with-as (ThreadPoolExecutor 10) executor (setv jobs (list-comp (.submit executor task-to-do) (x (range 0 10)))) - (for (future (as-completed jobs)) + (for [future (as-completed jobs)] (.result future))) diff --git a/hy/__init__.py b/hy/__init__.py index fd95f90..f3a26a1 100644 --- a/hy/__init__.py +++ b/hy/__init__.py @@ -32,6 +32,7 @@ from hy.models.symbol import HySymbol # NOQA from hy.models.float import HyFloat # NOQA from hy.models.dict import HyDict # NOQA from hy.models.list import HyList # NOQA +from hy.models.cons import HyCons # NOQA import hy.importer # NOQA diff --git a/hy/_compat.py b/hy/_compat.py index e30d0dc..096282a 100644 --- a/hy/_compat.py +++ b/hy/_compat.py @@ -1,5 +1,6 @@ # Copyright (c) 2013 Paul Tagliamonte # Copyright (c) 2013 Julien Danjou +# Copyright (c) 2013 Berker Peksag # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -23,9 +24,24 @@ try: import __builtin__ as builtins except ImportError: import builtins # NOQA +try: + from py_compile import MAGIC, wr_long +except ImportError: + # py_compile.MAGIC removed and imp.get_magic() deprecated in Python 3.4 + from importlib.util import MAGIC_NUMBER as MAGIC # NOQA + + def wr_long(f, x): + """Internal; write a 32-bit int to a file in little-endian order.""" + f.write(bytes([x & 0xff, + (x >> 8) & 0xff, + (x >> 16) & 0xff, + (x >> 24) & 0xff])) import sys +PY27 = sys.version_info >= (2, 7) PY3 = sys.version_info[0] >= 3 +PY33 = sys.version_info >= (3, 3) +PY34 = sys.version_info >= (3, 4) if PY3: str_type = str diff --git a/hy/cmdline.py b/hy/cmdline.py index 899224c..98f10cd 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -5,6 +5,7 @@ # Copyright (c) 2013 Konrad Hinsen # Copyright (c) 2013 Thom Neale # Copyright (c) 2013 Will Kahn-Greene +# Copyright (c) 2013 Bob Tolbert # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -32,7 +33,7 @@ import sys import hy from hy.lex import LexException, PrematureEndOfInput, tokenize -from hy.compiler import hy_compile +from hy.compiler import hy_compile, HyTypeError from hy.importer import ast_compile, import_buffer_to_module from hy.completer import completion @@ -73,17 +74,22 @@ def print_python_code(_ast): class HyREPL(code.InteractiveConsole): - def __init__(self, spy=False): + def __init__(self, spy=False, locals=None, filename=""): self.spy = spy - code.InteractiveConsole.__init__(self) + code.InteractiveConsole.__init__(self, locals=locals, + filename=filename) def runsource(self, source, filename='', symbol='single'): + global SIMPLE_TRACEBACKS try: tokens = tokenize(source) except PrematureEndOfInput: return True - except LexException: - self.showsyntaxerror(filename) + except LexException as e: + if e.source is None: + e.source = source + e.filename = filename + sys.stderr.write(str(e)) return False try: @@ -91,6 +97,15 @@ class HyREPL(code.InteractiveConsole): if self.spy: print_python_code(_ast) code = ast_compile(_ast, filename, symbol) + except HyTypeError as e: + if e.source is None: + e.source = source + e.filename = filename + if SIMPLE_TRACEBACKS: + sys.stderr.write(str(e)) + else: + self.showtraceback() + return False except Exception: self.showtraceback() return False @@ -153,22 +168,34 @@ def ideas_macro(): require("hy.cmdline", "__console__") require("hy.cmdline", "__main__") +SIMPLE_TRACEBACKS = True + def run_command(source): try: import_buffer_to_module("__main__", source) - except LexException as exc: - # TODO: This would be better if we had line, col info. - print(source) - print(repr(exc)) - return 1 + except (HyTypeError, LexException) as e: + if SIMPLE_TRACEBACKS: + sys.stderr.write(str(e)) + return 1 + raise + except Exception: + raise return 0 def run_file(filename): from hy.importer import import_file_to_module - import_file_to_module("__main__", filename) - return 0 # right? + try: + import_file_to_module("__main__", filename) + except (HyTypeError, LexException) as e: + if SIMPLE_TRACEBACKS: + sys.stderr.write(str(e)) + return 1 + raise + except Exception: + raise + return 0 def run_repl(hr=None, spy=False): @@ -187,8 +214,8 @@ def run_repl(hr=None, spy=False): return 0 -def run_icommand(source): - hr = HyREPL() +def run_icommand(source, spy=False): + hr = HyREPL(spy) hr.runsource(source, filename='', symbol='single') return run_repl(hr) @@ -217,6 +244,9 @@ def cmdline_handler(scriptname, argv): parser.add_argument("-v", action="version", version=VERSION) + parser.add_argument("--show-tracebacks", action="store_true", + help="show complete tracebacks for Hy exceptions") + # this will contain the script/program name and any arguments for it. parser.add_argument('args', nargs=argparse.REMAINDER, help=argparse.SUPPRESS) @@ -227,11 +257,12 @@ def cmdline_handler(scriptname, argv): options = parser.parse_args(argv[1:]) + if options.show_tracebacks: + global SIMPLE_TRACEBACKS + SIMPLE_TRACEBACKS = False + # reset sys.argv like Python - if options.args and len(options.args) > 0: - sys.argv = options.args - else: # Python default - sys.argv = [''] + sys.argv = options.args or [""] if options.command: # User did "hy -c ..." @@ -239,7 +270,7 @@ def cmdline_handler(scriptname, argv): if options.icommand: # User did "hy -i ..." - return run_icommand(options.icommand) + return run_icommand(options.icommand, spy=options.spy) if options.args: if options.args[0] == "-": diff --git a/hy/compiler.py b/hy/compiler.py index f14c79f..dbb4782 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -4,6 +4,7 @@ # Copyright (c) 2013 Julien Danjou # Copyright (c) 2013 Nicolas Dandrimont # Copyright (c) 2013 James King +# Copyright (c) 2013 Bob Tolbert # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -23,8 +24,6 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from hy.errors import HyError - from hy.models.lambdalist import HyLambdaListKeyword from hy.models.expression import HyExpression from hy.models.keyword import HyKeyword @@ -35,9 +34,13 @@ from hy.models.symbol import HySymbol from hy.models.float import HyFloat from hy.models.list import HyList from hy.models.dict import HyDict +from hy.models.cons import HyCons -from hy.macros import require, macroexpand -from hy._compat import str_type +from hy.errors import HyCompileError, HyTypeError + +import hy.macros +from hy._compat import str_type, long_type, PY27, PY33, PY3, PY34 +from hy.macros import require, macroexpand, reader_macroexpand import hy.importer import traceback @@ -71,39 +74,11 @@ def load_stdlib(): _stdlib[e] = module -class HyCompileError(HyError): - def __init__(self, exception, traceback=None): - self.exception = exception - self.traceback = traceback - - def __str__(self): - if isinstance(self.exception, HyTypeError): - return str(self.exception) - if self.traceback: - tb = "".join(traceback.format_tb(self.traceback)).strip() - else: - tb = "No traceback available. 😟" - return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s" - % (self.exception.__class__.__name__, - self.exception, tb)) - - -class HyTypeError(TypeError): - def __init__(self, expression, message): - super(HyTypeError, self).__init__(message) - self.expression = expression - - def __str__(self): - return (super(HyTypeError, self).__str__() + " (line %s, column %d)" - % (self.expression.start_line, - self.expression.start_column)) - - _compile_table = {} def ast_str(foobar): - if sys.version_info[0] >= 3: + if PY3: return str(foobar) try: @@ -340,7 +315,7 @@ def checkargs(exact=None, min=None, max=None, even=None): if min is not None and (len(expression) - 1) < min: _raise_wrong_args_number( expression, - "`%%s' needs at least %d arguments, got %%d" % (min)) + "`%%s' needs at least %d arguments, got %%d." % (min)) if max is not None and (len(expression) - 1) > max: _raise_wrong_args_number( @@ -419,7 +394,6 @@ class HyASTCompiler(object): def compile(self, tree): try: - tree = macroexpand(tree, self.module_name) _type = type(tree) ret = self.compile_atom(_type, tree) if ret: @@ -430,6 +404,8 @@ class HyASTCompiler(object): # nested; so let's re-raise this exception, let's not wrap it in # another HyCompileError! raise + except HyTypeError as e: + raise except Exception as e: raise HyCompileError(e, sys.exc_info()[2]) @@ -534,18 +510,21 @@ class HyASTCompiler(object): return ret, args, defaults, varargs, kwargs - def _storeize(self, name): + def _storeize(self, name, func=None): """Return a new `name` object with an ast.Store() context""" + if not func: + func = ast.Store + if isinstance(name, Result): if not name.is_expr(): - raise TypeError("Can't assign to a non-expr") + raise TypeError("Can't assign / delete a non-expression") name = name.expr if isinstance(name, (ast.Tuple, ast.List)): typ = type(name) new_elts = [] for x in name.elts: - new_elts.append(self._storeize(x)) + new_elts.append(self._storeize(x, func)) new_name = typ(elts=new_elts) elif isinstance(name, ast.Name): new_name = ast.Name(id=name.id, arg=name.arg) @@ -554,9 +533,9 @@ class HyASTCompiler(object): elif isinstance(name, ast.Attribute): new_name = ast.Attribute(value=name.value, attr=name.attr) else: - raise TypeError("Can't assign to a %s object" % type(name)) + raise TypeError("Can't assign / delete a %s object" % type(name)) - new_name.ctx = ast.Store() + new_name.ctx = func() ast.copy_location(new_name, name) return new_name @@ -619,6 +598,24 @@ class HyASTCompiler(object): return imports, HyExpression([HySymbol(name), contents]).replace(form), False + elif isinstance(form, HyCons): + ret = HyExpression([HySymbol(name)]) + nimport, contents, splice = self._render_quoted_form(form.car, + level) + if splice: + raise HyTypeError(form, "Can't splice dotted lists yet") + imports.update(nimport) + ret.append(contents) + + nimport, contents, splice = self._render_quoted_form(form.cdr, + level) + if splice: + raise HyTypeError(form, "Can't splice the cdr of a cons") + imports.update(nimport) + ret.append(contents) + + return imports, ret.replace(form), False + elif isinstance(form, (HySymbol, HyLambdaListKeyword)): return imports, HyExpression([HySymbol(name), HyString(form)]).replace(form), False @@ -776,7 +773,7 @@ class HyASTCompiler(object): ret = handler_results - if sys.version_info[0] >= 3 and sys.version_info[1] >= 3: + if PY33: # Python 3.3 features a merge of TryExcept+TryFinally into Try. return ret + ast.Try( lineno=expr.start_line, @@ -853,7 +850,7 @@ class HyASTCompiler(object): exceptions, "Exception storage target name must be a symbol.") - if sys.version_info[0] >= 3: + if PY3: # Python3 features a change where the Exception handler # moved the name from a Name() to a pure Python String type. # @@ -1037,6 +1034,10 @@ class HyASTCompiler(object): while len(expr) > 0: iexpr = expr.pop(0) + if not isinstance(iexpr, (HySymbol, HyList)): + raise HyTypeError(iexpr, "(import) requires a Symbol " + "or a List.") + if isinstance(iexpr, HySymbol): rimports += _compile_import(expr, iexpr) continue @@ -1083,18 +1084,81 @@ class HyASTCompiler(object): return rimports @builds("get") - @checkargs(2) + @checkargs(min=2) def compile_index_expression(self, expr): expr.pop(0) # index - val = self.compile(expr.pop(0)) # target - sli = self.compile(expr.pop(0)) # slice - return val + sli + ast.Subscript( + val = self.compile(expr.pop(0)) + slices, ret = self._compile_collect(expr) + + if val.stmts: + ret += val + + for sli in slices: + val = Result() + ast.Subscript( + lineno=expr.start_line, + col_offset=expr.start_column, + value=val.force_expr, + slice=ast.Index(value=sli), + ctx=ast.Load()) + + return ret + val + + @builds(".") + @checkargs(min=1) + def compile_attribute_access(self, expr): + expr.pop(0) # dot + + ret = self.compile(expr.pop(0)) + + for attr in expr: + if isinstance(attr, HySymbol): + ret += ast.Attribute(lineno=attr.start_line, + col_offset=attr.start_column, + value=ret.force_expr, + attr=ast_str(attr), + ctx=ast.Load()) + elif type(attr) == HyList: + if len(attr) != 1: + raise HyTypeError( + attr, + "The attribute access DSL only accepts HySymbols " + "and one-item lists, got {0}-item list instead".format( + len(attr), + ), + ) + compiled_attr = self.compile(attr.pop(0)) + ret = compiled_attr + ret + ast.Subscript( + lineno=attr.start_line, + col_offset=attr.start_column, + value=ret.force_expr, + slice=ast.Index(value=compiled_attr.force_expr), + ctx=ast.Load()) + else: + raise HyTypeError( + attr, + "The attribute access DSL only accepts HySymbols " + "and one-item lists, got {0} instead".format( + type(attr).__name__, + ), + ) + + return ret + + @builds("del") + @checkargs(min=1) + def compile_del_expression(self, expr): + expr.pop(0) + ld_targets, ret = self._compile_collect(expr) + + del_targets = [] + for target in ld_targets: + del_targets.append(self._storeize(target, ast.Del)) + + return ret + ast.Delete( lineno=expr.start_line, col_offset=expr.start_column, - value=val.force_expr, - slice=ast.Index(value=sli.force_expr), - ctx=ast.Load()) + targets=del_targets) @builds("slice") @checkargs(min=1, max=4) @@ -1159,14 +1223,18 @@ class HyASTCompiler(object): fn.stmts[-1].decorator_list = decorators return ret + fn - @builds("with") + @builds("with*") @checkargs(min=2) def compile_with_expression(self, expr): - expr.pop(0) # with + expr.pop(0) # with* args = expr.pop(0) - if len(args) > 2 or len(args) < 1: - raise HyTypeError(expr, "with needs [arg (expr)] or [(expr)]") + if not isinstance(args, HyList): + raise HyTypeError(expr, + "with expects a list, received `{0}'".format( + type(args).__name__)) + if len(args) < 1: + raise HyTypeError(expr, "with needs [[arg (expr)]] or [[(expr)]]]") args.reverse() ctx = self.compile(args.pop(0)) @@ -1195,7 +1263,7 @@ class HyASTCompiler(object): optional_vars=thing, body=body.stmts) - if sys.version_info[0] >= 3 and sys.version_info[1] >= 3: + if PY33: the_with.items = [ast.withitem(context_expr=ctx.force_expr, optional_vars=thing)] @@ -1220,54 +1288,192 @@ class HyASTCompiler(object): ctx=ast.Load()) return ret + def _compile_generator_iterables(self, trailers): + """Helper to compile the "trailing" parts of comprehensions: + generators and conditions""" + + generators = trailers.pop(0) + + cond = self.compile(trailers.pop(0)) if trailers != [] else Result() + + gen_it = iter(generators) + paired_gens = zip(gen_it, gen_it) + + gen_res = Result() + gen = [] + for target, iterable in paired_gens: + comp_target = self.compile(target) + target = self._storeize(comp_target) + gen_res += self.compile(iterable) + gen.append(ast.comprehension( + target=target, + iter=gen_res.force_expr, + ifs=[])) + + if cond.expr: + gen[-1].ifs.append(cond.expr) + + return gen_res + cond, gen + @builds("list_comp") @checkargs(min=2, max=3) def compile_list_comprehension(self, expr): # (list-comp expr (target iter) cond?) expr.pop(0) expression = expr.pop(0) - tar_it = iter(expr.pop(0)) - targets = zip(tar_it, tar_it) - cond = self.compile(expr.pop(0)) if expr != [] else Result() - - generator_res = Result() - generators = [] - for target, iterable in targets: - comp_target = self.compile(target) - target = self._storeize(comp_target) - generator_res += self.compile(iterable) - generators.append(ast.comprehension( - target=target, - iter=generator_res.force_expr, - ifs=[])) - - if cond.expr: - generators[-1].ifs.append(cond.expr) + gen_res, gen = self._compile_generator_iterables(expr) compiled_expression = self.compile(expression) - ret = compiled_expression + generator_res + cond + ret = compiled_expression + gen_res ret += ast.ListComp( lineno=expr.start_line, col_offset=expr.start_column, elt=compiled_expression.force_expr, - generators=generators) + generators=gen) return ret - @builds("kwapply") - @checkargs(2) - def compile_kwapply_expression(self, expr): - expr.pop(0) # kwapply - call = self.compile(expr.pop(0)) - kwargs = self.compile(expr.pop(0)) + @builds("set_comp") + @checkargs(min=2, max=3) + def compile_set_comprehension(self, expr): + if PY27: + ret = self.compile_list_comprehension(expr) + expr = ret.expr + ret.expr = ast.SetComp( + lineno=expr.lineno, + col_offset=expr.col_offset, + elt=expr.elt, + generators=expr.generators) - if type(call.expr) != ast.Call: - raise HyTypeError(expr, "kwapplying a non-call") + return ret - call.expr.kwargs = kwargs.force_expr + expr[0] = HySymbol("list_comp").replace(expr[0]) + expr = HyExpression([HySymbol("set"), expr]).replace(expr) + return self.compile(expr) - return kwargs + call + @builds("dict_comp") + @checkargs(min=3, max=4) + def compile_dict_comprehension(self, expr): + if PY27: + expr.pop(0) # dict-comp + key = expr.pop(0) + value = expr.pop(0) + + gen_res, gen = self._compile_generator_iterables(expr) + + compiled_key = self.compile(key) + compiled_value = self.compile(value) + ret = compiled_key + compiled_value + gen_res + ret += ast.DictComp( + lineno=expr.start_line, + col_offset=expr.start_column, + key=compiled_key.force_expr, + value=compiled_value.force_expr, + generators=gen) + + return ret + + # In Python 2.6, turn (dict-comp key value [foo]) into + # (dict (list-comp (, key value) [foo])) + + expr[0] = HySymbol("list_comp").replace(expr[0]) + expr[1:3] = [HyExpression( + [HySymbol(",")] + + expr[1:3] + ).replace(expr[1])] + expr = HyExpression([HySymbol("dict"), expr]).replace(expr) + return self.compile(expr) + + @builds("genexpr") + def compile_genexpr(self, expr): + ret = self.compile_list_comprehension(expr) + expr = ret.expr + ret.expr = ast.GeneratorExp( + lineno=expr.lineno, + col_offset=expr.col_offset, + elt=expr.elt, + generators=expr.generators) + return ret + + @builds("apply") + @checkargs(min=1, max=3) + def compile_apply_expression(self, expr): + expr.pop(0) # apply + + ret = Result() + + fun = expr.pop(0) + + # We actually defer the compilation of the function call to + # @builds(HyExpression), allowing us to work on method calls + call = HyExpression([fun]).replace(fun) + + if isinstance(fun, HySymbol) and fun.startswith("."): + # (apply .foo lst) needs to work as lst[0].foo(*lst[1:]) + if not expr: + raise HyTypeError( + expr, "apply of a method needs to have an argument" + ) + + # We need to grab the arguments, and split them. + + # Assign them to a variable if they're not one already + if type(expr[0]) == HyList: + if len(expr[0]) == 0: + raise HyTypeError( + expr, "apply of a method needs to have an argument" + ) + call.append(expr[0].pop(0)) + else: + if isinstance(expr[0], HySymbol): + tempvar = expr[0] + else: + tempvar = HySymbol(self.get_anon_var()).replace(expr[0]) + assignment = HyExpression( + [HySymbol("setv"), tempvar, expr[0]] + ).replace(expr[0]) + + # and add the assignment to our result + ret += self.compile(assignment) + + # The first argument is the object on which to call the method + # So we translate (apply .foo args) to (.foo (get args 0)) + call.append(HyExpression( + [HySymbol("get"), tempvar, HyInteger(0)] + ).replace(tempvar)) + + # We then pass the other arguments to the function + expr[0] = HyExpression( + [HySymbol("slice"), tempvar, HyInteger(1)] + ).replace(expr[0]) + + ret += self.compile(call) + + if not isinstance(ret.expr, ast.Call): + raise HyTypeError( + fun, "compiling the application of `{}' didn't return a " + "function call, but `{}'".format(fun, type(ret.expr).__name__) + ) + if ret.expr.starargs or ret.expr.kwargs: + raise HyTypeError( + expr, "compiling the function application returned a function " + "call with arguments" + ) + + if expr: + stargs = expr.pop(0) + if stargs is not None: + stargs = self.compile(stargs) + ret.expr.starargs = stargs.force_expr + ret = stargs + ret + + if expr: + kwargs = self.compile(expr.pop(0)) + ret.expr.kwargs = kwargs.force_expr + ret = kwargs + ret + + return ret @builds("not") @builds("~") @@ -1343,11 +1549,9 @@ class HyASTCompiler(object): lineno=e.start_line, col_offset=e.start_column) - @builds("+") @builds("%") @builds("/") @builds("//") - @builds("*") @builds("**") @builds("<<") @builds(">>") @@ -1384,6 +1588,23 @@ class HyASTCompiler(object): col_offset=child.start_column) return ret + @builds("+") + @builds("*") + def compile_maths_expression_mul(self, expression): + if len(expression) > 2: + return self.compile_maths_expression(expression) + else: + id_op = {"+": HyInteger(0), "*": HyInteger(1)} + + op = expression.pop(0) + arg = expression.pop(0) if expression else id_op[op] + expr = HyExpression([ + HySymbol(op), + id_op[op], + arg + ]).replace(expression) + return self.compile_maths_expression(expr) + @builds("-") @checkargs(min=1) def compile_maths_expression_sub(self, expression): @@ -1447,8 +1668,15 @@ class HyASTCompiler(object): @builds(HyExpression) def compile_expression(self, expression): + # Perform macro expansions + expression = macroexpand(expression, self.module_name) + if not isinstance(expression, HyExpression): + # Go through compile again if the type changed. + return self.compile(expression) + if expression == []: return self.compile_list(expression) + fn = expression[0] func = None if isinstance(fn, HyKeyword): @@ -1468,6 +1696,9 @@ class HyASTCompiler(object): fn.replace(ofn) # Get the object we want to take an attribute from + if len(expression) < 2: + raise HyTypeError(expression, + "attribute access requires object") func = self.compile(expression.pop(1)) # And get the attribute @@ -1518,23 +1749,36 @@ class HyASTCompiler(object): result += ld_name return result - @builds("foreach") + @builds("for*") @checkargs(min=1) def compile_for_expression(self, expression): expression.pop(0) # for - target_name, iterable = expression.pop(0) + + args = expression.pop(0) + + if not isinstance(args, HyList): + raise HyTypeError(expression, + "for expects a list, received `{0}'".format( + type(args).__name__)) + + try: + target_name, iterable = args + except ValueError: + raise HyTypeError(expression, + "for requires two forms in the list") + target = self._storeize(self.compile(target_name)) ret = Result() orel = Result() - # (foreach [] body (else …)) + # (for* [] body (else …)) if expression and expression[-1][0] == HySymbol("else"): else_expr = expression.pop() if len(else_expr) > 2: raise HyTypeError( else_expr, - "`else' statement in `foreach' is too long") + "`else' statement in `for' is too long") elif len(else_expr) == 2: orel += self.compile(else_expr[1]) orel += orel.expr_as_stmt() @@ -1592,12 +1836,32 @@ class HyASTCompiler(object): arglist = expression.pop(0) ret, args, defaults, stararg, kwargs = self._parse_lambda_list(arglist) + if PY34: + # Python 3.4+ requres that args are an ast.arg object, rather + # than an ast.Name or bare string. + args = [ast.arg(arg=ast_str(x), + annotation=None, # Fix me! + lineno=x.start_line, + col_offset=x.start_column) for x in args] + + # XXX: Beware. Beware. This wasn't put into the parse lambda + # list because it's really just an internal parsing thing. + + if kwargs: + kwargs = ast.arg(arg=kwargs, annotation=None) + + if stararg: + stararg = ast.arg(arg=stararg, annotation=None) + + # Let's find a better home for these guys. + else: + args = [ast.Name(arg=ast_str(x), id=ast_str(x), + ctx=ast.Param(), + lineno=x.start_line, + col_offset=x.start_column) for x in args] + args = ast.arguments( - args=[ast.Name(arg=ast_str(x), id=ast_str(x), - ctx=ast.Param(), - lineno=x.start_line, - col_offset=x.start_column) - for x in args], + args=args, vararg=stararg, kwarg=kwargs, kwonlyargs=[], @@ -1706,6 +1970,19 @@ 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(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): @@ -1721,19 +1998,50 @@ class HyASTCompiler(object): HyExpression([HySymbol("fn")] + expression), ]).replace(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(new_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(new_expression) - ret.add_imports('hy', [None]) + ret = self._compile_time_hack(new_expression) return ret + @builds("defreader") + @checkargs(min=2) + def compile_reader(self, expression): + expression.pop(0) + name = expression.pop(0) + NOT_READERS = [":", "&"] + if name in NOT_READERS or len(name) > 1: + raise NameError("%s can't be used as a macro reader symbol" % name) + if not isinstance(name, HySymbol): + raise HyTypeError(name, + ("received a `%s' instead of a symbol " + "for reader macro name" % type(name).__name__)) + name = HyString(name).replace(name) + new_expression = HyExpression([ + HySymbol("with_decorator"), + HyExpression([HySymbol("hy.macros.reader"), name]), + HyExpression([HySymbol("fn")] + expression), + ]).replace(expression) + + ret = self._compile_time_hack(new_expression) + + return ret + + @builds("dispatch_reader_macro") + @checkargs(exact=2) + def compile_dispatch_reader_macro(self, expression): + expression.pop(0) # dispatch-reader-macro + str_char = expression.pop(0) + if not type(str_char) == HyString: + raise HyTypeError( + str_char, + "Trying to expand a reader macro using `{0}' instead " + "of string".format(type(str_char).__name__), + ) + + module = self.module_name + expr = reader_macroexpand(str_char, expression.pop(0), module) + + return self.compile(expr) + @builds("eval_and_compile") def compile_eval_and_compile(self, expression): expression[0] = HySymbol("progn") @@ -1751,9 +2059,13 @@ class HyASTCompiler(object): self.module_name) return Result() + @builds(HyCons) + def compile_cons(self, cons): + raise HyTypeError(cons, "Can't compile a top-level cons cell") + @builds(HyInteger) def compile_integer(self, number): - return ast.Num(n=int(number), + return ast.Num(n=long_type(number), lineno=number.start_line, col_offset=number.start_column) diff --git a/hy/completer.py b/hy/completer.py index 96c46af..05ac88f 100644 --- a/hy/completer.py +++ b/hy/completer.py @@ -26,6 +26,7 @@ # DEALINGS IN THE SOFTWARE. import os +import sys from contextlib import contextmanager docomplete = True @@ -40,6 +41,12 @@ except ImportError: except ImportError: docomplete = False +if sys.platform == 'darwin' and 'libedit' in readline.__doc__: + readline_bind = "bind ^I rl_complete" +else: + readline_bind = "tab: complete" + + import hy.macros import hy.compiler @@ -94,7 +101,7 @@ def completion(completer=None): except IOError: open(history, 'a').close() - readline.parse_and_bind("tab: complete") + readline.parse_and_bind(readline_bind) yield diff --git a/hy/contrib/anaphoric.hy b/hy/contrib/anaphoric.hy new file mode 100644 index 0000000..2f37b47 --- /dev/null +++ b/hy/contrib/anaphoric.hy @@ -0,0 +1,108 @@ +;;; Hy anaphoric macros +;; +;; Copyright (c) 2013 James King +;; 2013 Paul R. Tagliamonte +;; 2013 Abhishek L +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. +;; +;;; These macros make writing functional programs more concise + + +(defmacro ap-if (test-form &rest args) + `(let [[it ~test-form]] (if it ~@args))) + + +(defmacro ap-each [lst &rest body] + "Evaluate the body form for each element in the list." + `(for [it ~lst] ~@body)) + + +(defmacro ap-each-while [lst form &rest body] + "Evalutate the body form for each element in the list while the + predicate form evaluates to True." + `(let [[p (lambda [it] ~form)]] + (for [it ~lst] + (if (p it) + ~@body + (break))))) + + +(defmacro ap-map [form lst] + "Yield elements evaluated in the form for each element in the list." + `(let [[f (lambda [it] ~form)]] + (for [v ~lst] + (yield (f v))))) + + +(defmacro ap-map-when [predfn rep lst] + "Yield elements evaluated for each element in the list when the + predicate function returns True." + `(let [[f (lambda [it] ~rep)]] + (for [it ~lst] + (if (~predfn it) + (yield (f it)) + (yield it))))) + + +(defmacro ap-filter [form lst] + "Yield elements returned when the predicate form evaluates to True." + `(let [[pred (lambda [it] ~form)]] + (for [val ~lst] + (if (pred val) + (yield val))))) + + +(defmacro ap-reject [form lst] + "Yield elements returned when the predicate form evaluates to False" + `(ap-filter (not ~form) ~lst)) + + +(defmacro ap-dotimes [n &rest body] + "Execute body for side effects `n' times, with it bound from 0 to n-1" + (unless (numeric? n) + (raise (TypeError (.format "{0!r} is not a number" n)))) + `(ap-each (range ~n) ~@body)) + + +(defmacro ap-first [predfn lst] + "Yield the first element that passes `predfn`" + `(let [[n (gensym)]] + (ap-each ~lst (when ~predfn (setv n it) (break))) + n)) + + +(defmacro ap-last [predfn lst] + "Yield the last element that passes `predfn`" + `(let [[n (gensym)]] + (ap-each ~lst (none? n) + (when ~predfn + (setv n it))) + n)) + + +(defmacro ap-reduce [form lst &optional [initial-value None]] + "Anaphoric form of reduce, `acc' and `it' can be used for a form" + (if (none? initial-value) + `(let [[acc (car ~lst)]] + (ap-each (cdr ~lst) (setv acc ~form)) + acc) + `(let [[acc ~initial-value]] + (ap-each ~lst (setv acc ~form)) + acc))) diff --git a/hy/contrib/dispatch/__init__.py b/hy/contrib/dispatch/__init__.py new file mode 100644 index 0000000..a14091b --- /dev/null +++ b/hy/contrib/dispatch/__init__.py @@ -0,0 +1,50 @@ +# -*- encoding: utf-8 -*- +# +# Decorator for defmulti +# +# Copyright (c) 2014 Morten Linderud +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from collections import defaultdict + + +class MultiDispatch(object): + _fns = defaultdict(dict) + + def __init__(self, fn): + self.fn = fn + self.__doc__ = fn.__doc__ + if fn.__name__ not in self._fns[fn.__module__].keys(): + self._fns[fn.__module__][fn.__name__] = {} + values = fn.__code__.co_varnames + self._fns[fn.__module__][fn.__name__][values] = fn + + def is_fn(self, v, args, kwargs): + """Compare the given (checked fn) too the called fn""" + com = list(args) + list(kwargs.keys()) + if len(com) == len(v): + return all([kw in com for kw in kwargs.keys()]) + return False + + def __call__(self, *args, **kwargs): + for i, fn in self._fns[self.fn.__module__][self.fn.__name__].items(): + if self.is_fn(i, args, kwargs): + return fn(*args, **kwargs) + raise TypeError("No matching functions with this signature!") diff --git a/hy/contrib/loop.hy b/hy/contrib/loop.hy new file mode 100644 index 0000000..2c2690a --- /dev/null +++ b/hy/contrib/loop.hy @@ -0,0 +1,91 @@ +;;; Hy tail-call optimization +;; +;; Copyright (c) 2014 Clinton Dreisbach +;; Copyright (c) 2014 Paul R. Tagliamonte +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. +;; +;;; The loop/recur macro allows you to construct functions that use tail-call +;;; optimization to allow arbitrary levels of recursion. + +(defn --trampoline-- [f] + "Wrap f function and make it tail-call optimized." + ;; Takes the function "f" and returns a wrapper that may be used for tail- + ;; recursive algorithms. Note that the returned function is not side-effect + ;; free and should not be called from anywhere else during tail recursion. + + (setv result None) + ;; We have to put this in a list because of Python's + ;; weirdness around local variables. + ;; Assigning directly to it later would cause it to + ;; shadow in a new scope. + (setv active [False]) + (setv accumulated []) + + (fn [&rest args] + (.append accumulated args) + (when (not (first active)) + (assoc active 0 True) + (while (> (len accumulated) 0) + (setv result (apply f (.pop accumulated)))) + (assoc active 0 False) + result))) + +(defn recursive-replace [old-term new-term body] + "Recurses through lists of lists looking for old-term and replacing it with new-term." + ((type body) + (list-comp (cond + [(= term old-term) new-term] + [(instance? hy.HyList term) + (recursive-replace old-term new-term term)] + [True term]) [term body]))) + + +(defmacro/g! fnr [signature &rest body] + (let [[new-body (recursive-replace 'recur g!recur-fn body)]] + `(do + (import [hy.contrib.loop [--trampoline--]]) + (with-decorator + --trampoline-- + (def ~g!recur-fn (fn [~@signature] ~@new-body))) + ~g!recur-fn))) + + +(defmacro defnr [name lambda-list &rest body] + (if (not (= (type name) HySymbol)) + (macro-error name "defnr takes a name as first argument")) + `(setv ~name (fnr ~lambda-list ~@body))) + + +(defmacro/g! loop [bindings &rest body] + ;; Use inside functions like so: + ;; (defun factorial [n] + ;; (loop [[i n] + ;; [acc 1]] + ;; (if (= i 0) + ;; acc + ;; (recur (dec i) (* acc i))))) + ;; + ;; If recur is used in a non-tail-call position, None is returned, which + ;; causes chaos. Fixing this to detect if recur is in a tail-call position + ;; and erroring if not is a giant TODO. + (let [[fnargs (map (fn [x] (first x)) bindings)] + [initargs (map second bindings)]] + `(do (defnr ~g!recur-fn [~@fnargs] ~@body) + (~g!recur-fn ~@initargs)))) diff --git a/hy/contrib/meth.hy b/hy/contrib/meth.hy index 1f4ae70..11e637c 100644 --- a/hy/contrib/meth.hy +++ b/hy/contrib/meth.hy @@ -1,54 +1,27 @@ -;;; Meth -;; based on paultag's meth library to access a Flask based application +;;; Hy on Meth +;;; based on paultag's meth library to access a Flask based application -(defmacro route [name path params code] - "Default get request" - `(let [[deco (.route app ~path)]] - (with-decorator deco - (defn ~name ~params ~@code)))) - -(defmacro route-with-methods [name path params code methods] +(defmacro route-with-methods [name path methods params &rest code] "Same as route but with an extra methods array to specify HTTP methods" `(let [[deco (kwapply (.route app ~path) {"methods" ~methods})]] (with-decorator deco - (defn ~name ~params ~@code)))) + (defn ~name ~params + (progn ~@code))))) ;; Some macro examples -(defmacro post-route [name path params code] +(defmacro route [name path params &rest code] + "Get request" + `(route-with-methods ~name ~path ["GET"] ~params ~@code)) + +(defmacro post-route [name path params &rest code] "Post request" - `(route-with-methods ~name ~path ~params ~code ["POST"])) + `(route-with-methods ~name ~path ["POST"] ~params ~@code)) -(defmacro put-route [name path params code] +(defmacro put-route [name path params &rest code] "Put request" - `(route-with-methods ~name ~path ~params ~code ["PUT"])) + `(route-with-methods ~name ~path ["PUT"] ~params ~@code)) -(defmacro delete-route [name path params code] +(defmacro delete-route [name path params &rest code] "Delete request" - `(route-with-methods ~name ~path ~params ~code ["DELETE"])) - - -;;; Simple example application -;;; Requires to have Flask installed - -;; (import [flask [Flask]]) -;; (setv app (Flask "__main__")) - -;; (require methy) - -;; (print "setup / with GET") -;; (route get-index "/" [] (str "Hy world!")) - -;; (print "setup /post with POST") -;; (post-route post-index "/post" [] (str "Hy post world!")) - -;; (route-with-methods both-index "/both" [] -;; (str "Hy to both worlds!") ["GET" "POST"]) - -;; (.run app) - -;;; Now you can do: -;;; curl 127.0.0.1:5000 -;;; curl -X POST 127.0.0.1:5000/post -;;; curl -X POST 127.0.0.1:5000/both -;;; curl 127.0.0.1:5000/both + `(route-with-methods ~name ~path ["DELETE"] ~params ~@code)) diff --git a/hy/contrib/multi.hy b/hy/contrib/multi.hy new file mode 100644 index 0000000..19246ee --- /dev/null +++ b/hy/contrib/multi.hy @@ -0,0 +1,41 @@ +;; Hy Arity-overloading +;; Copyright (c) 2014 Morten Linderud + +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: + +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. + +(import [collections [defaultdict]]) +(import [hy.models.string [HyString]]) + + +(defmacro defmulti [name &rest bodies] + (def comment (HyString)) + (if (= (type (first bodies)) HyString) + (do (def comment (car bodies)) + (def bodies (cdr bodies)))) + + (def ret `(do)) + + (.append ret '(import [hy.contrib.dispatch [MultiDispatch]])) + + (for [body bodies] + (def let-binds (car body)) + (def body (cdr body)) + (.append ret + `(with-decorator MultiDispatch (defn ~name ~let-binds ~comment ~@body)))) + ret) diff --git a/hy/contrib/profile.hy b/hy/contrib/profile.hy new file mode 100644 index 0000000..485f46b --- /dev/null +++ b/hy/contrib/profile.hy @@ -0,0 +1,51 @@ +;;; Hy profiling macros +;; +;; Copyright (c) 2013 Paul R. Tagliamonte +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. +;; +;;; These macros make debugging where bottlenecks exist easier. + + +(defmacro profile/calls [&rest body] + `(do + (import [pycallgraph [PyCallGraph]] + [pycallgraph.output [GraphvizOutput]]) + (with* [(apply PyCallGraph [] {"output" (GraphvizOutput)})] + ~@body))) + + +(defmacro/g! profile/cpu [&rest body] + " Profile a bit of code " + `(do + (import cProfile pstats) + + (if-python2 + (import [StringIO [StringIO]]) + (import [io [StringIO]])) + + (setv ~g!hy-pr (.Profile cProfile)) + (.enable ~g!hy-pr) + (do ~@body) + (.disable ~g!hy-pr) + (setv ~g!hy-s (StringIO)) + (setv ~g!hy-ps + (.sort-stats (kwapply (.Stats pstats ~g!hy-pr) {"stream" ~g!hy-s}))) + (.print-stats ~g!hy-ps) + (print (.getvalue ~g!hy-s)))) diff --git a/hy/contrib/walk.hy b/hy/contrib/walk.hy new file mode 100644 index 0000000..a846ed3 --- /dev/null +++ b/hy/contrib/walk.hy @@ -0,0 +1,58 @@ +;;; Hy AST walker +;; +;; Copyright (c) 2014 Gergely Nagy +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. + +(import [hy [HyExpression HyDict]] + [functools [partial]]) + +(defn walk [inner outer form] + "Traverses form, an arbitrary data structure. Applies inner to each + element of form, building up a data structure of the same type. + Applies outer to the result." + (cond + [(instance? HyExpression form) + (outer (HyExpression (map inner form)))] + [(instance? HyDict form) + (HyDict (outer (HyExpression (map inner form))))] + [(cons? form) + (outer (cons (inner (first form)) + (inner (rest form))))] + [(instance? list form) + ((type form) (outer (HyExpression (map inner form))))] + [true (outer form)])) + +(defn postwalk [f form] + "Performs depth-first, post-order traversal of form. Calls f on each + sub-form, uses f's return value in place of the original." + (walk (partial postwalk f) f form)) + +(defn prewalk [f form] + "Performs depth-first, pre-order traversal of form. Calls f on each + sub-form, uses f's return value in place of the original." + (walk (partial prewalk f) identity (f form))) + +(defn macroexpand-all [form] + "Recursively performs all possible macroexpansions in form." + (prewalk (fn [x] + (if (instance? HyExpression x) + (macroexpand x) + x)) + form)) diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy index 007d92b..3afd2c5 100644 --- a/hy/core/bootstrap.hy +++ b/hy/core/bootstrap.hy @@ -28,13 +28,13 @@ (defmacro macro-error [location reason] "error out properly within a macro" - `(raise (hy.compiler.HyTypeError ~location ~reason))) + `(raise (hy.errors.HyMacroExpansionError ~location ~reason))) (defmacro defmacro-alias [names lambda-list &rest body] "define one macro with several names" (setv ret `(do)) - (foreach [name names] + (for* [name names] (.append ret `(defmacro ~name ~lambda-list ~@body))) ret) @@ -52,7 +52,7 @@ (setv macroed_variables []) (if (not (isinstance variables HyList)) (macro-error variables "let lexical context must be a list")) - (foreach [variable variables] + (for* [variable variables] (if (isinstance variable HyList) (do (if (!= (len variable) 2) diff --git a/hy/core/language.hy b/hy/core/language.hy index a96b25d..19e8486 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -23,18 +23,35 @@ ;;;; to make functional programming slightly easier. ;;;; + +(import [hy._compat [long-type]]) ; long for python2, int for python3 +(import [hy.models.cons [HyCons]]) + + (defn _numeric-check [x] (if (not (numeric? x)) (raise (TypeError (.format "{0!r} is not a number" x))))) +(defn coll? [coll] + "Checks whether item is a collection" + (and (iterable? coll) (not (string? coll)))) + +(defn cons [a b] + "Return a fresh cons cell with car = a and cdr = b" + (HyCons a b)) + +(defn cons? [c] + "Check whether c can be used as a cons object" + (instance? HyCons c)) + (defn cycle [coll] "Yield an infinite repetition of the items in coll" (setv seen []) - (foreach [x coll] + (for* [x coll] (yield x) (.append seen x)) (while seen - (foreach [x seen] + (for* [x seen] (yield x)))) (defn dec [n] @@ -42,20 +59,33 @@ (_numeric-check n) (- n 1)) +(defn disassemble [tree &optional [codegen false]] + "Dump the python AST for a given Hy tree to standard output + If the second argument is true, generate python code instead." + (import astor) + (import hy.compiler) + + (fake-source-positions tree) + (setv compiled (hy.compiler.hy_compile tree (calling-module-name))) + (print ((if codegen + astor.codegen.to_source + astor.dump) + compiled))) + (defn distinct [coll] "Return a generator from the original collection with duplicates removed" - (let [[seen []] [citer (iter coll)]] - (foreach [val citer] + (let [[seen (set)] [citer (iter coll)]] + (for* [val citer] (if (not_in val seen) (do (yield val) - (.append seen val)))))) + (.add seen val)))))) (defn drop [count coll] "Drop `count` elements from `coll` and yield back the rest" (let [[citer (iter coll)]] - (try (foreach [i (range count)] + (try (for* [i (range count)] (next citer)) (catch [StopIteration])) citer)) @@ -63,10 +93,10 @@ (defn drop-while [pred coll] "Drop all elements of `coll` until `pred` is False" (let [[citer (iter coll)]] - (foreach [val citer] + (for* [val citer] (if (not (pred val)) (do (yield val) (break)))) - (foreach [val citer] + (for* [val citer] (yield val)))) (defn empty? [coll] @@ -78,17 +108,73 @@ (_numeric-check n) (= (% n 2) 0)) +(defn every? [pred coll] + "Return true if (pred x) is logical true for every x in coll, else false" + (all (map pred coll))) + +(defn fake-source-positions [tree] + "Fake the source positions for a given tree" + (if (and (iterable? tree) (not (string? tree))) + (for* [subtree tree] + (fake-source-positions subtree))) + (for* [attr '[start-line end-line start-column end-column]] + (if (not (hasattr tree attr)) + (setattr tree attr 1)))) + (defn filter [pred coll] "Return all elements from `coll` that pass `pred`" (let [[citer (iter coll)]] - (foreach [val citer] + (for* [val citer] (if (pred val) (yield val))))) +(defn flatten [coll] + "Return a single flat list expanding all members of coll" + (if (coll? coll) + (_flatten coll []) + (raise (TypeError (.format "{0!r} is not a collection" coll))))) + +(defn _flatten [coll result] + (if (and (iterable? coll) (not (string? coll))) + (do (for* [b coll] + (_flatten b result))) + (.append result coll)) + result) + (defn float? [x] "Return True if x is float" (isinstance x float)) +(import [threading [Lock]]) +(setv _gensym_counter 1234) +(setv _gensym_lock (Lock)) + +(defn gensym [&optional [g "G"]] + (let [[new_symbol None]] + (global _gensym_counter) + (global _gensym_lock) + (.acquire _gensym_lock) + (try (do (setv _gensym_counter (inc _gensym_counter)) + (setv new_symbol (HySymbol (.format ":{0}_{1}" g _gensym_counter)))) + (finally (.release _gensym_lock))) + new_symbol)) + +(defn calling-module-name [&optional [n 1]] + "Get the name of the module calling `n` levels up the stack from the + `calling-module-name` function call (by default, one level up)" + (import inspect) + + (setv f (get (.stack inspect) (+ n 1) 0)) + (get f.f_globals "__name__")) + +(defn first [coll] + "Return first item from `coll`" + (get coll 0)) + +(defn identity [x] + "Returns the argument unchanged" + x) + (defn inc [n] "Increment n by 1" (_numeric-check n) @@ -97,11 +183,20 @@ (defn instance? [klass x] (isinstance x klass)) +(defn integer [x] + "Return Hy kind of integer" + (long-type x)) + (defn integer? [x] "Return True if x in an integer" - (if-python2 - (isinstance x (, int long)) - (isinstance x int))) + (isinstance x (, int long-type))) + +(defn integer-char? [x] + "Return True if char `x` parses as an integer" + (try + (integer? (int x)) + (catch [e ValueError] False) + (catch [e TypeError] False))) (defn iterable? [x] "Return true if x is iterable" @@ -119,6 +214,26 @@ (try (= x (iter x)) (catch [TypeError] false))) +(defn list* [hd &rest tl] + "Return a dotted list construed from the elements of the argument" + (if (not tl) + hd + (cons hd (apply list* tl)))) + +(defn macroexpand [form] + "Return the full macro expansion of form" + (import hy.macros) + + (setv name (calling-module-name)) + (hy.macros.macroexpand form name)) + +(defn macroexpand-1 [form] + "Return the single step macro expansion of form" + (import hy.macros) + + (setv name (calling-module-name)) + (hy.macros.macroexpand-1 form name)) + (defn neg? [n] "Return true if n is < 0" (_numeric-check n) @@ -128,6 +243,10 @@ "Return true if x is None" (is x None)) +(defn nil? [x] + "Return true if x is nil (None)" + (is x None)) + (defn numeric? [x] (import numbers) (instance? numbers.Number x)) @@ -155,15 +274,19 @@ (defn remove [pred coll] "Return coll with elements removed that pass `pred`" (let [[citer (iter coll)]] - (foreach [val citer] + (for* [val citer] (if (not (pred val)) (yield val))))) +(defn rest [coll] + "Get all the elements of a coll, except the first." + (slice coll 1)) + (defn repeat [x &optional n] "Yield x forever or optionally n times" (if (none? n) (setv dispatch (fn [] (while true (yield x)))) - (setv dispatch (fn [] (foreach [_ (range n)] (yield x))))) + (setv dispatch (fn [] (for* [_ (range n)] (yield x))))) (dispatch)) (defn repeatedly [func] @@ -175,6 +298,16 @@ "Return second item from `coll`" (get coll 1)) +(defn some [pred coll] + "Return true if (pred x) is logical true for any x in coll, else false" + (any (map pred coll))) + +(defn string [x] + "Cast x as current string implementation" + (if-python2 + (unicode x) + (str x))) + (defn string? [x] "Return True if x is a string" (if-python2 @@ -185,7 +318,7 @@ "Take `count` elements from `coll`, or the whole set if the total number of entries in `coll` is less than `count`." (let [[citer (iter coll)]] - (foreach [_ (range count)] + (for* [_ (range count)] (yield (next citer))))) (defn take-nth [n coll] @@ -193,16 +326,16 @@ raises ValueError for (not (pos? n))" (if (pos? n) (let [[citer (iter coll)] [skip (dec n)]] - (foreach [val citer] + (for* [val citer] (yield val) - (foreach [_ (range skip)] + (for* [_ (range skip)] (next citer)))) (raise (ValueError "n must be positive")))) (defn take-while [pred coll] "Take all elements while `pred` is true" (let [[citer (iter coll)]] - (foreach [val citer] + (for* [val citer] (if (pred val) (yield val) (break))))) @@ -212,9 +345,16 @@ (_numeric_check n) (= n 0)) -(def *exports* ["cycle" "dec" "distinct" "drop" "drop_while" "empty?" - "even?" "filter" "float?" "inc" - "instance?" "integer?" "iterable?" "iterate" "iterator?" "neg?" - "none?" "nth" "numeric?" "odd?" "pos?" "remove" "repeat" - "repeatedly" "second" "string?" "take" "take_nth" "take_while" - "zero?"]) +(defn zipwith [func &rest lists] + "Zip the contents of several lists and map a function to the result" + (do + (import functools) + (map (functools.partial (fn [f args] (apply f args)) func) (apply zip lists)))) + +(def *exports* '[calling-module-name coll? cons cons? cycle dec distinct + disassemble drop drop-while empty? even? every? first filter + flatten float? gensym identity inc instance? integer + integer? integer-char? iterable? iterate iterator? + list* macroexpand macroexpand-1 neg? nil? none? nth + numeric? odd? pos? remove repeat repeatedly rest second + some string string? take take-nth take-while zero? zipwith]) diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 17792f9..ff497b4 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -26,27 +26,64 @@ ;;; They are automatically required in every module, except inside hy.core +(import [hy.models.list [HyList]] + [hy.models.symbol [HySymbol]]) + + + (defmacro for [args &rest body] - "shorthand for nested foreach loops: - (for [x foo y bar] baz) -> - (foreach [x foo] - (foreach [y bar] + "shorthand for nested for loops: + (for [x foo + y bar] + baz) -> + (for* [x foo] + (for* [y bar] baz))" - ;; TODO: that signature sucks. - ;; (for [[x foo] [y bar]] baz) would be more consistent - (if (% (len args) 2) - (macro-error args "for needs an even number of elements in its first argument")) - (if args - `(foreach [~(.pop args 0) ~(.pop args 0)] (for ~args ~@body)) - `(do ~@body))) + + (if (odd? (len args)) + (macro-error args "`for' requires an even number of args.")) + + (if (empty? body) + (macro-error None "`for' requires a body to evaluate")) + + (if (empty? args) + `(do ~@body) + (if (= (len args) 2) + ; basecase, let's just slip right in. + `(for* [~@args] ~@body) + ; otherwise, let's do some legit handling. + (let [[alist (slice args 0 nil 2)] + [ilist (slice args 1 nil 2)]] + `(do + (import itertools) + (for* [(, ~@alist) (itertools.product ~@ilist)] ~@body)))))) -(defmacro-alias [car first] [thing] +(defmacro with [args &rest body] + "shorthand for nested for* loops: + (with [[x foo] [y bar]] baz) -> + (with* [x foo] + (with* [y bar] + baz))" + + (if (not (empty? args)) + (let [[primary (.pop args 0)]] + (if (isinstance primary HyList) + ;;; OK. if we have a list, we can go ahead and unpack that + ;;; as the argument to with. + `(with* [~@primary] (with ~args ~@body)) + ;;; OK, let's just give it away. This may not be something we + ;;; can do, but that's really the programmer's problem. + `(with* [~primary] (with ~args ~@body)))) + `(do ~@body))) + + +(defmacro car [thing] "Get the first element of a list/cons" `(get ~thing 0)) -(defmacro-alias [cdr rest] [thing] +(defmacro cdr [thing] "Get all the elements of a thing, except the first" `(slice ~thing 1)) @@ -72,7 +109,7 @@ (setv root (check-branch branch)) (setv latest-branch root) - (foreach [branch branches] + (for* [branch branches] (setv cur-branch (check-branch branch)) (.append latest-branch cur-branch) (setv latest-branch cur-branch)) @@ -82,7 +119,7 @@ (defmacro -> [head &rest rest] ;; TODO: fix the docstring by someone who understands this (setv ret head) - (foreach [node rest] + (for* [node rest] (if (not (isinstance node HyExpression)) (setv node `(~node))) (.insert node 1 ret) @@ -93,7 +130,7 @@ (defmacro ->> [head &rest rest] ;; TODO: fix the docstring by someone who understands this (setv ret head) - (foreach [node rest] + (for* [node rest] (if (not (isinstance node HyExpression)) (setv node `(~node))) (.append node ret) @@ -101,6 +138,13 @@ ret) +(defmacro if-not [test not-branch &optional [yes-branch nil]] + "Like `if`, but execute the first branch when the test fails" + (if (nil? yes-branch) + `(if (not ~test) ~not-branch) + `(if (not ~test) ~not-branch ~yes-branch))) + + (defmacro when [test &rest body] "Execute `body` when `test` is true" `(if ~test (do ~@body))) @@ -108,11 +152,50 @@ (defmacro unless [test &rest body] "Execute `body` when `test` is false" - `(if ~test None (do ~@body))) + `(if-not ~test (do ~@body))) (defmacro yield-from [iterable] "Yield all the items from iterable" - ;; TODO: this needs some gensym love - `(foreach [_hy_yield_from_x ~iterable] - (yield _hy_yield_from_x))) + (let [[x (gensym)]] + `(for* [~x ~iterable] + (yield ~x)))) + +(defmacro with-gensyms [args &rest body] + `(let ~(HyList (map (fn [x] `[~x (gensym '~x)]) args)) + ~@body)) + +(defmacro defmacro/g! [name args &rest body] + (let [[syms (list (distinct (filter (fn [x] (.startswith x "g!")) (flatten body))))]] + `(defmacro ~name [~@args] + (let ~(HyList (map (fn [x] `[~x (gensym (slice '~x 2))]) syms)) + ~@body)))) + + +(defmacro kwapply [call kwargs] + "Use a dictionary as keyword arguments" + (let [[-fun (car call)] + [-args (cdr call)] + [-okwargs `[(list (.items ~kwargs))]]] + (while (= -fun "kwapply") ;; join any further kw + (if (not (= (len -args) 2)) + (macro-error + call + (.format "Trying to call nested kwapply with {0} args instead of 2" + (len -args)))) + (.insert -okwargs 0 `(list (.items ~(car (cdr -args))))) + (setv -fun (car (car -args))) + (setv -args (cdr (car -args)))) + + `(apply ~-fun [~@-args] (dict (sum ~-okwargs []))))) + + +(defmacro-alias [defn-alias defun-alias] [names lambda-list &rest body] + "define one function with several names" + (let [[main (first names)] + [aliases (rest names)]] + (setv ret `(do (defn ~main ~lambda-list ~@body))) + (for* [name aliases] + (.append ret + `(setv ~name ~main))) + ret)) diff --git a/hy/errors.py b/hy/errors.py index 323d67e..d0240c5 100644 --- a/hy/errors.py +++ b/hy/errors.py @@ -1,4 +1,7 @@ +# -*- encoding: utf-8 -*- +# # Copyright (c) 2013 Paul Tagliamonte +# Copyright (c) 2013 Bob Tolbert # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -18,6 +21,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import traceback + class HyError(Exception): """ @@ -25,3 +30,115 @@ class HyError(Exception): Exception. """ pass + + +try: + from clint.textui import colored +except Exception: + class colored: + + @staticmethod + def black(foo): + return foo + + @staticmethod + def red(foo): + return foo + + @staticmethod + def green(foo): + return foo + + @staticmethod + def yellow(foo): + return foo + + @staticmethod + def blue(foo): + return foo + + @staticmethod + def magenta(foo): + return foo + + @staticmethod + def cyan(foo): + return foo + + @staticmethod + def white(foo): + return foo + + +class HyCompileError(HyError): + def __init__(self, exception, traceback=None): + self.exception = exception + self.traceback = traceback + + def __str__(self): + if isinstance(self.exception, HyTypeError): + return str(self.exception) + if self.traceback: + tb = "".join(traceback.format_tb(self.traceback)).strip() + else: + tb = "No traceback available. 😟" + return("Internal Compiler Bug 😱\n⤷ %s: %s\nCompilation traceback:\n%s" + % (self.exception.__class__.__name__, + self.exception, tb)) + + +class HyTypeError(TypeError): + def __init__(self, expression, message): + super(HyTypeError, self).__init__(message) + self.expression = expression + self.message = message + self.source = None + self.filename = None + + def __str__(self): + + line = self.expression.start_line + start = self.expression.start_column + end = self.expression.end_column + + source = [] + if self.source is not None: + source = self.source.split("\n")[line-1:self.expression.end_line] + + if line == self.expression.end_line: + length = end - start + else: + length = len(source[0]) - start + + result = "" + + result += ' File "%s", line %d, column %d\n\n' % (self.filename, + line, + start) + + if len(source) == 1: + result += ' %s\n' % colored.red(source[0]) + result += ' %s%s\n' % (' '*(start-1), + colored.green('^' + '-'*(length-1) + '^')) + if len(source) > 1: + result += ' %s\n' % colored.red(source[0]) + result += ' %s%s\n' % (' '*(start-1), + colored.green('^' + '-'*length)) + if len(source) > 2: # write the middle lines + for line in source[1:-1]: + result += ' %s\n' % colored.red("".join(line)) + result += ' %s\n' % colored.green("-"*len(line)) + + # write the last line + result += ' %s\n' % colored.red("".join(source[-1])) + result += ' %s\n' % colored.green('-'*(end-1) + '^') + + result += colored.yellow("%s: %s\n\n" % + (self.__class__.__name__, + self.message)) + + return result.encode('utf-8') + + +class HyMacroExpansionError(HyTypeError): + pass diff --git a/hy/importer.py b/hy/importer.py index b32b71a..e588f1d 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -1,4 +1,5 @@ # Copyright (c) 2013 Paul Tagliamonte +# Copyright (c) 2013 Bob Tolbert # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -18,11 +19,9 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from py_compile import wr_long, MAGIC -from hy.compiler import hy_compile +from hy.compiler import hy_compile, HyTypeError from hy.models import HyObject -from hy.lex import tokenize - +from hy.lex import tokenize, LexException from io import open import marshal @@ -32,7 +31,7 @@ import ast import os import __future__ -from hy._compat import builtins, long_type +from hy._compat import PY3, PY33, MAGIC, builtins, long_type, wr_long def ast_compile(ast, filename, mode): @@ -73,17 +72,28 @@ def import_file_to_module(module_name, fpath): mod = imp.new_module(module_name) mod.__file__ = fpath eval(ast_compile(_ast, fpath, "exec"), mod.__dict__) + except (HyTypeError, LexException) as e: + if e.source is None: + with open(fpath, 'rt') as fp: + e.source = fp.read() + e.filename = fpath + raise except Exception: sys.modules.pop(module_name, None) raise - return mod def import_buffer_to_module(module_name, buf): - _ast = import_buffer_to_ast(buf, module_name) - mod = imp.new_module(module_name) - eval(ast_compile(_ast, "", "exec"), mod.__dict__) + try: + _ast = import_buffer_to_ast(buf, module_name) + mod = imp.new_module(module_name) + eval(ast_compile(_ast, "", "exec"), mod.__dict__) + except (HyTypeError, LexException) as e: + if e.source is None: + e.source = buf + e.filename = '' + raise return mod @@ -128,12 +138,12 @@ def write_hy_as_pyc(fname): open_ = builtins.open with open_(cfile, 'wb') as fc: - if sys.version_info[0] >= 3: + if PY3: fc.write(b'\0\0\0\0') else: fc.write('\0\0\0\0') wr_long(fc, timestamp) - if (sys.version_info[0] >= 3 and sys.version_info[1] >= 3): + if PY33: wr_long(fc, st.st_size) marshal.dump(code, fc) fc.flush() diff --git a/hy/lex/__init__.py b/hy/lex/__init__.py index 651f89a..8153aa8 100644 --- a/hy/lex/__init__.py +++ b/hy/lex/__init__.py @@ -33,6 +33,5 @@ def tokenize(buf): return parser.parse(lexer.lex(buf)) except LexingError as e: pos = e.getsourcepos() - raise LexException( - "Could not identify the next token at line %s, column %s" % ( - pos.lineno, pos.colno)) + raise LexException("Could not identify the next token.", + pos.lineno, pos.colno) diff --git a/hy/lex/exceptions.py b/hy/lex/exceptions.py index 21b2700..4c4b760 100644 --- a/hy/lex/exceptions.py +++ b/hy/lex/exceptions.py @@ -1,4 +1,5 @@ # Copyright (c) 2013 Nicolas Dandrimont +# Copyright (c) 2013 Bob Tolbert # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -23,9 +24,43 @@ from hy.errors import HyError class LexException(HyError): """Error during the Lexing of a Hython expression.""" - pass + def __init__(self, message, lineno, colno): + super(LexException, self).__init__(message) + self.message = message + self.lineno = lineno + self.colno = colno + self.source = None + self.filename = '' + + def __str__(self): + from hy.errors import colored + + line = self.lineno + start = self.colno + + result = "" + + source = self.source.split("\n") + + if line > 0 and start > 0: + result += ' File "%s", line %d, column %d\n\n' % (self.filename, + line, + start) + + if len(self.source) > 0: + source_line = source[line-1] + else: + source_line = "" + + result += ' %s\n' % colored.red(source_line) + result += ' %s%s\n' % (' '*(start-1), colored.green('^')) + + result += colored.yellow("LexException: %s\n\n" % self.message) + + return result class PrematureEndOfInput(LexException): """We got a premature end of input""" - pass + def __init__(self, message): + super(PrematureEndOfInput, self).__init__(message, -1, -1) diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py index ce3eae3..8032fc0 100644 --- a/hy/lex/lexer.py +++ b/hy/lex/lexer.py @@ -40,6 +40,7 @@ lg.add('QUASIQUOTE', r'`%s' % end_quote) lg.add('UNQUOTESPLICE', r'~@%s' % end_quote) lg.add('UNQUOTE', r'~%s' % end_quote) lg.add('HASHBANG', r'#!.*[^\r\n]') +lg.add('HASHREADER', r'#.') lg.add('STRING', r'''(?x) @@ -59,7 +60,7 @@ lg.add('STRING', r'''(?x) lg.add('IDENTIFIER', r'[^()\[\]{}\'"\s;]+') -lg.ignore(r';.*[\r\n]+') +lg.ignore(r';.*(?=\r|\n|$)') lg.ignore(r'\s+') diff --git a/hy/lex/parser.py b/hy/lex/parser.py index ce3b170..0f3a96a 100644 --- a/hy/lex/parser.py +++ b/hy/lex/parser.py @@ -24,6 +24,7 @@ from functools import wraps from rply import ParserGenerator from hy.models.complex import HyComplex +from hy.models.cons import HyCons from hy.models.dict import HyDict from hy.models.expression import HyExpression from hy.models.float import HyFloat @@ -95,9 +96,40 @@ def real_main_empty(p): return [] +def reject_spurious_dots(*items): + "Reject the spurious dots from items" + for list in items: + for tok in list: + if tok == "." and type(tok) == HySymbol: + raise LexException("Malformed dotted list", + tok.start_line, tok.start_column) + + @pg.production("paren : LPAREN list_contents RPAREN") @set_boundaries def paren(p): + cont = p[1] + + # Dotted lists are expressions of the form + # (a b c . d) + # that evaluate to nested cons cells of the form + # (a . (b . (c . d))) + if len(cont) >= 3 and isinstance(cont[-2], HySymbol) and cont[-2] == ".": + + reject_spurious_dots(cont[:-2], cont[-1:]) + + if len(cont) == 3: + # Two-item dotted list: return the cons cell directly + return HyCons(cont[0], cont[2]) + else: + # Return a nested cons cell + return HyCons(cont[0], paren([p[0], cont[1:], p[2]])) + + # Warn preemptively on a malformed dotted list. + # Only check for dots after the first item to allow for a potential + # attribute accessor shorthand + reject_spurious_dots(cont[1:]) + return HyExpression(p[1]) @@ -150,6 +182,15 @@ def term_unquote_splice(p): return HyExpression([HySymbol("unquote_splice"), p[1]]) +@pg.production("term : HASHREADER term") +@set_quote_boundaries +def hash_reader(p): + st = p[0].getstr()[1] + str_object = HyString(st) + expr = p[1] + return HyExpression([HySymbol("dispatch_reader_macro"), str_object, expr]) + + @pg.production("dict : LCURLY list_contents RCURLY") @set_boundaries def t_dict(p): @@ -220,6 +261,7 @@ def t_identifier(p): table = { "true": "True", "false": "False", + "nil": "None", "null": "None", } @@ -232,11 +274,19 @@ def t_identifier(p): if obj.startswith("&"): return HyLambdaListKeyword(obj) - if obj.startswith("*") and obj.endswith("*") and obj not in ("*", "**"): - obj = obj[1:-1].upper() + def mangle(p): + if p.startswith("*") and p.endswith("*") and p not in ("*", "**"): + p = p[1:-1].upper() - if "-" in obj and obj != "-": - obj = obj.replace("-", "_") + if "-" in p and p != "-": + p = p.replace("-", "_") + + if p.endswith("?") and p != "?": + p = "is_%s" % (p[:-1]) + + return p + + obj = ".".join([mangle(part) for part in obj.split(".")]) return HySymbol(obj) @@ -245,12 +295,11 @@ def t_identifier(p): def error_handler(token): tokentype = token.gettokentype() if tokentype == '$end': - raise PrematureEndOfInput + raise PrematureEndOfInput("Premature end of input") else: raise LexException( - "Ran into a %s where it wasn't expected at line %s, column %s" % - (tokentype, token.source_pos.lineno, token.source_pos.colno) - ) + "Ran into a %s where it wasn't expected." % tokentype, + token.source_pos.lineno, token.source_pos.colno) parser = pg.build() diff --git a/hy/macros.py b/hy/macros.py index 20d0cac..cf994a2 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -26,10 +26,12 @@ from hy.models.integer import HyInteger from hy.models.float import HyFloat from hy.models.complex import HyComplex from hy.models.dict import HyDict -from hy._compat import str_type +from hy._compat import str_type, long_type + +from hy.errors import HyTypeError, HyMacroExpansionError from collections import defaultdict - +import sys CORE_MACROS = [ "hy.core.bootstrap", @@ -40,6 +42,7 @@ EXTRA_MACROS = [ ] _hy_macros = defaultdict(dict) +_hy_reader = defaultdict(dict) def macro(name): @@ -63,6 +66,28 @@ def macro(name): return _ +def reader(name): + """Decorator to define a macro called `name`. + + This stores the macro `name` in the namespace for the module where it is + defined. + + If the module where it is defined is in `hy.core`, then the macro is stored + in the default `None` namespace. + + This function is called from the `defmacro` special form in the compiler. + + """ + def _(fn): + module_name = fn.__module__ + if module_name.startswith("hy.core"): + module_name = None + _hy_reader[module_name][name] = fn + + return fn + return _ + + def require(source_module, target_module): """Load the macros from `source_module` in the namespace of `target_module`. @@ -75,6 +100,11 @@ def require(source_module, target_module): for name, macro in macros.items(): refs[name] = macro + readers = _hy_reader[source_module] + reader_refs = _hy_reader[target_module] + for name, reader in readers.items(): + reader_refs[name] = reader + # type -> wrapping function mapping for _wrap_value _wrappers = { @@ -84,9 +114,14 @@ _wrappers = { complex: HyComplex, str_type: HyString, dict: lambda d: HyDict(_wrap_value(x) for x in sum(d.items(), ())), - list: lambda l: HyList(_wrap_value(x) for x in l) + list: lambda l: HyList(_wrap_value(x) for x in l), + tuple: lambda t: HyList(_wrap_value(x) for x in t), + type(None): lambda foo: HySymbol("None"), } +if sys.version_info[0] < 3: # do not add long on python3 + _wrappers[long_type] = HyInteger + def _wrap_value(x): """Wrap `x` into the corresponding Hy type. @@ -157,9 +192,35 @@ def macroexpand_1(tree, module_name): if m is None: m = _hy_macros[None].get(fn) if m is not None: - obj = _wrap_value(m(*ntree[1:])) + try: + obj = _wrap_value(m(*ntree[1:])) + except HyTypeError as e: + if e.expression is None: + e.expression = tree + raise + except Exception as e: + msg = "`" + str(tree[0]) + "' " + \ + " ".join(str(e).split()[1:]) + raise HyMacroExpansionError(tree, msg) obj.replace(tree) return obj return ntree return tree + + +def reader_macroexpand(char, tree, module_name): + """Expand the reader macro "char" with argument `tree`.""" + load_macros(module_name) + + if not char in _hy_reader[module_name]: + raise HyTypeError( + char, + "`{0}' is not a reader macro in module '{1}'".format( + char, + module_name, + ), + ) + + expr = _hy_reader[module_name][char](tree) + return _wrap_value(expr).replace(tree) diff --git a/hy/models/__init__.py b/hy/models/__init__.py index 42032ca..31820f1 100644 --- a/hy/models/__init__.py +++ b/hy/models/__init__.py @@ -29,7 +29,7 @@ class HyObject(object): if isinstance(other, HyObject): for attr in ["start_line", "end_line", "start_column", "end_column"]: - if not hasattr(self, attr): + if not hasattr(self, attr) and hasattr(other, attr): setattr(self, attr, getattr(other, attr)) else: raise TypeError("Can't replace a non Hy object with a Hy object") diff --git a/hy/models/cons.py b/hy/models/cons.py new file mode 100644 index 0000000..180c5cb --- /dev/null +++ b/hy/models/cons.py @@ -0,0 +1,108 @@ +# Copyright (c) 2013 Nicolas Dandrimont +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from hy.macros import _wrap_value +from hy.models import HyObject +from hy.models.expression import HyExpression +from hy.models.symbol import HySymbol + + +class HyCons(HyObject): + """ + HyCons: a cons object. + + Building a HyCons of something and a HyList really builds a HyList + """ + + __slots__ = ["car", "cdr"] + + def __new__(cls, car, cdr): + if isinstance(cdr, list): + + # Keep unquotes in the cdr of conses + if type(cdr) == HyExpression: + if len(cdr) > 0 and type(cdr[0]) == HySymbol: + if cdr[0] in ("unquote", "unquote_splice"): + return super(HyCons, cls).__new__(cls) + + return cdr.__class__([_wrap_value(car)] + cdr) + + elif cdr is None: + return HyExpression([_wrap_value(car)]) + + else: + return super(HyCons, cls).__new__(cls) + + def __init__(self, car, cdr): + self.car = _wrap_value(car) + self.cdr = _wrap_value(cdr) + + def __getitem__(self, n): + if n == 0: + return self.car + if n == slice(1, None): + return self.cdr + + raise IndexError( + "Can only get the car ([0]) or the cdr ([1:]) of a HyCons") + + def __setitem__(self, n, new): + if n == 0: + self.car = new + return + if n == slice(1, None): + self.cdr = new + return + + raise IndexError( + "Can only set the car ([0]) or the cdr ([1:]) of a HyCons") + + def __iter__(self): + yield self.car + try: + iterator = (i for i in self.cdr) + except TypeError: + if self.cdr is not None: + yield self.cdr + raise TypeError("Iteration on malformed cons") + else: + for i in iterator: + yield i + + def replace(self, other): + if self.car is not None: + self.car.replace(other) + if self.cdr is not None: + self.cdr.replace(other) + + HyObject.replace(self, other) + + def __repr__(self): + if isinstance(self.cdr, self.__class__): + return "(%s %s)" % (repr(self.car), repr(self.cdr)[1:-1]) + else: + return "(%s . %s)" % (repr(self.car), repr(self.cdr)) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + self.car == other.car and + self.cdr == other.cdr + ) diff --git a/hy/models/dict.py b/hy/models/dict.py index 1bf020c..e0e99bd 100644 --- a/hy/models/dict.py +++ b/hy/models/dict.py @@ -28,3 +28,12 @@ class HyDict(HyList): def __repr__(self): return "{%s}" % (" ".join([repr(x) for x in self])) + + def keys(self): + return self[0::2] + + def values(self): + return self[1::2] + + def items(self): + return list(zip(self.keys(), self.values())) diff --git a/hy/models/integer.py b/hy/models/integer.py index cb46af4..0eacef7 100644 --- a/hy/models/integer.py +++ b/hy/models/integer.py @@ -19,14 +19,16 @@ # DEALINGS IN THE SOFTWARE. from hy.models import HyObject +from hy._compat import long_type -class HyInteger(HyObject, int): +class HyInteger(HyObject, long_type): """ Internal represntation of a Hy Integer. May raise a ValueError as if - int(foo) was caled, given HyInteger(foo). + int(foo) was called, given HyInteger(foo). On python 2.x long will + be used instead """ def __new__(cls, number, *args, **kwargs): - number = int(number) + number = long_type(number) return super(HyInteger, cls).__new__(cls, number) diff --git a/hy/models/keyword.py b/hy/models/keyword.py index f2633a9..6d25633 100644 --- a/hy/models/keyword.py +++ b/hy/models/keyword.py @@ -29,5 +29,8 @@ class HyKeyword(HyObject, str_type): """ def __new__(cls, value): - obj = str_type.__new__(cls, "\uFDD0" + value) + if not value.startswith("\uFDD0"): + value = "\uFDD0" + value + + obj = str_type.__new__(cls, value) return obj diff --git a/hy/models/list.py b/hy/models/list.py index d2d4da7..e45e40b 100644 --- a/hy/models/list.py +++ b/hy/models/list.py @@ -36,5 +36,16 @@ class HyList(HyObject, list): def __add__(self, other): return self.__class__(super(HyList, self).__add__(other)) + def __getslice__(self, start, end): + return self.__class__(super(HyList, self).__getslice__(start, end)) + + def __getitem__(self, item): + ret = super(HyList, self).__getitem__(item) + + if isinstance(item, slice): + return self.__class__(ret) + + return ret + def __repr__(self): return "[%s]" % (" ".join([repr(x) for x in self])) diff --git a/hy/version.py b/hy/version.py index e96cb4c..2c33563 100644 --- a/hy/version.py +++ b/hy/version.py @@ -20,4 +20,4 @@ __appname__ = "hy" -__version__ = "0.9.11" +__version__ = "0.9.12" diff --git a/make.bat b/make.bat index ccf6138..7046e96 100644 --- a/make.bat +++ b/make.bat @@ -20,8 +20,9 @@ if "%1" == "help" ( echo. - tox echo. - d echo. - r + echo. - clean echo. - goto end + goto :EOF ) if "%1" == "docs" ( @@ -109,8 +110,25 @@ if "%1" == "r" ( goto :EOF ) -if "%1" == full ( +if "%1" == "full" ( call :docs call :d call :tox -) \ No newline at end of file +goto :EOF +) + +if "%1" == "clean" ( +:clean + if EXIST hy\*.pyc cmd /C del /S /Q hy\*.pyc + if EXIST tests\*pyc cmd /C del /S /Q tests\*pyc + for /r %%R in (__pycache__) do if EXIST %%R (rmdir /S /Q %%R) + if EXIST .tox\NUL cmd /C rmdir /S /Q .tox + if EXIST dist\NUL cmd /C rmdir /S /Q dist + if EXIST hy.egg-info\NUL cmd /C rmdir /S /Q hy.egg-info + if EXIST docs\_build\NUL cmd /C rmdir /S /Q docs\_build + goto :EOF +) + +echo.Error: '%1' - unknown target +echo. +goto :help diff --git a/requirements-dev.txt b/requirements-dev.txt index abeab4f..a2797cf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ -r requirements.txt -astor tox nose Sphinx diff --git a/requirements.txt b/requirements.txt index ef71c52..ab8080a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ # Check site / dev for more deps! flake8 --e git+https://github.com/hylang/rply.git#egg=rply +rply>=0.7.0 diff --git a/scripts/update-coreteam.hy b/scripts/update-coreteam.hy new file mode 100644 index 0000000..394bbd8 --- /dev/null +++ b/scripts/update-coreteam.hy @@ -0,0 +1,37 @@ +;; You need to install the requests package first + +(import os.path) +(import requests) + + +(setv *api-url* "https://api.github.com/{}") +(setv *rst-format* "* `{} <{}>`_") +(setv *missing-names* {"khinsen" "Konrad Hinsen"}) +;; We have three concealed members on the hylang organization +;; and GitHub only shows public members if the requester is not +;; an owner of the organization. +(setv *concealed-members* [(, "aldeka" "Karen Rustad") + (, "tuturto" "Tuukka Turto") + (, "cndreisbach" "Clinton N. Dreisbach")]) + +(defn get-dev-name [login] + (setv name (get (.json (requests.get (.format *api-url* (+ "users/" login)))) "name")) + (if-not name + (.get *missing-names* login) + name)) + +(setv coredevs (requests.get (.format *api-url* "orgs/hylang/members"))) + +(setv result (set)) +(for [dev (.json coredevs)] + (result.add (.format *rst-format* (get-dev-name (get dev "login")) + (get dev "html_url")))) + +(for [(, login name) *concealed-members*] + (result.add (.format *rst-format* name (+ "https://github.com/" login)))) + +(setv filename (os.path.abspath (os.path.join os.path.pardir + "docs" "coreteam.rst"))) + +(with [[fobj (open filename "w+")]] + (fobj.write (+ (.join "\n" result) "\n"))) diff --git a/setup.py b/setup.py index 24621ee..1808022 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ long_description = """Hy is a Python <--> Lisp layer. It helps make things work nicer, and lets Python and the Hy lisp variant play nice together. """ -install_requires = ['rply>=0.6.2'] +install_requires = ['rply>=0.7.0', 'astor>=0.3'] if sys.version_info[:2] < (2, 7): install_requires.append('argparse>=1.2.1') install_requires.append('importlib>=1.0.2') @@ -56,7 +56,6 @@ setup( name=PKG, version=__version__, install_requires=install_requires, - dependency_links=['https://github.com/hylang/rply/zipball/master#egg=rply-0.6.2'], entry_points={ 'console_scripts': [ 'hy = hy.cmdline:hy_main', diff --git a/tests/__init__.py b/tests/__init__.py index 5e935a2..f336ad0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,6 +2,7 @@ import hy # noqa +from .native_tests.cons import * # noqa from .native_tests.defclass import * # noqa from .native_tests.math import * # noqa from .native_tests.native_macros import * # noqa @@ -11,3 +12,10 @@ from .native_tests.unless import * # noqa from .native_tests.when import * # noqa from .native_tests.with_decorator import * # noqa from .native_tests.core import * # noqa +from .native_tests.reader_macros import * # noqa +from .native_tests.with_test import * # noqa +from .native_tests.contrib.anaphoric import * # noqa +from .native_tests.contrib.loop import * # noqa +from .native_tests.contrib.meth import * # noqa +from .native_tests.contrib.walk import * # noqa +from .native_tests.contrib.multi import * # noqa diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index a362d66..f4fdf41 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -22,7 +22,10 @@ from __future__ import unicode_literals from hy import HyString -from hy.compiler import hy_compile, HyCompileError, HyTypeError +from hy.models import HyObject +from hy.compiler import hy_compile +from hy.errors import HyCompileError, HyTypeError +from hy.lex.exceptions import LexException from hy.lex import tokenize import ast @@ -42,10 +45,14 @@ def can_compile(expr): def cant_compile(expr): - expr = tokenize(expr) try: - hy_compile(expr, "__main__") + hy_compile(tokenize(expr), "__main__") assert False + except HyTypeError as e: + # Anything that can't be compiled should raise a user friendly + # error, otherwise it's a compiler bug. + assert isinstance(e.expression, HyObject) + assert e.message except HyCompileError as e: # Anything that can't be compiled should raise a user friendly # error, otherwise it's a compiler bug. @@ -253,7 +260,6 @@ def test_ast_bad_get(): "Make sure AST can't compile invalid get" cant_compile("(get)") cant_compile("(get 1)") - cant_compile("(get 1 2 3)") def test_ast_good_slice(): @@ -295,9 +301,9 @@ def test_ast_bad_assoc(): def test_ast_bad_with(): "Make sure AST can't compile invalid with" - cant_compile("(with)") - cant_compile("(with [])") - cant_compile("(with [] (pass))") + cant_compile("(with*)") + cant_compile("(with* [])") + cant_compile("(with* [] (pass))") def test_ast_valid_while(): @@ -305,14 +311,14 @@ def test_ast_valid_while(): can_compile("(while foo bar)") -def test_ast_valid_foreach(): - "Make sure AST can compile valid foreach" - can_compile("(foreach [a 2])") +def test_ast_valid_for(): + "Make sure AST can compile valid for" + can_compile("(for [a 2] (print a))") -def test_ast_invalid_foreach(): - "Make sure AST can't compile invalid foreach" - cant_compile("(foreach [a 1] (else 1 2))") +def test_ast_invalid_for(): + "Make sure AST can't compile invalid for" + cant_compile("(for* [a 1] (else 1 2))") def test_ast_expression_basics(): @@ -423,8 +429,54 @@ def test_compile_error(): """Ensure we get compile error in tricky cases""" try: can_compile("(fn [] (= 1))") - except HyCompileError as e: - assert(str(e) - == "`=' needs at least 2 arguments, got 1 (line 1, column 8)") + except HyTypeError as e: + assert(e.message == "`=' needs at least 2 arguments, got 1.") else: assert(False) + + +def test_for_compile_error(): + """Ensure we get compile error in tricky 'for' cases""" + try: + can_compile("(fn [] (for)") + except LexException as e: + assert(e.message == "Premature end of input") + else: + assert(False) + + try: + can_compile("(fn [] (for)))") + except LexException as e: + assert(e.message == "Ran into a RPAREN where it wasn't expected.") + else: + assert(False) + + try: + can_compile("(fn [] (for [x]))") + except HyTypeError as e: + assert(e.message == "`for' requires an even number of args.") + else: + assert(False) + + try: + can_compile("(fn [] (for [x xx]))") + except HyTypeError as e: + assert(e.message == "`for' requires a body to evaluate") + else: + assert(False) + + +def test_attribute_access(): + """Ensure attribute access compiles correctly""" + can_compile("(. foo bar baz)") + can_compile("(. foo [bar] baz)") + can_compile("(. foo bar [baz] [0] quux [frob])") + can_compile("(. foo bar [(+ 1 2 3 4)] quux [frob])") + cant_compile("(. foo bar :baz [0] quux [frob])") + cant_compile("(. foo bar baz (0) quux [frob])") + cant_compile("(. foo bar baz [0] quux {frob})") + + +def test_cons_correct(): + """Ensure cons gets compiled correctly""" + can_compile("(cons a b)") diff --git a/tests/compilers/test_error_reporting.py b/tests/compilers/test_error_reporting.py new file mode 100644 index 0000000..879f358 --- /dev/null +++ b/tests/compilers/test_error_reporting.py @@ -0,0 +1,27 @@ +# Copyright (c) 2013 Nicolas Dandrimont +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from .test_ast import can_compile, cant_compile + + +def test_macro_nested_kwapply(): + "Make sure nested kwapply compile correctly" + can_compile("(kwapply (kwapply (foo) bar) baz)") + cant_compile("(kwapply (kwapply (foo)) bar)") diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py index 5f4dbf1..edfbb5a 100644 --- a/tests/importer/test_importer.py +++ b/tests/importer/test_importer.py @@ -1,4 +1,5 @@ from hy.importer import import_file_to_module, import_buffer_to_ast, MetaLoader +from hy.errors import HyTypeError import os import ast @@ -27,3 +28,16 @@ def test_imports(): assert _import_test() == "Error" assert _import_test() is not None + + +def test_import_error_reporting(): + "Make sure that (import) reports errors correctly." + + def _import_error_test(): + try: + import_buffer_to_ast("(import \"sys\")", '') + except HyTypeError: + return "Error reported" + + assert _import_error_test() == "Error reported" + assert _import_error_test() is not None diff --git a/tests/lex/test_lex.py b/tests/lex/test_lex.py index 359999b..7ab30e5 100644 --- a/tests/lex/test_lex.py +++ b/tests/lex/test_lex.py @@ -1,4 +1,5 @@ # Copyright (c) 2013 Paul Tagliamonte +# Copyright (c) 2014 Nicolas Dandrimont # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -26,6 +27,8 @@ from hy.models.complex import HyComplex from hy.models.symbol import HySymbol from hy.models.string import HyString from hy.models.dict import HyDict +from hy.models.list import HyList +from hy.models.cons import HyCons from hy.lex import LexException, PrematureEndOfInput, tokenize @@ -252,3 +255,82 @@ def test_complex(): assert entry == HyComplex("1.0j") entry = tokenize("(j)")[0][0] assert entry == HySymbol("j") + + +def test_reader_macro(): + """Ensure reader macros are handles properly""" + entry = tokenize("#^()") + assert entry[0][0] == HySymbol("dispatch_reader_macro") + assert entry[0][1] == HyString("^") + assert len(entry[0]) == 3 + + +def test_lex_comment_382(): + """Ensure that we can tokenize sources with a comment at the end""" + entry = tokenize("foo ;bar\n;baz") + assert entry == [HySymbol("foo")] + + +def test_lex_mangling_star(): + """Ensure that mangling starred identifiers works according to plan""" + entry = tokenize("*foo*") + assert entry == [HySymbol("FOO")] + entry = tokenize("*") + assert entry == [HySymbol("*")] + entry = tokenize("*foo") + assert entry == [HySymbol("*foo")] + + +def test_lex_mangling_hyphen(): + """Ensure that hyphens get translated to underscores during mangling""" + entry = tokenize("foo-bar") + assert entry == [HySymbol("foo_bar")] + entry = tokenize("-") + assert entry == [HySymbol("-")] + + +def test_lex_mangling_qmark(): + """Ensure that identifiers ending with a question mark get mangled ok""" + entry = tokenize("foo?") + assert entry == [HySymbol("is_foo")] + entry = tokenize("?") + assert entry == [HySymbol("?")] + entry = tokenize("im?foo") + assert entry == [HySymbol("im?foo")] + entry = tokenize(".foo?") + assert entry == [HySymbol(".is_foo")] + entry = tokenize("foo.bar?") + assert entry == [HySymbol("foo.is_bar")] + entry = tokenize("foo?.bar") + assert entry == [HySymbol("is_foo.bar")] + entry = tokenize(".foo?.bar.baz?") + assert entry == [HySymbol(".is_foo.bar.is_baz")] + + +def test_simple_cons(): + """Check that cons gets tokenized correctly""" + entry = tokenize("(a . b)")[0] + assert entry == HyCons(HySymbol("a"), HySymbol("b")) + + +def test_dotted_list(): + """Check that dotted lists get tokenized correctly""" + entry = tokenize("(a b c . (d . e))")[0] + assert entry == HyCons(HySymbol("a"), + HyCons(HySymbol("b"), + HyCons(HySymbol("c"), + HyCons(HySymbol("d"), + HySymbol("e"))))) + + +def test_cons_list(): + """Check that cons of something and a list gets tokenized as a list""" + entry = tokenize("(a . [])")[0] + assert entry == HyList([HySymbol("a")]) + assert type(entry) == HyList + entry = tokenize("(a . ())")[0] + assert entry == HyExpression([HySymbol("a")]) + assert type(entry) == HyExpression + entry = tokenize("(a b . {})")[0] + assert entry == HyDict([HySymbol("a"), HySymbol("b")]) + assert type(entry) == HyDict diff --git a/tests/macros/test_reader_macros.py b/tests/macros/test_reader_macros.py new file mode 100644 index 0000000..791a046 --- /dev/null +++ b/tests/macros/test_reader_macros.py @@ -0,0 +1,11 @@ +from hy.macros import macroexpand +from hy.compiler import HyTypeError +from hy.lex import tokenize + + +def test_reader_macro_error(): + """Check if we get correct error with wrong disptach character""" + try: + macroexpand(tokenize("(dispatch_reader_macro '- '())")[0], __name__) + except HyTypeError as e: + assert "with the character `-`" in str(e) diff --git a/tests/models/test_cons.py b/tests/models/test_cons.py new file mode 100644 index 0000000..51b3b10 --- /dev/null +++ b/tests/models/test_cons.py @@ -0,0 +1,56 @@ +# Copyright (c) 2013 Nicolas Dandrimont +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from hy.models.cons import HyCons + + +def test_cons_slicing(): + """Check that cons slicing works as expected""" + cons = HyCons("car", "cdr") + assert cons[0] == "car" + assert cons[1:] == "cdr" + try: + cons[:] + assert True is False + except IndexError: + pass + + try: + cons[1] + assert True is False + except IndexError: + pass + + +def test_cons_replacing(): + """Check that assigning to a cons works as expected""" + cons = HyCons("foo", "bar") + cons[0] = "car" + + assert cons == HyCons("car", "bar") + + cons[1:] = "cdr" + assert cons == HyCons("car", "cdr") + + try: + cons[:] = "foo" + assert True is False + except IndexError: + pass diff --git a/tests/models/test_dict.py b/tests/models/test_dict.py new file mode 100644 index 0000000..cf3b2fd --- /dev/null +++ b/tests/models/test_dict.py @@ -0,0 +1,16 @@ +from hy.models.dict import HyDict + + +hydict = HyDict(["a", 1, "b", 2, "c", 3]) + + +def test_dict_items(): + assert hydict.items() == [("a", 1), ("b", 2), ("c", 3)] + + +def test_dict_keys(): + assert hydict.keys() == ["a", "b", "c"] + + +def test_dict_values(): + assert hydict.values() == [1, 2, 3] diff --git a/tests/models/test_list.py b/tests/models/test_list.py index f403802..cc49c70 100644 --- a/tests/models/test_list.py +++ b/tests/models/test_list.py @@ -2,8 +2,21 @@ from hy.models.list import HyList def test_list_add(): + """Check that adding two HyLists generates a HyList""" a = HyList([1, 2, 3]) b = HyList([3, 4, 5]) c = a + b assert c == [1, 2, 3, 3, 4, 5] assert c.__class__ == HyList + + +def test_list_slice(): + """Check that slicing a HyList produces a HyList""" + a = HyList([1, 2, 3, 4]) + sl1 = a[1:] + sl5 = a[5:] + + assert type(sl1) == HyList + assert sl1 == HyList([2, 3, 4]) + assert type(sl5) == HyList + assert sl5 == HyList([]) diff --git a/tests/native_tests/cons.hy b/tests/native_tests/cons.hy new file mode 100644 index 0000000..5bd8fbd --- /dev/null +++ b/tests/native_tests/cons.hy @@ -0,0 +1,63 @@ +(defn test-cons-mutability [] + "Test the mutability of conses" + (setv tree (cons (cons 1 2) (cons 2 3))) + (setv (car tree) "foo") + (assert (= tree (cons "foo" (cons 2 3)))) + (setv (cdr tree) "bar") + (assert (= tree (cons "foo" "bar")))) + + +(defn test-cons-quoting [] + "Test quoting of conses" + (assert (= (cons 1 2) (quote (1 . 2)))) + (assert (= (quote foo) (car (quote (foo . bar))))) + (assert (= (quote bar) (cdr (quote (foo . bar)))))) + + +(defn test-cons-behavior [] + "NATIVE: test the behavior of cons is consistent" + (defn t= [a b] + (and (= a b) (= (type a) (type b)))) + (assert (t= (cons 1 2) '(1 . 2))) + (assert (t= (cons 1 nil) '(1))) + (assert (t= (cons nil 2) '(nil . 2))) + (assert (t= (cons 1 []) [1])) + (setv tree (cons (cons 1 2) (cons 2 3))) + (assert (t= (car tree) (cons 1 2))) + (assert (t= (cdr tree) (cons 2 3)))) + + +(defn test-cons-iteration [] + "NATIVE: test the iteration behavior of cons" + (setv x '(0 1 2 3 4 . 5)) + (setv it (iter x)) + (for* [i (range 6)] + (assert (= i (next it)))) + (assert + (= 'success + (try + (do + (next it) + 'failurenext) + (except [e TypeError] (if (= e.args (, "Iteration on malformed cons")) + 'success + 'failureexc)) + (except [e Exception] 'failureexc2))))) + + +(defn test-cons? [] + "NATIVE: test behavior of cons?" + (assert (cons? (cons 1 2))) + (assert (cons? '(1 . 2))) + (assert (cons? '(1 2 3 . 4))) + (assert (cons? (list* 1 2 3))) + (assert (not (cons? (cons 1 [2])))) + (assert (not (cons? (list* 1 nil))))) + + +(defn test-list* [] + "NATIVE: test behavior of list*" + (assert (= 1 (list* 1))) + (assert (= (cons 1 2) (list* 1 2))) + (assert (= (cons 1 (cons 2 3)) (list* 1 2 3))) + (assert (= '(1 2 3 4 . 5) (list* 1 2 3 4 5)))) diff --git a/tests/native_tests/contrib/__init__.hy b/tests/native_tests/contrib/__init__.hy new file mode 100644 index 0000000..e69de29 diff --git a/tests/native_tests/contrib/anaphoric.hy b/tests/native_tests/contrib/anaphoric.hy new file mode 100644 index 0000000..791b24c --- /dev/null +++ b/tests/native_tests/contrib/anaphoric.hy @@ -0,0 +1,101 @@ +;; Copyright (c) 2013 James King + +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: + +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. + +;;;; some simple helpers + +(require hy.contrib.anaphoric) + +(defn assert-true [x] + (assert (= True x))) + +(defn assert-false [x] + (assert (= False x))) + +(defn assert-equal [x y] + (assert (= x y))) + + +(defn test-ap-if [] + "NATIVE: testing anaphoric if" + (ap-if true (assert-true it)) + (ap-if false true (assert-false it))) + +(defn test-ap-each [] + "NATIVE: testing anaphoric each" + (setv res []) + (ap-each [1 2 3 4] (.append res it)) + (assert-equal res [1 2 3 4])) + +(defn test-ap-each-while [] + "NATIVE: testing anaphoric each-while" + (setv res []) + (ap-each-while [2 2 4 3 4 5 6] (even? it) (.append res it)) + (assert-equal res [2 2 4])) + +(defn test-ap-map [] + "NATIVE: testing anaphoric map" + (assert-equal (list (ap-map (* it 3) [1 2 3])) + [3 6 9]) + (assert-equal (list (ap-map (* it 3) [])) + [])) + +(defn test-ap-map-when [] + "NATIVE: testing anaphoric map-when" + (assert-equal (list (ap-map-when even? (* it 2) [1 2 3 4])) + [1 4 3 8])) + +(defn test-ap-filter [] + "NATIVE: testing anaphoric filter" + (assert-equal (list (ap-filter (> it 2) [1 2 3 4])) + [3 4]) + (assert-equal (list (ap-filter (even? it) [1 2 3 4])) + [2 4])) + +(defn test-ap-reject [] + "NATIVE: testing anaphoric filter" + (assert-equal (list (ap-reject (> it 2) [1 2 3 4])) + [1 2]) + (assert-equal (list (ap-reject (even? it) [1 2 3 4])) + [1 3])) + +(defn test-ap-dotimes [] + "NATIVE: testing anaphoric dotimes" + (assert-equal (let [[n []]] (ap-dotimes 3 (.append n 3)) n) + [3 3 3]) + (assert-equal (let [[n []]] (ap-dotimes 3 (.append n it)) n) + [0 1 2])) + +(defn test-ap-first [] + "NATIVE: testing anaphoric first" + (assert-equal (ap-first (> it 5) (range 10)) 6) + (assert-equal (ap-first (even? it) [1 2 3 4]) 2)) + +(defn test-ap-last [] + "NATIVE: testing anaphoric last" + (assert-equal (ap-last (> it 5) (range 10)) 9) + (assert-equal (ap-last (even? it) [1 2 3 4]) 4)) + +(defn test-ap-reduce [] + "NATIVE: testing anaphoric reduce" + (assert-equal (ap-reduce (* acc it) [1 2 3]) 6) + (assert-equal (ap-reduce (* acc it) [1 2 3] 6) 36) + (assert-equal (ap-reduce (+ acc " on " it) ["Hy" "meth"]) + "Hy on meth") + (assert-equal (ap-reduce (+ acc it) [] 1) 1)) diff --git a/tests/native_tests/contrib/loop.hy b/tests/native_tests/contrib/loop.hy new file mode 100644 index 0000000..520b840 --- /dev/null +++ b/tests/native_tests/contrib/loop.hy @@ -0,0 +1,46 @@ +(require hy.contrib.loop) +(import sys) + +(defn tco-sum [x y] + (loop [[x x] [y y]] + (cond + [(> y 0) (recur (inc x) (dec y))] + [(< y 0) (recur (dec x) (inc y))] + [True x]))) + +(defn non-tco-sum [x y] + (cond + [(> y 0) (inc (non-tco-sum x (dec y)))] + [(< y 0) (dec (non-tco-sum x (inc y)))] + [True x])) + +(defn test-loop [] + ;; non-tco-sum should fail + (try + (setv n (non-tco-sum 100 10000)) + (catch [e RuntimeError] + (assert true)) + (else + (assert false))) + + ;; tco-sum should not fail + (try + (setv n (tco-sum 100 10000)) + (catch [e RuntimeError] + (assert false)) + (else + (assert (= n 10100))))) + +(defn test-recur-in-wrong-loc [] + (defn bad-recur [n] + (loop [[i n]] + (if (= i 0) + 0 + (inc (recur (dec i)))))) + + (try + (bad-recur 3) + (catch [e TypeError] + (assert true)) + (else + (assert false)))) diff --git a/tests/native_tests/contrib/meth.hy b/tests/native_tests/contrib/meth.hy new file mode 100644 index 0000000..446bb81 --- /dev/null +++ b/tests/native_tests/contrib/meth.hy @@ -0,0 +1,54 @@ +(require hy.contrib.meth) + +(defclass FakeMeth [] + "Mocking decorator class" + [[rules {}] + [route (fn [self rule &kwargs options] + (fn [f] + (assoc self.rules rule (, f options)) + f))]]) + + +(defn test_route [] + (let [[app (FakeMeth)]] + (route get-index "/" [] (str "Hy world!")) + (setv app-rules (getattr app "rules")) + (assert (in "/" app-rules)) + (let [[(, rule-fun rule-opt) (get app-rules "/")]] + (assert (not (empty? rule-opt))) + (assert (in "GET" (get rule-opt "methods"))) + (assert (= (getattr rule-fun "__name__") "get_index")) + (assert (= "Hy world!" (rule-fun)))))) + +(defn test_post_route [] + (let [[app (FakeMeth)]] + (post-route get-index "/" [] (str "Hy world!")) + (setv app-rules (getattr app "rules")) + (assert (in "/" app-rules)) + (let [[(, rule-fun rule-opt) (get app-rules "/")]] + (assert (not (empty? rule-opt))) + (assert (in "POST" (get rule-opt "methods"))) + (assert (= (getattr rule-fun "__name__") "get_index")) + (assert (= "Hy world!" (rule-fun)))))) + +(defn test_put_route [] + (let [[app (FakeMeth)]] + (put-route get-index "/" [] (str "Hy world!")) + (setv app-rules (getattr app "rules")) + (assert (in "/" app-rules)) + (let [[(, rule-fun rule-opt) (get app-rules "/")]] + (assert (not (empty? rule-opt))) + (assert (in "PUT" (get rule-opt "methods"))) + (assert (= (getattr rule-fun "__name__") "get_index")) + (assert (= "Hy world!" (rule-fun)))))) + +(defn test_delete_route [] + (let [[app (FakeMeth)]] + (delete-route get-index "/" [] (str "Hy world!")) + (setv app-rules (getattr app "rules")) + (assert (in "/" app-rules)) + (let [[(, rule-fun rule-opt) (get app-rules "/")]] + (assert (not (empty? rule-opt))) + (assert (in "DELETE" (get rule-opt "methods"))) + (assert (= (getattr rule-fun "__name__") "get_index")) + (assert (= "Hy world!" (rule-fun)))))) diff --git a/tests/native_tests/contrib/multi.hy b/tests/native_tests/contrib/multi.hy new file mode 100644 index 0000000..5ce9932 --- /dev/null +++ b/tests/native_tests/contrib/multi.hy @@ -0,0 +1,57 @@ +;; Copyright (c) 2014 Morten Linderud + +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: + +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. + +(require hy.contrib.multi) + + +(defn test-basic-multi [] + "NATIVE: Test a basic defmulti" + (defmulti fun + ([] "Hello!") + ([a] a) + ([a b] "a b") + ([a b c] "a b c")) + + (assert (= (fun) "Hello!")) + (assert (= (fun "a") "a")) + (assert (= (fun "a" "b") "a b")) + (assert (= (fun "a" "b" "c") "a b c"))) + + +(defn test-kw-args [] + "NATIVE: Test if kwargs are handled correctly" + (defmulti fun + ([a] a) + ([&optional [a "nop"] [b "p"]] (+ a b))) + + (assert (= (fun 1) 1)) + (assert (= (apply fun [] {"a" "t"}) "t")) + (assert (= (apply fun ["hello "] {"b" "world"}) "hello world")) + (assert (= (apply fun [] {"a" "hello " "b" "world"}) "hello world"))) + + +(defn test-docs [] + "NATIVE: Test if docs are properly handled" + (defmulti fun + "docs" + ([a] (print a)) + ([a b] (print b))) + + (assert (= fun.--doc-- "docs"))) diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy new file mode 100644 index 0000000..d6ecbf8 --- /dev/null +++ b/tests/native_tests/contrib/walk.hy @@ -0,0 +1,29 @@ +(import [hy.contrib.walk [*]]) + +(def walk-form '(print {"foo" "bar" + "array" [1 2 3 [4]] + "something" (+ 1 2 3 4) + "cons!" (cons 1 2) + "quoted?" '(foo)})) + +(defn collector [acc x] + (.append acc x) + nil) + +(defn test-walk-identity [] + (assert (= (walk identity identity walk-form) + walk-form))) + +(defn test-walk [] + (let [[acc '()]] + (assert (= (walk (partial collector acc) identity walk-form) + [nil nil])) + (assert (= acc walk-form))) + (let [[acc []]] + (assert (= (walk identity (partial collector acc) walk-form) + nil)) + (assert (= acc [walk-form])))) + +(defn test-macroexpand-all [] + (assert (= (macroexpand-all '(with [a b c] (for [d c] foo))) + '(with* [a] (with* [b] (with* [c] (do (for* [d c] foo)))))))) diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy index 6e81f39..15debf4 100644 --- a/tests/native_tests/core.hy +++ b/tests/native_tests/core.hy @@ -30,6 +30,14 @@ (defn assert-equal [x y] (assert (= x y))) +(defn test-coll? [] + "NATIVE: testing coll?" + (assert-true (coll? [1 2 3])) + (assert-true (coll? {"a" 1 "b" 2})) + (assert-true (coll? (range 10))) + (assert-false (coll? "abc")) + (assert-false (coll? 1))) + (defn test-cycle [] "NATIVE: testing cycle" (assert-equal (list (cycle [])) []) @@ -115,6 +123,13 @@ (try (even? None) (catch [e [TypeError]] (assert (in "not a number" (str e)))))) +(defn test-every? [] + "NATIVE: testing the every? function" + (assert-true (every? even? [2 4 6])) + (assert-false (every? even? [1 3 5])) + (assert-false (every? even? [2 4 5])) + (assert-true (every? even? []))) + (defn test-filter [] "NATIVE: testing the filter function" (setv res (list (filter pos? [ 1 2 3 -4 5]))) @@ -133,6 +148,26 @@ (setv res (list (filter none? [1 2 None 3 4 None 4 6]))) (assert-equal res [None None])) +(defn test-flatten [] + "NATIVE: testing the flatten function" + (setv res (flatten [1 2 [3 4] 5])) + (assert-equal res [1 2 3 4 5]) + (setv res (flatten ["foo" (, 1 2) [1 [2 3] 4] "bar"])) + (assert-equal res ["foo" 1 2 1 2 3 4 "bar"]) + (setv res (flatten [1])) + (assert-equal res [1]) + (setv res (flatten [])) + (assert-equal res []) + (setv res (flatten (, 1))) + (assert-equal res [1]) + ;; test with None + (setv res (flatten (, 1 (, None 3)))) + (assert-equal res [1 None 3]) + (try (flatten "foo") + (catch [e [TypeError]] (assert (in "not a collection" (str e))))) + (try (flatten 12.34) + (catch [e [TypeError]] (assert (in "not a collection" (str e)))))) + (defn test-float? [] "NATIVE: testing the float? function" (assert-true (float? 4.2)) @@ -141,6 +176,24 @@ (assert-true (float? -3.2)) (assert-false (float? "foo"))) +(defn test-gensym [] + "NATIVE: testing the gensym function" + (import [hy.models.symbol [HySymbol]]) + (setv s1 (gensym)) + (assert (isinstance s1 HySymbol)) + (assert (= 0 (.find s1 ":G_"))) + (setv s2 (gensym "xx")) + (setv s3 (gensym "xx")) + (assert (= 0 (.find s2 ":xx_"))) + (assert (not (= s2 s3))) + (assert (not (= (str s2) (str s3))))) + +(defn test-identity [] + "NATIVE: testing the identity function" + (assert (= 4 (identity 4))) + (assert (= "hy" (identity "hy"))) + (assert (= [1 2] (identity [1 2])))) + (defn test-inc [] "NATIVE: testing the inc function" (assert-equal 3 (inc 2)) @@ -163,7 +216,7 @@ (assert-false (instance? Foo2 foo)) (assert-true (instance? Foo foo3)) (assert-true (instance? float 1.0)) - (assert-true (instance? int 3)) + (assert-true (instance? int (int 3))) (assert-true (instance? str (str "hello")))) (defn test-integer? [] @@ -171,10 +224,20 @@ (assert-true (integer? 0)) (assert-true (integer? 3)) (assert-true (integer? -3)) + (assert-true (integer? (integer "-3"))) + (assert-true (integer? (integer 3))) (assert-false (integer? 4.2)) (assert-false (integer? None)) (assert-false (integer? "foo"))) +(defn test-integer-char? [] + "NATIVE: testing the integer-char? function" + (assert-true (integer-char? "1")) + (assert-true (integer-char? "-1")) + (assert-true (integer-char? (str (integer 300)))) + (assert-false (integer-char? "foo")) + (assert-false (integer-char? None))) + (defn test-iterable [] "NATIVE: testing iterable? function" ;; should work for a string @@ -259,6 +322,15 @@ (assert-false (none? 0)) (assert-false (none? ""))) +(defn test-nil? [] + "NATIVE: testing for `is nil`" + (assert-true (nil? nil)) + (assert-true (nil? None)) + (setv f nil) + (assert-true (nil? f)) + (assert-false (nil? 0)) + (assert-false (nil? ""))) + (defn test-nth [] "NATIVE: testing the nth function" (assert-equal 2 (nth [1 2 4 7] 1)) @@ -334,6 +406,13 @@ (assert-equal 2 (second [1 2])) (assert-equal 3 (second [2 3 4]))) +(defn test-some [] + "NATIVE: testing the some function" + (assert-true (some even? [2 4 6])) + (assert-false (some even? [1 3 5])) + (assert-true (some even? [1 3 6])) + (assert-false (some even? []))) + (defn test-string? [] "NATIVE: testing string?" (assert-true (string? "foo")) @@ -391,3 +470,11 @@ (assert-equal res [None None]) (setv res (list (take-while (fn [x] (not (none? x))) [1 2 3 4 None 5 6 None 7]))) (assert-equal res [1 2 3 4])) + +(defn test-zipwith [] + "NATIVE: testing the zipwith function" + (import operator) + (setv res (zipwith operator.add [1 2 3] [3 2 1])) + (assert-equal (list res) [4 4 4]) + (setv res (zipwith operator.sub [3 7 9] [1 2 4])) + (assert-equal (list res) [2 5 5])) diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index d19ca17..5e6f6c7 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -41,6 +41,46 @@ (assert (= count 150))) +(defn test-nasty-for-nesting [] + "NATIVE: test nesting for loops harder" + ;; This test and feature is dedicated to @nedbat. + + ;; let's ensure empty iterating is an implicit do + (setv t 0) + (for [] (setv t 1)) + (assert (= t 1)) + + ;; OK. This first test will ensure that the else is hooked up to the + ;; for when we break out of it. + (for [x (range 2) + y (range 2)] + (break) + (else (throw Exception))) + + ;; OK. This next test will ensure that the else is hooked up to the + ;; "inner" iteration + (for [x (range 2) + y (range 2)] + (if (= y 1) (break)) + (else (throw Exception))) + + ;; OK. This next test will ensure that the else is hooked up to the + ;; "outer" iteration + (for [x (range 2) + y (range 2)] + (if (= x 1) (break)) + (else (throw Exception))) + + ;; OK. This next test will ensure that we call the else branch exactly + ;; once. + (setv flag 0) + (for [x (range 2) + y (range 2)] + (+ 1 1) + (else (setv flag (+ flag 2)))) + (assert (= flag 2))) + + (defn test-while-loop [] "NATIVE: test while loops?" (setv count 5) @@ -86,9 +126,10 @@ (defn test-is [] "NATIVE: test is can deal with None" - (setv a null) - (assert (is a null)) - (assert (is-not a "b"))) + (setv a nil) + (assert (is a nil)) + (assert (is-not a "b")) + (assert (none? a))) (defn test-branching [] @@ -129,7 +170,16 @@ (defn test-index [] "NATIVE: Test that dict access works" (assert (= (get {"one" "two"} "one") "two")) - (assert (= (get [1 2 3 4 5] 1) 2))) + (assert (= (get [1 2 3 4 5] 1) 2)) + (assert (= (get {"first" {"second" {"third" "level"}}} + "first" "second" "third") + "level")) + (assert (= (get ((fn [] {"first" {"second" {"third" "level"}}})) + "first" "second" "third") + "level")) + (assert (= (get {"first" {"second" {"third" "level"}}} + ((fn [] "first")) "second" "third") + "level"))) (defn test-lambda [] @@ -152,7 +202,39 @@ (assert (= (kwapply (kwtest) {"one" "two"}) {"one" "two"})) (setv mydict {"one" "three"}) (assert (= (kwapply (kwtest) mydict) mydict)) - (assert (= (kwapply (kwtest) ((fn [] {"one" "two"}))) {"one" "two"}))) + (assert (= (kwapply (kwtest) ((fn [] {"one" "two"}))) {"one" "two"})) + (assert (= (kwapply + (kwapply + (kwapply + (kwapply + (kwapply (kwtest) {"x" 4}) + mydict) + {"x" 8}) + {"x" (- 3 2) "y" 2}) + {"y" 5 "z" 3}) + {"x" 1 "y" 5 "z" 3 "one" "three"}))) + + +(defn test-apply [] + "NATIVE: test working with args and functions" + (defn sumit [a b c] (+ a b c)) + (assert (= (apply sumit [1] {"b" 2 "c" 3}) 6)) + (assert (= (apply sumit [1 2 2]) 5)) + (assert (= (apply sumit [] {"a" 1 "b" 1 "c" 2}) 4)) + (assert (= (apply sumit ((fn [] [1 1])) {"c" 1}) 3)) + (defn noargs [] [1 2 3]) + (assert (= (apply noargs) [1 2 3]))) + + +(defn test-apply-with-methods [] + "NATIVE: test apply to call a method" + (setv str "foo {bar}") + (assert (= (apply .format [str] {"bar" "baz"}) + (apply .format ["foo {0}" "baz"]) + "foo baz")) + (setv lst ["a {0} {1} {foo} {bar}" "b" "c"]) + (assert (= (apply .format lst {"foo" "d" "bar" "e"}) + "a b c d e"))) (defn test-dotted [] @@ -407,43 +489,43 @@ (defn test-context [] "NATIVE: test with" - (with [fd (open "README.md" "r")] (assert fd)) - (with [(open "README.md" "r")] (do))) + (with [[fd (open "README.md" "r")]] (assert fd)) + (with [[(open "README.md" "r")]] (do))) (defn test-with-return [] "NATIVE: test that with returns stuff" (defn read-file [filename] - (with [fd (open filename "r")] (.read fd))) + (with [[fd (open filename "r")]] (.read fd))) (assert (!= 0 (len (read-file "README.md"))))) (defn test-for-doodle [] "NATIVE: test for-do" (do (do (do (do (do (do (do (do (do (setv (, x y) (, 0 0))))))))))) - (foreach [- [1 2]] + (for [- [1 2]] (do (setv x (+ x 1)) (setv y (+ y 1)))) (assert (= y x 2))) -(defn test-foreach-else [] - "NATIVE: test foreach else" +(defn test-for-else [] + "NATIVE: test for else" (let [[x 0]] - (foreach [a [1 2]] + (for* [a [1 2]] (setv x (+ x a)) (else (setv x (+ x 50)))) (assert (= x 53))) (let [[x 0]] - (foreach [a [1 2]] + (for* [a [1 2]] (setv x (+ x a)) (else)) (assert (= x 3)))) -(defn test-comprehensions [] +(defn test-list-comprehensions [] "NATIVE: test list comprehensions" (assert (= (list-comp (* x 2) (x (range 2))) [0 2])) (assert (= (list-comp (* x 2) (x (range 4)) (% x 2)) [2 6])) @@ -454,6 +536,41 @@ (assert (= (list-comp j (j [1 2])) [1 2]))) +(defn test-set-comprehensions [] + "NATIVE: test set comprehensions" + (assert (instance? set (set-comp x [x (range 2)]))) + (assert (= (set-comp (* x 2) (x (range 2))) (set [0 2]))) + (assert (= (set-comp (* x 2) (x (range 4)) (% x 2)) (set [2 6]))) + (assert (= (set-comp (* y 2) ((, x y) (.items {"1" 1 "2" 2}))) + (set [2 4]))) + (assert (= (set-comp (, x y) (x (range 2) y (range 2))) + (set [(, 0 0) (, 0 1) (, 1 0) (, 1 1)]))) + (assert (= (set-comp j (j [1 2])) (set [1 2])))) + + +(defn test-dict-comprehensions [] + "NATIVE: test dict comprehensions" + (assert (instance? dict (dict-comp x x [x (range 2)]))) + (assert (= (dict-comp x (* x 2) (x (range 2))) {1 2 0 0})) + (assert (= (dict-comp x (* x 2) (x (range 4)) (% x 2)) {3 6 1 2})) + (assert (= (dict-comp x (* y 2) ((, x y) (.items {"1" 1 "2" 2}))) + {"2" 4 "1" 2})) + (assert (= (dict-comp (, x y) (+ x y) (x (range 2) y (range 2))) + {(, 0 0) 0 (, 1 0) 1 (, 0 1) 1 (, 1 1) 2}))) + + +(defn test-generator-expressions [] + "NATIVE: test generator expressions" + (assert (not (instance? list (genexpr x [x (range 2)])))) + (assert (= (list (genexpr (* x 2) (x (range 2)))) [0 2])) + (assert (= (list (genexpr (* x 2) (x (range 4)) (% x 2))) [2 6])) + (assert (= (list (sorted (genexpr (* y 2) ((, x y) (.items {"1" 1 "2" 2}))))) + [2 4])) + (assert (= (list (genexpr (, x y) (x (range 2) y (range 2)))) + [(, 0 0) (, 0 1) (, 1 0) (, 1 1)])) + (assert (= (list (genexpr j (j [1 2]))) [1 2]))) + + (defn test-defn-order [] "NATIVE: test defn evaluation order" (setv acc []) @@ -551,6 +668,12 @@ (assert (= -_- "what?")))) +(defn test-symbol-question-mark [] + "NATIVE: test foo? -> is_foo behavior" + (let [[foo? "nachos"]] + (assert (= is_foo "nachos")))) + + (defn test-and [] "NATIVE: test the and function" (let [[and123 (and 1 2 3)] @@ -769,13 +892,99 @@ (defn test-continue-continuation [] "NATIVE: test checking if continue actually continues" (setv y []) - (for [x (range 10)] - (if (!= x 5) - (continue)) + (for [x (range 10)] + (if (!= x 5) + (continue)) (.append y x)) (assert (= y [5]))) + (defn test-empty-list [] "Evaluate an empty list to a []" (assert (= () []))) + +(defn test-string [] + (assert (string? (string "a"))) + (assert (string? (string 1))) + (assert (= u"unicode" (string "unicode")))) + +(defn test-del [] + "NATIVE: Test the behavior of del" + (setv foo 42) + (assert (= foo 42)) + (del foo) + (assert (= 'good + (try + (do foo 'bad) + (except [NameError] 'good)))) + (setv test (list (range 5))) + (del (get test 4)) + (assert (= test [0 1 2 3])) + (del (get test 2)) + (assert (= test [0 1 3]))) + + +(defn test-macroexpand [] + "Test macroexpand on ->" + (assert (= (macroexpand '(-> (a b) (x y))) + '(x (a b) y))) + (assert (= (macroexpand '(-> (a b) (-> (c d) (e f)))) + '(e (c (a b) d) f)))) + + +(defn test-macroexpand-1 [] + "Test macroexpand-1 on ->" + (assert (= (macroexpand-1 '(-> (a b) (-> (c d) (e f)))) + '(-> (a b) (c d) (e f))))) + + +(defn test-calling-module-name [] + "NATIVE: Test the calling-module-name function" + (assert (= (calling-module-name -1) "hy.core.language")) + (assert (= (calling-module-name 0) "tests.native_tests.language"))) + + +(defn test-disassemble [] + "NATIVE: Test the disassemble function" + (import sys) + (if-python2 + (import [io [BytesIO :as StringIO]]) + (import [io [StringIO]])) + (setv prev-stdout sys.stdout) + (setv sys.stdout (StringIO)) + (disassemble '(do (leaky) (leaky) (macros))) + (setv stdout (.getvalue sys.stdout)) + (setv sys.stdout prev-stdout) + (assert (in "leaky" stdout)) + (assert (in "macros" stdout)) + (setv sys.stdout (StringIO)) + (disassemble '(do (leaky) (leaky) (macros)) true) + (setv stdout (.getvalue sys.stdout)) + (setv sys.stdout prev-stdout) + (assert (= stdout "leaky()\nleaky()\nmacros()\n"))) + + +(defn test-attribute-access [] + "NATIVE: Test the attribute access DSL" + (defclass mycls [object]) + + (setv foo [(mycls) (mycls) (mycls)]) + (assert (is (. foo) foo)) + (assert (is (. foo [0]) (get foo 0))) + (assert (is (. foo [0] --class--) mycls)) + (assert (is (. foo [1] --class--) mycls)) + (assert (is (. foo [(+ 1 1)] --class--) mycls)) + (assert (= (. foo [(+ 1 1)] --class-- --name-- [0]) "m")) + (assert (= (. foo [(+ 1 1)] --class-- --name-- [1]) "y")) + + (setv bar (mycls)) + (setv (. foo [1]) bar) + (assert (is bar (get foo 1))) + (setv (. foo [1] test) "hello") + (assert (= (getattr (. foo [1]) "test") "hello"))) + +(defn test-keyword-quoting [] + "NATIVE: test keyword quoting magic" + (assert (= :foo "\ufdd0:foo")) + (assert (= `:foo "\ufdd0:foo"))) diff --git a/tests/native_tests/math.hy b/tests/native_tests/math.hy index ba350e6..0824876 100644 --- a/tests/native_tests/math.hy +++ b/tests/native_tests/math.hy @@ -8,7 +8,9 @@ (setv test_mult (fn [] "NATIVE: Test multiplication." - (assert (= 4 (square 2))))) + (assert (= 4 (square 2))) + (assert (= 8 (* 8))) + (assert (= 1 (*))))) (setv test_sub (fn [] @@ -19,7 +21,9 @@ (setv test_add (fn [] "NATIVE: Test addition" - (assert (= 4 (+ 1 1 1 1))))) + (assert (= 4 (+ 1 1 1 1))) + (assert (= 8 (+ 8))) + (assert (= 0 (+))))) (setv test_div (fn [] @@ -132,3 +136,7 @@ (let [[x 1]] (^= x 1) (assert (= x 0)))) + +(defn overflow-int-to-long [] + "NATIVE: test if int does not raise an overflow exception" + (assert (integer? (+ 1 1000000000000000000000000)))) diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy index f5b1b74..5bdf821 100644 --- a/tests/native_tests/native_macros.hy +++ b/tests/native_tests/native_macros.hy @@ -31,9 +31,15 @@ (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-none []) +(assert (= (a-none) None)) + ; A macro calling a previously defined function (eval-when-compile (defn foo [x y] @@ -53,7 +59,7 @@ (defn test-midtree-yield-in-for [] "NATIVE: test yielding in a for with a return" (defn kruft-in-for [] - (for [i (range 5)] + (for* [i (range 5)] (yield i)) (+ 1 2))) @@ -69,7 +75,7 @@ (defn test-multi-yield [] "NATIVE: testing multiple yields" (defn multi-yield [] - (for [i (range 3)] + (for* [i (range 3)] (yield i)) (yield "a") (yield "end")) @@ -91,11 +97,10 @@ (assert initialized) (assert (test-initialized)) - (defn test-yield-from [] "NATIVE: testing yield from" (defn yield-from-test [] - (for [i (range 3)] + (for* [i (range 3)] (yield i)) (yield-from [1 2 3])) (assert (= (list (yield-from-test)) [0 1 2 1 2 3]))) @@ -104,3 +109,92 @@ (import sys) (assert (= (get sys.version_info 0) (if-python2 2 3)))) + +(defn test-gensym-in-macros [] + (import ast) + (import [astor.codegen [to_source]]) + (import [hy.importer [import_buffer_to_ast]]) + (setv macro1 "(defmacro nif [expr pos zero neg] + (let [[g (gensym)]] + `(let [[~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 (import_buffer_to_ast macro1 "foo")) + (setv _ast2 (import_buffer_to_ast macro1 "foo")) + (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)) + ;; but make sure the two don't match each other + (assert (not (= s1 s2)))) + +(defn test-with-gensym [] + (import ast) + (import [astor.codegen [to_source]]) + (import [hy.importer [import_buffer_to_ast]]) + (setv macro1 "(defmacro nif [expr pos zero neg] + (with-gensyms [a] + `(let [[~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 (import_buffer_to_ast macro1 "foo")) + (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 (not (= s1 s2)))) + +(defn test-defmacro-g! [] + (import ast) + (import [astor.codegen [to_source]]) + (import [hy.importer [import_buffer_to_ast]]) + (setv macro1 "(defmacro/g! nif [expr pos zero neg] + `(let [[~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 (import_buffer_to_ast macro1 "foo")) + (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 (not (= s1 s2)))) + + +(defn test-if-not [] + (assert (= (if-not True :yes :no) + :no)) + (assert (= (if-not False :yes :no) + :yes)) + (assert (nil? (if-not True :yes))) + (assert (= (if-not False :yes) + :yes))) + + +(defn test-defn-alias [] + (defn-alias [tda-main tda-a1 tda-a2] [] :bazinga) + (defun-alias [tda-main tda-a1 tda-a2] [] :bazinga) + (assert (= (tda-main) :bazinga)) + (assert (= (tda-a1) :bazinga)) + (assert (= (tda-a2) :bazinga)) + (assert (= tda-main tda-a1 tda-a2))) diff --git a/tests/native_tests/reader_macros.hy b/tests/native_tests/reader_macros.hy new file mode 100644 index 0000000..84a48a5 --- /dev/null +++ b/tests/native_tests/reader_macros.hy @@ -0,0 +1,36 @@ +(defn test-reader-macro [] + "Test a basic redaer macro" + (defreader ^ [expr] + expr) + + (assert (= #^"works" "works"))) + + +(defn test-reader-macro-expr [] + "Test basic exprs like lists and arrays" + (defreader n [expr] + (get expr 1)) + + (assert (= #n[1 2] 2)) + (assert (= #n(1 2) 2))) + + +(defn test-reader-macro-override [] + "Test if we can override function symbols" + (defreader + [n] + (+ n 1)) + + (assert (= #+2 3))) + + +(defn test-reader-macros-macros [] + "Test if defreader is actually a macro" + (defreader t [expr] + `(, ~@expr)) + + (def a #t[1 2 3]) + + (assert (= (type a) tuple)) + (assert (= (, 1 2 3) a))) + + diff --git a/tests/native_tests/unless.hy b/tests/native_tests/unless.hy index f8c298a..2b957fe 100644 --- a/tests/native_tests/unless.hy +++ b/tests/native_tests/unless.hy @@ -4,5 +4,7 @@ (assert (= (unless false 1 2) 2)) (assert (= (unless false 1 3) 3)) (assert (= (unless true 2) null)) + (assert (= (unless true 2) nil)) (assert (= (unless (!= 1 2) 42) null)) + (assert (= (unless (!= 1 2) 42) nil)) (assert (= (unless (!= 2 2) 42) 42))) diff --git a/tests/native_tests/when.hy b/tests/native_tests/when.hy index 1409b3b..c281439 100644 --- a/tests/native_tests/when.hy +++ b/tests/native_tests/when.hy @@ -5,4 +5,6 @@ (assert (= (when true 1 3) 3)) (assert (= (when false 2) null)) (assert (= (when (= 1 2) 42) null)) + (assert (= (when false 2) nil)) + (assert (= (when (= 1 2) 42) nil)) (assert (= (when (= 2 2) 42) 42))) diff --git a/tests/native_tests/with_test.hy b/tests/native_tests/with_test.hy new file mode 100644 index 0000000..b094f35 --- /dev/null +++ b/tests/native_tests/with_test.hy @@ -0,0 +1,44 @@ +(defclass WithTest [object] + [(--init-- + (fn [self val] + (setv self.val val) + None)) + + (--enter-- + (fn [self] + self.val)) + + (--exit-- + (fn [self type value traceback] + (setv self.val None)))]) + +(defn test-single-with [] + "NATIVE: test a single with" + (with [[t (WithTest 1)]] + (assert (= t 1)))) + +(defn test-two-with [] + "NATIVE: test two withs" + (with [[t1 (WithTest 1)] + [t2 (WithTest 2)]] + (assert (= t1 1)) + (assert (= t2 2)))) + +(defn test-thrice-with [] + "NATIVE: test three withs" + (with [[t1 (WithTest 1)] + [t2 (WithTest 2)] + [t3 (WithTest 3)]] + (assert (= t1 1)) + (assert (= t2 2)) + (assert (= t3 3)))) + +(defn test-quince-with [] + "NATIVE: test four withs, one with no args" + (with [[t1 (WithTest 1)] + [t2 (WithTest 2)] + [t3 (WithTest 3)] + [(WithTest 4)]] + (assert (= t1 1)) + (assert (= t2 2)) + (assert (= t3 3)))) diff --git a/tests/test_bin.py b/tests/test_bin.py index f916f98..a6087bd 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -22,7 +22,8 @@ # DEALINGS IN THE SOFTWARE. import os import subprocess -import sys + +from nose.plugins.skip import SkipTest def run_cmd(cmd, stdin_data=None): @@ -40,8 +41,8 @@ def run_cmd(cmd, stdin_data=None): # Read stdout and stderr otherwise if the PIPE buffer is full, we might # wait for ever… while p.poll() is None: - stdout += str(p.stdout.read()) - stderr += str(p.stderr.read()) + stdout += p.stdout.read().decode('utf-8') + stderr += p.stderr.read().decode('utf-8') return p.returncode, stdout, stderr @@ -63,7 +64,7 @@ def test_bin_hy_cmd(): ret = run_cmd("hy -c \"(koan\"") assert ret[0] == 1 - assert "PrematureEndOfInput" in ret[1] + assert "Premature end of input" in ret[2] def test_bin_hy_icmd(): @@ -75,10 +76,12 @@ def test_bin_hy_icmd(): assert "figlet" in output -def test_bin_hy_file(): - ret = run_cmd("hy eg/nonfree/halting-problem/halting.hy") +def test_bin_hy_icmd_and_spy(): + ret = run_cmd("hy -i \"(+ [] [])\" --spy", "(+ 1 1)") assert ret[0] == 0 - assert "27" in ret[1] + output = ret[1] + + assert "([] + [])" in output def test_bin_hy_missing_file(): @@ -122,20 +125,16 @@ def test_bin_hyc_missing_file(): def test_hy2py(): - # XXX Astor doesn't seem to support Python3 :( - if sys.version_info[0] == 3: - return - # and running this script this way doesn't work on Windows if os.name == "nt": - return + raise SkipTest("doesn't work on Windows") i = 0 for dirpath, dirnames, filenames in os.walk("tests/native_tests"): for f in filenames: if f.endswith(".hy"): i += 1 - ret = run_cmd("bin/hy2py " + os.path.join(dirpath, f)) + ret = run_cmd("bin/hy2py -s -a " + os.path.join(dirpath, f)) assert ret[0] == 0, f assert len(ret[1]) > 1, f assert len(ret[2]) == 0, f diff --git a/tox.ini b/tox.ini index 9b17b7a..44c1903 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,7 @@ commands = nosetests deps = nose setuptools - -e - git+https://github.com/hylang/rply.git#egg=rply + rply [testenv:pypy] commands = nosetests @@ -14,8 +13,7 @@ deps = astor nose setuptools - -e - git+https://github.com/hylang/rply.git#egg=rply + rply [testenv:py27] commands = nosetests @@ -23,8 +21,7 @@ deps = astor nose setuptools - -e - git+https://github.com/hylang/rply.git#egg=rply + rply [testenv:py26] deps = @@ -33,12 +30,10 @@ deps = setuptools unittest2 importlib - -e - git+https://github.com/hylang/rply.git#egg=rply + rply [testenv:flake8] deps = flake8 - -e - git+https://github.com/hylang/rply.git#egg=rply + rply commands = flake8 hy bin tests