Merge pull request #1593 from Kodiologist/compiler-pattern-matching

Pattern-matching compiler
This commit is contained in:
Kodi Arfer 2018-05-20 14:24:24 -07:00 committed by GitHub
commit 370563567a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 855 additions and 1205 deletions

View File

@ -33,7 +33,9 @@ Other Breaking Changes
* `hy-repr` uses registered functions instead of methods * `hy-repr` uses registered functions instead of methods
* `HyKeyword` no longer inherits from the string type and has been * `HyKeyword` no longer inherits from the string type and has been
made into its own object type. made into its own object type.
* `HySymbol` no longer inherits from `HyString`.
* `(except)` is no longer allowed. Use `(except [])` instead. * `(except)` is no longer allowed. Use `(except [])` instead.
* `(import [foo])` is no longer allowed. Use `(import foo)` instead.
New Features New Features
------------------------------ ------------------------------

View File

@ -119,13 +119,12 @@ the following order:
HyString HyString
~~~~~~~~ ~~~~~~~~
``hy.models.HyString`` is the base class of string-equivalent Hy ``hy.models.HyString`` represents string literals (including bracket strings),
models. It also represents string literals (including bracket strings), which which compile down to unicode string literals in Python. ``HyStrings`` inherit
compile down to unicode string literals in Python. ``HyStrings`` inherit unicode objects in Python 2, and string objects in Python 3 (and are therefore
unicode objects in Python 2, and string objects in Python 3 (and are not encoding-dependent).
therefore not encoding-dependent).
``HyString`` based models are immutable. ``HyString``\s are immutable.
Hy literal strings can span multiple lines, and are considered by the Hy literal strings can span multiple lines, and are considered by the
parser as a single unit, respecting the Python escapes for unicode parser as a single unit, respecting the Python escapes for unicode
@ -163,8 +162,9 @@ valid numeric python literals will be turned into their Hy counterpart.
HySymbol HySymbol
~~~~~~~~ ~~~~~~~~
``hy.models.HySymbol`` is the model used to represent symbols ``hy.models.HySymbol`` is the model used to represent symbols in the Hy
in the Hy language. It inherits :ref:`HyString`. language. Like ``HyString``, it inherits from ``str`` (or ``unicode`` on Python
2).
Symbols are :ref:`mangled <mangling>` when they are compiled Symbols are :ref:`mangled <mangling>` when they are compiled
to Python variable names. to Python variable names.

View File

@ -186,8 +186,8 @@ def ideas_macro(ETname):
""")]) """)])
require("hy.cmdline", "__console__", all_macros=True) require("hy.cmdline", "__console__", assignments="ALL")
require("hy.cmdline", "__main__", all_macros=True) require("hy.cmdline", "__main__", assignments="ALL")
SIMPLE_TRACEBACKS = True SIMPLE_TRACEBACKS = True

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@ class Completer(object):
if not isinstance(namespace, dict): if not isinstance(namespace, dict):
raise TypeError('namespace must be a dictionary') raise TypeError('namespace must be a dictionary')
self.namespace = namespace self.namespace = namespace
self.path = [hy.compiler._compile_table, self.path = [hy.compiler._special_form_compilers,
builtins.__dict__, builtins.__dict__,
hy.macros._hy_macros[None], hy.macros._hy_macros[None],
namespace] namespace]

View File

@ -72,9 +72,7 @@
;; TODO: move to hy.extra.reserved? ;; TODO: move to hy.extra.reserved?
(import hy) (import hy)
(setv special-forms (list-comp k (setv special-forms (list (.keys hy.compiler._special-form-compilers)))
[k (.keys hy.compiler._compile-table)]
(isinstance k hy._compat.string-types)))
(defn lambda-list [form] (defn lambda-list [form]

View File

@ -18,6 +18,6 @@
hy.core.shadow.EXPORTS hy.core.shadow.EXPORTS
(list (.keys (get hy.macros._hy_macros None))) (list (.keys (get hy.macros._hy_macros None)))
keyword.kwlist keyword.kwlist
(list-comp k [k (.keys hy.compiler.-compile-table)] (list (.keys hy.compiler._special_form_compilers))
(isinstance k hy._compat.string-types)))))))) (list hy.compiler._bad_roots)))))))
_cache) _cache)

