rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Generalized indenting macro

Open sorawee opened this issue 6 years ago • 13 comments

elisp has indenting macros which allow us to specify in macro definitions how we want the macro body to be indented. The indenter then uses this information to figure out how to indent properly.

The idea of generalized indenting macro is similar, but it should work with whatever syntax Racket2 is going to be. It could also help with more than indentation.

(Backporting this to Racket1 would also be very great!)

sorawee avatar Aug 17 '19 02:08 sorawee

Ultimately, we want to give control to users, so we might have something like:

#lang s-exp framework/indenter-lang

(define (indent the-macro indent-config editing-file-path)
  (syntax-parse the-macro
    [{~literal cond} (make-indent-config ...)] ; override cond indenting macro config
    [_ indent-config])) ; use the indenting macro config for the rest

so that users can control the indenter however they want.

sorawee avatar Aug 17 '19 02:08 sorawee

Since indenter config is attached to bindings, there must be macro expansion in the background. The more lightweight but less accurate alternative might be to use the textual name instead.

sorawee avatar Aug 17 '19 02:08 sorawee

Should it be possible to define "indentation rules" for non-macro bindings, like normal functions? For example, could a call-with-input-file function definition specify that its second argument (the function) should be indented only 2 spaces, instead of being lined up with the first argument, without needing to define it as a macro?

AlexKnauth avatar Aug 17 '19 04:08 AlexKnauth

@AlexKnauth What if indentation information was stored in something more like a rename transformer? Say you had something like this:

(define (plain-call-with-input-file ...) ...)

(define-syntax call-with-input-file
  (make-indentation-rule-transformer #'plain-call-with-input-file
    (... indentation rules here ...)))

jackfirth avatar Aug 17 '19 05:08 jackfirth

Should it be possible to define "indentation rules" for non-macro bindings, like normal functions? For example, could a call-with-input-file function definition specify that its second argument (the function) should be indented only 2 spaces, instead of being lined up with the first argument, without needing to define it as a macro?

This is a good question.

One possible answer is no. Indentation for function applications should be in the same style to keep surprises at minimum.

I personally have been thinking that these call-with-* are not really good as an interface, and in most cases will use the macro variant instead. I use call-with-* willingly only when I define functions somewhere already, and can use call-with-* without needing to create a new lambda, and in this case, regular indentation suffices.

@jackfirth's idea is very appealing, however!

sorawee avatar Aug 17 '19 06:08 sorawee

One possible answer is no. Indentation for function applications should be in the same style to keep surprises at minimum.

This is a good principle, but those call-with- procedures are so widespread that it causes a lot of practical problems (you write one of those things in the obvious way, it jumps toward the right margin and you need to stop and consider how to layout your code).

lassik avatar Aug 17 '19 07:08 lassik

Since indenter config is attached to bindings, there must be macro expansion in the background.

Exactly. An indenter that doesn't "cheat" will be very difficult to write (or very intricately bound to the target language implementation, which may keep changing).

How about this? Since (eq? 'foo '|foo|) is true, you could have a dumb magic indenter that indents a macro froz in a particular way. So it would just accept any list (froz ...) but would not accept '(froz ...) or ('froz ...). If the user notices some incorrect indentation, they can put vertical bars around the froz list head in that form: (|froz| ...). The indenter would ignore any symbol with vertical bars when looking for list heads to indent magically.

The regular S-expression reader forgets whether or not a particular symbol had vertical bars, but the indenter can use a modified reader implementation that remembers.

lassik avatar Aug 17 '19 07:08 lassik

@lassik What if froz is imported with rename-in? Or qualified-in?

jackfirth avatar Aug 17 '19 19:08 jackfirth

Clojure seems to have something similar too: https://docs.cider.mx/cider/indent_spec.html

sorawee avatar May 18 '20 00:05 sorawee

