hylang / hy-mode

Hy mode for Emacs
GNU General Public License v3.0
191 stars 47 forks source link

Macros with a body shouldn't get indented like a function call #44

Closed gilch closed 5 years ago

gilch commented 7 years ago

Hy macros with an &rest parameter named body should indent the body two spaces, like a defn, not aligned with the other arguments like a call.

I think Common Lisp uses &body instead of &rest to designate this. Clojure editors are more advanced and can get a custom indent spec from macro metadata.

I'm not sure how to do this, but this information is available at compile time, so it ought to be possible.

ekaschalk commented 7 years ago

Can you give an example, I haven't worked in either CL or Clojure.

gilch commented 7 years ago

In the defn-style indent the body argument indicates that there's an implicit do around these body arguments. The body is indented two spaces past the opening bracket.

opening bracket
|
v
(foomacro arg1 arg2 arg3
  body1
  (barmacro arg
    body)  ; 2 spaces past its opening bracket
  body3)
^^
two spaces before body arguments.

The "function call" style indent always lines up with the first arg.

(foofunction arg1
             (barfunction bararg1
                          bararg2)  ; aligned with bararg1
             arg3)

And, of course, [] and {} don't treat the first argument as special.

[a
 [b1
  b2]
 {c-key1 c-val1
  c-key2 c-val2}
 #{x
   y
   z}
 `[p
   q]
 d
 e]

Hy's Style Guide needs some work. We haven't updated it lately, and it was probably for an audience already familiar with Clojure. The Clojure Style Guide has more examples. Since Hy borrows so heavily from Clojure, that's a good reference. Other Lisps have their own style rules that differ somewhat. But no matter our indents, we should not violate Parlinter's basic indentation constraints.

oskarkv commented 6 years ago

Here is a description of Clojure's "indent specs" http://cider.readthedocs.io/en/latest/indent_spec/ Users can then put specs in their Emacs configs, or in macro metadata.

I'm new to Hy; is there metadata for macros? Otherwise one can put the spec in the docstring. But anyway, the Hy process must of course communicate back to Emacs. Or one can just rely on the user's Emacs config.

I'm not sure of exactly how to do this, but I guess looking at how clojure-mode does it would be a reasonable start.

ekaschalk commented 6 years ago

We do have some customization available in hy-indent-special-forms, where :exact will match a symbol exactly and :fuzzy a regex match to determine special indentation (args ignored). I use this for my own macros already.

I'm quite familiar with clojure's indentation - Hy's indentation was reverse engineered from Clojure's implementation.

It is not unreasonable to update to allow matches to specify the following lines indentations, but I have not seen the usecase.

Parsing indentation metadata is more involved, as are indentation scheme's that have more partitions than no/some args, so there would need to be a compelling reason to make the indentation code more complex than it already is.

ekaschalk commented 6 years ago

@gilch I never addressed your example.

I handle indentation for my personal macros via the above. For example, I maintain a hy-personal.el I load that:

(setq hy-indent-special-forms
      '(:exact
        ("when" "unless"
         "for" "for*"
         "while"
         "except" "catch"
         "when-then" "unless-then"
         "->" "->>")

        :fuzzy
        ("def"
         "with"
         "fn"
         "lambda")))

Most of my macros are caught by the :fuzzy match (like a with-fixture macro for instance), and I catch some specific ones like when-then in the :exact match.

If you replace foomacro/barmacro with with-foo and with-bar it will match your example. Your lists example is already passing.

It is not reasonable to detect user-defined macros automatically.

oskarkv commented 6 years ago

@ekaschalk OK!

By the way, I noticed that out-of-the-box, defmacro itself was not properly indented for me. But defn was. How come? Then I added defmacro to hy-indent-special-forms and it worked. But defn is not there, and it works anyway. What's up with that?

ekaschalk commented 6 years ago

It was, then at some point I accidentally removed it. Since I overwrite with my own indents, I never noticed the regression. It should be fixed in master again.

I'm not sure why defn was working, it's interesting, I'll probably look into that.

gilch commented 6 years ago

It is not reasonable to detect user-defined macros automatically.

Statically you mean? I do think Common Lisp has the &rest &body distinction for just this purpose. And Clojure has the indent spec metadata, as documented in CIDER. Maybe that only works if you're running a background repl though? You could look at how they do it. We don't have metadata support yet, but it might be coming. https://github.com/hylang/hy/issues/1482 Clojure-style indent specs would probably have to wait for that, but for now it might demonstrate a way you could detect &rest body.

The :fuzzy for def and such helps a lot though. Macros would only have to follow certain naming conventions.

Another possibility would be to let the human decide. So for the first newline, hy-mode would accept either a 2-space indentation under the head symbol, or aligned with the first argument, while remaining newlines would align with that.

;; default, since most forms will be function calls
(foo (bar a b c
          x ; aligned with 'a, the first argument
          y ; aligned with 'x
          z))
;; or, if user manually de-indents x, it snaps to the alternate position
(foo (bar a b c
       x  ; indented 2 spaces under 'bar, the head of x's list
       y  ; aligned with 'x
       z))

A reformat won't change the indent if it's exactly in the alternate 2-space indent column, but if it's in any other column, it will revert to function-call style under a on a reformat.

And, of course, if the head is some collection rather than a symbol, newlines can't be indented futher than the head without violating parlinter's constraints.


(foo ((bar) a b c
      x  ; must be aligned with the head--it looks like it belongs to the wrong list anywhere else.
      y  ; still aligned with 'x
      z))
ekaschalk commented 6 years ago

I didn't see that Clojure had indentation spec options that could be parsed from function definitions. That is possible (though not easy). Of course Hylang doesn't support that yet.

If I understand your second example, you are suggesting that the user can force specific forms to be specially indented (ignore preceding args)?

gilch commented 6 years ago

Yes, if we both mean the same thing by "specially indented". And it's just a suggestion, there could be other ways to do it.

I meant the single actual written form the user is working on, and only if they manually de-indent the first newline, like with backspace or << when using evil. Here | indicates the point. User has just typed enter, so the next line is indented under the first arg of the current list, like a function call.

(foo (bar a b c
          |

Then if the user hits backspace,

(foo (bar a b c
       |

the point snaps back to the alternate 2-space indent for the current list. The "special" style. Lines thereafter will follow the indent set in the first newline, so the args remain aligned with each other.

That's the basics of it, but furthermore,

Asking emacs to re-indent the form will cause forms to indent in the function style by default (under the first arg), unless

I'm not sure how hard it would be to implement something like that.

ekaschalk commented 6 years ago

Special indentation is an emacs term, you've interpreted it fine.

Point 3 is entirely possible and I expect easy. If I'm understanding, a form opening with a bracket-like or another form? In the bracket case, when does this occur? Macros?

I'm not sure of point 2. Do you have any example languages that implement such a variable indent? I'd have to research this, I believe it can be done though, some manner or another.

gilch commented 6 years ago

Looking back a point 3, I'm not sure I've explained it well enough.

It's fine to use function-style indent if the form has a collection in the function position.

(foo ((bar) a b c
            x-ray
            yankee))

Special indent is also OK, but only if the newline doesn't have the first argument.

(foo ((bar) a b c
       x-ray  ; first argument was 'a. This is probably permissible; meets Parlinter's constraints.
       yankee))
(foo ((bar)  ; line ended in )!
       x-ray  ; unacceptable. 'x seems to be in the list with head 'bar, instead of '(bar).
       yankee))
;; because unless you're counting brackets it looks like this.
(foo ((bar
       x-ray
       yankee)))

;; so we indent like a [] instead
(foo ((bar)
      x-ray
      yankee))

Try the above cases in Parinfer to see what I mean.


In Clojure, collections are callable.

user=> ([:foo :bar :baz] 1)
:bar
user=> ({:foo 2, :bar 4, :baz 7} :bar)
4

So it's not unusual to see a bracketed collection in the function position. And actually, so are keywords,

user=> (:foo {:foo 2, :bar 4, :baz 7})
2

This is also true for keywords in Hy. So for indentation purposes, you can treat Hy keywords like symbols.

=> (:spam {:spam 1  :eggs 2})
from hy import HyKeyword
{HyKeyword('spam'): 1, HyKeyword('eggs'): 2}[HyKeyword('spam')]

1

And even in Hy, a () form in the function position may evaluate to a callable.

=> ((fn [x] (* x x)) 3)
(lambda x: x * x)(3)

9

I'd probably indent such a thing like this:

((fn [x]
   (* x x))
 3)

Also, Clojure's defn can take multiple arities, e.g.

(defn average
  ([& args]
   (/ (reduce + args)
      (count args)))
  ([] 0))  ; avoids zero division error

Note the [] in the head of a ().

Hy also has an alternate defn macro in hy.contrib.multi that does this.


I'll experiment more with some other lisp modes to see if they have that kind of variable indent behavior.

ekaschalk commented 6 years ago

Thanks for the explanation. I had no idea in particular that keywords were callable in Hy, nor the contrib defn case.

Yes I can adjust the indentation to satisfy collection openers, and thanks for the supplied test-cases.

ekaschalk commented 6 years ago

Hm, collections with bracket-likes were already indenting appropriately for me (eg. your clojure defn example).

I've fixed indentation of forms opening with callables.

ekaschalk commented 5 years ago

Closing this.

FYI I'm working on hy-mode again (with a lot more knowledge of emacs behind me) so if you have complaints, concerns, etc. shoot them my way so I can consider them while updating.

Also related: If you make core changes, (eg. __macros__ construct, renaming hy.eval kwargs), shooting me a message will help me make the needed updates quicker and save me some time.