View File

@ -80,11 +80,10 @@ def tag(name):
return _ return _
def require(source_module, target_module, def require(source_module, target_module, assignments, prefix=""):
all_macros=False, assignments={}, prefix=""):
"""Load macros from `source_module` in the namespace of """Load macros from `source_module` in the namespace of
`target_module`. `assignments` maps old names to new names, but is `target_module`. `assignments` maps old names to new names, or
ignored if `all_macros` is true. If `prefix` is nonempty, it is should be the string "ALL". If `prefix` is nonempty, it is
prepended to the name of each imported macro. (This means you get prepended to the name of each imported macro. (This means you get
macros named things like "mymacromodule.mymacro", which looks like macros named things like "mymacromodule.mymacro", which looks like
an attribute of a module, although it's actually just a symbol an attribute of a module, although it's actually just a symbol
@ -97,17 +96,18 @@ def require(source_module, target_module,
seen_names = set() seen_names = set()
if prefix: if prefix:
prefix += "." prefix += "."
assignments = {mangle(str_type(k)): v for k, v in assignments.items()} if assignments != "ALL":
assignments = {mangle(str_type(k)): v for k, v in assignments}
for d in _hy_macros, _hy_tag: for d in _hy_macros, _hy_tag:
for name, macro in d[source_module].items(): for name, macro in d[source_module].items():
seen_names.add(name) seen_names.add(name)
if all_macros: if assignments == "ALL":
d[target_module][mangle(prefix + name)] = macro d[target_module][mangle(prefix + name)] = macro
elif name in assignments: elif name in assignments:
d[target_module][mangle(prefix + assignments[name])] = macro d[target_module][mangle(prefix + assignments[name])] = macro
if not all_macros: if assignments != "ALL":
unseen = frozenset(assignments.keys()).difference(seen_names) unseen = frozenset(assignments.keys()).difference(seen_names)
if unseen: if unseen:
raise ImportError("cannot require names: " + repr(list(unseen))) raise ImportError("cannot require names: " + repr(list(unseen)))

76
hy/model_patterns.py Normal file
View File

@ -0,0 +1,76 @@
# Copyright 2018 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
"Parser combinators for pattern-matching Hy model trees."
from hy.models import HyExpression, HySymbol, HyKeyword, HyString, HyList
from funcparserlib.parser import (
some, skip, many, finished, a, Parser, NoParseError, State)
from functools import reduce
from itertools import repeat
from operator import add
from math import isinf
FORM = some(lambda _: True)
SYM = some(lambda x: isinstance(x, HySymbol))
STR = some(lambda x: isinstance(x, HyString))
def sym(wanted):
"Parse and skip the given symbol or keyword."
if wanted.startswith(":"):
return skip(a(HyKeyword(wanted[1:])))
return skip(some(lambda x: isinstance(x, HySymbol) and x == wanted))
def whole(parsers):
"""Parse the parsers in the given list one after another, then
expect the end of the input."""
if len(parsers) == 0:
return finished >> (lambda x: [])
if len(parsers) == 1:
return parsers[0] + finished >> (lambda x: x[:-1])
return reduce(add, parsers) + skip(finished)
def _grouped(group_type, parsers): return (
some(lambda x: isinstance(x, group_type)) >>
(lambda x: group_type(whole(parsers).parse(x)).replace(x, recursive=False)))
def brackets(*parsers):
"Parse the given parsers inside square brackets."
return _grouped(HyList, parsers)
def pexpr(*parsers):
"Parse the given parsers inside a parenthesized expression."
return _grouped(HyExpression, parsers)
def dolike(head):
"Parse a `do`-like form."
return pexpr(sym(head), many(FORM))
def notpexpr(*disallowed_heads):
"""Parse any object other than a HyExpression beginning with a
HySymbol equal to one of the disallowed_heads."""
return some(lambda x: not (
isinstance(x, HyExpression) and
x and
isinstance(x[0], HySymbol) and
x[0] in disallowed_heads))
def times(lo, hi, parser):
"""Parse `parser` several times (`lo` to `hi`) in a row. `hi` can be
float('inf'). The result is a list no matter the number of instances."""
@Parser
def f(tokens, s):
result = []
for _ in range(lo):
(v, s) = parser.run(tokens, s)
result.append(v)
end = s.max
try:
for _ in (repeat(1) if isinf(hi) else range(hi - lo)):
(v, s) = parser.run(tokens, s)
result.append(v)
except NoParseError as e:
end = e.state.max
return result, State(s.pos, end)
return f

View File

@ -109,13 +109,13 @@ class HyBytes(HyObject, bytes_type):
_wrappers[bytes_type] = HyBytes _wrappers[bytes_type] = HyBytes
class HySymbol(HyString): class HySymbol(HyObject, str_type):
""" """
Hy Symbol. Basically a String. Hy Symbol. Basically a string.
""" """
def __init__(self, string): def __new__(cls, s=None):
self += string return super(HySymbol, cls).__new__(cls, s)
_wrappers[bool] = lambda x: HySymbol("True") if x else HySymbol("False") _wrappers[bool] = lambda x: HySymbol("True") if x else HySymbol("False")
_wrappers[type(None)] = lambda foo: HySymbol("None") _wrappers[type(None)] = lambda foo: HySymbol("None")
@ -238,10 +238,10 @@ class HySequence(HyObject, list):
An abstract type for sequence-like models to inherit from. An abstract type for sequence-like models to inherit from.
""" """
def replace(self, other): def replace(self, other, recursive=True):
for x in self: if recursive:
replace_hy_obj(x, other) for x in self:
replace_hy_obj(x, other)
HyObject.replace(self, other) HyObject.replace(self, other)
return self return self

View File

@ -30,7 +30,7 @@ class Install(install):
"." + filename[:-len(".hy")]) "." + filename[:-len(".hy")])
install.run(self) install.run(self)
install_requires = ['rply>=0.7.5', 'astor', 'clint>=0.4'] install_requires = ['rply>=0.7.5', 'astor', 'funcparserlib>=0.3.6', 'clint>=0.4']
if os.name == 'nt': if os.name == 'nt':
install_requires.append('pyreadline>=2.1') install_requires.append('pyreadline>=2.1')

View File

@ -243,6 +243,7 @@ def test_ast_bad_lambda():
cant_compile("(fn ())") cant_compile("(fn ())")
cant_compile("(fn () 1)") cant_compile("(fn () 1)")
cant_compile("(fn (x) 1)") cant_compile("(fn (x) 1)")
cant_compile('(fn "foo")')
def test_ast_good_yield(): def test_ast_good_yield():
@ -442,8 +443,7 @@ def test_lambda_list_keywords_kwonly():
exception = cant_compile(kwonly_demo) exception = cant_compile(kwonly_demo)
assert isinstance(exception, HyTypeError) assert isinstance(exception, HyTypeError)
message, = exception.args message, = exception.args
assert message == ("keyword-only arguments are only " assert message == "&kwonly parameters require Python 3"
"available under Python 3")
def test_lambda_list_keywords_mixed(): def test_lambda_list_keywords_mixed():
@ -451,7 +451,7 @@ def test_lambda_list_keywords_mixed():
can_compile("(fn [x &rest xs &kwargs kw] (list x xs kw))") can_compile("(fn [x &rest xs &kwargs kw] (list x xs kw))")
cant_compile("(fn [x &rest xs &fasfkey {bar \"baz\"}])") cant_compile("(fn [x &rest xs &fasfkey {bar \"baz\"}])")
if PY3: if PY3:
can_compile("(fn [x &rest xs &kwargs kwxs &kwonly kwoxs]" can_compile("(fn [x &rest xs &kwonly kwoxs &kwargs kwxs]"
" (list x xs kwxs kwoxs))") " (list x xs kwxs kwoxs))")
@ -511,7 +511,6 @@ def test_compile_error():
"""Ensure we get compile error in tricky cases""" """Ensure we get compile error in tricky cases"""
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyTypeError) as excinfo:
can_compile("(fn [] (in [1 2 3]))") can_compile("(fn [] (in [1 2 3]))")
assert excinfo.value.message == "`in' needs 2 arguments, got 1"
def test_for_compile_error(): def test_for_compile_error():
@ -567,6 +566,7 @@ def test_defn():
cant_compile("(defn \"hy\" [] 1)") cant_compile("(defn \"hy\" [] 1)")
cant_compile("(defn :hy [] 1)") cant_compile("(defn :hy [] 1)")
can_compile("(defn &hy [] 1)") can_compile("(defn &hy [] 1)")
cant_compile('(defn hy "foo")')
def test_setv_builtins(): def test_setv_builtins():
@ -585,11 +585,11 @@ def test_setv_builtins():
def test_top_level_unquote(): def test_top_level_unquote():
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyTypeError) as excinfo:
can_compile("(unquote)") can_compile("(unquote)")
assert excinfo.value.message == "`unquote' can't be used at the top-level" assert excinfo.value.message == "The special form 'unquote' is not allowed here"
with pytest.raises(HyTypeError) as excinfo: with pytest.raises(HyTypeError) as excinfo:
can_compile("(unquote-splice)") can_compile("(unquote-splice)")
assert excinfo.value.message == "`unquote-splice' can't be used at the top-level" assert excinfo.value.message == "The special form 'unquote-splice' is not allowed here"
def test_lots_of_comment_lines(): def test_lots_of_comment_lines():

