Skip to content

Commit

Permalink
Make HyKeyword a first class objects
Browse files Browse the repository at this point in the history
HyKeywords are no longer an instances of string with a particular
prefix, but a completely separate object.

This means keywords no longer trip isinstance str checks, adding a
little bit of type safety to the compiler.

It also means that HyKeywords evaluate to themselves.

Closes hylang#1352
  • Loading branch information
vodik committed Mar 25, 2018
1 parent f57463c commit 75a99b7
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 49 deletions.
35 changes: 24 additions & 11 deletions hy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,19 +502,18 @@ def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
except StopIteration:
raise HyTypeError(expr,
"Keyword argument {kw} needs "
"a value.".format(kw=str(expr[1:])))
"a value.".format(kw=expr))

compiled_value = self.compile(value)
ret += compiled_value

keyword = expr[2:]
if not keyword:
if not expr:
raise HyTypeError(expr, "Can't call a function with the "
"empty keyword")
keyword = ast_str(keyword)

compiled_value = self.compile(value)
ret += compiled_value

arg = str_type(expr)[1:]
keywords.append(asty.keyword(
expr, arg=keyword, value=compiled_value.force_expr))
expr, arg=ast_str(arg), value=compiled_value.force_expr))

else:
ret += self.compile(expr)
Expand Down Expand Up @@ -744,6 +743,9 @@ def _render_quoted_form(self, form, level):
return imports, HyExpression([HySymbol(name),
HyString(form)]).replace(form), False

elif isinstance(form, HyKeyword):
return imports, form, False

elif isinstance(form, HyString):
x = [HySymbol(name), form]
if form.brackets is not None:
Expand Down Expand Up @@ -1170,7 +1172,7 @@ def _compile_import(expr, module, names=None, importer=asty.Import):
if isinstance(iexpr, HyList) and iexpr:
module = iexpr.pop(0)
entry = iexpr[0]
if isinstance(entry, HyKeyword) and entry == HyKeyword(":as"):
if entry == HyKeyword(":as"):
if not len(iexpr) == 2:
raise HyTypeError(iexpr,
"garbage after aliased import")
Expand Down Expand Up @@ -1766,7 +1768,7 @@ def compile_expression(self, expression):
# An exception for pulling together keyword args is if we're doing
# a typecheck, eg (type :foo)
with_kwargs = fn not in (
"type", "HyKeyword", "keyword", "name", "keyword?")
"type", "HyKeyword", "keyword", "name", "keyword?", "identity")
args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect(
expression[1:], with_kwargs, oldpy_unpack=True)

Expand Down Expand Up @@ -2182,7 +2184,18 @@ def compile_symbol(self, symbol):

return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load())

@builds(HyString, HyKeyword, HyBytes)
@builds(HyKeyword)
def compile_keyword(self, string, building):
ret = Result()
ret += asty.Call(
string,
func=asty.Name(string, id="HyKeyword", ctx=ast.Load()),
args=[asty.Str(string, s=str_type(string))],
keywords=[])
ret.add_imports("hy", {"HyKeyword"})
return ret

@builds(HyString, HyBytes)
def compile_string(self, string, building):
node = asty.Bytes if PY3 and building is HyBytes else asty.Str
f = bytes_type if building is HyBytes else str_type
Expand Down
10 changes: 5 additions & 5 deletions hy/contrib/hy_repr.hy
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

(global -quoting)
(setv started-quoting False)
(when (and (not -quoting) (instance? HyObject obj))
(when (and (not -quoting) (instance? HyObject obj) (not (instance? HyKeyword obj)))
(setv -quoting True)
(setv started-quoting True))

Expand Down Expand Up @@ -82,10 +82,10 @@
(+ (get syntax (first x)) (hy-repr (second x)))
(+ "(" (-cat x) ")"))))

