Make HyKeyword a first class objects

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 #1352
This commit is contained in:
Simon Gomizelj 2018-02-09 22:56:33 -05:00 committed by Kodi Arfer
parent 2ad3401b36
commit 1b7dfd2839
6 changed files with 73 additions and 58 deletions

View File

@ -500,19 +500,18 @@ class HyASTCompiler(object):
except StopIteration: except StopIteration:
raise HyTypeError(expr, raise HyTypeError(expr,
"Keyword argument {kw} needs " "Keyword argument {kw} needs "
"a value.".format(kw=str(expr[1:]))) "a value.".format(kw=expr))
if not expr:
raise HyTypeError(expr, "Can't call a function with the "
"empty keyword")
compiled_value = self.compile(value) compiled_value = self.compile(value)
ret += compiled_value ret += compiled_value
keyword = expr[2:] arg = str_type(expr)[1:]
if not keyword:
raise HyTypeError(expr, "Can't call a function with the "
"empty keyword")
keyword = ast_str(keyword)
keywords.append(asty.keyword( keywords.append(asty.keyword(
expr, arg=keyword, value=compiled_value.force_expr)) expr, arg=ast_str(arg), value=compiled_value.force_expr))
else: else:
ret += self.compile(expr) ret += self.compile(expr)
@ -742,6 +741,9 @@ class HyASTCompiler(object):
return imports, HyExpression([HySymbol(name), return imports, HyExpression([HySymbol(name),
HyString(form)]).replace(form), False HyString(form)]).replace(form), False
elif isinstance(form, HyKeyword):
return imports, form, False
elif isinstance(form, HyString): elif isinstance(form, HyString):
x = [HySymbol(name), form] x = [HySymbol(name), form]
if form.brackets is not None: if form.brackets is not None:
@ -1168,7 +1170,7 @@ class HyASTCompiler(object):
if isinstance(iexpr, HyList) and iexpr: if isinstance(iexpr, HyList) and iexpr:
module = iexpr.pop(0) module = iexpr.pop(0)
entry = iexpr[0] entry = iexpr[0]
if isinstance(entry, HyKeyword) and entry == HyKeyword(":as"): if entry == HyKeyword(":as"):
if not len(iexpr) == 2: if not len(iexpr) == 2:
raise HyTypeError(iexpr, raise HyTypeError(iexpr,
"garbage after aliased import") "garbage after aliased import")
@ -1764,7 +1766,7 @@ class HyASTCompiler(object):
# An exception for pulling together keyword args is if we're doing # An exception for pulling together keyword args is if we're doing
# a typecheck, eg (type :foo) # a typecheck, eg (type :foo)
with_kwargs = fn not in ( 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( args, ret, keywords, oldpy_star, oldpy_kw = self._compile_collect(
expression[1:], with_kwargs, oldpy_unpack=True) expression[1:], with_kwargs, oldpy_unpack=True)
@ -2187,7 +2189,18 @@ class HyASTCompiler(object):
return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load()) return asty.Name(symbol, id=ast_str(symbol), ctx=ast.Load())
@builds(HyString, HyKeyword, HyBytes) @builds(HyKeyword)
def compile_keyword(self, string):
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): def compile_string(self, string, building):
node = asty.Bytes if PY3 and building is HyBytes else asty.Str node = asty.Bytes if PY3 and building is HyBytes else asty.Str
f = bytes_type if building is HyBytes else str_type f = bytes_type if building is HyBytes else str_type

View File