View File

@ -55,7 +55,7 @@ def test_compiler_yield_return():
HyExpression([HySymbol("+"), HyExpression([HySymbol("+"),
HyInteger(1), HyInteger(1),
HyInteger(1)])) HyInteger(1)]))
ret = compiler.HyASTCompiler('test').compile_function_def(e) ret = compiler.HyASTCompiler('test').compile_atom(e)
assert len(ret.stmts) == 1 assert len(ret.stmts) == 1
stmt, = ret.stmts stmt, = ret.stmts

View File

@ -87,8 +87,8 @@
(assert (= b 2)) (assert (= b 2))
(setv y 0 x 1 y x) (setv y 0 x 1 y x)
(assert (= y 1)) (assert (= y 1))
(try (eval '(setv a 1 b)) (with [(pytest.raises HyTypeError)]
(except [e [TypeError]] (assert (in "`setv' needs an even number of arguments" (str e)))))) (eval '(setv a 1 b))))
(defn test-setv-returns-none [] (defn test-setv-returns-none []
@ -177,16 +177,6 @@
None) ; Avoid https://github.com/hylang/hy/issues/1320 None) ; Avoid https://github.com/hylang/hy/issues/1320
(defn test-fn-corner-cases []
"NATIVE: tests that fn/defn handles corner cases gracefully"
(try (eval '(fn "foo"))
(except [e [Exception]] (assert (in "to `fn' must be a list"
(str e)))))
(try (eval '(defn foo "foo"))
(except [e [Exception]]
(assert (in "takes a parameter list as second" (str e))))))
(defn test-for-loop [] (defn test-for-loop []
"NATIVE: test for loops" "NATIVE: test for loops"
(setv count1 0 count2 0) (setv count1 0 count2 0)
@ -280,7 +270,15 @@
y (range 2)] y (range 2)]
(+ 1 1) (+ 1 1)
(else (setv flag (+ flag 2)))) (else (setv flag (+ flag 2))))
(assert (= flag 2))) (assert (= flag 2))
(setv l [])
(defn f []
(for [x [4 9 2]]
(.append l (* 10 x))
(yield x)))
(for [_ (f)])
(assert (= l [40 90 20])))
(defn test-while-loop [] (defn test-while-loop []
@ -1388,8 +1386,6 @@
(import [os.path [basename :as bn]]) (import [os.path [basename :as bn]])
(assert (= bn basename)) (assert (= bn basename))
(import [sys])
;; Multiple stuff to import ;; Multiple stuff to import
(import sys [os.path [dirname]] (import sys [os.path [dirname]]
[os.path :as op] [os.path :as op]

View File

@ -15,42 +15,42 @@
(rev (.append x 1) (.append x 2) (.append x 3)) (rev (.append x 1) (.append x 2) (.append x 3))
(assert (= x [3 2 1]))) (assert (= x [3 2 1])))
; Macros returning constants (defn test-macros-returning-constants []
(defmacro an-int [] 42)
(assert (= (an-int) 42))
(defmacro an-int [] 42) (defmacro a-true [] True)
(assert (= (an-int) 42)) (assert (= (a-true) True))
(defmacro a-false [] False)
(assert (= (a-false) False))
(defmacro a-true [] True) (defmacro a-float [] 42.)
(assert (= (a-true) True)) (assert (= (a-float) 42.))
(defmacro a-false [] False)
(assert (= (a-false) False))
(defmacro a-float [] 42.) (defmacro a-complex [] 42j)
(assert (= (a-float) 42.)) (assert (= (a-complex) 42j))
(defmacro a-complex [] 42j) (defmacro a-string [] "foo")
(assert (= (a-complex) 42j)) (assert (= (a-string) "foo"))
(defmacro a-string [] "foo") (defmacro a-bytes [] b"foo")
(assert (= (a-string) "foo")) (assert (= (a-bytes) b"foo"))
(defmacro a-bytes [] b"foo") (defmacro a-list [] [1 2])
(assert (= (a-bytes) b"foo")) (assert (= (a-list) [1 2]))
(defmacro a-list [] [1 2]) (defmacro a-tuple [&rest b] b)
(assert (= (a-list) [1 2])) (assert (= (a-tuple 1 2) [1 2]))
(defmacro a-tuple [&rest b] b) (defmacro a-dict [] {1 2})
(assert (= (a-tuple 1 2) [1 2])) (assert (= (a-dict) {1 2}))
(defmacro a-dict [] {1 2}) (defmacro a-set [] #{1 2})
(assert (= (a-dict) {1 2})) (assert (= (a-set) #{1 2}))
(defmacro a-set [] #{1 2}) (defmacro a-none [])
(assert (= (a-set) #{1 2})) (assert (= (a-none) None)))
(defmacro a-none [])
(assert (= (a-none) None))
; A macro calling a previously defined function ; A macro calling a previously defined function
(eval-when-compile (eval-when-compile

View File

@ -19,7 +19,8 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little
(setv mynan NaN) (setv mynan NaN)
(setv pinf Inf) (setv pinf Inf)
(setv ninf -Inf) (setv ninf -Inf)
;(setv mycomplex -Inf+5j) ; https://github.com/berkerpeksag/astor/issues/100 (setv mycomplex -Inf+5j)
(setv mycomplex2 NaN-Infj)
(setv num-expr (+ 3 (* 5 2) (- 6 (// 8 2)) (* (+ 1 2) (- 3 5)))) ; = 9 (setv num-expr (+ 3 (* 5 2) (- 6 (// 8 2)) (* (+ 1 2) (- 3 5)))) ; = 9

View File

@ -41,6 +41,12 @@ def assert_stuff(m):
assert m.pinf > 0 assert m.pinf > 0
assert math.isinf(m.ninf) assert math.isinf(m.ninf)
assert m.ninf < 0 assert m.ninf < 0
assert math.isinf(m.mycomplex.real)
assert m.mycomplex.real < 0
assert m.mycomplex.imag == 5
assert math.isnan(m.mycomplex2.real)
assert math.isinf(m.mycomplex2.imag)
assert m.mycomplex2.imag < 0
assert m.num_expr == 9 assert m.num_expr == 9