I think @sorawee and @notjack's approaches reflect two dimensions of an expression problem:

  • @sorawee's approach is good because a new editor feature like this needs to work with existing syntaxes. The editor can code-walk by default, and any respect it gives to the abstractions it traverses over is a courtesy. It can always resort to text processing if needed, but if it can use a free-identifier=? check, then at least its courtesy will be sufficient to respect rename-in.

  • @notjack's approach is good because new syntaxes will sometimes be designed with this existing editor feature in mind and therefore have their own batteries-included configuration.

Taking both approaches into account at once, the flow of information has a feedback loop here. We can imagine arbitrary complex back-and-forth negotiation between the preferences of:

  • The user of the editor, whose intentions are expressed in the editor's default indentation rules.

  • The author of the code being indented, whose intentions are expressed in the form of the text they've chosen to save in the file. Some of the text is the intentation itself, which we might use as a hint to predict future indentation by inductive reasoning. Some of the text is the choice of like (let ...), (define ...), parentheses, #lang 2d, and so on, which can bring to mind different conventional styles of indentation.

  • (In between, the authors of the editor and other infrastructure. These people's choices may interfere in the negotiation process even though they have relatively little at stake.)

Of course, the user of the editor is frequently the same person as the author of the code being edited, so the negotiation feedback loop is mostly between various people authoring the same code.

The most expressive way these authors can express their intent is through their choice of syntaxes like (let ...) and (define ...). Since the syntaxes' programmatic behavior can help express this intent in detail, the intention information should probably flow through the macro system:

  • First: Before beginning background expansion, the editor decides what environment of free-identifier=?-based rules represents the user's preferred indentation settings.

  • Last: Unless there's a rule that overrides it, a macro has the chance to determine its own indentation. If it doesn't take this chance, then a default rule applies.

  • In between: Macro calls may perform local macroexpansions. When they do, perhaps they can meddle with the indentation environment they pass in (maybe using parameterize or syntax-parameterize), and perhaps they can meddle with the indentation results they get back (maybe using syntax properties).

In summary, I think a new editor feature like this justifies both the use of free-identifier=?-style code-walking rules and the ability for macros to provide explicit custom support for the feature. I think a good synthesis of these approaches will involve an expansion-time indentation environment.

rocketnia avatar May 19 '20 00:05 rocketnia

Potentially a good example to "stress" a design: https://github.com/greghendershott/racket-mode/issues/521

greghendershott avatar Feb 23 '21 17:02 greghendershott

I think I mentioned this on Slack at some point, but to record it here:

Speed is important. Editors ask "how do I indent this line" frequently. When a user presses ENTER, they often ask it twice! In some langs, the editor will re-indent the line before the newline, and then of course indent the new fresh line.

For things like check-syntax, it's fine to have some small delay after an edit, before the screen refreshes. Even some small number of seconds might be fine.

But for indent, it's generally very not fine.

So, whatever approach is used to gather the information, e.g. through macro expansion, somehow it needs to be distilled and saved in a form that can be queried quickly.

Probably that's already obvious to everyone but I wanted to point it out just in case.

greghendershott avatar Feb 24 '21 17:02 greghendershott

Thinking about https://github.com/greghendershott/racket-mode/issues/521, which @sorawee pointed out is handled in framework, I have a suggestion.

Someone mentioned back-porting, above. My suggestion is instead to "pre-port".

Even if the idea is to deprecate the Racket name/culture/sexps and start over with some new name/culture/syntax (:disappointed:) it might make sense to have an intermediate step on the roadmap.

Which is: Flesh out this indent proposal. Change tools like DrRacket and Racket Mode so that #lang racket is not privileged --- no more special magic for things like for/fold or define-judgement-form. Make Racket fully "eat its own dogfood" -- use no more than the same mechanisms available to any other #lang -- for things like indent, syntax-highlighting, etc.

Having accomplished that possibly non-trivial step, it should be much shorter strokes for NewLang. As well as telling an even stronger story about LOP.

greghendershott avatar Feb 24 '21 17:02 greghendershott