(hy-repr-register HySymbol str)
(hy-repr-register [str-type bytes-type HyKeyword] (fn [x]
(if (and (instance? str-type x) (.startswith x HyKeyword.PREFIX))
(return (cut x 1)))
(hy-repr-register [HySymbol HyKeyword] str)
(hy-repr-register [str-type bytes-type] (fn [x]
(if (and (instance? str-type x) (.startswith x ":"))
(return x))
(setv r (.lstrip (-base-repr x) "ub"))
(+
(if (instance? bytes-type x) "b" "")
Expand Down
31 changes: 15 additions & 16 deletions hy/core/language.hy
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@

(defn keyword? [k]
"Check whether `k` is a keyword."
(and (instance? (type :foo) k)
(.startswith k (get :foo 0))))
(instance? HyKeyword k))

(defn dec [n]
"Decrement `n` by 1."
Expand Down Expand Up @@ -458,26 +457,26 @@ as EOF (defaults to an empty string)."
"Create a keyword from `value`.
Strings numbers and even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX))
(unmangle value)
(if (string? value)
(HyKeyword (+ ":" (unmangle value)))
(try
(unmangle (.__name__ value))
(except [] (HyKeyword (+ ":" (string value))))))))
(if (keyword? value)
(HyKeyword (unmangle value))
(if (string? value)
(HyKeyword (unmangle value))
(try
(unmangle (.__name__ value))
(except [] (HyKeyword (string value)))))))

(defn name [value]
"Convert `value` to a string.
Keyword special character will be stripped. String will be used as is.
Even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX))
(unmangle (cut value 2))
(if (string? value)
(unmangle value)
(try
(unmangle (. value __name__))
(except [] (string value))))))
(if (keyword? value)
(unmangle (cut (str value) 1))
(if (string? value)
(unmangle value)
(try
(unmangle (. value __name__))
(except [] (string value))))))

(defn xor [a b]
"Perform exclusive or between `a` and `b`."
Expand Down
35 changes: 26 additions & 9 deletions hy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,39 @@ def __init__(self, string):
_wrappers[type(None)] = lambda foo: HySymbol("None")


class HyKeyword(HyObject, str_type):
class HyKeyword(HyObject):
"""Generic Hy Keyword object. It's either a ``str`` or a ``unicode``,
depending on the Python version.
"""

PREFIX = "\uFDD0"
__slots__ = ['_value']

def __new__(cls, value):
if not value.startswith(cls.PREFIX):
value = cls.PREFIX + value

obj = str_type.__new__(cls, value)
return obj
def __init__(self, value):
if value[0] != ':':
value = ':' + value
self._value = value

def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self[1:]))
return "%s(%r)" % (self.__class__.__name__, self._value)

def __str__(self):
return self._value

def __hash__(self):
return hash(self._value)

def __eq__(self, other):
if not isinstance(other, HyKeyword):
return NotImplemented
return self._value == other._value

def __ne__(self, other):
if not isinstance(other, HyKeyword):
return NotImplemented
return self._value != other._value

def __bool__(self):
return bool(self._value[1:])


def strip_digit_separators(number):
Expand Down
11 changes: 3 additions & 8 deletions tests/native_tests/language.hy
Original file line number Diff line number Diff line change
Expand Up @@ -552,9 +552,7 @@
(assert (= (.a.b.meth x) "meth"))
(assert (= (.a.b.meth x "foo" "bar") "meth foo bar"))
(assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1"))
(assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar"))

(assert (is (.isdigit :foo) False)))
(assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar")))


(defn test-do []
Expand Down Expand Up @@ -1221,9 +1219,11 @@
"NATIVE: test if keywords are recognised"

(assert (= :foo :foo))
(assert (= :foo ':foo))
(assert (= (get {:foo "bar"} :foo) "bar"))
(assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux")))


(defn test-keyword-clash []
"NATIVE: test that keywords do not clash with normal strings"

Expand Down Expand Up @@ -1646,11 +1646,6 @@ macros()
(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")))

(defn test-only-parse-lambda-list-in-defn []
"NATIVE: test lambda lists are only parsed in defn"
(try
Expand Down

0 comments on commit 75a99b7

Please sign in to comment.