@ -32,7 +32,7 @@
(global -quoting) (global -quoting)
(setv started-quoting False) (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 -quoting True)
(setv started-quoting True)) (setv started-quoting True))
@ -82,10 +82,8 @@
(+ (get syntax (first x)) (hy-repr (second x))) (+ (get syntax (first x)) (hy-repr (second x)))
(+ "(" (-cat x) ")")))) (+ "(" (-cat x) ")"))))
(hy-repr-register HySymbol str) (hy-repr-register [HySymbol HyKeyword] str)
(hy-repr-register [str-type bytes-type HyKeyword] (fn [x] (hy-repr-register [str-type bytes-type] (fn [x]
(if (and (instance? str-type x) (.startswith x HyKeyword.PREFIX))
(return (cut x 1)))
(setv r (.lstrip (-base-repr x) "ub")) (setv r (.lstrip (-base-repr x) "ub"))
(+ (+
(if (instance? bytes-type x) "b" "") (if (instance? bytes-type x) "b" "")

View File

@ -65,8 +65,7 @@
(defn keyword? [k] (defn keyword? [k]
"Check whether `k` is a keyword." "Check whether `k` is a keyword."
(and (instance? (type :foo) k) (instance? HyKeyword k))
(.startswith k (get :foo 0))))
(defn dec [n] (defn dec [n]
"Decrement `n` by 1." "Decrement `n` by 1."
@ -460,21 +459,21 @@ as EOF (defaults to an empty string)."
"Create a keyword from `value`. "Create a keyword from `value`.
Strings numbers and even objects with the __name__ magic will work." Strings numbers and even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX)) (if (keyword? value)
(unmangle value) (HyKeyword (unmangle value))
(if (string? value) (if (string? value)
(HyKeyword (+ ":" (unmangle value))) (HyKeyword (unmangle value))
(try (try
(unmangle (.__name__ value)) (unmangle (.__name__ value))
(except [] (HyKeyword (+ ":" (string value)))))))) (except [] (HyKeyword (string value)))))))
(defn name [value] (defn name [value]
"Convert `value` to a string. "Convert `value` to a string.
Keyword special character will be stripped. String will be used as is. Keyword special character will be stripped. String will be used as is.
Even objects with the __name__ magic will work." Even objects with the __name__ magic will work."
(if (and (string? value) (value.startswith HyKeyword.PREFIX)) (if (keyword? value)
(unmangle (cut value 2)) (unmangle (cut (str value) 1))
(if (string? value) (if (string? value)
(unmangle value) (unmangle value)
(try (try

View File

@ -121,22 +121,37 @@ _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")
class HyKeyword(HyObject, str_type): class HyKeyword(HyObject):
"""Generic Hy Keyword object. It's either a ``str`` or a ``unicode``, """Generic Hy Keyword object."""
depending on the Python version.
"""
PREFIX = "\uFDD0" __slots__ = ['_value']
def __new__(cls, value): def __init__(self, value):
if not value.startswith(cls.PREFIX): if value[0] != ':':
value = cls.PREFIX + value value = ':' + value
self._value = value
obj = str_type.__new__(cls, value)
return obj
def __repr__(self): 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): def strip_digit_separators(number):

View File

@ -79,14 +79,6 @@
(assert (is (type (get orig 1)) float)) (assert (is (type (get orig 1)) float))
(assert (is (type (get result 1)) HyFloat))) (assert (is (type (get result 1)) HyFloat)))
(when PY3 (defn test-bytes-keywords []
; Make sure that keyword-like bytes objects aren't hy-repred as if
; they were real keywords.
(setv kw :mykeyword)
(assert (= (hy-repr kw) ":mykeyword"))
(assert (= (hy-repr (str ':mykeyword)) ":mykeyword"))
(assert (= (hy-repr (.encode kw "UTF-8") #[[b"\xef\xb7\x90:hello"]])))))
(when PY3 (defn test-dict-views [] (when PY3 (defn test-dict-views []
(assert (= (hy-repr (.keys {1 2})) "(dict-keys [1])")) (assert (= (hy-repr (.keys {1 2})) "(dict-keys [1])"))
(assert (= (hy-repr (.values {1 2})) "(dict-values [2])")) (assert (= (hy-repr (.values {1 2})) "(dict-values [2])"))

View File

@ -554,7 +554,7 @@
(assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) (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 (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar"))
(assert (is (.isdigit :foo) False))) (assert (= (.__str__ :foo) ":foo")))
(defn test-do [] (defn test-do []
@ -1221,9 +1221,12 @@
"NATIVE: test if keywords are recognised" "NATIVE: test if keywords are recognised"
(assert (= :foo :foo)) (assert (= :foo :foo))
(assert (= :foo ':foo))
(assert (is (type :foo) (type ':foo)))
(assert (= (get {:foo "bar"} :foo) "bar")) (assert (= (get {:foo "bar"} :foo) "bar"))
(assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux"))) (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux")))
(defn test-keyword-clash [] (defn test-keyword-clash []
"NATIVE: test that keywords do not clash with normal strings" "NATIVE: test that keywords do not clash with normal strings"
@ -1649,11 +1652,6 @@ macros()
(setv (. foo [1] test) "hello") (setv (. foo [1] test) "hello")
(assert (= (getattr (. 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 [] (defn test-only-parse-lambda-list-in-defn []
"NATIVE: test lambda lists are only parsed in defn" "NATIVE: test lambda lists are only parsed in defn"
(try (try