Repository: hylang/hy

Issue:
problem with the get attribute and defclass
There is an error when trying to define `get` attribute of the class.

**Code:**

```
(import [flask_restful [Resource]])

(defclass HelloWorld [Resource]
  [[get (fn [self] "Hello World")]])
```

**Error:**

```
    [[get (fn [self] "Hello World")]]])
      ^--^
HyTypeError: Can't assign to a builtin: `get'
```

**Flask RESTful Python example:**

```
from flask import Flask
from flask.ext import restful

app = Flask(__name__)
api = restful.Api(app)

class HelloWorld(restful.Resource):
    def get(self):
        return {'hello': 'world'}

api.add_resource(HelloWorld, '/')

if __name__ == '__main__':
    app.run(debug=True)
```

**Note:** The `get` attribute can be defined using `setv` as described in  #721.



Discussion:
The problem is that we want to have two things:
- An error message when one tries to do stuff like `(defn get [&rest args] 42)` at the top level. We want this, because otherwise it'd be surprising that sometimes the built-in gets called, sometimes our function.
- Yet, we also want to allow names with builtins in cases where there's a surrounding context (such as a class)

This is hard. Hard, because the compiler - as far as I see - doesn't see the context.

There are a number of solutions I can imagine:

1) Teach the compiler about context. If it can walk up the expressions to see if it is under a particular form, that'd help. This has the drawback that this isn't entirely trivial. We can't just store the parsed stuff, we have to macroexpand it too. Doesn't sound pretty to me.

2) Teach `(setv)` to take a special argument, something like this: `(setv :^allow-builtins ...)`. If the flag is present, we allow deffing stuff which would otherwise shadow builtins. Then `(defclass)` could be taught to rewrite setvs and defns within itself to add an `:^allow-builtins` flag. This sounds like a bit of a hack, though.

3) Store some state within the compiler: `(defclass)` would set `self.allow_builtins` to true while compiling its body, and set it back to its original value at the end. This is a hack, but at the python level, and doesn't leak through to Hy code. Not sure it would work, though.

I'll have a stab at the third option.


Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -370,6 +370,7 @@ def checker(self, expression):
 class HyASTCompiler(object):
 
     def __init__(self, module_name):
+        self.allow_builtins = False
         self.anon_fn_count = 0
         self.anon_var_count = 0
         self.imports = defaultdict(set)
@@ -2005,7 +2006,8 @@ def _compile_assign(self, name, result,
                         start_line, start_column):
 
         str_name = "%s" % name
-        if _is_hy_builtin(str_name, self.module_name):
+        if _is_hy_builtin(str_name, self.module_name) and \
+           not self.allow_builtins:
             raise HyTypeError(name,
                               "Can't assign to a builtin: `%s'" % str_name)
 
@@ -2275,6 +2277,8 @@ def compile_class_expression(self, expressions):
                                          docstring.start_column)
             body += body.expr_as_stmt()
 
+        allow_builtins = self.allow_builtins
+        self.allow_builtins = True
         if expressions and isinstance(expressions[0], HyList) \
            and not isinstance(expressions[0], HyExpression):
             expr = expressions.pop(0)
@@ -2287,6 +2291,8 @@ def compile_class_expression(self, expressions):
         for expression in expressions:
             body += self.compile(macroexpand(expression, self.module_name))
 
+        self.allow_builtins = allow_builtins
+
         if not body.stmts:
             body += ast.Pass(lineno=expressions.start_line,
                              col_offset=expressions.start_column)


Test patch:
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -551,3 +551,17 @@ def test_defn():
     cant_compile("(defn \"hy\" [] 1)")
     cant_compile("(defn :hy [] 1)")
     can_compile("(defn &hy [] 1)")
+
+
+def test_setv_builtins():
+    """Ensure that assigning to a builtin fails, unless in a class"""
+    cant_compile("(setv nil 42)")
+    cant_compile("(defn get [&rest args] 42)")
+    can_compile("(defclass A [] (defn get [self] 42))")
+    can_compile("""
+    (defclass A []
+      (defn get [self] 42)
+      (defclass B []
+        (defn get [self] 42))
+      (defn if [self] 0))
+    """)